Composition tool

I’m coming to SC with a traditional music background so am interested in working with midi notes and durations represented in a conventional way ( quarter notes, 16th notes etc. ), and doing sequencing and generative music, more so than synth design at the moment.

For midi notes, is there any support or extensions for representing them as note names, like C4, G5 etc rather than midi note numbers? I’d like to create sequences of midi notes, but the note names are more intuitive to me than the midi note numbers.

For durations with TempoClock, I see that numbers are used to represent the subdivisions but wondering if there’s any support for something like the Max Time Format syntax in Max/Msp where you can use abbreviations like 4n ( quarter note ) or 16n ( 16th note ) when creating rhythm sequences ( Time Value Syntax | Cycling '74 Documentation ). That is pretty intuitive.

And generally, am wondering if there are code examples of people using SC as a compositional tool in the sense of creating a lot of midi note and chord sequences, and if there are ways of making that less cumbersome. So far, most of the code examples I’ve seen focus on synth/sound design. Thanks for any tips!

The SC Quark wslib has a few tools for this:
GitHub - supercollider-quarks/wslib: various language additions, GUI classes and moreMain features: SimpleMIDIFile, SVG, RoundButton, SmoothSlider, AutoBackup

See the midiname help file, which is a .html file so you can open it in a browser.
Here’s a quote from it:

midiname
notenumber to notename and backwards conversion
also includes convenience methods for frequency (cps)
midivoicetype can be used to test if a certain note
or frequency is included in a certain voice type
voice types are currently in english and dutch
note names are as found in Logic Pro. Sibelius and some other brands
use other: C4 in Logic = C3 in Sibelius

SimpleNumber-midiname ( sign )
convert a midi notenumber to a notename
adds a ‘cents’ method

64.midiname; // → “E3”

SimpleNumber-cpsname ( sign )
convert frequency to a notename

440.cpsname; // → “A3”

String-namemidi ( cents )
convert a notename to a midi notenumber

“G2”.namemidi; // → 55

String-namecps ( cents )
convert a notename to a frequency

“A2”.namecps; // → 220

String-namename ( cents, sign )
convert a notename to a standardized notename

“A#2”.namename; // → “Bb2”

1 Like

Maybe you could use the SC preprocessor to substitute abbreviations for other code:
https://docs.supercollider.online/Classes/Interpreter.html#-preProcessor

Best,
Paul

It’s less slick than a special syntax for rhythmic note values, but one way to write in terms of note values and convert them to beats is to divide the note values into 4.

Pbind(
    \degree, Pseq([0, 7, 6, 4, 5, 6, 7]),
    \dur, 4 / Pseq([2, 2, 4, 8, 8, 4, 4])
).play;

… where the dur Pseq specifies half, quarter and 8th notes, and the 4 / divides a whole note’s duration by these.

A dotted 8th would be 8/1.5 then; if you assign d = 1.5 then you could write 8/d which is not awful for a dotted syntax. (4 / (8/d) = 4d / 8 = d/2 = 3/4 which is correct for a dotted eighth.)

Or t = 2/3 for triplet (4/t for a single quarter note triplet value) etc… there aren’t enough letters for Ferneyhough rhythms but you might not need all that :wink:

WRT TXMod’s preprocessor suggestion, the SC Book ed. 2 has a new chapter about the preprocessor (by me), worth a look (though the book is rather expensive).

hjh

2 Likes

Thanks @TXMod and @jamshark70 for the suggestions! While I’ve found SC to be a lot more elegant than Max in many ways, it appears Max does seem to have a few simpler ways of doing things for working with familiar musical concepts ( would be nice if SC had something like the Max bach/cage packages for notation too ).

You may wish to refer to the following post:

The following is my quark (a work in progress):

This project is still in the testing stage. I need to correct a note‑naming error concerning microtonality and enharmonic spelling, and I also plan to add further functionality. Nevertheless, it already supports some musical composition and notation.

1 Like

You might want to check out the Panola Quark…

for myself I wrote methods that let me use 1-indexed degrees along with note names aoctaves and scales so 1.dm(\c, 4) or [1, 3, 5].dm(\c, 4), Both of these methods further accept scales, so [1, 4].df(\c, \lydian) will give a tritone… One of the beauties of Supercollider is that its straightforward to ‘roll your own’ for this kind of thing!. I also wrote a little method that lets me write strings “q q q qe Q QQ”.beats to mean: quarter quarter dotted-quarter and quarter-and-half-triplet (result [1, 1, 1, 1.5, 1/3, 2/3].

I have been working on a little system to handle diatonic spelling and addition of intervals - so that if you add a minor third to c you get e-flat and a major third to c# gives e-double-sharp. and major third plus major second gives augmented fourth but its not quite working yet!

1 Like

I’d second semiquaver’s recommendation for the Panola quark (“pattern notation language” or something like that). SC’s syntax is extensible using SC language itself; just because standard SC syntax doesn’t support a rhythm notation that you’re accustomed to does not mean that it’s impossible to introduce new syntax that might be more appealing to you.

Prko has mentioned his own project to bridge SC and notation, and that’s not the only one – there’s a SuperFOMUS extension, for instance. Maybe someone has done LilyPond code generation? That one I forget.

A problem that SC, Max and Pd share in common is that a lot of the cool stuff is in extension packages, but it’s hard to know which package is relevant for your needs – wait and see, something might pop up.

hjh

as a composition tool i would recommend opusmodus.

2 Likes

I second this. In my view, a notation system developed within SC itself would be more intuitive for the community than relying on LilyPond, Guido, or ABC.

@eboats

Just an example: It provides note names, rhythm notation, and chord/scale helpers while staying within SC’s pattern system.

(
~noteToPC = (\c:0,\d:2,\e:4,\f:5,\g:7,\a:9,\b:11,\C:0,\D:2,\E:4,\F:5,\G:7,\A:9,\B:11);


~n = { |noteSymbol|
	var s = noteSymbol.asString, acc = 0, i = 1, note = s[0].asSymbol;
	~noteToPC[note] ?? { Error("Invalid note: %".format(note)).throw };
	while({ i < s.size and: { s[i].isAlpha or: { "#+sbfqh".includes(s[i].asString) } } }, {
		[\c,\C,\d,\D,\e,\E,\f,\F,\g,\G,\a,\A,\b,\B].includes(s[i].asSymbol).if({i=s.size},{
			case
				{s[i]==$#||{s[i]==$s}}{acc=acc+1}
				{s[i]==$b||{s[i]==$f}}{acc=acc-1}
				{s[i]==$q}{acc=acc+0.5}  // Quarter sharp
				{s[i]==$h}{acc=acc-0.5}; // Half flat
			i=i+1
		})
	});
	i=1; while({i<s.size and:{s[i].isAlpha or:{"#+sbfqh".includes(s[i].asString)}}},{i=i+1});
	~noteToPC[note] + acc + ((s[i..].asInteger + 1) * 12)
};

~nt = ~n;
~notes = { |xs| xs.collect(~n.(_)) };

~dur = (\w:4,\h:2,\q:1,\e:0.5,\s:0.25,\t:0.125,\wd:6,\hd:3,\qd:1.5,\ed:0.75,\sd:0.375,\qt:2/3,\et:1/3,\st:1/6,\hq:3,\qe:1.25,\eq:0.75);

~rhythmDict = ('1n':4,'2n':2,'4n':1,'8n':0.5,'16n':0.25,'32n':0.125,'2nd':3,'4nd':1.5,'8nd':0.75,'16nd':0.375,'4nt':2/3,'8nt':1/3,'16nt':1/6);

~rhythm = { |notation| ~rhythmDict[notation.asSymbol] };
~r = ~rhythm;
~rhythms = { |xs| xs.collect { |x| x.isNumber.if({ x }, { ~rhythm.(x) }) } };

~chords = (
	\major:[0,4,7],\minor:[0,3,7],\dim:[0,3,6],\aug:[0,4,8],
	\maj7:[0,4,7,11],\min7:[0,3,7,10],\dom7:[0,4,7,10],
	\dim7:[0,3,6,9],\hdim7:[0,3,6,10],
	\maj9:[0,4,7,11,14],\min9:[0,3,7,10,14],
	\sus2:[0,2,7],\sus4:[0,5,7],\add9:[0,4,7,14],
	\6:[0,4,7,9],\m6:[0,3,7,9],
	// Quartertone chords
	\neutral:[0,3.5,7],  // Neutral third
	\zalzal:[0,3.5,6.5]  //  
);

~chord = { |root, type=\major|
	~chords[type] + root.isNumber.if(root, ~n.(root))
};

~maqams = (
	\rast:[0,2,3.5,5,7,9,10.5],  // C D Eq F G A Bh
	\bayati:[0,1.5,3,5,7,8,10],   // D Eh F G A Bb C
	\saba:[0,1.5,3,4,6,8,10],     // D Eh F Gb A Bb C
	\sikah:[0,1.5,3.5,5.5,7,9.5,10.5], // Eh F Gq Aq Bh Cq D
	\hijaz:[0,1,4,5,7,8.5,10]     // D Eb F# G A Bh C
);

~maqam = { |root, type=\rast, octaves=1|
	var r = root.isNumber.if(root, ~n.(root));
	octaves.collect{|o| ~maqams[type] + r + (o*12)}.flat
};

~scales = (\major:[0,2,4,5,7,9,11],\minor:[0,2,3,5,7,8,10],\harmonic:[0,2,3,5,7,8,11],\melodic:[0,2,3,5,7,9,11],\dorian:[0,2,3,5,7,9,10],\phrygian:[0,1,3,5,7,8,10],\lydian:[0,2,4,6,7,9,11],\mixolydian:[0,2,4,5,7,9,10],\locrian:[0,1,3,5,6,8,10],\pentatonic:[0,2,4,7,9],\blues:[0,3,5,6,7,10],\chromatic:(0..11),\wholeTone:[0,2,4,6,8,10],\quartertone:(0,0.5..11.5));

~scale = { |root, type=\major, octaves=1|
	var r = root.isNumber.if(root, ~n.(root));
	octaves.collect{|o| ~scales[type] + r + (o*12)}.flat
};

~mtof = { |m| m.midicps };
~ftom = { |f| f.cpsmidi };
~noteInfo = { |note|
	var m = ~n.(note);
	postf("% = MIDI %, % Hz\n", note, m, m.midicps.round(0.01));
};

~melody = { |notes, rhythms, repeats = 1|
	Pbind(
		\midinote, Pseq(~notes.(notes), repeats),
		\dur, Pseq(~rhythms.(rhythms), repeats)
	)
};

~progression = { |chordList, durations|
	var midiChords = chordList.collect { |item|
		if(item.isArray) {
			~chord.(item[0], item[1])
		} {
			if(item.isNumber) { [item] } { [~n.(item)] }
		}
	};
	Pbind(
		\midinote, Pseq(midiChords, inf),
		\dur, Pseq(~rhythms.(durations), inf)
	)
};

~iv_make = { |st, number=nil, quality=nil, name=nil, cents=nil, ratio=nil|
	(type:\interval, semitones:st.asFloat, number:number, quality:quality, name:name, cents:cents, ratio:ratio)
};

~iv_base = [0, 2, 4, 5, 7, 9, 11];
~iv_isPerfectClass = { |n| [1, 4, 5].includes(((n - 1) % 7) + 1) };

~strip = { |str|
	var s = str.asString, i = 0, j = s.size - 1;
	var isWS = { |ch| ch.isSpace or: { ch == $\t or: { ch == $\n or: { ch == $\r }}} };
	while({ i <= j and: { isWS.(s[i]) } }, { i = i + 1 });
	while({ j >= i and: { isWS.(s[j]) } }, { j = j - 1 });
	(i > j).if({ "" }, { s.copyRange(i, j) })
};

~iv_parse = { |token|
	var s = ~strip.(token);
	var i = 0, prefix = "", numStr, n, k, oct, base, delta, st;
	var originalPrefix = "";
	while({ i < s.size and: { s[i].isAlpha } }, {
		originalPrefix = originalPrefix ++ s[i];
		i = i + 1;
	});
	if(originalPrefix.size == 1) {
		prefix = case
			{ originalPrefix == "M" } { "mj" }
			{ originalPrefix == "P" } { "p" }
			{ originalPrefix == "A" } { "a" }
			{ originalPrefix == "D" } { "d" }
			{ originalPrefix == "m" } { "m" }
			{ true } { originalPrefix.toLower };
	} {
		prefix = originalPrefix.toLower;
		prefix = prefix.replace("major", "mj").replace("maj", "mj");
		prefix = prefix.replace("minor", "m").replace("min", "m");
		prefix = prefix.replace("perfect", "p").replace("perf", "p");
		prefix = prefix.replace("augmented", "a").replace("aug", "a");
		prefix = prefix.replace("diminished", "d").replace("dim", "d");
	};
	numStr = s.copyRange(i, s.size - 1);
	if(numStr.isEmpty) {
		Error("Interval needs a number: %".format(s)).throw
	};
	n = numStr.asInteger;
	if(n < 1) {
		Error("Interval number must be positive: %".format(s)).throw
	};
	k = ((n - 1) % 7) + 1;
	oct = ((n - 1) div: 7);
	base = ~iv_base[k - 1] + (12 * oct);
	delta = ~iv_qualityDelta.(prefix, ~iv_isPerfectClass.(n));
	st = base + delta;
	~iv_make.(st, n, prefix, token)
};

~iv_qualityDelta = { |qual, perfectClass|
	var q = qual;
	var a = q.findAll("a").size;
	var d = q.findAll("d").size;
	var res = 0;
	if(perfectClass) {
		if(q == "p") { res = 0 };
		if(a > 0) { res = a };
		if(d > 0) { res = -1 * d };
	} {
		if(q == "mj") { res = 0 };
		if(q == "m") { res = -1 };
		if(a > 0) { res = a };
		if(d > 0) { res = -1 - d };
	};
	res
};

~iv_fromSemitones = { |st| ~iv_make.(st) };
~iv_fromRatio = { |num, den = 1|
	var r = num.asFloat / den.asFloat;
	var cents = 1200 * (r.log / 2.log);
	var st = cents / 100.0;
	~iv_make.(st, nil, nil, nil, cents, (num: num, den: den))
};

~iv_add = { |a, b| ~iv_make.(a.semitones + b.semitones) };
~iv_sub = { |a, b| ~iv_make.(a.semitones - b.semitones) };
~iv_neg = { |a| ~iv_make.(-1 * a.semitones) };
~iv_invert = { |a|
	var st = a.semitones % 12;
	if(st < 0) { st = st + 12 };
	st = (st == 0).if({ 0 }, { 12 - st });
	~iv_make.(st)
};

~iv_apply = { |iv, note|
	var st = iv.semitones;
	var m = note.isNumber.if({ note }, { ~n.(note) });
	m + st
};

~iv_applyAll = { |iv, xs|
	xs.collect { |x|
		x.isArray.if({ ~iv_applyAll.(iv, x) }, { ~iv_apply.(iv, x) })
	}
};

~iv_between = { |a, b|
	var ma = a.isNumber.if({ a }, { ~n.(a) });
	var mb = b.isNumber.if({ b }, { ~n.(b) });
	~iv_fromSemitones.(mb - ma)
};
)

////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Examples
////////////////////////////////////////////////////////////////////////////////////////////////////////////

(
Pbind(
	\midinote, Pseq(~notes.(['c4','cq4','d4','eh4','e4','f4','e4','eh4','d4','cq4','c4']), 1),
	\dur, 0.25
).play;
)

(
Pbind(
	\midinote, Pseq(~maqam.('c4', \rast, 2), 1),
	\dur, Pseq(~rhythms.(['8n','8n','8n','8n','4n','8n','8n','2n']), inf)
).play;
)


(
Pbind(
	\midinote, Pseq(~maqam.('d4', \hijaz, 1) ++ ~maqam.('d5', \hijaz, 1).reverse, 1),
	\dur, Prand([0.125, 0.25], inf),
	\legato, 0.9
).play;
)


(
Pbind(
	\midinote, Pseq([
		~chord.('c3', \neutral),
		~chord.('f3', \neutral),
		~chord.('g3', \major),
		~chord.('c3', \neutral)
	], 2),
	\dur, 1,
	\strum, 0.05
).play;
)


(
Pbind(
	\midinote, Pseq([60, 60.5, 62, 63.5, 65, 63.5, 62, 60.5, 60], inf),
	\dur, Pseq(~rhythms.(['8n','8n','8n','8n','4n','8n','8n','8n','2n']), inf),
	\amp, 0.2,
	\legato, 0.95
).play;
)

(
Ppar([
	Pbind(
		\midinote, Pseq(~maqam.('ch6', \rast, 1), inf),
		\dur, 0.5,
		\amp, 0.15
	),
	Pbind(
		\midinote, Prand(~scale.('c5', \pentatonic), inf),
		\dur, Pseq([0.125, 0.125, 0.25], inf),
		\amp, 0.1
	)
]).play;
)
1 Like

@jamshark70 Regarding the “complex” rhythm comment - I actually have that working too (this one below is 10+ years old hehe). The complex rhythm generation can output duration arrays that feed directly into these patterns. But that’s a separate beast entirely. This is meant for the typical case where you want to write \c4 instead of 60.

1 Like

Another way to do that. I think the challenge there is to combine the power of the representation with convenient syntax/notation.

(
~makeRhythm = { |spec|
    spec.collect { |item|
        case
        { item.isNumber } { item }
        { item == \w } { 4 }
        { item == \h } { 2 }
        { item == \q } { 1 }
        { item == \e } { 0.5 }
        { item == \s } { 0.25 }
        { item == \t32 } { 0.125 }  
        
        // Simple subdivision (just divide equally)
        { item.isArray and: { item[0].isInteger } } {
            var numNotes = item[0];
            var content = item[1..];
            content.collect { |c| 
                ~makeRhythm.([c]).sum / numNotes 
            }
        }
        
        // Tuplet with ratio
        { item[0] == \t } {
            var beats = item[1];
            var time = item[2];
            var content = item[3..];
            var unit = time.asFloat / beats; 
            ~makeRhythm.(content).collect(_ * unit)  
        }
        { true } { 1 }
    }.flat
};

~rhythm1 = ~makeRhythm.([
    \q,                                    
    [\t, 5, 4, \e, \e, [\t, 3, 2, \s, \s, \s], \e, \e],
    \h                                     
]);
"Original: %".format(~rhythm1.round(0.001)).postln;

~rhythm2 = ~makeRhythm.([
    \q,
    [\t, 5, 4, 
        \e, 
        \e, 
        [3, \s, \s, \s],  // subdivision into 3
        \e, 
        \e
    ],
    \h
]);
"Clearer: %".format(~rhythm2.round(0.001)).postln;

~rhythm3 = ~makeRhythm.([
    \q,
    [\t, 5, 4, \e, \e, \e, \e, \e],  //  quintuplet
    [\t, 7, 4, \s, \s, \s, \s, \s, \s, \s],  // Septuplet
    \h
]);
"Explicit: %".format(~rhythm3.round(0.001)).postln;


Ppar([
    Pbind(
        \degree, Pseq([0, 2, 5], inf),
        \dur, Pseq(~rhythm1, 2),
        \amp, 0.3
    ),
    Pbind(
        \degree, Pseq([9, 11, 13], inf),
		\dur, Pseq(~rhythm2, 2),
        \amp, 0.3,
        \pan, 0.5
    )
]).play;


)