Single KRate Pulse

It’s actually a bit more than that – for a kr input to an ar UGen, there is no block at all, just a single sample.

The calculation function gets called at control rate – so, a kr input only needs the single value at the input’s index – ZIN0(index).

An ar input needs n samples, which is done by assigning one of the server’s wire buffers to that input. That needs to be accessed by incrementing the pointer for each audio sample (ZXP(pointer) in most of the core plug-ins).

If you increment a single-value pointer, I’d expect a crash.

So – “are non-audio parameter inputs actually updated at audio rate” – do not try to read an audio rate block from a non-ar input!!

One of the long-standing complaints in the plug-in interface is that different rates become a combinatorics problem. Open303 has three inputs. If you want every possible combination of ar and kr, you’d need eight calculation functions: aaa, aak, aka, akk, kaa, kak, kka, kkk. It’s too much, so most plug-ins don’t support every possible combination – they simply read some of the parameters at control rate, even if an audio rate input was provided. (E.g., FOS will read the coefficients at audio rate only if all inputs are audio rate; if any input is kr, then all of the coefficients are read at kr: 2 funcs instead of 16, plus a next_1 which is probably never used.)

For Open303, the benefit of audio rate inputs may be less significant than the cost (complexity), i.e., not worth it.

hjh

Oh yeah, that IS confusing…

I’m not clear if MIDI input reception is forced to alight with k-rate cycles or not.

Obviously a MIDI message could be received at any point, but maybe the messages are buffered and only made available at kr boundaries.

Maybe moving MIDI note-related stuff to the audio render function of my plugin will fix those stuck notes…

Messages are processed only on control block boundaries.

I tested and I can’t get a sub-block offset when setting an audio-rate control.

(
a = {
	var arInput = NamedControl.ar(\in, 0);
	var block = Impulse.ar(ControlRate.ir);
	var timer = Sweep.ar(block, SampleRate.ir);
	var trig = Changed.ar(arInput);
	[arInput, timer].poll(trig);
	Silent.ar(1)
}.play;
)

a.set(\in, rrand(1, 10000));  // Sweep is always "-0"

// random OSC timestamp is likely to be in the middle of a block
// but still, the reported offset is always -0
// so 'set' messages are always quantized to block boundaries
s.makeBundle(rrand(0.1, 0.3), { a.set(\in, rrand(1, 10000)) });

UGen Array [0]: 8841
UGen Array [1]: -0
UGen Array [0]: 3373
UGen Array [1]: -0

// you *can* get mid-block triggers by mapping an audio signal
// but that's not compatible with external control
b = Bus.audio(s, 1);

c = {
	Demand.ar(Dust.ar(10), 0, Dwhite(0.0, 1.0, inf))
}.play(outbus: b);

a.set(\in, b.asMap);

UGen Array [0]: 0.942877
UGen Array [1]: 51         <<-- nonzero mid-block offsets
UGen Array [0]: 0.457051
UGen Array [1]: 11
UGen Array [0]: 0.504048
UGen Array [1]: 3

[a, b, c].do(_.free);

So I’d guess audio-rate inputs could safely be lower priority for Open303.

hjh

1 Like

That would suggest the stuck notes aren’t related to triggers not occurring at kr boundaries then.

Damn, that would have been an easy fix, potentially.

What are the inputs supposed to be when a note is triggered? And, when a note is released?

hjh

The inputs that need to be updated at each note event are the MIDI note number and velocity.

The synth treats note events with velocity 0 as note-off.

The original Open303 code is meant to keep track of currently-held keys, and handle slides if more than one key is down. The problem is if for some reason it misses a note-off event it slides to subsequent notes, and eventually goes silent as the envelopes aren’t retriggered and the VCA envelope fades output volume to 0.

I’m away from the computer at the moment, but when I get a chance I will try moving the nonevent-detection to the ar render loop.

If that doesn’t help, Open303 has separate lower-level functions for triggering, releasing and sliding notes. These are protected by default, but I can make them public and try handling note on/off/slide logic on the SuperCollider side (I already did this with my previous SC-native 303 emulation attempt).

Hm, I’m not sure a single gate signal (velocity) can accurately represent poly notes on/off. (The 303 sound isn’t poly but tracking multiple keys is essentially polyphonic logic.) If you release two keys in a row, the gate goes from closed to closed = no state change, tricky to detect.

