Drawing Grids with note names from Scales

Hello,

I wanted to draw only the note names of scales on the grid of a semitone axis, and after some fiddling I came up with the following rather compact combination of the classes Warp, Scale and ControlSpec. Maybe it can be helpful to someone? Comments are welcomed.

First, make these two definitions in a suitable .sc file:

ScaleGridLines : AbstractGridLines {

    // There is another, similar midinote method in the SC3-plugins,	
    // but it appends a non-standard octave number.
	midi2note { arg midi;
		var notes;
		midi = (midi + 0.5).asInteger;
		notes = ["c", "c#", "d", "d#", "e", "f", "f#", "g", "g#", "a", "a#", "b"];
		^(notes[midi%12] ++ (midi.div(12)-1))
	}

	// I found that the appended tab in str prevents end-clipping
	// when the label is drawn, but it should not be needed.
	formatLabel { | midi |
		var str = this.midi2note(midi) ++ $\t;
		^str.toUpper
	}

	getParams { |valueMin, valueMax, pixelMin, pixelMax, numTicks, tickSpacing = 64|
		var lines, p;
		var graphmin, graphmax, domain;
		var offsetFromC = spec.default.asInteger;
		var symScale = spec.units.asSymbol;
		var theScale;
		theScale = Scale.all[symScale];

		graphmin = valueMin.round(1).asInteger;
		graphmax = valueMax.round(1).asInteger;

		lines = [];
		domain = (graphmin..graphmax);
		domain.do { | note, ix |
			var deg = (note-offsetFromC).mod(theScale.pitchesPerOctave).round(1).asInteger;
			if (theScale.degrees.includes(deg), {
				lines = lines.add( note );
			});
		};

		p = ();
		p['lines'] = lines;
		p['labels'] = lines.collect({ arg val; [val, this.formatLabel(val)] });
		^p
	}
}

ScaleWarp : LinearWarp {
	gridClass { ^ScaleGridLines }
}

Now, make the Warp class aware of a new warp called ‘scale’, and add a Scale with an array of the degrees for which you want gridlines to be plotted. Grid.numTicks and Grid.tickSpacing will be ignored by ScaleGridLines.getParams(), since the number of semitones between scale degrees can be irregular. Instead, you will need to specify a suitably dense scale for your axis length.

