MusicEngine - A dynamic chord library implemented in SuperCollider

Hello everyone. I’ve been working on a dynamic chord library for my next harmonizer project. Harmonizer 01 had an obvious limitation of only allowing triads, so a flexible chord library capable of handling, not only midi note ranges, but also note names and chord degrees was needed to take my projects to the next level. MusicEngine is also being developed as a stand-alone project to serve as an independent tool, following a OOP paradigm.

I’m looking for feedback specifically on the class structure and overall architecture of my Music engine library. This is my first serious attempt at building a system using OOP in SuperCollider, so I’d like to know if my design choices make sense or if I’m misusing classes, initialization patterns, or data encapsulation.

About MusicEngine

MusicEngine doesn’t rely on an in internal table of MIDI offsets. Instead, it parses what I call a verbose syntax (e.g. "CM3P5m7M9"), and from this generates:

  • Midi offsets
  • note name and accidental offsets
  • chord degrees
  • ranges trimmed by octaves

There’s also an alias system that maps common chord symbols to their verbose equivalents, for example:

<root>9 = <root>M3P5m7M9

The dictionary of aliases is comprehensive, but, obviously, not all inclusive[1].

Creating and inspecting ranges

Ranges are generated as instances of MENoteRanges:

~range = MENoteRanges("Fm3P5M6")

From there we can obtain an array of MENotes objects:

~range.notes // [a MENotes, a MENotes, a MENotes, a MENotes, ...]

Since all note data is encapsulated inside each MENotes object. We can retrieve it by calling on its properties:

~range.notes[10].degree // Rt
~range.notes[10].midi   // 53
~range.notes[10].freq   // 174.6141157165
~range.notes[10].name   // F3

Or by calling on MENoteRanges instance methods, to extract the data into arrays:

~range.degrees // [P5, M6, Rt, m3, P5, M6, Rt, m3, ...]
~range.midi    // [24, 26, 29, 32, 36, 38, 41, 44, ...]
~range.freq    // [32.703195662575, 36.708095989676, 43.653528929125, ...]
~range.names   // [C1, D1, F1, Ab1, C2, D2, F2, Ab2, ...]

This is just a brief description of what it does and what its intent is. That said, I’d like to know if there’s anyone who’d be willing to review it and provide feedback and improvement ideas.

To those interested there are two files (inspectME.scd and inspecttools.scd), at the root of the repository, for the purpose of trying different symbols and inspecting the results. The project still needs systematic testing, but so far seems to be working as intended.

There are also two important additions I haven’t had the chance to implement yet:

  1. A register system where users may create registers of their custom symbols, paired with the respective verbose representation, and have that be stored in a file.
  2. See if it’s possible to, somehow, integrate the Tuning class into MENoteRanges. I haven’t used that class and haven’t looked into that yet, so I have no idea if it’s possible.

That’s all for now.
I’d greatly appreciate any architectural or class-design feedback.
All the best to you all!


  1. For reference see the wiki page Chord Symbols ↩︎

2 Likes

I’ve added a more detailed Usage section to the repository README file. I’m sorry I forgot about that.

For my understanding: how many MENotes does ~range = MENoteRanges.new("Gm3P5M6M7"); generate?

The readme on github examines ~range.notes[19].degree which seems to imply the chord consists of at least 20 notes? (which seems a little excessive?). Or is it generating something like a scale instead? A list of notes in many possible octaves?

I’m a bit confused.

