Controlling multiple arguments of a SynthDef from Ableton via MIDI

I managed to send MIDI on/off notes from Ableton to SuperCollider via the macOS-internal IAC-Driver. With this, I can set for example the freq or amp argument of a SynthDef by the MIDI note number and its velocity.

What is the best practice to control multiple parameters of my SynthDef? Setting up more IAC-Driver busses and convert/scale the MIDI note number? Or are there any other UGens/Quarks I should have a look on? (Thinking about dewdrop_lib, LinkUGen, ???)

Thank you for your help…

A single IAC bus should support 127 CC numbers (and 16 MIDI channels, 16 x 127 = 2032 possible distinct continuous controllers), so I shouldn’t think it’s necessary to open more IAC buses.

MIDIFunc.cc and MIDIdef.cc have a ccNum argument specifying the controller number to listen to. I’d create one cc responder for each cc you need, each with its own action. I think this is the best way.

I have an old MIDI response framework but tbh I haven’t used it in a long time. MIDIFunc/def covers all the cases I need in a more straightforward way. I’d say (as the author of the ddwMIDI quark), don’t bother with it.

hjh

1 Like

This won’t be so useful for yourself @LuxEtObscuritas, but if anyone else reads this looking to do something similar…

The most idiomatic way for supercollider would be to use OSC to do cross application communication, this lets you communicate far more complex information.

However, on Reaper running on linux (can’t speak of other DAW or OSs), I have found OSC message to be capped at 30Hz (?) making it unusable for a lot of applications as the latency was far too high… all of this to say, MIDI is definitively the way to go as it is blazingly fast and most applications optimise for it.

1 Like

I tested the round trip latency of sending midi on the IAC bus from Ableton to SC and back to Ableton on another IAC bus channel using MIDIOut with a latency of 0. The roundtrip latency with a buffer size of 64 in Ableton is 6 ms on my system. The larger the buffer size the longer the latency, even if you are only sending MIDI from Ableton, which is not surprising but something to watch out for especially if you are also playing audio from Ableton and trying to sync this with SC. I haven’t tested this, it could be that the latency is handled transparently by Ableton so everything is still in sync, not sure.

1 Like

MIDI in SC is all client side, with no connection to the audio driver. So at least on our side, the audio hardware buffer size shouldn’t matter. If Ableton is making sound in response to the MIDI, then it would probably quantize to the hardware buffer.

hjh

Yes, I was doing the test without the server on (not that it should matter, but just be sure), so this is all about Ableton. When sending midi out the IAC bus from SC and receiving it back in SC the latency is very small, something like 0.22 ms.

I have written a poc which uses M4L to transfer automation curves from Ableton to SC via OSC, where the messages are used to controls parameters of a Ndef.
I wanted to use it for recording/looping automation with a controller in live coding settings but never got to use it so far. There are still some bugs on the M4L device (like the storing of parameter names) and I think the proper way to do this would be to write a VST with JUCE to make it more sustainable/maintainable, but maybe it helps you at least some bit.

SuperCollider code:

(
OSCdef(\abletonNdef, {|m|
	var ndef = Ndef(m[1].asSymbol);
	ndef.tryPerform(\set, m[2].asSymbol, m[3].asFloat);
}, path: "/ndef");
)


(
Ndef(\bass, {
	var clock = Impulse.ar(130/60);
	var seq = Dseq([2, 0, 0,2, 0.3, 0.5, 2.0, 0.5, 0.2, 0.0], inf);
	var sig = PMOsc.ar(
		carfreq: \baseFreq.kr(100.0) * (1 + Demand.kr(clock, reset: 0, demandUGens: seq)),
		modfreq: \modFreq.kr(200.0) * (1 + Demand.kr(clock, reset: 0, demandUGens: seq)),
		pmindex: 1.0,
		modphase: 0.5,
	);
	sig = sig * EnvGen.kr(Env.perc(0.0, releaseTime: 0.01), gate: clock);
	sig.dup;
}).play;
)

M4L patch


