In the last post I described how I used Supercollider to model Modular Synthesis. In this post I will describe how I communicate with Supercollider and create music.
The main purpose for me is to write full pieces of music. Therefore I want to have a concise DSL language for interacting with Supercollider where I can play and interact with the Modular Synthesis instruments. The language of choice, currently, is Scala. Early in the composition process I want to have a more interactive way where I can try out different ideas. There I use often Midi to initiate playback. Later in the process I want to write a whole piece of music with many channels and often up to 20 minutes long. I will describe these high level ways of working in future posts.
Supercollider have two parts. The server part is where synthesis is performed. To communicate with the server you usually use a protocol called OSC. It stands for Open Sound Control and can be described as a more generic version of MIDI. In Supercollider the transport is either via UDP or TCP. OSC is a generic communication protocol where you have both simple messages and what is called bundles. Bundles have a timestamp which means that OSC, in comparison to MIDI, have a built in sequencer. Supercollider has a number of messages that it responds too that are described in the Server Command Reference. To communicate with Supercollider via OSC I use the Scala library ScalaOSC.
There is code that is able to generate OSC messages to play the modular Instruments. Each Instrument has its own implementation which means that you get type safe code for each Instrument.
SynthDef(\lineControl, {
arg dur = 1, startValue = 1, endValue = 1, out = 0;
var lineEnv;
lineEnv = Line.kr(start: startValue,
end: endValue,
dur: dur, doneAction:2);
Out.kr(out, lineEnv);
}).add;
class LineControl extends ControlInstrument {
val instrumentName: String = "lineControl"
var startValue: Double = _
var endValue: Double = _
def control(startValue: Double, endValue: Double): SelfType = {
this.startValue = startValue
this.endValue = endValue
self()
}
override def internalBuild(startTime: Double,
duration: Double): Seq[Any] =
Seq("startValue", startValue, "endValue", endValue)
}
Above we see how the earlier described lineControl
instrument is implemented in the Scala client code. We need to say how we should send the startValue
and endValue
. The dur
and out bus
is handed in a generic way.
We saw in the previous post that in Supercollider communication between Instruments are handled via buses. In the Scala code you pass instances of Instruments as arguments instead. Behind the scenes the instruments are assigned buses and OSC messages are generated before the main instrument are generated. The OSC messages for all instruments in the graph is put together as one OSC bundle and sent to Supercollider with a common timestamp.
class SineOsc extends AudioInstrument {
type SelfType = SineOsc
def self(): SelfType = this
val instrumentName: String = "sineOsc"
var ampBus: ControlInstrument = _
var freqBus: ControlInstrument = _
}
Buses in Supercollider are either control buses or audio buses. There are external audio buses that are correspond to the actual audio channels that Supercollider is playing sound on. Usually you have two. One left and one right. Supercollider also have a number of internal audio buses that are used to route audio internally. Control buses are used to route control signals. There are a fixed number of buses that are allocated when the server starts.
There is code in the Scala client that can allocate buses. You specify start and end-time and the allocator knows which buses are free during that time period and allocates them for you. That way we can reuse buses once they are not used in any instruments.
Another important thing to get right is how the instruments are organised. When you play a synth in Supercollider it is always part of what is called a group. You also specify how the synth should be added to the group. In our case it’s important that the control instruments are earlier in the group then the audio instruments because this is the order in which they will be generated by Supercollider.
The Scala client code results in a fluent and type safe API that makes it easy to write tight code to execute complex modular synths.
sineOsc(ampBus = staticControl(1), freqBus = staticControl(440))
.addAction(TAIL_ACTION)
panning(inBus = audio, panBus = lineControl(-1, 1))
.addAction(TAIL_ACTION)
.withNrOfChannels(2)
In the above example we first create a sine oscillator where the amplitude is 1 and the frequency is 440 hertz. The second example is a panning where you supply the audio instrument and the panning is a line between -1 and 1. Both of these have TAIL_ACTION which means that they will be put last in the group. The panning also specify that it needs two channels (stereo).
The source code for the Scala client on GitHub is organised in two libraries. soundming-tools and soundming-modular.
Here is an example of a finished piece. It is called Concrete Music 9. It uses another abstraction called a SynthPlayer
that makes it even easier to write fluent and tight code.