Processing a buffer in and out with NRT

Hello

I’m trying to do a few processes offline, quickly. So I am trying NRT. I manage to find a few examples but this is puzzling me. In the following code I’m reading from a buffer with PlayBuf and writing using BufWr… yet I cannot get anything else than the first sample of the output buffer to be updated.

Any NRT specialist with wisdom here? In the end, I’d like to batch process many buffers in a row… and this would be a great way. The processes I’m planning to use are custom \b_xxx methods and that would be ace. I know I can do it in the language but it is slower.

Any pointer welcome

pa

(
fork {
	var server, source, resultbuf, resultpath, oscpath, score, cond, sf, data, job;

	server = Server(\nrt,
		options: ServerOptions.new
		.numOutputBusChannels_(2)
	);

	job = UniqueID.next.postln;
	resultpath = "/tmp/NRT-" ++ job ++ ".wav";
	oscpath = "/tmp/NRT-" ++ job ++ ".osc";

	score = Score([
		[0, (resultbuf = Buffer.new(server, 44100,1,0)).allocMsg],
		[0, (source = Buffer.new(server, 1,1,0)).allocReadMsg(Platform.resourceDir +/+ "sounds/a11wlk01.wav")],
		[0, [\d_recv, SynthDef(\testNRT, {BufWr.ar(PlayBuf.ar(1,source,1,doneAction: 2), resultbuf, Phasor.ar());}).asBytes]],
		[0, Synth.basicNew(\testNRT, server).newMsg],
		[1, resultbuf.writeMsg(resultpath, headerFormat: "WAVE", sampleFormat: "float")]
	]);

	cond = Condition.new;

	score.recordNRT(oscpath, "/dev/null",
		sampleRate: 44100,
		options: server.options,
		action: { cond.unhang }  // this re-awakens the process after NRT is finished
	);
	cond.hang;  // wait for completion
	"done".postln;

    sf = SoundFile.openRead(resultpath);
	sf.readData(data = FloatArray.newClear(sf.numFrames));
    sf.close;

	// File.delete(oscpath);
	// File.delete(resultpath);
    server.remove;
	data.size.postln;
    data.postln;  // these are your onsets!
};
)

Phasor’s default arguments will produce only 0 as output.

This is a memory-inefficient way to do it btw (2x the file size). The NRT guide explains how to use the input file to supply audio to SoundIn.

hjh

Corrected:

(
fork {
	var server, source, resultbuf, resultpath, oscpath, score, cond, sf, data, job;
	
	server = Server(\nrt,
		options: ServerOptions.new
		.numOutputBusChannels_(2)
	);
	
	job = UniqueID.next.postln;
	resultpath = "/tmp/NRT-" ++ job ++ ".wav";
	oscpath = "/tmp/NRT-" ++ job ++ ".osc";
	
	score = Score([
		[0, (resultbuf = Buffer.new(server, 44100, 1, 0)).allocMsg],
		[0, (source = Buffer.new(server, 1, 1, 0)).allocReadMsg(Platform.resourceDir +/+ "sounds/a11wlk01.wav")],
		[0, [\d_recv, SynthDef(\testNRT, {
			BufWr.ar(
				PlayBuf.ar(1, source, 1, doneAction: 2),
				resultbuf,
				// here's your problem:
				// phasor's default 'end' is 1
				// so it will produce 0
				// and then wrap immediately back to 0
				// Phasor.ar()
				
				// instead, use the output numframes
				Phasor.ar(0, 1, 0, 44100, 0)
			);
		}).asBytes]],
		[0, Synth.basicNew(\testNRT, server).newMsg],
		[1, resultbuf.writeMsg(resultpath, headerFormat: "WAVE", sampleFormat: "float")]
	]);
	
	cond = Condition.new;
	
	score.recordNRT(oscpath, "/dev/null",
		sampleRate: 44100,
		options: server.options,
		action: { cond.unhang }  // this re-awakens the process after NRT is finished
	);
	cond.hang;  // wait for completion
	"done".postln;
	
	sf = SoundFile.openRead(resultpath);
	sf.readData(data = FloatArray.newClear(sf.numFrames));
	sf.close;
	
	File.delete(oscpath);
	File.delete(resultpath);
	server.remove;
	data.size.postln;
	data.postln;  // these are your onsets!
};
)

