multi-track recording in SuperDirt
when making music in TidalCycles and other SuperDirt frontends, it is easy enough to record what is currently being output to the audio device by running s.record
. but this mixes down all the orbits to stereo (or whatever number of output channels you happen to be using).
what if you want to record each orbit separately, a.k.a. stems? one approach is to use separate audio channels and route audio from SuperDirt to separate tracks in a DAW, which then handles the recording and mixdown. the routing could be done with tools such as BlackHole (macOS), JACK or PipeWire (Linux), or VB-CABLE (Windows).
personally, the DAW approach is too much hassle for me. i would like to accommodate situations where i am just noodling in Tidal, suddenly realize i need to record this, and just run a simple line of code. SuperCollider is a very powerful audio processing environment, so this is of course possible to implement, but it is not entirely obvious how. the following code is extracted from my startup file and hopefully commented well enough to explain the workings.
(
// create a data structure (Event) to hold our orbit related things
~orbits = ();
// define the number of orbits
~orbits.num = 12;
// define the number of channels per orbit (only tested with 2)
~orbits.channels = 2;
// define the hardware output buses
// here, each orbit goes to bus 0 (the first output)
~orbits.outs = 0 ! ~orbits.num;
// placeholders for the buses and Recorder object we will create later
~orbits.orbitBuses = nil;
~orbits.recBus = nil;
~orbits.recorder = nil;
// define shortcut methods to start, stop and check the status of recording
~orbits.record = { |self|
self.recorder.record(
bus: ~orbits.recBus,
numChannels: ~orbits.recBus.numChannels
);
};
~orbits.stopRecording = { |self|
self.recorder.stopRecording;
};
~orbits.isRecording = { |self|
self.recorder.isRecording;
};
// set some server options
// see https://tidalcycles.org/docs/getting-started/tidal_start#start-supercollider-and-superdirt
s.options.numBuffers = 1024 * 256;
s.options.memSize = 8192 * 32;
s.options.numWireBufs = 64;
s.options.maxNodes = 1024 * 32;
// boot the server
s.waitForBoot {
// create an internal audio bus for each orbit
~orbits.orbitBuses = ~orbits.num.collect { Bus.audio(s, ~orbits.channels) };
// create a big bus with all the channels for recording
~orbits.recBus = Bus.audio(s, ~orbits.num * ~orbits.channels);
// create the Recorder object
~orbits.recorder = Recorder(s);
// start SuperDirt, passing our internal buses
~dirt = SuperDirt(~orbits.channels, s);
~dirt.start(outBusses: ~orbits.orbitBuses);
// wait for SuperDirt startup
s.sync;
// load samples
~dirt.loadSoundFiles;
// create a background synth for routing audio
// i used Ndef for easy editing, but it could just as well be a plain Synth
Ndef(\orbitsRouter, {
var ins;
// read audio from each orbit
ins = ~orbits.orbitBuses.collect { |o|
In.ar(o, ~orbits.channels);
};
// write to the hardware outputs
~orbits.outs.do { |o, i|
Out.ar(o, ins[i]);
};
// also write to our big recording bus
Out.ar(~orbits.recBus, ins.flatten);
DC.ar(0);
}).play;
};
)
with this, i can use the methods
~orbits.record;
~orbits.stopRecording;
~orbits.isRecording;
at any time!
the output of this recorder is a multi-channel file containing each channel of each orbit. i found this to be the easiest way to get everything recorded in sync. however, a lot of audio software does not play nice with such files, so i also use this Bash script called multi2stereo
to split the files to separate stereo files:
#!/bin/bash
file=$1
if [ ! -n "$file" ]; then
echo "usage: $(basename "$0") <multichannel-file>"
exit 1
fi
num_channels=$(soxi -c "$file")
echo "channels: $num_channels"
if [[ $((num_channels % 2)) -ne 0 ]]; then
echo 'expected even number of channels'
exit 1
fi
filename=$(basename "$file")
file_base=${filename%.*}
file_ext=${filename#$file_base}
num_pairs=$((num_channels / 2))
num_digits=${#num_pairs}
for i in $(seq 0 "$((num_pairs - 1))"); do
left=$((2*i + 1))
right=$((left + 1))
pair_num=$(printf "%0${num_digits}d-%0${num_digits}d" "$left" "$right")
pair_file="$(dirname "$file")/${file_base}_${pair_num}${file_ext}"
if [ -e "$pair_file" ]; then
echo "$pair_file already exists"
else
(set -x; sox "$file" "$pair_file" remix "$left" "$right")
fi
done
the script uses the soxi
and sox
command line tools, so SoX must be installed.
for example, say i have a recording called SC_250911_182657.wav
. i run
multi2stereo SC_250911_182657.wav
and it outputs the stereo files:
SC_250911_182657_01-02.wav
SC_250911_182657_03-04.wav
SC_250911_182657_05-06.wav
SC_250911_182657_07-08.wav
SC_250911_182657_09-10.wav
SC_250911_182657_11-12.wav
SC_250911_182657_13-14.wav
SC_250911_182657_15-16.wav
SC_250911_182657_17-18.wav
SC_250911_182657_19-20.wav
SC_250911_182657_21-22.wav
SC_250911_182657_23-24.wav
ready to use in pretty much any audio software!
even if only some of the orbits have audio, this will produce files for the silent ones too. an idea for future improvement might be to detect this and skip creating silent files.