How in the {func} do you make a monophonic MIDI synth?

This is something that still has me totally baffled…

I have solved this for my EWI since it’s a monophonic instrument, but not my keyboard controller.

I’m thinking of the classic analog monosynth behavior, where if you have any key pressed, the sound will sustain and if you have multiple keys pressed, it remembers the order they were played in for note off messages so when you release a key, the frequency goes back to the most recent, still active MIDI note number. The velocity is set from the first note of a “slur group” (no idea what else to call it, hope that makes sense).

I’m fairly new to sclang, so examples are helpful if it’s not too much to ask…

Here’s what I think I might need to do but I don’t really know how or if this is right:

  • make a function that puts the active notes into an array in the order noteOn is received
  • first noteOn sets envelope gate to 1 (synthDef initialized with gate=0) and velocity; these values are set until no notes are active, at which point the gate closes and then a new velocity value is set with the next noteOn and the gate is reopened. DoneAction:0 on the Env to keep synthDef alive
  • function sets the freq of a synthdef to the last MIDI note number value in the array
  • noteOff messages remove corresponding MIDI notes from the array

Is this the right direction? And if so how would I set up the array?

Apologies if this is posted incorrectly or if I overlooked a previous topic that answers this… I did look for one but was unable to find anything.

Easiest: Install the ddwVoicer quark and use MonoPortaVoicer.

hjh

1 Like

Thanks James. So I create all of the MIDIdefs and then use MonoPortaVoicer.new(1, \synthname)? Or does that go into the MIDIdef? A bit confused about how to work the MIDI part in.

A very common pattern in SuperCollider is to have a responder (GUI, MIDI, OSC, HID…) that controls a resource.

playerThingy = Something(....);

guiControl = AGuiWidget(...)
.action_({ |view|
	playerThingy.setSomeControl(... value based on view...);
});

This is a general pattern that can be used in many contexts… including this one. The resource is the MonoPortaVoicer. The MIDIdefs are the responders.