If you want the BufWr phase to increase but never wrap back to 0, you can use Sweep.ar(0, SampleRate.ir) instead of Phasor.

Or, to skip the source buffer (but this way, you can’t start in the middle of the input file):

(
fork {
	var server, source, resultbuf, resultpath, oscpath, score, cond, sf, data, job;
	
	server = Server(\nrt,
		options: ServerOptions.new
		.numOutputBusChannels_(2)
	);
	
	job = UniqueID.next.postln;
	resultpath = "/tmp/NRT-" ++ job ++ ".wav";
	oscpath = "/tmp/NRT-" ++ job ++ ".osc";
	
	score = Score([
		[0, (resultbuf = Buffer.new(server, 44100, 1, 0)).allocMsg],
		[0, [\d_recv, SynthDef(\testNRT, {
			BufWr.ar(
				SoundIn.ar(0),
				resultbuf,
				Phasor.ar(0, 1, 0, 44100, 0)
			);
		}).asBytes]],
		[0, Synth.basicNew(\testNRT, server).newMsg],
		[1, resultbuf.writeMsg(resultpath, headerFormat: "WAVE", sampleFormat: "float")]
	]);
	
	cond = Condition.new;
	
	score.recordNRT(oscpath,
		outputFilePath: "/dev/null",
		inputFilePath: Platform.resourceDir +/+ "sounds/a11wlk01.wav",
		sampleRate: 44100,
		options: server.options,
		action: { cond.unhang }  // this re-awakens the process after NRT is finished
	);
	cond.hang;  // wait for completion
	"done".postln;
	
	sf = SoundFile.openRead(resultpath);
	sf.readData(data = FloatArray.newClear(sf.numFrames));
	sf.close;
	
	File.delete(oscpath);
	File.delete(resultpath);
	server.remove;
	data.size.postln;
	data.postln;  // these are your onsets!
};
)

hjh

1 Like

Hello!

Thanks for this. I am in need of triggering buffer processes (like normalize) hence the exploration (but I am a bit ashamed that I made a Phasor mistake…) I will continue exploring this approach as I want to trigger it on thousand of files. I know SC is not made for batch processing and have not found any example of NRT that do it that way either so that might be an ill-matched endeavour.

A lack of examples doesn’t imply that it’s difficult or a bad fit… it only means nobody has posted an example.

The procedure here would be to get the NRT processing working for one file first. You could even write that into a function, which takes a Condition as an argument:

~doOneFile = { |path, condition|
	fork {
		... do NRT stuff...
		// when NRT stuff is finished:
		condition.unhang;
	}
};

Then the batch processing is just to run a routine iterating over the paths:

~doManyFiles = { |arrayOfPaths|
	fork {
		var condition = Condition.new;
		arrayOfPaths.do { |path|
			~doOneFile.(path, condition);
			condition.hang;
		};
	};
};

Here’s a concrete example where modularizing the singular operation (wrapping it in a function) makes it nearly trivial to “batch-ify” it – by contrast, if you tried to write everything into one massive routine, the likelihood of getting confused and introducing bugs increases.

hjh

Thanks. That is exactly the plan indeed. I have the same workflow working now in RT and try to make it work in NRT. I am puzzled on the NRT scheme how to use Done.kr to trigger stuff, and/or how to use nested actions, to make sure dependencies are processed, but I’ll keep fighting a bit before asking too many dumb questions.

If you don’t mind an articulated confused student to voice his learning process confusions:

  • from your working code (the first one), I get the full 4 seconds in my destination buffer. I like that, and even if I put my last line at time 0 instead of 1, I get that. What I don’t understand is how is that happening, aka the dance/order of operations and their respective dependencies on the server’s stack of tasks to do. Is there a clear text explaining that anywhere? I am also puzzled since my destination buffer is 44100 sample long…