Or were you thinking that a note off would be a single positive pulse in noteevent, with the pitch and velocity being set at the same time? That might work.

hjh

Yes, that was my plan. I don’t track gates at all at the moment, just send note and velocity of every note event to the plugin (setting velocity to 0 for note-offs).

The plugin is meant to handle a note queue, and determine which notes are still held, and slide to the next or previous note where notes overlap.

Here’s my SuperCollider plugin-testing script:

//"https://doc.sccode.org/Guides/UsingMIDI.html"
MIDIClient.init;
MIDIIn.connectAll;

s.boot;

(
// Init bassline synth object
var bassline;
var m_on, m_off, m_waveform, m_cutoff, m_resonance, m_envmod, m_decay, m_accent, m_volume;

SynthDef.new("AcidBass", {
	arg out,
	notenum = 60.0,
	notevel = 64.0,
	notealloff = 0.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 noteevent = NamedControl.tr(\noteevent); // Ensures 'noteevent' is only ever positive for 1 k-rate cycle!
	var result;
	// Create output
    result = Open303.ar(noteevent, notenum, notevel, notealloff, waveform, cutoff, resonance, envmod, decay, accent, volume);
	// Output output
	Out.ar(out, result);
}).add;

// Instantiate Open303 synth
bassline = Synth("AcidBass");

// MIDI functions
m_on = MIDIFunc.noteOn({ |vel, num, chan, src|
	//"NOTEON note % @ velocity %\n".postf(num, vel);
	bassline.set(\notenum, num);
	bassline.set(\notevel, vel);
	bassline.set(\noteevent, 1.0);
});

m_off = MIDIFunc.noteOff({ |vel, num, chan, src|
	//"NOTEOFF note %\n".postf(num);
	bassline.set(\notenum, num);
	bassline.set(\notevel, 0.0);
	bassline.set(\noteevent, 1.0);
});

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

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

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

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

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

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

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

)

s.free;

I’ve noticed some note-on events haven’t been getting through, too.

Here’s the plugin implementation file:

// PluginOpen303.cpp
// toneburst (the_voder@yahoo.co.uk)

#include "SC_PlugIn.hpp"

// Include Open303 DSP header
#include "lib/Open303/Source/DSPCode/rosic_Open303.h"

// Global functions (for parameter scaling)
#include "lib/Open303/Source/DSPCode/GlobalFunctions.h"

#include "Open303.hpp"

static InterfaceTable* ft;

namespace Open303 {

    // Instantiate Open303 object
    rosic::Open303 o303;

    // CONSTRUCTOR
    Open303::Open303() {

        // Initialize the state of member variables that depend on input aruguments
        m_waveform  = in0(WAVEFORM);
        m_cutoff    = in0(CUTOFF);
        m_resonance = in0(RESONANCE);
        m_envmod    = in0(ENVMOD);
        m_decay     = in0(DECAY);
        m_accent    = in0(ACCENT);
        m_volume    = in0(VOLUME);

        // Get Sample-Rate
        srate = fullSampleRate();

        // Set Open303 sample-rate
        o303.setSampleRate(srate);

        mCalcFunc = make_calc_function<Open303, &Open303::next>();
        next(1);
    }