The ranges are generated by first wrapping all chord notes around the first midi octave, which is octave -1, in this case 2, 4, 6, 7, 10 (D, E, F#, G, Bb), and then repeating the pattern throughout the entire midi range. By default MENoteRanges returns all notes from octaves 1 to 9, unless expressed otherwise.

If you call the method degrees on range:

~r = MENoteRanges("Gm3P5M6M7");
~r.degrees(-1, 9) // [P5, M6, M7, Rt, m3, P5, M6, M7, Rt, m3, P5, M6, M7, Rt, m3, P5, M6, M7, Rt, ...]

You can see the pattern repeats itself across the different octaves.

This maps to the midi range:

~r.midi(-1, 9); // [2, 4, 6, 7, 10, 14, 16, 18, 19, 22, 26, 28, 30, 31, 34, 38, 40, 42, 43, 46,

In essence returning all notes belonging to Gm3P5M6M7 across all octaves.

I hope this example makes things more clear.

I’m at a bit of a cross road regarding data validation in my MusicEngine project, and was wondering that perhaps someone could help shed some light.

I’ve implemented a series of validator classes to ensure input passed to methods is valid. Since those methods are, on one hand, part of the public API, and, on the other, called internally when generating ranges and related data, I wanted to be able to switch on validation, when a class method is called directly by a user, and switch off, when a method is called internally. Since data generated inside the program is presumed to be safe.

What I did

I was able to achieve this by adding a validate argument to class methods, that defaults to true. For example, a call to:

MEMIDINotes.getOffsetFromName(<noteName>);

Will trigger validation, if called directly by a user, but when called inside other methods, like:

getRange { |symbol|
	var tempM, tempL, tempD;

	MEDebug.log("MERanges", "*getRange");

	this.getOffsets(symbol.degrees);

	MEMIDIValidators.midiOffsetArrayIsValid(midiOffsets, diatonic: false);

	midiRoot = MEMIDINotes.getOffsetFromName(symbol.root, validate: false);
	tempM    = MEMIDINotes.transposeMidiOffset(midiOffsets, midiRoot, validate: false);
	tempL    = MENoteName.getNoteLetters(letterOffsets, symbol.root[0], validate: false);

	#tempM, tempL, tempD = this.wrapAndExtend(
		tempM,
		tempL,
		intervals
	);

	^this.getMENotes(tempM, tempL, tempD);
}

Validation will be set to false.

Although this works, I don’t really like having a validation argument in most class methods, to switch validation on and off, and would much rather prefer to find a way to handle this internally.

Alternative A

One alternative to this could be creating different versions of the methods, one to be used externally with validation, and another to be used internally, without validation. The public version being a wrapper over the private, for example:

MEParentClass { 

    *privateClassMethod { |arg1, arg2, ...| }
}

MEChildClass : MEParentClass {

    *publicClassMethod { |arg1, arg2, ...|
        
        MEValidators.argsAreValid(arg1, arg2, ...); // Add validation
        
        ^super.privateClassMethod(arg1, arg2, ...) // Call private version
    }
}

This solution would allow me to dispense with the validate argument, although it would require two version of each class method and additional classes.

Alternative B

Another alternative is to simply remove those methods from the public API and only use them internally. Although methods like getOffsetFromName, getOffsetFromInterval, getClosestOctave, etc, are important to resolve data internally, they’re very specific and I’m not sure if they’d be of any real use to anyone (My initial thought was, of course, that perhaps someone would find them useful, and that I should make them available). The real important stuff, from a users perspective, are the MENoteRange instance methods, as exemplified in the original post and in a subsequent response.

I’m inclined to go with alternatives A or B. But I’m not sure. I suspect I could be over-complicating things a little. I’d love to hear some thoughts! Maybe there are other alternatives I did not consider? or best practices regarding data validation I’m am not aware of? maybe wanting to include those methods as part of the public API is just silly, and I should simplify things by only using them internally?

Apart from that, development is going well, with MusicEngine seemingly behaving as expected.

All the best!

If you implement the printOn method for relevant objects so that it gives you more information, the system is easier to inspect.

1 Like

I currently have a simple debug setup that helps me track computation:

MEDebug.debug = true;
MEDebug.count = 0;
r = MENoteRange("F");

What I then do, when something breaks and inspection in needed, is to append the .postln method, as needed, until I narrow down the cause.

I can see now, from your reply, how this may be insufficient in communicating data-flow to others in a context other than debugging. Specially when asking for help.

I will look into printOn, and revise my debugger to better reflect how data is being handled internally.

Thank you so much for taking the time. At this stage every little tip is incredibly valuable.

I’ve implemented it for MENote, MESymbol, MENoteName, MEAccidental and MENoteRange.

Since MENote has a nested MENoteName object, which, in turn, has a nested MEAccidental object, calling on a MENote object now reads:

MENote [ MIDI: 25, Degree: P5, Name: MENoteName [ Letter: C, Accidental: MEAccidental [ Offset: 1, Sign: # ] ] ]

I also added namesObj and accidentalObj instance methods to MENote so that nested objects can be accessed directly.

My debugger was debloated, and it’s now showing clearly how data is transformed internally:

MEDebug.debug = true;
MEDebug.count = 0;
~r = MENoteRange("F#9");

Prompts:

#0   @MENoteRanges.init
in:  F#9
        
#1   @MESymbols.init
in:  F#9
        
#2   @MESymbolValidator.*getRoot
in:  F#9;
out: F#

#3   @MEAliases.*checkAliases
in:  9;
out: M3P5m7M9

#4   @MESymbolValidator.*getDegrees
in:  M3P5m7M9;
out: [M3, P5, m7, M9]

#5   @MERanges.*getRange
in:  MESymbol [ Root: F#, Intervals: [M3, P5, m7, M9], Symbol: M3P5m7M9, Alias: 9 ]

#6   @MERanges.*getOffsets
in:  [M3, P5, m7, M9];
out: [[Rt, 0, 0], [M3, 4, 2], [P5, 7, 4], [m7, 10, 6], [M9, 2, 1]]

#7   @MERanges.*sortAndSplit
in:  [[Rt, 0, 0], [M3, 4, 2], [P5, 7, 4], [m7, 10, 6], [M9, 2, 1]]
out: midiOffsets: [0, 2, 4, 7, 10]
out: letterOffsets: [0, 1, 2, 4, 6]
out: intervals: [Rt, M9, M3, P5, m7]

#8   @MEMidiNotes.*getOffsetFromName
in:  F#
out: 6
  
#9   @MEMidiNotes.*transposeMidiOffset
in:  6, [0, 2, 4, 7, 10]
out: [6, 8, 10, 13, 16]

#10  @MENoteNames.*getNoteNames
in:  F, [0, 1, 2, 4, 6]
out: [F, G, A, C, E]

#11  @MERanges.*wrapFirstOctave
in:  [6, 8, 10, 13, 16], [F, G, A, C, E], [Rt, M9, M3, P5, m7]
out: [1, 4, 6, 8, 10], [C, E, F, G, A], [P5, m7, Rt, M9, M3]

#12  @MERanges.*extendMidiRange
in:  [1, 4, 6, 8, 10]
out: [1, 4, 6, 8, 10, 13, 16, 18, 20, 22, 25, 28, 30, 32, 34, 37, 40, 42, 44, 46, 49, 52, 54, 56, 58, 61, 64, 66, 68, 70, 73, 76, 78, 80, 82, 85, 88, 90, 92, 94, 97, 100, 102, 104, 106, 109, 112, 114, 116, 118, 121, 124, 126]

#13  @MERanges.*wrapAndExtend
in:  [1, 4, 6, 8, 10], [F, G, A, C, E], [Rt, M9, M3, P5, m7]
out: [1, 4, 6, 8, 10, 13, 16, 18, 20, 22, 25, 28, 30, 32, 34, 37, 40, 42, 44, 46, 49, 52, 54, 56, 58, 61, 64, 66, 68, 70, 73, 76, 78, 80, 82, 85, 88, 90, 92, 94, 97, 100, 102, 104, 106, 109, 112, 114, 116, 118, 121, 124, 126]
out: [C, E, F, G, A, C, E, F, G, A, C, E, F, G, A, C, E, F, G, A, C, E, F, G, A, C, E, F, G, A, C, E, F, G, A, C, E, F, G, A, C, E, F, G, A, C, E, F, G, A, C, E, F]
out: [P5, m7, Rt, M9, M3, P5, m7, Rt, M9, M3, P5, m7, Rt, M9, M3, P5, m7, Rt, M9, M3, P5, m7, Rt, M9, M3, P5, m7, Rt, M9, M3, P5, m7, Rt, M9, M3, P5, m7, Rt, M9, M3, P5, m7, Rt, M9, M3, P5, m7, Rt, M9, M3, P5, m7, Rt]

#14  @MERanges.*getMENotes

Accidentals and octaves get resolved when a new MENote object is instantiated. Since that’s done in a loop, showing that information would clutter the prompt.

I added the file inspectInternals.scd to the root of the repository.

I think this makes inspection more straight forward.

Thanks again!