Also, a useful methodology in cases like this is to be figure out how to control the resource programmatically first – only after you’re sure you can get the desired result by code, then you can put that code into the responders. (A development principle of “solve one problem at a time” – instead of thinking “how do I use MIDI to control this object that I never used before” = two problems muddled into one, you could think “1/ how do I use this object? 2/ how do I do #1 with MIDI” = step by step, solve two problems in turn).

So, MonoPortaVoicer first:

(
SynthDef(\lead, { |out, gate = 1,
	freq = 440, freqlag = 0.1, detun = 1.008, pan = 0,
	ffreq = 1200, ffreqMul = 3, ffreqDcy = 0.15, rq = 0.2,
	atk = 0.005, dcy = 0.2, sus = 0.6, rel = 0.1|
	
	// Voicer uses 'gate' for velocity
	var amp = Latch.kr(gate, gate).linlin(0, 1, 0.05, 0.25);
	
	var freqs = Lag.kr(freq, freqlag) * [1, detun];
	
	var filtEg = EnvGen.kr(Env([ffreqMul, ffreqMul, 1], [0, ffreqDcy], \exp), gate);
	var eg = EnvGen.kr(Env.adsr(atk, dcy, sus, rel), gate, doneAction: 2);
	
	var sig = Saw.ar(freqs).sum;
	
	sig = RLPF.ar(sig, (ffreq * filtEg).clip(20, 20000), rq);
	
	Out.ar(out, Pan2.ar(sig, pan, amp * eg));
}).add;
)

v = MonoPortaVoicer(1, \lead);
v.portaTime = 0.15;

v.trigger(60.midicps, 0.5);  // plays C
v.trigger(64.midicps, 0.5);  // glides to E

// let go of E -- glides back to C
v.release(64.midicps);

// after this, no more keys are held
// so the note fully releases
v.release(60.midicps);

Then for MIDI, all that is necessary is to take the control statements (which were just worked out) and put them into the MIDIdefs:

(
MIDIdef.noteOn(\on, { |vel, num|
	v.trigger(num.midicps, vel/127);
});

MIDIdef.noteOff(\off, { |vel, num|
	v.release(num.midicps);
});
)

hjh

1 Like

Wow this is so helpful! Thank you so much for your time. I learned a lot by looking at some of your code. I read through your help file for voicer, but I think it’s just still being new to sclang that had me confused as to where it fit into the code and then how I could manipulate it, but thank you!
I’m coming to SuperCollider from PureData and VCV. SuperCollider seems much better suited for what I’m wanting to do with computer music

James, your quark is working beautifully, but I can’t figure out how to send cc messages with the noteOn, so that for example if I leave the mod wheel all the way at the top, the LFO depth is still where I left it. I tried adding those as arguments to the noteOn MIDIdef, but it just made a nasty popping sound. I tried putting v.set with my global variables in the noteOn MIDIdef. I also tried making an argument array in MonoPortaVoicer, but it still resulted in the same nasty popping sound.

(
MIDIClient.init;
MIDIIn.connectAll;
)
(
SynthDef(\lead, {
arg out, gate = 1,
freq = 261.61, freqlag = 0.1, detun = 1.008, pan = 0, bend=0,
ffreq = 1200, ffreqMul = 4, fAtk = 0.005, fDec = 0.01,
atk = 0.005, dcy = 0.2, sus = 0.6, rel = 0.1, mod=0, res=2.5;

var amp = Latch.kr(gate, gate).linlin(0, 1, 0.05, 0.25);

var peak = freq * ffreqMul;

var	lfo = LFTri.kr(5, 0, 24*(mod.midiratio - 1));
var pitch = freq * lfo.midiratio;

var freqs = Lag.kr(pitch * bend.midiratio, freqlag) * [0.05.midiratio,0.5];

var filtEnv = EnvGen.ar(Env.adsr(fAtk, fDec, 1, fDec, peak.clip(10,20000), -4, 1200), doneAction:2);
var eg = EnvGen.ar(Env.adsr(atk, dcy, sus, rel), gate, doneAction: 2);

var sig = Saw.ar(freqs, 0.1);

sig = MoogFF.ar(sig, (filtEnv).clip(20, 20000), res, mul:25.dbamp);

Out.ar(out, Pan2.ar(sig, pan, amp * eg));

}).add;
)

v = MonoPortaVoicer(1, \lead);
v.portaTime = 0.3;

(
MIDIdef.noteOn(\on, {
arg vel, num;
v.trigger(num.midicps, vel.lincurve(0,127,0,1));
});

MIDIdef.bend(\pitchbend, {
arg val;
~bend = val;
v.set([\bend, val.linlin(0,16383,-2,2)]);
}, chan:0);

MIDIdef.cc(\modwheel, {
arg vel;
~mod = vel.linlin(0,127,0,1);
v.set([\mod, ~mod]);
},1,0);

MIDIdef.noteOff(\off, {
arg vel, num;
v.release(num.midicps);
});
)

Also sorry I have no idea how to format this post correctly

The only missing piece is to make the pitch bend and lfo mod into “global controls.”

In a hardware synth, the mod wheel affects all voices equally (i.e. “globally” for that channel).

v.set, as you found, affects only the notes that are playing at this time.

v = MonoPortaVoicer( /* ... your stuff here ... */ );
v.mapGlobal(\bend, nil, 0, [-2, 2]);
v.mapGlobal(\mod, nil, 0, [0, 1]);

For the [-2, 2] etc., see ControlSpec.

BTW if you’re using global controls, make sure to v.free when you’re finished with the Voicer – without that, then the control buses for the global controls will stay allocated forever (which isn’t catastrophic, but it’s nicer to clean them up).

After that, the set statements should work as you have them.

Yeah… I’ve just started a thread to complain about the lack of formatting help on this forum. It seems that we assume a/ that you already know that the forum uses markdown for formatting and b/ that you already know markdown, or you know where to find a reference on markdown… which is really not a very welcoming a set of assumptions.

FWIW there are formatting buttons on the toolbar: quote-post, bold, italic, link, blockquote, code <<-- this one!, image upload, lists, smilies etc. So you could select a block of code and click the code formatting button (which is meant to be identified by a superficial resemblance to HTML/XML markup: </>).

I do it this way:

```
code
in
here
```

Using the backtick character.

hjh

1 Like