Single KRate Pulse

So I’ve integrated Pichenettes’ note-stack class into my plugin. I’m stillll getting stuck notes.

This is a bit maddening…

This is what gets logged around a stuck note:

SCLANG NOTEOFF 57
PLUGIN SLIDETO NOTE 57
SCLANG NOTEOFF 65
PLUGIN RELEASE NOTE 65
SCLANG NOTEON 57 
PLUGIN TRIGGER NOTE 57
SCLANG NOTEOFF 57
PLUGIN RELEASE NOTE 57
SCLANG NOTEON 66 
PLUGIN TRIGGER NOTE 66
SCLANG NOTEON 57 
PLUGIN SLIDETO NOTE 57
SCLANG NOTEOFF 57
SCLANG NOTEOFF 66
PLUGIN SLIDETO NOTE 66
SCLANG NOTEON 66 
PLUGIN SLIDETO NOTE 66
SCLANG NOTEOFF 66
PLUGIN SLIDETO NOTE 66
SCLANG NOTEON 55 
PLUGIN SLIDETO NOTE 55
SCLANG NOTEOFF 55
PLUGIN SLIDETO NOTE 55
SCLANG NOTEON 60 
PLUGIN SLIDETO NOTE 60
SCLANG NOTEOFF 60
PLUGIN SLIDETO NOTE 60

Every detected MIDI note on/off should trigger an action in the plugin, so the “SCLANG” and “PLUGIN” lines in the log above should always alternate. You can see that’s not happening though, ie

SCLANG NOTEON 57 
PLUGIN SLIDETO NOTE 57
SCLANG NOTEOFF 57
SCLANG NOTEOFF 66
PLUGIN SLIDETO NOTE 66
SCLANG NOTEON 66 

I don’t understand why this is happening. I don’t think it’s a problem with my plugin logic. It seems to me that either the triggers are not always being sent to the plugin, or the synth “set” commands are not always being executed in the same order, so the note-number have not always been updated when the plugin UGen receives the noteevent trigger.

Those duplicate lines in the log suggests the former, and that this has been the problem all along.

I’ve worked around the problem by adding a note-counter to my SC script. If there aren’t meant to be any notes playing when a note-off is received, I send an “all-notes-off” signal to the plugin.

( // SynthDef
s.waitForBoot {
	SynthDef.new("AcidBass", {
		arg out,
		notenum = 60.0,
		notevel = 64.0,
		waveform = 0.85,
		cutoff = 0.229,
		resonance = 0.5,
		envmod = 0.25,
		decay = 0.5,
		accent = 0.5,
		volume = 0.9;
		// Declare vars
		var notetrigger = NamedControl.tr(\notetrigger); // Ensures 'noteevent' is only ever positive for 1 k-rate cycle!
		var notealloff = NamedControl.tr(\notealloff);
		// Create output
		var result;
		result = Open303.ar(notetrigger, notenum, notevel, notealloff, waveform, cutoff, resonance, envmod, decay, accent, volume);
		// Output output
		Out.ar(out, result);
	}).add;
};
)

( // Start it

// Create bassline synth object
~bassline = Synth(\AcidBass);
// Active note-counter
~notecount = 0;

// Start MIDI
MIDIClient.init;
MIDIIn.connectAll;

// MIDI functions
MIDIFunc.noteOn({ |vel, num|
	"SCLANG NOTEON % \n".postf(num);
	~bassline.set(\notenum, num, \notevel, vel, \notetrigger, 1.0);
	// Increment note-counter
	~notecount = ~notecount + 1;
});
MIDIFunc.noteOff({ |vel, num|
	"SCLANG NOTEOFF %\n".postf(num);
	// Decrement note-counter
	~notecount = ~notecount -1;
	// Check if all notes should be off at this point
	if(~notecount == 0) {
		// If yes send all-note-off signal to synth to un-stick any stuck notes
		"SCLANG ALL NOTES OFF %\n".postf();
		~bassline.set(\notealloff, 1.0);
	} {
		// At least one note is still being held, so we send note off for this note
		"SCLANG NOTEOFF % \n".postf(num);
		~bassline.set(\notenum, num, \notevel, 0.0, \notetrigger, 1.0);
	}
});
)

// Stop it
~bassline.free;
MIDIdef.freeAll;

…of course, these all-notes-off signals may also occasionally fail to be received by my plugin UGen, but at least it should mean that most stuck notes are unstuck before they’re too noticeable.

This might just be good enough for my purposes (synth will be triggered by sequencer in Monome Norns script), but still not great for general use.

