Using Voicer to play a VST

Something fun I was playing with yesterday, and a peek at my working method.

I just made a small change to Voicer (ddwVoicer quark) to allow it to work with VSTPlugin. readProgram is using a file I had prepared before with writeProgram.

s.boot;

(
SynthDef(\vstplugin2, { |out, gate = 1|
	var sig = VSTPlugin.ar(numOut: 2);
	var eg = EnvGen.kr(Env.asr(0.01, 1, 0.1), gate, doneAction: 2);
	Out.ar(out, sig * eg);
}).add;
)

a = Synth(\vstplugin2);
c = VSTPluginController(a);
c.open("sfizz.vst3");
c.readProgram("/home/dlm/share/SC/scd/misc/headroom-piano.vstpreset");

v = Voicer(15, c.midi);

v.gate([60, 64, 67].midicps, 2, 0.5);

v.free;
c.close;
a.release;

With the ddwChucklib quark, I can package that into an instrument:

(
(1..2).do { |numCh|
	SynthDef(("vstplugin" ++ numCh).asSymbol, { |out, gate = 1|
		var sig = VSTPlugin.ar(numOut: numCh);
		var eg = EnvGen.kr(Env.asr(0.01, 1, 0.1), gate, doneAction: 2);
		Out.ar(out, sig * eg);
	}).add;
};

(
vst: "sfizz.vst3",
program: "headroom-piano.vstpreset",
numCh: 2,
make: { |name|
	var out;
	~vstSynth = Synth(("vstplugin" ++ ~numCh).asSymbol);

	~target = MixerChannel(name, s, 2, 2, ~initLevel, outbus: ~master, completionFunc: { |chan|
		~vstSynth.moveToHead(chan.synthgroup).set(\out, chan.inbus.index);
	});

	~vstCtl = VSTPluginController(~vstSynth);
	fork {
		var cond = CondVar.new;
		var path, loaded;
		fork {  // yes, we really need to double-fork
			~vstCtl.open(~vst, action: { |controller, didLoad|
				loaded = didLoad;
				cond.signalOne;
			});
		};
		cond.wait;
		if(loaded) {
			path = ~program;
			if(PathName(path).isAbsolutePath.not and: {
				thisProcess.nowExecutingPath.isString
			}) {
				path = thisProcess.nowExecutingPath.dirname +/+ path
			};
			~vstCtl.readProgram(path, inEnvir { |controller, didLoad|
				if(didLoad.not) {
					"VC(%) failed to load preset '%'".format(
						name.asCompileString, ~program
					).warn;
				};
			});
		} {
			"VC(%) failed to load plugin '%'".format(name.asCompileString, ~vst).warn;
		};
	};
	~midi = ~vstCtl.midi;

	out = Voicer(15, ~midi, target: ~target);
	// out.mapGlobal();
	out
},
free: { ~vstCtl.close; ~target.free; },
type: \vc) => Fact(\vstVC);
)

Then I can do things like play Piano Phase on a VST, in my live coding system (ddwChucklib-livecode and ddwLivecodeInstruments quarks):

\loadAllCl.eval;
/changeKey.(\bdor);
/changeMeter.(3);
TempoClock.tempo = 126/60;

/make(vstVC:piano/melBP:piano(octave:4));

/piano = "\ins(\seq("*@"), 12, 0.25)::\seq("157", "*")::\seq("26", "@")";
/piano+

/piano-  // stop

hjh

Cool! But why don’t you read the preset directly in the callback function passed to the .open method?

Coding style preference: I strongly dislike nested callbacks, because it becomes more difficult to read the sequence of events. If it’s supposed to happen later, I want to read it below, not within.

hjh

I totally see the problem with callback hell, but the extra forking + Condition isn’t exactly pretty, either. It really is a shame that sclang doesn’t have a better standard mechanism for async function calls (like returning a future). Let’s keep that in mind for SC4 :wink:

This is not always needed – but, open uses forkIfNeeded and this breaks condition-wait. (Same problem with Buffer:getToFloatArray.)

We should probably discourage the use of forkIfNeeded in cases where we might reasonably expect a user to use Condition, CondVar or Semaphore to block for an async action and the method itself is blocking the thread for its own async actions.

hjh

but, open uses forkIfNeeded and this breaks condition-wait

Why/how does it break condition-wait? Should I use fork instead?

I’ll illustrate the problem with getToFloatArray – actually, copying the logic into a function, so that I can insert tracing posts.