Don’t worry if these questions are irritant. I’ve read and re-read the friendly manual on NRT and I’m still unsure of the behaviours of dependencies on async server tasks for instance, hence this example, and the following which will use copyData

I haven’t been following the thread, but did you try setting the duration argument in recordNRT? That has tripped me up before.

You can’t.

There’s no interaction with a NRT server. Everything has to be written into the score file, exactly as it should be rendered.

I’m not sure of your use case?

One thing that’s quite nice to do with Condition is to have a count of pending actions, and have the Condition object check that the count has been exhausted.

(
var todo = 5;

c = Condition { todo <= 0 };

5.do { |i|
	SystemClock.sched(rrand(1.0, 5.0), {
		"done %\n".postf(i);
		todo = todo - 1;
		c.signal;
	});
};

fork {
	c.wait;
	"All done".postln;
}
)

That doesn’t sound right. resultbuf is 44100 samples – there is no way it can write 4 seconds to disk.

hjh

Indeed yet if you run the code you provided, the saved buffer is definitely 4 seconds long. Even more interesting, the following code, which has only native osc-able buffer function but that are async, yields the composited material first then appends the full size of the buffer. If I was confident in my NRT understanding I’d called that an unexpected behaviour (I was told ‘bug’ was offensive :slight_smile: )

(
fork {
	var server, source, resultbuf, resultpath, oscpath, score, cond, sf, data, job;

	server = Server(\nrt,
		options: ServerOptions.new
		.numOutputBusChannels_(2)
	);

	job = UniqueID.next.postln;
	resultpath = "/tmp/NRT-" ++ job ++ ".wav";
	oscpath = "/tmp/NRT-" ++ job ++ ".osc";

	score = Score([
		[0, (resultbuf = Buffer.new(server, 44100,1,0)).allocMsg],
		[0, (source = Buffer.new(server, 1,1,0)).allocReadMsg(Platform.resourceDir +/+ "sounds/a11wlk01.wav")],
		[0, source.copyMsg(resultbuf, dstStartAt: 0, srcStartAt: 15500, numSamples: 15500)],
		[0, source.copyMsg(resultbuf, dstStartAt: 15500, srcStartAt: 15500, numSamples: 15500)],
		[0, source.copyMsg(resultbuf, dstStartAt: 31000, srcStartAt: 15500, numSamples: 15500)],
		[1 , resultbuf.writeMsg(resultpath, headerFormat: "WAVE", sampleFormat: "float")]
	]);

	cond = Condition.new;

	score.recordNRT(oscpath, "/dev/null",
		sampleRate: 44100,
		options: server.options,
		action: { cond.unhang }  // this re-awakens the process after NRT is finished
	);
	cond.hang;  // wait for completion
    server.remove;
	"done".postln;

    sf = SoundFile.openRead(resultpath);
	sf.readData(data = FloatArray.newClear(sf.numFrames));
    sf.close;

	// File.delete(oscpath);
	// File.delete(resultpath);
	data.size.postln;
    data.postln;
};
)


///rt test

b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav")
b.play
c = Buffer.alloc(s, 120000);
c.zero
b.copyData(c, dstStartAt: 0, srcStartAt: 15500, numSamples: 15500);
b.copyData(c, dstStartAt: 15500, srcStartAt: 15500, numSamples: 15500);
c.play

s.dumpOSC

Oh, now I see it.

// buffer ID 0
resultbuf = Buffer.new(server, 44100,1,0)).allocMsg

// also buffer ID 0
source = Buffer.new(server, 1,1,0))

They should be different buffer IDs if you want them to have different sizes.

hjh

1 Like

arghhhhhh! Indeed, sorry for the noise. I will try to do like in RT and not give them numbers (and hope that my server object deals with the numbering)

Now I’m happy I did not call it a bug :slight_smile: Meatware issue… I hope it’ll help someone else.