Warp.warps.add(\scale -> ScaleWarp);
Scale.all.put(\semitones, Scale((0..11), name: "Semitones"));
Scale.all.put(\majorTriad, Scale(#[0, 4, 7], name: "Major Triad"));
Scale.all.put(\octaves, Scale(#[0], name: "Octaves"));

Now, it’s testing time! When creating the ControlSpec for an axis with the new warp ‘scale’,
the minval: and maxval: arguments must be MIDI note numbers. You specify the desired Scale with units: (its name can be a Symbol or a String, as you please), and the root note of the scale relative to C with default: (an Integer). Any of the predefined Scales with .pitchesPerOctave == 12 can be chosen.

x = ControlSpec.new(minval: 36, maxval: 72, warp: 'scale', default: 0, units: \majorTriad);
d = DrawGrid((600@250).asRect, x.grid, nil);

// generate a preview
~testView = d.preview;

Admittedly, this is unconventional usage of the ControlSpec, and it will only work with the custom warp \scale as implemented by the subclass ScaledGridLines shown above. Here is an example of plotting every semitone. Follow it with d = DrawGrid (…) and d.preview as before.

x = ControlSpec.new(48, 72, \scale, default: 0, units: \semitones);

Here’s the F minor scale, rather than the default C major. ‘minor’ is one of Scale’s predefined scales, and F is 5 semitones up from C. Again, follow it with d = DrawGrid (…) and d.preview as before.

x = ControlSpec.new(46, 80, \scale, default: 5, units: \minor);

And while we’re at it, has anyone made a class for drawing a piano keyboard along an axis?

Cheers,
sternsc

Thank you very much for sharing this.

It looks extremely useful. I believe that, once the following two points are clarified, it could be a strong candidate for inclusion in SuperCollider, either as a Quark or as a built‑in class.

  1. The matter of enharmonic notation. In the F minor example you provided, the note names are not displayed in accordance with the F minor scale.

  2. How might one draw additional information on top of these grid lines? For example, could the frequency of occurrence of each note from a given array be shown?

As for the keyboard, to the best of my knowledge there is a Quark available called ‘keyboard’:

Glad you liked it!

  1. Assuming that we stay on the conventional circle of fifths, we can modify the labels to show sharps or flats appropriate for the key specified by the default argument to ControlSpec, as follows.
ScaleGridLines : AbstractGridLines {
	const sharpNotes = #["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
	const flatNotes  = #["C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"];
	// Evaluate ((60, 55..0)+6).mod(12) to get this array:
	const circFifths = #[6, 1, 8, 3, 10, 5, 0, 7, 2, 9, 4, 11, 6];
	var notes;

	// There is also a midinote extension in the SC3-plugins,
	// but that one shows a non-standard octave number.
	midi2note { arg midi;
		midi = (midi + 0.5).asInteger;
		^(notes[midi%12] ++ (midi.div(12)-1))
	}

	// The trailing tab in str prevents end-clipping
	// when the label is drawn (but it should not be needed).
	formatLabel { | midi |
		^this.midi2note(midi) ++ $\t;
	}

	getParams { |valueMin, valueMax, pixelMin, pixelMax, numTicks, tickSpacing = 64|
		var lines, p;
		var graphmin, graphmax, domain;
		var symScale, theScale, fifths;
		var offsetFromC;
        offsetFromC = spec.default.asInteger.mod(12);
 		symScale = spec.units.asSymbol;
		theScale = Scale.all[symScale];

		// fifths is the position of offsetFromC on the circle of fifths
		// abs(fifths) is the number of flats (-) or sharps (+)
		// in the key of offsetFromC.
		fifths = circFifths.indexOf(offsetFromC)-6;

		// Resolve the offset +6 as F# and -6 as Gb
		if (spec.default.asInteger == 6, { fifths = 6 }); // instead of -6

		if (fifths >= 0, { notes = sharpNotes }, { notes = flatNotes });

		graphmin = valueMin.round(1).asInteger;
		graphmax = valueMax.round(1).asInteger;

		lines = [];
		domain = (graphmin..graphmax);
		domain.do { | note, ix |
			var deg = (note-offsetFromC).mod(theScale.pitchesPerOctave).round(1).asInteger;
			if (theScale.degrees.includes(deg), {
				lines = lines.add( note );
			});
		};

		p = ();
		p['lines'] = lines;
		p['labels'] = lines.collect({ arg val; [val, this.formatLabel(val)] });
		^p
	}
}

ScaleWarp : LinearWarp {
	gridClass { ^ScaleGridLines }
}

I’ll just repeat the demo code here, with the scale of E-flat major (offset 3). The note scale can also go on the vertical axis, by just swapping x.grid to y.grid.

Warp.warps.add(\scale -> ScaleWarp);

// Grid.numTicks and Grid.tickSpacing will be ignored.
// Instead, you will need to specify a suitably dense scale
// for your axis length. Here are some examples:
Scale.all.put(\semitones, Scale((0..11), name: "Semitones"));
Scale.all.put(\majorTriad, Scale(#[0, 4, 7], name: "Major Triad"));
Scale.all.put(\octaves, Scale(#[0], name: "Octaves"));

// When creating the ControlSpec for an axis with the new warp "\scale",
// the minval and maxval arguments must be MIDI note numbers.
// You specify the desired Scale with "units:" (a Symbol),
// and the root note of the scale relative to C - as "default:" (any Integer)

x = ControlSpec.new(36, 72, \scale, default: 3, units: \major);
d = DrawGrid((700@250).asRect, x.grid, nil);

// generate a preview
~testView = d.preview;
  1. This would be very application-dependent, but immediately after drawing the grid, you can access the positions and texts of the newly drawn labels, like so, for the given example:
x.grid.getParams(36, 72).at('labels').postln;

This is probably easier to work with than trying to customize the formatLabel method of the ScaledGridLines class

Finally, many thanks for your tips on keyboards - I’ll have a look!

1 Like