(
f = { |buffer, startFrame = 0, numFrames = -1, action|
	var cond = CondVar.new;
	var result;
	var i = 0;
	var chunkSize = 1633;  // imitate the method
	
	if(numFrames < 0) { numFrames = buffer.numFrames };
	result = FloatArray.newClear(numFrames);
	
	forkIfNeeded {
		while { i < numFrames } {
			[i + startFrame, min(chunkSize, numFrames - i)].debug("requesting");
			buffer.getn(i + startFrame, min(chunkSize, numFrames - i), { |data|
				data.size.debug("received size");
				result.overWrite(data.as(FloatArray), i);
				cond.signalOne;
			});
			cond.wait;
			i = i + chunkSize;
		};
		action.value(result)
	};
};
)

s.boot;

b = Buffer.alloc(s, 4096, 1, { |buf| buf.sine1Msg([1]) });

f.(b, action: _.postln);  // OK!

requesting: [ 0, 1633 ]
received size: 1633
requesting: [ 1633, 1633 ]
received size: 1633
requesting: [ 3266, 830 ]
received size: 830
FloatArray[ -0.0030679572373629, 0.0030679572373629, ...

Called in isolation, the function does what it should. But when you block it with CondVar (or Condition):

(
fork {
	var cond = CondVar.new;
	var data;
	f.(b, action: { |array|
		"called action".debug;
		data = array;
		cond.signalOne;
	});
	cond.wait;
	data.postln;  // We never get here :-O
}
)

requesting: [ 0, 1633 ]
received size: 1633
requesting: [ 1633, 1633 ]
received size: 1633
requesting: [ 3266, 830 ]
received size: 830
called action

… it goes through all the steps, calls the action function, and the action function tries to unblock the thread, and… nothing.

I’m not 100% sure where it goes wrong, but there is definitely a problem with having one thread being blocked simultaneously (nestedly) by two separate Cond-things. And the problem disappears if f uses fork instead of forkIfNeeded. (I think I will file a bug about this. If I’m extremely grumpy, I think I could even make a case for deprecating forkIfNeeded as its behavior is misleading.)

The reason for using forkIfNeeded was to avoid creating a second thread if you’re already in a thread. Another solution is recursion – if an asynchronous callback issues a recursive call, then the stack doesn’t grow, so this implementation could continue indefinitely without forking a second thread:

(
var readChunkAndContinue = { |buffer, array, frame, numFrames, arrayPos, chunkSize, action|
	buffer.getn(frame, min(chunkSize, numFrames - arrayPos), { |data|
		data.size.debug("received size");
		array.overWrite(data.as(FloatArray), arrayPos);
		arrayPos = arrayPos + data.size;
		if(arrayPos < numFrames) {
			readChunkAndContinue.(buffer, array,
				frame + data.size, numFrames, arrayPos,
				chunkSize, action
			)
		} { action.value(array) };
	});
};

f = { |buffer, startFrame = 0, numFrames = -1, action|
	var chunkSize = 1633;  // imitate the method
	var result;
	
	if(numFrames < 0) { numFrames = buffer.numFrames };
	result = FloatArray.newClear(numFrames);
	
	readChunkAndContinue.(buffer, result, startFrame, numFrames, 0, chunkSize, action);
};
)

(
fork {
	var cond = CondVar.new;
	var data;
	f.(b, action: { |array|
		"called action".debug;
		data = array;
		cond.signalOne;
	});
	cond.wait;
	data.postln;  // OK!
}
)

received size: 1633
received size: 1633
received size: 830
called action
FloatArray[ -0.0030679572373629, 0.0030679572373629, ...  // yep!

hjh

1 Like

Thanks for the elaborate example!

but there is definitely a problem with having one thread being blocked simultaneously (nestedly) by two separate Cond-things.

This definitely sounds like a bug…

I think I will file a bug about this.

Please do! I’m surprised this hasn’t come up already (at least I don’t remember).

It’s come up for me dozens of times in my own code, but I didn’t follow up on it.

I think I see it now – when the inner loop exits, the routine ends at that point and it can’t resume. Basically the inner function or method hijacks the thread, and when it terminates, it takes down the caller as well. Edit: This suggests a solution – forkIfNeeded could yield something at the end of the non-forking branch, to keep the thread from reaching “ended” state.

Edit edit: That hypothesis didn’t pan out.

hjh