----------begin_max5_patcher----------
915.3oc2Y0zaaBCF9L4WAhyYQXi4qca6zNMs6QQUNINstBri.STZq1+8YafT
xBDbyRGgrpAEaie+34A+7Z22lX4rjumj6X+U641VVuMwxR2jpAqpmsbRw6Wk
fy0CyQP1KHqoBmokctCmwvoji5advh5tUMo6ayx5l1lQxILAVP4rGxHqDklG
Acm4N0t7Jvs7VzLW6EUuGcsdh3Ke9K9vCSFNSZcAI6ABCuLQ6GtU8wJRorDh
P63fpF2vYhb5q5ABTF4Tmp4vkyAuPTOInFSxgn9aYTbh824Iqe2oDqdhxdrQ
zA7hCmA8mZGCaFeAMhuR6HdYKo7UbblZ6PYB0M8+sWnF3umLQcYpg3UBcGY1
ZoG1Ffoydn9.FezLoiiBUdLBnu1EtDcNbAbNbwL..dlLLxCLKT5n.PbGdZqY
3MIbrbJpGSNdGY8CXgHitrPPd+2xqRxUYYUhLofv2T2bc6Mi8DN6w1S1GMrT
Ihoi4Z53Q8l+DOSXvrTGWsMGELpHW7RENT1ulHUe6RXUoDVQMdiYL96fmSaj
sCuybuE8tVfeyuRpt42NoK.7IS57NGoyEp+rFDB6xUak08Yy7jo4zJyIwdk0
Vpu9zqGLX2L0VPpV4qvdYq+MOoUBK7emOZjpj+EnJ4ojfLTUxKbzoJI+ml9F
EdqoJ4MGz2pDd9PSElPvATXBf7qDlP2tBSdWEgIuwsvDrWJGzybgIDZ.El.x
uh0BSAg2iBSv6IgIzEHLAUqRanvDbzscoHzs5tkf8gK9tlJJACGNQonnQvlk
fWEMI33VSp2xffPf4ZRv3gSSJx+tduRfQhjj1Nq3oRSINQ24mqIa5guAzKvc
7NF5XENPc8fsynTV84hbAcCc0AZ0GShw8Lk.0kedsyY+RhhFkyP.SxYvAOm0
hedsyY+fmaVNKvjTl6fmxBtFYLiJVz6BJVrZmmFUrXr+XqXQ+xuw+eeFFFgV
vSQqso77UFw8At5hyhOToeq3k2n6Tm5I3FpR6A8AJx5iM8ODx.VZefrdva8S
aBbUprGLpqru20riMurdPvvUVue0lzuKOno6nSYBbpTjeH.5ZVYXMKIrKonv
woRT2w1vU3vIPE.J+7R9CvH3xnJ8BFmvU2w1GCtzulCd61cjr7JOViWRz5Yd
l5wno5GorxG0KB5jQ1QyaFgN3LomJjtYQVYzrOn7n+cR4qIYrBZkdijiHMol
Inh77s3UkufhvL42S9ycap6B
-----------end_max5_patcher-----------
3 Likes

Thank you all!

@jamshark70 I see. I’m now trying to find the right way on how to do that…
Is still get problems here for the cc messages – the note on/off messages do work like expected.

I’m not sure if switching to a Ndef makes sense since my SynthDef behaves like a kind of one-shot instrument which is then being sequenced pretty fast. Maybe a pattern would be easier and more efficient? So another questions would be how to send midi values to a Pbind/Pdef/Pbindef?

(
// Setup MIDI
MIDIClient.init;

// connect to IAC-Driver and consider latency
MIDIIn.connect;

// Array with one slot per possible MIDI note
~notes = Array.newClear(128);

x = Synth(\plucked);
)

(
// MIDI Note-On
~on = MIDIFunc.noteOn({
	arg veloc, num, chan, src;
	~notes[num] = x.set(
		\freq, num.midicps,
	);
}, chan: 0);

// MIDI Note-Off
~off = MIDIFunc.noteOff({
	arg veloc, num, chan, src;
	~notes[num].release;
}, chan: 0);

// MIDI CC Func #1 for undefined CC channel #20
~cc1 = MIDIFunc.cc({
	arg veloc, num, chan, src;
	~notes[num] = x.set(
		\pan, veloc.linlin(0, 127, 0.0, 1.0),
	);
}, ccNum: 20, chan: 0);

// MIDI CC Func #2 for undefined CC channel #21
~cc1 = MIDIFunc.cc({
	arg veloc, num, chan, src;
	~notes[num] = x.set(
		\synthesisParameter1, veloc.linlin(0, 127, 0.0, 1.0),
	);
}, ccNum: 21, chan: 0);

~q1 = { ~on.free; ~off.free; ~cc1.free; };
)

// clear with:
~q1.value;

Also looks very interesting!
After loading the Max patch – how can I create an Ableton device? Do I need to use the standalone version or Abletons “Max Instrument” template from the browser?

One problem is that you’re mixing styles: x = Synth() is monophonic, but ~notes[num] is a way to implement polyphony. If you’re not using polyphony, then ~notes[num] is an unnecessary complication, and the usual results of unnecessary complication are confusion, and bugs. So I would remove that.

Every element of ~notes[num] is either nil, or the same synth = redundancy. x already holds the one-and-only synth so you don’t need other storage.

Now, think through the note on/off logic a bit more carefully. What if the player hits note 60, then note 62, overlapping slightly, like this:

  1. Start: on 60
  2. Start + 1: on 62 (set freq)
  3. Start + 1.05: off 60 (release but this isn’t the current note)
  4. Start + 2: off 62 (release but no sound).

So I think minimum logic here is to release when note off = the last note on value.