One potential problem with the set-and-trigger approach is that it can’t handle two note-off messages in the same control cycle. In these cases where there are two note-off messages in a row, how much time elapses between the two?

Note also that the messages aren’t timestamped in your script, so they’ll be quantized to the audio hardware buffer boundary, e.g., if it’s a 512 sample buffer, you’d need ~12 ms between set messages to the synth. If it’s less than the buffer duration, then scsynth would perform two set messages at the same time, and the synth would see only the latter set of values.

I think this is why spacechild1 recommended unit commands instead of synth args.

hjh

That does make sense. But if that were the case, wouldn’t that mean that any synth controlled using the “set” method would miss MIDI messages in the same way? That would have caused loads of problems for anyone building a synth for triggering from a keyboard.

Maybe that is what’s happening, in fact.

Does that get around this problem, then? ie allow data to be sent to the synth at faster than k-rate?

I thought we decided that MIDI messages only became available at k-rate boundaries, and if only one value for a particular message-type is available (ie only the most recent note-on/off message that occurred during the previous k-rate cycle is detected), then MIDI data must be getting missed all the time… That can’t be right.

@Spacechild1 I did have a look at your plugin, but I’m afraid I didn’t know what I was looking for, so wasn’t able to glean much from it, sadly. Could you possibly give me some clues on where I should be looking?

I can’t seem to find any documentation on unit commands, either. Could either of you point me in the direction of some, maybe?

I’m thinking set control for parameter updates is fine, so any unit commands I might need would only be required for note on/off/velocity and all-note-off messages.

To my recollection, nobody in the past made a serious effort at controlling poly note-on/off(+) by set messages, because that way of transmitting this type of information is inherently fragile.

[ (+) Open303 is mono, but tracks multiple pressed keys for slides. ]

More typical MIDI usage in SC is (here, assuming mono-portamento behavior):

  • Note-on:
    • No existing note playing → new synth
    • Existing note playing → set message
  • Note-off:
    • Other keys still pressed → set message
    • No other keys → destroy the synth node

… with keyboard state tracking being done in the language, not on the server side. In that scenario, there’s no problem with the timing resolution – if two keys are released at the same time and at least one is still holding, two ‘/n_set’ messages would be issued, but only the final state matters. The intermediate state doesn’t have to be tracked on the server side this way.

(In general, part of the SC Server design is that the language is the brains of the operation, and the server is just a dumb animal, a DSP pack mule. It does what it’s told but it doesn’t make decisions on its own. Trying to change that relationship often leads to trouble.)

u_cmd is a totally different communication channel that is not subject to audio or control rate at all. The UGen would received discrete messages, and it would get all of them.

I’m not deeply familiar with unit commmands myself – Some things I observe in VSTPlugin:

#define UnitCmd(x) DefineUnitCmd("VSTPlugin", "/" #x, (UnitCmdFunc)runUnitCmd<vst_##x>)

^^ namespace – VSTPlugin unit commands have a vst_ prefix for function names. So UnitCmd(midi_msg); (line 3532) hooks up to a function vst_midi_msg() – sc/src/VSTPlugin.cpp · master · Pure Data libraries / vstplugin · GitLab

Then on the language side, the VSTPluginController finds the synthIndex of the UGen to be controlled, and can issue commands like '/u_cmd', synth.nodeID, synthIndex, '/midi_msg', some, data, and, maybe, more, data – this would dispatch to that vst_midi_msg() C++ function.

Documentation… maybe(?) The SuperCollider Book (of which a second edition is coming very soon, I think).

hjh

Thanks very much for the detailed response, @jamshark70. That does all make perfect sense.

Yes, exactly. It has to be able to handle overlapping notes internally.

Unless of course I broke out all the separate parts to discrete UGens - oscillator, filter/vca envelopes and filter, and recreated the signal and control flow of the original in SCLang.

That actually might be quite useful, but it’s would be a lot of work!

…or I could do that! Sounds like just what I need.

I wonder if I’d need to keep a buffer of incoming messages and then dispatch them at k-rate (or even a-rate), or if Open303 could cope with processing note on/off/slide messages at arbitrary times.

That makes sense. I will have another look at that.

Thanks again.

I wonder if I’d need to keep a buffer of incoming messages and then dispatch them at k-rate (or even a-rate)

All Unit commands are dispatched before the corresponding DSP tick. If you ignore the (sub)sample offset, then you are essentially dispatching at control rate.

