I have a function which performs some Tasks running some Synths. I want to save the result of a call to my function as Audio, write now I am routing the Supercollider outputs into my DAW (Ardour) session, let DAW record and run the function. But I need all results of my function calls to have a synchronised start, what I am doing painfully write now by hand in my DAW, and it never get’s exact synchronized starting points. Is there a way to record the result a function-call directly to the disk? I think this might help having various audio-files and then load them at once into my DAW, to get their starting points all synced together.
Yes – well, rather, there’s a way to run multiple synced recordings, yes.
Do you want to run the tasks at the same time and record each task to a separate file?
Or, record one, then record another one, then another, while making sure that file starts exactly at the beginning of the task?
I’m happy to share specific code tricks to do this, but the structure of the code is a little bit different depending on the way you want to do it… give a bit more detail and I’ll send something concrete later.
hjh
I don’t have a need to run the tasks at the same time, perhaps recording each to a separate file one-at-time is more resource-friendly? ![]()
More important is that all files have exact same beginning, as the recording to the disk starts.
My goal is to be able to import them in my DAW (ardour) at the same time, and they are listed under each other, and are timely synced exact same beginning.
I hope I could express clearly what my goal is ![]()
Thank you for your help btw ![]()
Sure, no problem. It’s quite late where I live, though, so it’ll be after I get up tomorrow. ![]()
hjh
So… recording in SC takes two steps: preparation and the actual recording. Preparation can’t be synchronous because it involves memory allocation and file system access, neither of which are guaranteed to return quickly. This part is why s.record can’t be guaranteed to line up with other sounding events. Once it’s prepared, however, recording happens in a synth, which can be synchronized with other synths.
So to guarantee time alignment between the onset of some musical process and the recording synth, it’s necessary to split the recording process explicitly into the two stages, and run the musical process only after preparation is finished.
This code block packages it into a function.
Important: Getting clean sync between two synths requires sending the messages in advance with latency. The usage example at the bottom plays a pattern, which implicitly uses s.latency, and the ~syncRecord function by default uses the same latency. Those two operations occur at the same logical clock time with the same latency, so they will happen at the same time in the server = synced recording.
If you are playing a pattern with quant, then you would need to modify the function to schedule the s.makeBundle call.
(
~syncRecord = { |func, latency, bus, path,
headerFormat("wav"), sampleFormat("int16"), numChannels(2)|
var return = Event.new;
// 1. prepare
bus = bus ?? { Bus(\audio, 0, numChannels, Server.default) };
fork {
var s = bus.server;
var buf = Buffer.alloc(s, 1 << 17, numChannels, completionMessage: { |buf|
buf.writeMsg(path, headerFormat, sampleFormat, numFrames: 0,
leaveOpen: true
);
});
var recSynth;
s.sync;
// 2. run it
latency = latency ?? { s.latency };
s.makeBundle(latency, {
// "system_diskout_": see SystemSynthDefs
recSynth = Synth("system_diskout_" ++ numChannels, [
i_in: bus, i_bufNum: buf
], target: s.defaultGroup, addAction: \addAfter);
});
func.fork;
return.putAll((
buf: buf,
recSynth: recSynth
));
};
return
};
~stopRecord = { |spec|
fork {
spec[\recSynth].free;
0.5.wait;
spec[\buf].close;
spec[\buf].server.sync;
spec[\buf].free;
}
};
)
s.boot;
(
~recordSpec = ~syncRecord.({
p = Pbind(
\degree, Pseries(0, 1, 8),
\dur, 0.125,
\amp, 0.5
).play;
}, path: "~/demo-rec.wav".standardizePath);
)
~stopRecord.(~recordSpec);
It should be possible to call this function multiple times with different buses, to record several signals at the same time, in sync.
hjh
Thanks James, for this. There are some hip idioms in your code that I haven’t seen or used before.
It should be possible to call this function multiple times with different buses, to record several signals at the same time, in sync.
Is it enough to invoke the functions like this, or do they need to be wrapped in s.makeBundle to make them run in sync?:
(
syncRecord({ /* function 1 */ });
syncRecord({ /* function 2 */ });
syncRecord({ /* function 3 */ });
);
Hm, I see now that my statement was incorrect. The function needs to be restructured to open several buffers at the same time, .sync once, and then start all the recording synths in the same bundle. Sorry for the confusion.
A bit swamped now, hope you don’t mind if I leave that as the “exercise for the reader” that mathematicians sometimes invoke ![]()
hjh
Of course. Thanks for the clarification.
If sc can run all your processing simultaneously and if you don’t need to be able to layer recordings at different times, sync is basically free, meaning that all files will inherently be in sync. Recording straight to disk is very cheap cpu wise so if SC can run all it can almost certainly record all at the same time. This might not apply to your use case, just saying…
This isn’t exactly true. To record, you need to allocate a buffer – this grabs memory from the system, which is not time-bounded (it won’t take long but you have no guarantee of a maximum duration). Then you have to open the disk output file by b_write. Filesystem operations are also inherently time-unbounded.
If you start two prepare-for-record sequences at the same time, you have no guarantee that they will complete in the same amount of time – so if you sync the recording synth to the completion of this process, you have no guarantee that the recording synths will be in sync. There is no “sync is free” – setting up recording is inherently asynchronous (doubly so).
The only way that I know to control the time relationship between the recording synth and the audio-processing synths is to manage the process yourself: prepare several buffers all at once, s.sync once, and run all the recording synths in the same bundle.
Edit: Well, there are two different things here. One is syncing recording synths to each other, where it doesn’t matter how long ago the buffers were prepared as long as the recording synths’ s_new messages are in the same bundle. (If you don’t use a bundle, then you’d “probably” get sync on the local machine, but for a guarantee of it, put them in a bundle.) The other is the original question, which was syncing recording synths to the output of an audio process. For this, the recording synth and the audio process should be launched at the same logical time with the same latency. Then the two will start with sample-accurate sync.
hjh
Ok that sounds reasonable. I have to look at what I actually do in my code where I record 7 stereo busses ‘in sync’, I have never noticed any drift when I pull these into a DAW, but it could be the general jitter which is worsened in my case by the fact that I set latency = nil favoring fast response over more accurate timing (I tradeoff I know and accept) masks the small sync offset.
That’s fair – there’s “close enough” sync vs “sample accurate” sync. On a local machine, “close enough” should be within one control block, plus or minus, and that would be good enough for a lot of purposes.
hjh
Wouldn’t using a multibus fix the interbus offset/sampleoffset? Then a syndef which routes inputs to the multibus. But maybe no DAW can unpack this format to a number of stereo-busses?
Or… the recording SynthDef could unpack the channels into whatever configuration, and use multiple DiskOut objects – that’s a good idea
Or, multiple In.ar → DiskOut.ar pairs living together in a single SynthDef, and then they would be synced.
So, a couple of ways, then ![]()
hjh