var lastOn;

~on = MIDIFunc.noteOn({
	arg veloc, num, chan, src;
	lastOn = num;
	x.set(
		\freq, num.midicps,
		\gate, 1  // shouldn't note-on reopen?
	);
}, chan: 0);

// MIDI Note-Off
~off = MIDIFunc.noteOff({
	arg veloc, num, chan, src;
	if(num == lastOn) {
		x.release;
	};
}, chan: 0);

In your MIDIFunc.cc objects, the function arguments are not velocity and note number – they’re val and cc number. You were putting into the note array based on cc number, which is conceptually mixed up. Even in a polyphonic context, this wouldn’t be right.

But tbh I can’t see a problem with setting the parameters in your cc objects? What is their actual result? (That is: “It isn’t working” usually doesn’t explain much. “I expected this, but it did that” is much more informative. Here, the “expected” is obvious; the “actually did” is not.)

Maybe try dumping server OSC messages (Server menu) to verify what messages are being sent in response to those MIDI ccs?

hjh

Excuse my unprecise statement…

Actually polyphony would be important for me. I was following the steps from the “Playing notes on your MIDI keyboard” section in the “Using MIDI” helpfile, so this was my approach…

Regarding the cc messages:
When I observe with
~cc1 = MIDIFunc.cc({ arg ...args; args.postln }, ccNum: 9, chan: 0);
I get this array
[ 7, 9, 0, 1156886449 ]
with changing values in the first index. At my first sight this seems to follow the same order of arguments as with the midiOn/Offs… Why is it not like this?

I then tried this

~cc1 = MIDIFunc.cc({
	arg veloc, num, chan, src;
	x.set(
		\pan, veloc.linlin(0, 127, 0.0, 1.0),
	);
}, ccNum: 9, chan: 0);

but I get tons of FAILURE IN SERVER /n_set Node 1010 not found (as well as with my first apporach) mixed with several versions of

[ 15, 1005, "pan", 0.826772 ]
[ 15, 1005, "freq", 146.832 ]

when dumping the OSC messages from the server.

I also get the same failures when I try your code, also after rebooting…

Feels like I am overseeing something very simple here >.<

When you call

x.release

this is probably freeing your Synth (it sets the gate to 0 – usually this is used to release the synth’s envelope and subsequently free it). So when you call

x.set(....)

again, the synth doesn’t exist anymore!

You could either do what James suggested and modify the SynthDef to not free itself when the gate is closed, and then reopen the gate on a note on message, or simply don’t release the synth on a note off message. If you post your SynthDef code here we could help you more specifically.

1 Like

What confused me is that you’re putting things into the ~notes array, but never using what you put in there.

x can hold only one synth. So when you do x.set, this cannot be polyphonic. So it turns out that x is the thing to delete, then.

For polyphony, each note-on needs to create an all new Synth instance. You cannot reuse an old synth instance with x.set there – x.set is changing the frequency of an old note, which is what you’d do in a mono synth.

 ~on = MIDIFunc.noteOn({
	arg veloc, num, chan, src;
	~notes[num] = Synth(\plucked,
		[\freq, num.midicps,
		... full arg list including control bus mappings...]
	);
}, chan: 0);

Your example follows ccnum 9. Your code logic then retrieves a synth with note number 9 and sets its parameter. Thus ccnum 9 will control notes with frequency 13.75 only, and ignore all other notes.

That is, it isn’t about order of parameters – it’s about their semantics. A ccnum does not mean the same thing as a note num. Your notes array is indexed in terms of note numbers :+1: so it is invalid to access synths from it based on cc numbers.

IMO in a polyphonic context, the easiest way to handle MIDI CCs is with control buses.

// MIDI CC Func #1 for undefined CC channel #20
~cc1Bus = Bus.control(s, 1);
~cc1 = MIDIFunc.cc({
	arg veloc, num, chan, src;
	~cc1Bus.set(
		veloc.linlin(0, 127, 0.0, 1.0),
	);
}, ccNum: 20, chan: 0);

// MIDI CC Func #2 for undefined CC channel #21

~cc2Bus = Bus.control(s, 1);

// btw you wrote cc1 for this one too
// fixing that... the variable name collision means that your .free doesn't free everything

~cc2 = MIDIFunc.cc({
	arg veloc, num, chan, src;
	~cc2Bus.set(
		veloc.linlin(0, 127, 0.0, 1.0),
	);
}, ccNum: 21, chan: 0);

Then the note on func should do ~notes[num] = Synth(\plucked, [freq: num.midicps, \pan, ~cc1Bus.asMap, \synthesisParameter1, ~cc2Bus.asMap]); .

hjh

1 Like

Mhh I see… What a mess I had here! Things are much clearer now, thank you so much!

Funny that it’s often kind of obvious in the end, but yeah – getting a second view on things is I guess the reason forums are made for… x)