or if Open303 could cope with processing note on/off/slide messages at arbitrary times.

It absolutely should! In a DAW, the block size can be up to 1024 samples and without timestamping you would get horrible jitter. For this reason, all DAW plugin formats support timestamped MIDI events.

That being said, I would only try to use Unit command if absolutely necessary as they are just so awkward to work with. Since your synth is actually monophonic I would just follow @jamshark70’s advice and implement the voice stealing on the client side, so you can simply use UGen inputs for pitch and velocity (and possibly a trigger input for note retriggering). You can write your own Event type that sets the appropriate Synth arguments.

So do they all arrive at the same time at the Unit then (but with a timestamp recording their offset from the start of the last k-rate cycle)?

What do you mean by “DSP tick”? I didn’t know there was a tick running at audio-rate.

I think they might be the best option.

I don’t think that will work, though. The plugin needs to keep track of the state of multiple notes internally, because it needs to slide between overlapping notes, without retriggering the internal envelopes. That makes it impossible to simply spin up a new instance of the synth for each note played.

Every way I look at it, the plugin needs to know when note on and off events occur, and the note number and velocity of the note at that exact point. That just doesn’t seem to work reliably with synth .set commands.

I can’t see how moving note-handling outside of the plugin could work, unless I disassembled the synth into individual custom UGen components, and re-recreated the signal and audio flow in SCLang.

Maybe I’m overthinking this though, and there IS a way to do it in SCLang after all. I was hoping to keep that side of things was simple as possible, so users didn’t have to write a load of obscure SuperCollider code to use the plugin.

Also, I like the Shruthi-1 way of handling released notes when overlapping notes are played. I’m not sure how easy it would be to recreate that in SCLang.

[quote=“Spacechild1, post:49, topic:11416”]

but with a timestamp recording their offset from the start of the last k-rate cycle)?

mWorld->mSampleOffset contains the offset of the current Unit command for this control period.

What do you mean by “DSP tick”?

DSP computation for a control period.

That makes it impossible to simply spin up a new instance of the synth for each note played.

I certainly wouldn’t create a new instance for each voice! After all, you want to preserve the current synth parameters.

Every way I look at it, the plugin needs to know when note on and off events occur, and the note number and velocity of the note at that exact point.

I think you can do the following in the UGen:

  1. check the note and velocity input (control rate or audio rate)
  2. is the velocity > 0 ?
    • yes → has the previous velocity been 0?
      • yes → new note without glissando → just send a new note message to the synth
      • no → new note with glissando → first send the new note message, then the previous note with velocity 0 (possibly with a tiny time offset if necessary)
    • no → send a note-off message
  3. store current note and velocity

Also, I like the Shruthi-1 way of handling released notes when overlapping notes are played. I’m not sure how easy it would be to recreate that in SCLang.

I have no experience with Shruthi-1, but if you want a monophonic Synth to somehow react to released “stolen” notes, then this would indeed be impossible to implement with only UGen inputs.

Well, I feel like I’ve wasted everyone’s time a bit, because I’ve got it working (finally!!), but the setup is very similar to the monosynth example waaay upthread.

I wasn’t quite happy with the way the example responded to released legato notes though, so I created this variation, which seems to work perfectly with the updated note-handling setup in my plugin, similar to that suggested by @Spacechild1 above.

Here’s the latest SCLang script:

( // SynthDef
s.waitForBoot {
	SynthDef.new("AcidBass", {
		arg out,
		gate = 0.0,
		notenum = 60.0,
		notevel = 64.0,
		waveform = 0.85,
		cutoff = 0.229,
		resonance = 0.5,
		envmod = 0.25,
		decay = 0.5,
		accent = 0.5,
		volume = 0.9;

		// Declare vars
		var notealloff = NamedControl.tr(\notealloff);
		// Create output
		var result;
		result = Open303.ar(gate, notenum, notevel, notealloff, waveform, cutoff, resonance, envmod, decay, accent, volume);
		// Output output
		Out.ar(out, result);
	}).add;
};
)