    // Control-rate loop
    void Open303::next(int nSamples) {
        // Control parameters
        // (at Audio rate, the input values are supplied in a block of nSamples values (as floats).
        // "in0" is the first value in the block (although all other values are probably the same))

        // Note parameters. Synth expects ints
        const int noteevent    = static_cast<int>(in0(NOTEEVENT));
        const int notenum      = static_cast<int>(in0(NOTENUM));
        const int notevel      = static_cast<int>(in0(NOTEVEL));
        const int notealloff   = static_cast<int>(in0(NOTEALLOFF));

        // Interpolated synth control parameters. Synth expects doubles, inputs are floats.
        // Conversion functions from Globalfunctions.h
        // Conversion function args: double linToLin(double in, double inMin, double inMax, double outMin, double outMax);
        // Param conversion values copied from Open303VST.cpp
        // All params 0.0 - 1.0 range
        const float waveformParam   = in0(WAVEFORM);    // No scaling required (already in 0-1 range)
        const float cutoffParam     = linToExp(in0(CUTOFF),    0.0, 1.0, 314.0, 2394.0);        
        const float resonanceParam  = linToLin(in0(RESONANCE), 0.0, 1.0,   0.0,  100.0);
        const float envmodParam     = linToLin(in0(ENVMOD),    0.0, 1.0,   0.0,  100.0);
        const float decayParam      = linToExp(in0(DECAY),     0.0, 1.0, 200.0, 2000.0);
        const float accentParam     = linToLin(in0(ACCENT),    0.0, 1.0,   0.0,  100.0);
        const float volumeParam     = linToLin(in0(VOLUME),    0.0, 1.0, -60.0,    0.0);
        
        // Create interpolation slopes
        SlopeSignal<float> slopedWaveform   = makeSlope(waveformParam,  m_waveform);
        SlopeSignal<float> slopedCutoff     = makeSlope(cutoffParam,    m_cutoff);
        SlopeSignal<float> slopedResonance  = makeSlope(resonanceParam, m_resonance);
        SlopeSignal<float> slopedEnvmod     = makeSlope(envmodParam,    m_envmod);
        SlopeSignal<float> slopedDecay      = makeSlope(decayParam,     m_decay);
        SlopeSignal<float> slopedAccent     = makeSlope(accentParam,    m_accent);
        SlopeSignal<float> slopedVolume     = makeSlope(volumeParam,    m_volume);

        ///////////////////
        // Note-Handling //
        ///////////////////

        if(noteevent == 1 && lastnoteevent == 0) {
            // Check note velocity to determine if this is a note-on or note-off event
            if(notevel > 0) {
                // Note-ON
                // 3rd arg is 'detune'
                o303.noteOn(notenum, notevel, 0.0);
            } else {
                // Note-OFF (note-on event with velocity 0, slightly confusingly)
                o303.noteOn(notenum, 0, 0.0);
            }
        }
        // Update previous note-event
        lastnoteevent = noteevent;

        // Detect all-notes-off trigger
        if(notealloff == 1 && lastnotealloff == 0) {
            o303.allNotesOff();
        }
        // Update previous all-note-off
        lastnotealloff = notealloff;

        //////////////////
        // Audio Render //
        //////////////////

        // Create output buffer
        float* outbuf = out(0);

        // Fill output buffer with nSamples Open303 synth samples
        for (int i = 0; i < nSamples; ++i) {

            // Update synth params with interpolated value
            // Cast floats to doubles
            o303.setWaveform(   static_cast<double>(slopedWaveform.consume()));
            o303.setCutoff(     static_cast<double>(slopedCutoff.consume()));
            o303.setResonance(  static_cast<double>(slopedResonance.consume()));
            o303.setEnvMod(     static_cast<double>(slopedEnvmod.consume()));
            o303.setDecay(      static_cast<double>(slopedDecay.consume()));
            o303.setAccent(     static_cast<double>(slopedAccent.consume()));
            o303.setVolume(     static_cast<double>(slopedVolume.consume()));
 
            // Call Open303 render function
            outbuf[i] = o303.getSample();
        }

        ////////////////////////
        // Update Param State //
        ////////////////////////

        m_waveform  = slopedWaveform.value;
        m_cutoff    = slopedCutoff.value;
        m_resonance = slopedResonance.value;
        m_envmod    = slopedEnvmod.value;
        m_decay     = slopedDecay.value;
        m_accent    = slopedAccent.value;
        m_volume    = slopedVolume.value;

    }

} // Close namespace Open303

PluginLoad(Open303UGens) {
    // Plugin magic
    ft = inTable;
    registerUnit<Open303::Open303>(ft, "Open303", false);
}

And the SuperCollider class file:

