A few questions about SC in interaction with a MIDI Keyboard: How many SynthDefs, pitchbend, master volume

Hey there

I have the following code:

MIDIClient.init;
MIDIIn.connectAll;


(
SynthDef(\saw_1, {

	arg freq=440,gate=1,out=0,pitchBend=0,amp=0.7;

	var snd, env, feedb, lfo, noise, rev;

	env = EnvGen.ar(Env.adsr(0, 0, 0.6, 0.01), gate: gate, doneAction:2);

	feedb = LocalIn.ar(5);


	lfo = Latch.ar(SinOsc.ar(100,mul:3),Impulse.ar(40));

	noise = LFDNoise3.ar(3,1).tanh;

	snd = LFSaw.ar(freq+(pitchBend/10)+lfo+noise,mul:1)
	+LFSaw.ar(freq*0.985+(pitchBend/10)+lfo+noise,mul:0.3)
	+LFSaw.ar(freq*1.015+(pitchBend/10)+lfo+noise,mul:0.3);

	snd = snd+feedb;

	snd = snd/3;


	rev = Greyhole.ar(snd,2,0.3,10,0.6);

	LocalOut.ar(snd);


	snd = [snd,rev]*amp*(freq.cpsmidi/58)*env;

	Out.ar(out,snd);

}).add
)

MIDIFunc.free
MIDIIn.bend


(
var notes, onsaw_1, off, amp;


notes = Array.newClear(128);

onsaw_1 = MIDIFunc.noteOn({ arg veloc, num, chan, src;
	notes[num] = Synth(\saw_1, [\freq, num.midicps]);
});


off = MIDIFunc.noteOff({ arg veloc, num, chan, src;
    notes[num].release;
});

amp = MIDIFunc.cc({ arg val;
	"amp : ".post; val.postln;
	notes.do({arg synth;
		if( synth != nil , { synth.set(\amp, val * (1.0/128.0)) });
	});
},31);
)

I am a bit confused about how SynthDefs are initialised by the last section of my code (var notes, onsaw_1, off, amp;…)

If I evaluate this part once I can see one Synth appearing in the Node Tree when I hit a key on the MIDI keyboard. If I evaluate it twice there appear two Synths in the Node Tree for playing one key on the keyboard.

Well, I can understand the logic behind this, but somehow I would have expected that the previous evaluation is overwritten when I evaluate the code once again.
What can I do to clear the previous evaluation so that synths aren’t added?
The only idea I could figure out so far was to reboot the interpreter…

But as a matter of fact I really like the sound when two of these SynthDefs are added. How can I create this sound with one evaluation of the code?
I tried something like:

onsaw_1 = MIDIFunc.noteOn({ arg veloc, num, chan, src;
	notes[num] = Synth(\saw_1, [\freq, num.midicps]);
});

onsaw_2 = MIDIFunc.noteOn({ arg veloc, num, chan, src;
	notes[num] = Synth(\saw_1, [\freq, num.midicps]);
});

But then the keys aren’t released.

I couldn’t quite figure out how to create a well-working pitchbend.
So if someone could lend me his/her code I would really appreciate that.