( // Start it
// Create note-stack list
~notestack = List[ ];
// Create bassline synth object
~bassline = Synth(\AcidBass);

// Start MIDI
MIDIClient.init;
MIDIIn.connectAll;

// MIDI functions
MIDIFunc.noteOn({ |vel, num|
	// Add new note to end of note-stack
	~notestack.add(num);
	// If note-stack size is now 1, this is a non-legato note
	if (~notestack.size == 1) {
		// Switch gate high and update synth MIDI note index and velocity. Synth will play note
		//postf("SCLANG NOTEON % STACK SIZE % STACK % \n", num, ~notestack.size, ~notestack);
		~bassline.set(\gate, 1.0, \notenum, num, \notevel, vel);
	} {
		// ...else this is a legato note
		// Hold gate high and update synth note number and velocity. Synth will slide to new note
		//postf("SCLANG SLIDETO % STACK SIZE % STACK % \n", num, ~notestack.size, ~notestack);
		~bassline.set(\gate, 1.0, \notenum, num, \notevel, vel);
	}
});

MIDIFunc.noteOff({ |vel, num|
	// Seach for note index in note-stack and remove
	~notestack.do({ arg item, i; if (item == num) { ~notestack.removeAt(i); }});
	// note-stack could sorted at this point to add lowest/highest-note priority legato release response
	// Check if this we've just released the last held note
	if (~notestack.size == 0) {
		// ...we have. Pull gate low and send note index to synth (velocity not required). Synth will release note
		//postf("SCLANG LAST NOTE OFF % STACK SIZE % STACK % \n", num, ~notestack.size, ~notestack);
		~bassline.set(\gate, 0.0, \notenum, num);
	} {
		// Update synth with most recent note index remaining in note-stack. Synth will slide back to note
		//postf("SCLANG SLIDETO % STACK SIZE % STACK % \n", ~notestack.last, ~notestack.size, ~notestack);
		~bassline.set(\gate, 1.0, \notenum, ~notestack.last);
	}
});

MIDIFunc.cc({ |val, ccNum, chan, src|
	~bassline.set(\waveform, val / 127);
}, ccNum:21);

MIDIFunc.cc({ |val, ccNum, chan, src|
	~bassline.set(\cutoff, val / 127);
}, ccNum:22);

MIDIFunc.cc({ |val, ccNum, chan, src|
	~bassline.set(\resonance, val / 127);
}, ccNum:23);

MIDIFunc.cc({ |val, ccNum, chan, src|
	~bassline.set(\envmod, val / 127);
}, ccNum:24);

MIDIFunc.cc({ |val, ccNum, chan, src|
	~bassline.set(\decay, val / 127);
}, ccNum:25);

MIDIFunc.cc({ |val, ccNum, chan, src|
	~bassline.set(\accent, val / 127);
}, ccNum:26);

MIDIFunc.cc({ |val, ccNum, chan, src|
	~bassline.set(\volume, val / 127);
}, ccNum:27);

)

// Stop it
~bassline.free;
MIDIdef.freeAll;

I’ve implemented a dynamic note-stack where the most recent note is always at the last index, which I think is how Pichenettes’ Shruthi-1 legato note-handling works. If a key is still being held when a note is released, the synth slides to the note at the top of the stack (the most recent remaining note).

The list could probably be sorted such that released legato notes slide back to the highest or lowest remaining notes. Using a SortedList instead of the standard List might achieve that quite easily.

I’m going to test this against my MB-33 and T-8 when I get a chance, but for the moment, I’m just happy it’s working reliably without stuck notes!

Here’s the relevant chunk of my Plugin code:

// New gate
if(gate && !lastgate) {
    //cout << "PLUGIN NOTEON " << notenum << "\n";
    o303.triggerNote(notenum, accent);
}
// Gate still high but note changed. Slide to new note
if((notenum != lastnotenum) && (gate && lastgate)) {
    //cout << "PLUGIN SLIDETO " << notenum << "\n";
    o303.slideToNote(notenum, accent);
}
// Last note off
if(lastgate && !gate) {
    //cout << "PLUGIN LAST NOTE OFF " << notenum << "\n";
    o303.releaseNote(notenum);
}
// Detect all-notes-off trigger
if(notealloff == 1 && lastnotealloff == 0) {
    // Trigger synth all-notes-off
    //cout << "PLUGIN ALL NOTES OFF\n"
    o303.allNotesOff();
}

Thank you so much for all your advice, guys. I’ve learnt a lot! I’m sure I’ll be back soon with more Stupid Questions.

If anyone is interested in following this project, the repo is here.

There will eventually be an updated version of my bline Norns script featuring this plugin as an alternative internal sound-engine (assuming I can get this thing compiled on Norns’ Raspberry Pi hardware).

1 Like

I was overconfident, it seems! While I got my Open303 plugin working flawlessly on macOS, and compiled and built for Windows and Unix systems, it appears not to work on the Raspberry Pi-based Norns platform that was the intended eventual target.

I’m going to start another thread on that though.