Open303 : UGen {

	// Required parameters:
	// Note number: int 0-127 (MIDI note number)
	// Note velocity: int 0-127 (MIDI note velocity. Sending velocity = 0 is note-off)
	
	// 'noteevent' parameter going positive will trigger a new Open303 note event (noteOn or noteOff)
	// noteevent must go positive for only one k-rate cycle for each note-on/off received!!
	// Defaults values from Open303VST.cpp starting at line 14
	*ar { | noteevent = 0.0, notenum = 60.0, notevel = 64.0, notealloff = 0.0, waveform = 0.85, cutoff = 0.229, resonance = 0.5, envmod = 0.25, decay = 0.5, accent = 0.5, volume = 0.9 |
      ^this.multiNew('audio', noteevent, notenum, notevel, notealloff, waveform, cutoff, resonance, envmod, decay, accent, volume);
    }

	checkInputs {		
		^this.checkValidInputs;
	}
}

Your comment here suggests it’s possible to have KR inputs to an AR plugin. I can’t work out how to do that, but maybe it would fix my problem if at least the ‘noteevent’ input was k-rate.

Assuming my inputs remain AR, I can’t work out how to index into the input block in my audio render loop, in order to test if MIDI (as opposed to OSC) events are in fact coming in between KR boundaries.

I’ve changed my render loop to the below, but now the plugin crashes the server (with no error reported) when I try and run it.

// Fill output buffer with nSamples Open303 synth samples
for (int i = 0; i < nSamples; ++i) {

    const int noteevent = static_cast<int>(in(i)[NOTEEVENT]);

    if(noteevent == 1 && lastnoteevent == 0) {
        // Check note velocity to determine if this is a note-on or note-off event
        if(notevel > 0) {
            // Note-ON
            // 3rd arg is 'detune'
            o303.noteOn(notenum, notevel, 0.0);
        } else {
            // Note-OFF (note-on event with velocity 0, slightly confusingly)
            o303.noteOn(notenum, 0, 0.0);
        }
    }
    // Update previous note-event
    lastnoteevent = noteevent;


    // Update synth params with interpolated value
    // Cast floats to doubles
    o303.setWaveform(   static_cast<double>(slopedWaveform.consume()));
    o303.setCutoff(     static_cast<double>(slopedCutoff.consume()));
    o303.setResonance(  static_cast<double>(slopedResonance.consume()));
    o303.setEnvMod(     static_cast<double>(slopedEnvmod.consume()));
    o303.setDecay(      static_cast<double>(slopedDecay.consume()));
    o303.setAccent(     static_cast<double>(slopedAccent.consume()));
    o303.setVolume(     static_cast<double>(slopedVolume.consume()));

    // Call Open303 render function
    outbuf[i] = o303.getSample();
}
Server 'localhost' exited with exit code 0.
server 'localhost' disconnected shared memory interface

This sounds like what you warned about above when you said

do not try to read an audio rate block from a non-ar input!!

but the inputs should all be AR, according to the class file.

If I add input-rate checking to my class file

checkInputs {		
		// If you want to do custom rate checking...
        if(this.rate == \audio and: { inputs.at(0).rate == \control }, {
          //^"All inputs should run at Audio rate. Input 0 seems to be running an Control rate. Something is wrong"
        });

		^this.checkValidInputs;
	}

I get the error above in the console when running my SC script, so it seems in(0) at least IS running at KR.

Of course it’s possible the note-handling in the Open303 source is broken. I’ve been assuming not, but I may be wrong…

It’s very common, for instance, to have a control-rate frequency input to an audio rate filter. Extremely common.

Most plugins when running at ar will interpolate kr values over the duration of the control block. Look for “slope” variables in calc functions.

But for triggers, interpolating may not be necessary.

hjh

I’m doing that for all the synth parameters, actually.

If I change the inputs to KR in the class file

*ar { | noteevent = 0.0, notenum = 60.0, notevel = 64.0, notealloff = 0.0, waveform = 0.85, cutoff = 0.229, resonance = 0.5, envmod = 0.25, decay = 0.5, accent = 0.5, volume = 0.9 |
      ^this.multiNew('control', noteevent, notenum, notevel, notealloff, waveform, cutoff, resonance, envmod, decay, accent, volume);
    }

I get an error

ERROR: Out  input at index  1 ( an Open303 ) is not audio rate

when running my test script.

What is being used in your cpp file, in() or in0()? If you are using in(), it is going to want audio rate. If you are using in0() it is going to want control rate.

If you look at the source code for SinOsc, for example, it has 4 different next functions, 1 for each possible input:

void SinOsc_next_ikk(SinOsc* unit, int inNumSamples);
void SinOsc_next_ika(SinOsc* unit, int inNumSamples);
void SinOsc_next_iak(SinOsc* unit, int inNumSamples);
void SinOsc_next_iaa(SinOsc* unit, int inNumSamples);

With the number of inputs you have, I would pick kr or ar for all inputs.

Sam

Oh, I thought “in0(0)” was the same as “in(0)[0]”.

Is it the case that if you use in0 in the KR loop, all inputs will then run at KRate, even if the plug-in class file specifies all inputs should be audio rate?

That seems odd.

If it’s an ar input, then the c++ side will get a pointer to a wire buffer. The calculation function may choose to read the entire wire buffer, or it may choose to use only the first sample. See, for example, EnvGen, where envelope breakpoints are read at control rate only, no matter what rate the user supplied.

You can decide per input, per calculation function, whether the input will read ar or kr – there’s no all or nothing here. A plugin may internally “promote” kr to ar by interpolation, or it may demote an ar input to kr by simply ignoring the wire buffer and reading only a single sample per block.

This isn’t changing the inputs to kr – it’s changing the output rate. The error is a warning that you supplied a kr unit as an ar input for Out, which isn’t allowed (because Out.ar needs the full 64 samples per block, but you’d be giving it only one).

My opinion here is: first version, audio rate output, control rate for all inputs. Get that working first. I don’t see a strong reason to deal with the combinatorics until the logic for kr parameters is solid. Then you could consider adding other calculation functions to support some ar inputs (though I suspect, in this case, that it would add complexity for perhaps not that much gain, so maybe not worth it).

If you’re expecting users to put a trigger control into noteevent, that is by definition control rate (I don’t think there is a TrigControl.ar). So users who are sequencing notes by .set-ting parameters (which is probably most of them) wouldn’t have access to audio rate triggers anyway – another reason to consider ar triggering as “nice to have” but perhaps secondary.

hjh

Hm, I’m not sure a single gate signal (velocity) can accurately represent poly notes on/off. (The 303 sound isn’t poly but tracking multiple keys is essentially polyphonic logic.)

I think you are right. What if two keys are pressed at the same exact time, but released at different time points? Since the note and velocity info is transmitted over UGen inputs, they can only hold a single piece of information at the time.

To be honest, I think that for polyphonic events you really need to use Unit commands (/u_cmd). They are terrible to work with, but that’s how I’ve implemented MIDI messaging for my VSTPlugin extension, for example.

Side note: one nice thing about Unit commands is that they are actually subsample accurate, i.e. you can use mWorld->mSampleOffset and mWorld->mSubsampleOffset to get the current (sub)sample offset when dispatching the command.

1 Like

Cool, that makes sense.

I’ve added debug lines to my SuperCollider script and plugin, so I can now see in the SC IDE console which MIDI notes are received, and what’s received by the plugin.

In SC:

m_on = MIDIFunc.noteOn({ |vel, num, chan, src|
	"SCLANG NOTEON note % \n".postf(num);
	bassline.set(\notenum, num);
	bassline.set(\notevel, vel);
	bassline.set(\noteevent, 1.0);
});

m_off = MIDIFunc.noteOff({ |vel, num, chan, src|
	"SCLANG NOTEOFF note %\n".postf(num);
	bassline.set(\notenum, num);
	bassline.set(\notevel, 0.0);
	bassline.set(\noteevent, 1.0);
});

And in my plugin KRate calculation function:

// Process note on/off messages
// Leave note-handling to Open303 builtin functions
if(noteevent == 1 && lastnoteevent == 0) {
    // Check note velocity to determine if this is a note-on or note-off event
    if(notevel > 0) {
        // Note-ON
        // 3rd arg is 'detune'. Alternative tunings not implemented, so we'll leave this at 0.0
        cout << "PLUGIN NOTE ON " << notenum << "\n";
        o303.noteOn(notenum, notevel, 0.0);
    } else {
        // Note-OFF (note-on event with velocity 0, slightly confusingly)
        cout << "PLUGIN NOTE OFF " << notenum << "\n";
        o303.noteOn(notenum, 0, 0.0);
    }
}

Which gives me something like this in the Console (once I’ve removed the duplicate lines and Server Errors messages):