I would like to be able to control the overall amplitude through my MIDI keyboard. You can see this in the very last part of my code
(amp = MIDIFunc.cc({ arg val;…).
It’s actually possible to change the volume of a note I have played. But that way I can’t change the volume before a note is played.
What could I do about that?

Thanks a lot.

You’re creating an object that is registered (permanently – well, semi-permanently) to receive external messages. Note that it’s the MIDIFunc that is registered, not the variable. Re-evaluating the code discards the old on and off variables but it doesn’t unregister the MIDIFunc objects. You would have to do that explicitly by freeing them.

I think cmd-. will remove them, actually. That was done intentionally for this case – “I created some responders and now I just want to reset.”

But there’s an even better way: MIDIdef. If you create a MIDIdef.noteOn(\on, ...), you can re-evaluate as many times as you want, but there will only ever be one MIDIdef with the name \on. The second, third etc runs will overwrite the old one, but not add more responses. (Then you don’t need the on/off variables.)

You’re creating two synths, but only the second one is retained in the array.

Better solution is to create a SynthDef that implements detuning and/or phase shift, and still play only one synth.

  1. Convert pitch bend values to a semitone ratio (see midiratio).

  2. Write this to a control bus.

  3. The SynthDef should have a pitch bend argument, which you map to the control bus (Synth(\mySynth, [freq: ..., bend: bendBus.asMap])).

Control bus mapping is probably the easiest way for this too.

But I use a volume synth instead. The note synth may respond to velocity, but a “main volume” for the instrument is a separate synth (so, all mixing is standardized).

hjh

Cool, yeah MIDIdef really works.

I am sorry, how can I write it to a control bus?
The part concerning MIDI looks like this now:

(
var notes, val;

notes = Array.newClear(128);

MIDIdef.noteOn(\on, { arg veloc, num, chan, src;
	notes[num] = Synth(\saw_1, [\freq, num.midicps]);
});


MIDIdef.noteOff(\off, { arg veloc, num, chan, src;
    notes[num].release;
});

MIDIdef.cc(\amp, { arg val;
	"amp : ".post; val.postln;
	notes.do({arg synth;
		if( synth != nil , { synth.set(\amp, val * (1.0/128.0)) });
	});
},31);


MIDIdef.bend(\bend, { arg val;
	"Bend : ".post; val.postln;
	notes.do({arg synth;
		if( synth != nil , { synth.set(\bend, val.midiratio ) });
	});
});
)

Just in case anybody likes kind of distorted sounds, here is how my SynthDef looks like now :wink:

(
SynthDef(\saw_1, {

	arg freq=440,gate=1,out=0,pitchBend=0,amp=0.7,phsh;

	var snd, env, feedb, feedb_2, lfo, noise, rev;

	env = EnvGen.ar(Env.adsr(0, 0, 0.6, 0.02), gate: gate, doneAction:2);

	feedb = LocalIn.ar(10);

	phsh= SinOsc.kr(1,mul:3);


	lfo = Latch.ar(SinOsc.ar(100,mul:3),Impulse.ar(40));

	noise = LFDNoise3.ar(3,0.5).tanh;

	snd = LFSaw.ar(freq+(pitchBend/10)+lfo+noise,phsh,mul:1).tanh
	+LFSaw.ar(freq*0.985+(pitchBend/10)+lfo+noise,phsh,mul:0.3).tanh
	+LFSaw.ar(freq*1.017+(pitchBend/10)+lfo+noise,phsh,mul:0.3).tanh
	+LFSaw.ar(freq*0.98+(pitchBend/10)+lfo+noise,phsh,mul:0.2).tanh
	+LFSaw.ar(freq*1.02+(pitchBend/10)+lfo+noise,phsh,mul:0.2).tanh;


	snd = snd+feedb;

	snd = snd/3;


	rev = Greyhole.ar(snd,4,0.1,20,1,0.7,1,5);

	rev = RLPF.ar(rev,freq*25,0.1);

	LocalOut.ar(snd);


	snd = [snd,rev]*amp*(freq.cpsmidi/55)*env;


	Out.ar(out,snd);

}).add
)

Step 1, see the Bus help file :wink:

OK, so, taking pitch bend as an example.

First, I noticed that you did freq+(pitchBend/10).

You can use + for a linear scale. MIDI note numbers are linear: +12 is always up an octave, no matter which note you’re starting from.

Frequency is an exponential scale. +12 is up an octave if you’re starting with 12 Hz. But, starting from 1200 Hz, +12 is not even 1/5 of a semitone. An octave is always times 2. Looking at it the other way: MIDI pitch bend values range +/- 8191 – so pitchBend/10 is +/- 819.1. If the note’s frequency is below 819 and you pull the pitch wheel all the way down, then you get negative frequencies. Pretty sure that isn’t what you want.

Oh wait, actually it’s worse – you’re passing 8191.midiratio = 3.0064769102887e+205, or 3 followed by 204 zeroes. No no no, don’t do that.

To “bend” an exponential scale, you need to multiply by something. midiratio returns a ratio, which also implies multiplication.

The usual default setting for pitch bend is +/- 2 semitones. So, first you have to convert the MIDI pitch bend range into the desired number of semitones: val / 8192 * semitones. Then midiratio to get the amount by which to multiply frequency.

Also adding the control bus logic here:

(
var notes = Array.newClear(128);

if(~bendBus.isNil) {
	~bendBus = Bus.control(s, 1);
};

MIDIdef.bend(\bend, { |val|
	// assuming 2 semitones here
	~bendBus.set((val * (2 / 8192)).midiratio);
});

MIDIdef.noteOn(\on, { arg veloc, num, chan, src;
	notes[num] = Synth(\saw_1, [
		\freq, num.midicps,
		\pitchBend, ~bendBus.asMap
	]);
});

... etc.
)

And in the SynthDef, instead of freq+(pitchBend/10), write freq * pitchBend.

snd = can be optimized rather a lot. freq * pitchBend will appear five times, meaning the multiplication will be done five separate times – but the result is always the same – so you should save this multiplication in a variable and reuse the single calculation. Same for lfo + noise.

snd = LFSaw.ar(freq+(pitchBend/10)+lfo+noise,phsh,mul:1).tanh
	+LFSaw.ar(freq*0.985+(pitchBend/10)+lfo+noise,phsh,mul:0.3).tanh
	+LFSaw.ar(freq*1.017+(pitchBend/10)+lfo+noise,phsh,mul:0.3).tanh
	+LFSaw.ar(freq*0.98+(pitchBend/10)+lfo+noise,phsh,mul:0.2).tanh
	+LFSaw.ar(freq*1.02+(pitchBend/10)+lfo+noise,phsh,mul:0.2).tanh;

// vs

var bentFreq = freq * pitchBend;
var modulation = lfo + noise;

snd = LFSaw.ar(bentFreq + modulation, phsh, mul: 1).tanh
	+ LFSaw.ar(bentFreq * 0.985 + modulation, phsh, mul: 0.3).tanh
	+ LFSaw.ar(bentFreq * 1.017 + modulation, phsh, mul: 0.3).tanh
	+ LFSaw.ar(bentFreq * 0.98 + modulation, phsh, mul: 0.2).tanh
	+ LFSaw.ar(bentFreq * 1.02 + modulation, phsh, mul: 0.2).tanh;

hjh

Thanks a lot for your thorough explanation!
Now I understand it :slight_smile:

I have another question in this area.

I would like to use multiple MIDI keyboards and use specific sounds just for one keyboard.

I guessed that I could limit it to one keyboard by srcID, which would look like this:

MIDIdef.noteOn(\on, { arg veloc, num, chan, src;
	notes[num] = Synth(\saw_1, [
		\freq, (num-2+36).midicps,
		\pitchBend, ~bendBus.asMap,
		\amp, ~ampBus.asMap
	]);
},srcID:1);

But that way I am not able to produce any sound. I tried various integers for the srcID without success.

I am on Linux. When I am evaluating “MIDIClient.init” I receive the following:

-> MIDIClient
MIDI Sources:
	MIDIEndPoint("System", "Timer")
	MIDIEndPoint("System", "Announce")
	MIDIEndPoint("Midi Through", "Midi Through Port-0")
	MIDIEndPoint("VI49", "VI49 MIDI 1")
	MIDIEndPoint("VI49", "VI49 MIDI 2")
	MIDIEndPoint("SuperCollider", "out0")
	MIDIEndPoint("SuperCollider", "out1")
	MIDIEndPoint("SuperCollider", "out2")
	MIDIEndPoint("SuperCollider", "out3")
	MIDIEndPoint("SuperCollider", "out4")
	MIDIEndPoint("SuperCollider", "out5")
MIDI Destinations:
	MIDIEndPoint("Midi Through", "Midi Through Port-0")
	MIDIEndPoint("VI49", "VI49 MIDI 1")
	MIDIEndPoint("VI49", "VI49 MIDI 2")
	MIDIEndPoint("SuperCollider", "in0")
	MIDIEndPoint("SuperCollider", "in1")
	MIDIEndPoint("SuperCollider", "in2")
	MIDIEndPoint("SuperCollider", "in3")
	MIDIEndPoint("SuperCollider", "in4")
	MIDIEndPoint("SuperCollider", "in5")

So what to do?

Where did you get the 1? Maybe you guessed it’s an index into the list of devices, but – for MIDI input, you’re interested in MIDI sources. MIDIClient.sources[1] points to MIDIEndPoint("System", "Announce"), which seems unlikely to be the one you wanted. Your keyboard is at index 1 in “MIDI Destinations,” but these are the devices to which you can send MIDI output, so that part of the list is not relevant to receiving MIDI.

srcID is supposed to be the uid field of the MIDIEndPoint object representing the device.

So, srcID: MIDIClient.sources[theIndexOfYourDevice].uid.

hjh

Yeah, was trying other integers than “1”, too. But without success.

I didn’t know I had to use “MIDIClient.sources[theIndexOfYourDevice].uid” after srcID, but now it works great.

Thanks!

The documentation says “srcID: An Integer corresponding to the uid of the MIDI input.” That’s accurate as far as it goes – unfortunately, it goes on to say “See MIDIClient” and MIDIClient explains exactly nothing about what a UID is. So, yeah, there wasn’t much information for you to go on.

In any case, UID (unique identifier) is not the same as an index.

I’ll update the docs.

hjh

Hello

I have a question to the pitchbend.

So I have this code:


if(~bendBus.isNil) {
	~bendBus = Bus.control(s, 1);
};

MIDIdef.bend(\bend, { |val|
	// assuming 2 semitones here
//	"bend : ".post; (val * (2/8192)).cpsmidi.postln;

	~bendBus.set((val * (2 / 8192)).midiratio);
});

MIDIdef.noteOn(\bla, { arg veloc, num, chan, src;
	notes[num] = Synth(\bla_1, [
		\freq, (num-2-12).midicps,
		\pitchBend, ~bendBus.asMap,
		\amp, ~ampBus2.asMap,
	]);
},(52..63));

At least to me it seems like the pitchbend is initialized in a way that everything is transposed down two semitones. When I move the pitchbend around it goes to the center.

What could I do that the pitchbend initializes without transposition?

I tried something like:

~bendBus = Bus.control(s, 1).set("some value");

But that didn’t have the effect I was looking for.

Doesn’t ~bendBus = Bus.control(s, 1).set(1) work? Because, if you want no transposition, that means multiplying the frequency by 1 – so you should initialize the bus to 1.

hjh

So my computer/software/keyboard are:

Thinkpad T430 with Manjaro Linux, SC 3.11.1 and an Arturia midi keyboard.

Let’s take this basic example:

MIDIClient.init;
MIDIIn.connectAll;

(
SynthDef(\test, {
	
	arg freq, gate= 1, out=0, pitchBend=1;
	
	var osc, env;
	
	env = EnvGen.ar(Env.asr(0.1,0.3,0.1),gate: gate, doneAction:2);
	
	osc = SinOsc.ar(freq*pitchBend) * env;
	
	Out.ar(out,osc!2);
	
}).add
)

(
var notes;

notes = Array.newClear(128);

if(~bendBus.isNil) {
	~bendBus = Bus.control(s, 1).set(1);
};

MIDIdef.bend(\bend, { |val|
	
	~bendBus.set((val * (2 / 8192)).midiratio);
});

MIDIdef.noteOn(\midi_on, { arg veloc, num, chan, src;
	notes[num] = Synth(\test, [
		//experience tells me I have to write (num-2) to compensate for the pitchBend
		\freq, (num-2).midicps,
		\pitchBend, ~bendBus.asMap,
		]);
	});

MIDIdef.noteOff(\midi_off, { arg veloc, num, chan, src;
		notes[num].do { |oneSynth| oneSynth.release }

});
)

When I evaluate all this code and just start playing on the keyboard at least here everything starts at 2 semitones lower as if the pitchbend is initialized at its lowest possible value. When I then just move the pitchbend around a little it goes to the center.

Can you reproduce that behaviour on your side?
For me it would just be a little more convenient if it started on a centered value from the beginning.

Oh, OK, I see what it is.

I made a mistake initially. I forgot that the pitch bend data range is 0 - 16383, and not -8192 to +8191 as I first thought.

So the right formula is ~bendBus.set(((val - 8192) * (2 / 8192)).midiratio);.

With that, then it should no longer be necessary to write (num-2).midicps – just num.midicps should be fine.

hjh

Thanks! Now it works they way I was looking for!