Of course it’s not limited to that… it’s only the convenience method that is limited.
To do this right, there’s a bit more fine print than you expect, so I’ll just post the full function. This is adapted from a utility function in my live coding setup.
~recordBuses
takes these arguments:
buses
: An Array of Bus objects. I’m assuming you have already allocated buses for the various sources. If you didn’t, you can create them:Bus(\audio, busNumber, numChannels, server)
. (Each recorder needs to know the starting bus index and number of channels. We already have an object to represent that – Bus – so, instead of requiring a more complex data structure, I’m just using the existing object.)path
: A template for the file paths, e.g.,"/home/myUser/Recordings/23-0412/bus%.wav"
. The ‘%’ (there should be only one) will be filled in with the bus index.recHeaderFormat
(default “wav”),recSampleFormat
(default “float”): As in Server and Recorder.recBufSize
(default 131072): If you want a bigger or smaller buffer, you can override. This default should be OK.latency
(default 0.2): Syncing the recording start requires this. Again, just use the default.
~recordBuses
returns an array with one Recorder object for each Bus. You will need these to stop recording later (~stopRecording
function).
So, e.g., if I do r = ~recordBuses.value([Bus(\audio, 4, 2, s), Bus(\audio, 6, 2, s)], Platform.recordingsDir +/+ "test/try/%.wav");
then, under my recording directory, I get directories test/try
and within this, 0004.wav and 0006.wav.
(If it looks a little inconvenient to write Bus(…) multiple times, note that this is not necessary if you used Bus.audio
earlier to get separate channels for your sources – if you already have bus objects, just reuse them here.)
(
// IN:
// 'buses' should be an array of bus objects
// 'path' should be "/path/to/recordings/name%.wav"
// and the '%' will be filled in with the bus number
// OUT:
// array of Recorder objects -- use these to stop recording
~recordBuses = { |buses, path, recHeaderFormat = "wav", recSampleFormat = "float", recBufSize = 131072, latency = 0.2|
var dir = path.dirname;
var recorders, servers = Set.new;
var digits = 1;
var pad = { |num, digits|
num = num.asString;
if(num.size < digits) {
String.fill(digits - num.size, $0) ++ num
} {
num
}
};
if(File.exists(dir)) {
if(File.type(dir) != \directory) {
Error(
"Path '%' exists but is not a directory"
.format(dir)
).throw;
}
} {
// didn't exist
// throws error upon failure (good)
File.mkdir(dir);
};
recorders = buses.collect { |bus|
digits = max(digits, bus.server.options.numAudioBusChannels.log10.ceil.asInteger);
servers.add(bus.server);
Recorder(bus.server)
.recHeaderFormat_(recHeaderFormat)
.recSampleFormat_(recSampleFormat)
.numChannels_(bus.numChannels)
.recBufSize_(recBufSize);
};
fork {
recorders.do { |rec, i|
rec.prepareForRecord(
path.format(pad.value(buses[i].index, digits)),
buses[i].numChannels // yes, must pass again, hm
);
};
// normally this will be just one server,
// but let's handle the unusual case too
servers.do { |server| server.sync };
servers.do { |server|
server.makeBundle(latency, {
recorders.do { |rec, i|
if(rec.server == server) {
rec.record(bus: buses[i]);
};
};
});
};
};
recorders
};
~stopRecording = { |recorders|
recorders.do { |rec| rec.stopRecording };
};
)
Little example:
a = { Out.ar(4, DC.ar([1, 2, 3, 4])) }.play;
~recorders = ~recordBuses.value(
[Bus(\audio, 4, 2, s), Bus(\audio, 6, 2, s)],
Platform.recordingsDir +/+ "test/try/%.wav"
);
~stopRecording.value(~recorders);
(
[
"/home/[redacted]/share/SC/Recordings/test/try/0004.wav",
"/home/[redacted]/share/SC/Recordings/test/try/0006.wav"
].do { |path|
var f = SoundFile.openRead(path);
var data;
if(f.notNil) {
protect {
data = Signal.newClear(10);
f.readData(data);
data.postln;
} { f.close };
};
};
)
Signal[ 1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0 ] // correct
Signal[ 3.0, 4.0, 3.0, 4.0, 3.0, 4.0, 3.0, 4.0, 3.0, 4.0 ] // correct
hjh