I'm not sure exactly what you want, given that you've looked at the examples. The odd bit is the "total separation" requirement; usually a score needs to make some assumptions about what parameters are relevant to what instruments - although there are enough introspective methods in SynthDef that a program could make educated guesses.
But the basic schematic is pretty standard: SynthDef defines instruments, Collection and its subclasses store data, Routine and other classes can interpret data structures in scheduled time to make music.
At the bottom I'm pasting some boilerplate code for a very simple c-like approach to such a structure, using SynthDef, Routine, and Array. Which instrument to use is arbitrarily chosen at note generation time, and the "score" is instrument-agnostic.
However, the idiomatic approach in SC is to use Patterns and Events, and the Pbind class in particular. Personally I find these a little restrictive and verbose, but they'll certainly do what you ask. Check out the "Streams-Patterns-Events" series of helpfiles.
And various people have written third-party extensions like Instr and Voicer to accommodate their own flavors of the score-instrument model. Check out the Quarks listing or consider rolling your own?
s = Server.local.boot;
s.waitForBoot{ Routine {
/// in a "real" patch, i'd make these local variables,
/// but in testing its convenient to use environment variables.
// var inst, tclock, score, playr, switchr;
// the current instrument
~inst = \ding;
// a fast TempoClock
~tclock = TempoClock.new(8);
// two instruments that take the same arguments
SynthDef.new(\ding, {
arg dur=0.2, hz=880, out=0, level=0.25, pan=0.0;
var snd;
var amp = EnvGen.ar(Env.perc, doneAction:2, timeScale:dur);
snd = SinOsc.ar(hz) * amp * level;
Out.ar(out, Pan2.ar(snd, pan));
}).send(s);
SynthDef.new(\tick, {
arg dur=0.1, hz=880, out=0, level=0.25, pan=0.0;
var snd;
var amp = EnvGen.ar(Env.perc, doneAction:2, timeScale:dur);
snd = LPF.ar(WhiteNoise.ar, hz) * amp * level;
Out.ar(out, Pan2.ar(snd, pan));
}).send(s);
s.sync;
// the "score" is just a nested array of argument values
// there are also many kinds of associative collections in SC if you prefer
~score = [
// each entry:
// midi note offset, note duration in seconds, wait time in beats
[0, 0.4, 2],
[0, 0.4, 1],
[7, 0.2, 1],
[0, 0.2, 1],
[7, 0.15, 1],
[10, 0.5, 2],
[7, 0.1, 1],
[2, 0.3, 1]
];
// a routine that plays the score, not knowing which instrument is the target
~playr = Routine { var note, hz; inf.do({ arg i;
// get the next note
note = ~score.wrapAt(i);
// interpret scale degree as MIDI note plus offset
hz = (note[0] + 60).midicps;
// play the note
Synth.new(~inst, [\hz, hz, \dur, note[1] ], s);
// wait
note[2].wait;
}); }.play(~tclock);
// a routine that randomly switches instruments
~switchr = Routine { var note, hz; inf.do({ arg i;
if(0.2.coin, {
if(~inst == \ding, {
~inst = \tick;
}, {
~inst = \ding;
});
~inst.postln;
});
// wait
1.wait;
}); }.play(~tclock);
}.play; };