Recording different sources simultaneously?

Hi,

the only option for recording in SC I know of is recording the entire server output, but I need to record multiple sources simultaneously inside of the same patch (in this case, 3 Ndefs) for a project. I won’t go into the details, but it’s really not the same if I record them separately and then edit them together in a DAW, I need them to start playing at the same time, and I need to record each one on its own track.

How can I do this?
Thanks!

You can send the sources to different outs and record multichannel, e.g.:

// you'd have to prepare

(
s.options.numOutputBusChannels = 4;
s.recChannels = 4;
s.reboot;
)


// record with gui

s.makeGui

(
Ndef(\a, { Out.ar(0, SinOsc.ar([1, 2] * 500, 0, 0.1)) }).play;
Ndef(\b, { Out.ar(2, Saw.ar([1, 2] * 500, 0.1)) }).play;
s.scope(4);
)

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

3 Likes

wow, really useful. thanks!

Additionally to these methods beeing mentioned, you could also consider to route the output of the different busses to a DAW by for example installing Jack and make a multichannel recording directly in the DAW. You would also be able to send audio and midi back and forth between DAW and SC. But for MIDI im using loopMIDI on a windows machine.

1 Like