SCLANG NOTEOFF note 24
PLUGIN NOTE OFF 24

SCLANG NOTEON note 19
PLUGIN NOTE ON 19

SCLANG NOTEOFF note 19
PLUGIN NOTE OFF 19

SCLANG NOTEON note 23
PLUGIN NOTE ON 23

SCLANG NOTEOFF note 23
PLUGIN NOTE OFF 23

SCLANG NOTEON note 24
PLUGIN NOTE ON 24

SCLANG NOTEOFF note 24
PLUGIN NOTE OFF 24

SCLANG NOTEON note 29
PLUGIN NOTE ON 29

SCLANG NOTEOFF note 29
PLUGIN NOTE OFF 29

SCLANG NOTEON note 28
PLUGIN NOTE ON 28

SCLANG NOTEOFF note 28
PLUGIN NOTE OFF 28

This was a period during which there was a stuck note. It looks like the plugin is receiving the correct information (note on/off messages and note-numbers appear to correlate, and there are no instances of MIDI notes being received in SCLang, and not being detected by the plugin).

This seems to suggest the problem may be in the Open303 note-handling code itself. I may have been barking up the wrong tree all this time…

Do you have any examples of Unit commands in use?

I’ve made a start on implementing note-handing in my SC script. Slide handling isn’t quite there at the moment, but I don’t seem to be getting any stuck notes.

The Open303 code provides lower-level separate methods for triggering notes and slides and releasing notes. These are protected object methods, but I’ve made them public and am triggering those directly from my SC script, bypassing Open303’s builtin note-handling.

Ultimately, it would be great to not have to write lots of extra SC code to handle MIDI notes gracefully, but my C++ chops aren’t great, so as a first step, if I can get it working well doing this in SCLANG, I’ll at least have an idea what might be going wrong in Open303’s code.

Do you have any examples of Unit commands in use?

Here’s the code for the VSTPlugin extensions: sc/src · master · Pure Data libraries / vstplugin · GitLab

But as I said, it’s not pretty… I’m actively working on improving the plugin API, including better unit command handling, but realistically it will take at least a few months before this could land in SC.

If it helps, here is a minimal mono synth template, basically doing what an analog mono synth does, favoring the last pressed note:

( // SynthDef
s.waitForBoot {
  SynthDef(\monosynth, { |out, freq = 440, amp, gate, bend, pan, portamento = 0.1, 
                          atk = 0.1, dec = 0.3, sus = 0.5, rel = 0.5|
    var env = Env.adsr(atk, dec, sus, rel).ar(gate: gate);
    var sig = SinOsc.ar(freq.lag2(portamento) * bend.midiratio);
    Out.ar(out, Pan2.ar(sig, pan, amp) * env);
  }).add;
};
)


(// start it
~noteStack = [];
~synth = Synth(\monosynth);

MIDIClient.init;
MIDIIn.connectAll;

MIDIdef.noteOn(\noteOn, { |vel, num|
  var freq = num.midicps;
  var amp = vel.linexp(0, 127, 0.05, 1);
  ~synth.set(\freq, freq, \amp, amp, \gate, 1);
  ~noteStack = ~noteStack.add(num);
});
MIDIdef.noteOff(\noteOff, { |vel, num|
  ~noteStack.remove(num);
  if (~noteStack.size > 0) {
    ~synth.set(\freq, ~noteStack.last.midicps);
  } {
    ~synth.set(\gate, 0)
  };
});
MIDIdef.bend(\bend, { |val|
  var bendSemitones = 12;
  ~synth.set(\bend, (val / 8192.0 - 1) * bendSemitones);
});
)



// stop it
~synth.free;
MIDIdef.freeAll;

Cool, thanks very much for that! I will have a go at implementing that in my plugin test script.

I’ve also found this (from everyone’s favourite open-source synth-related DSP guru):

…which might be good to add to my plugin, ultimately.

It’s a much more complicated setup than the above, or the note-handling that’s built in to Open303, but I suspect it does handle releases of multiple notes when other notes are held in a more realistic monosynth-like way.

I actually have a couple of hardware 303-like baseline synths (an MAM MB-33 and a newly-acquired Roland Aira T-8 with builtin 303 emulator), and several Shruthi-1s, so when I’m back home I can compare their slide behaviour.