Control hardware and server synth with one Pbind?

I’m trying to sync an external hardware synth and an internal synth using the same note events.

Here’s a boiled down example of what I’m trying to do:

MIDIClient.init;
m = MIDIOut(1).latency = 0.01;  
(
SynthDef(\tone, {|out=0, gate=1, freq=440|	
	var env = EnvGen.ar(Env.adsr(releaseTime:0.1 ), gate, doneAction:2);
	Out.ar(out, SinOsc.ar(freq!2, mul: env, mul:0.3));
}).add;
)

a = Pbind(\degree, Prand([1, 2, 3], inf), \dur, Pwhite(1/4, 1, inf));
(
(a <> (instrument: \tone)).play; 
(a <> (type: \midi, midiout: m)).play; 
)

I want the instrument notes and the midi notes sent out with the same timing and pitch. Duplicating the Pbind isn’t really an option since there’s some randomness involved.

The above code plays two different patterns in parallel, when I want both patterns to be identical.

Is this possible? Am I on the wrong path?

Thanks!

there is a thread over here that has some interesting ideas that might be relevant

Another approach is to use Pspawner

(
Pdef(\ptrn,
	Pspawner({arg ps;
		
		var degree = Prand([1, 2, 3], inf).asStream;
		var dur = Pwhite(1/4, 1, inf).asStream;
		
		inf.do({
			var d = degree.next;
			ps.par(Pn((instrument: \tone, degree:d), 1));
			ps.par(Pn((type: \midi, midiout: m, \degree: d), 1));
			ps.wait(dur.next);
		})
	})
).play;
)
Pdef(\ptrn).stop;

Hi,

not on the wrong path, but it’s one of those pattern questions, that sound very easy, but are tricky in detail. But first, there are two problems in the posted code:

If you want to set the latency, you’d have to do it in a different way, otherwise latency isn’t set (check with m.latency after evaluating the above line). Also I wouldn’t set the latency to such a low value, because the server latency is still 0.2 (however you could set both to 0.05 or so). With my setup I need MIDIOut(0), but of course that can be different on yours, e.g.

MIDIClient.init;
m = MIDIOut(0);
m.latency = 0.05;
s.latency = 0.05;

The second thing is the doubled definition of SinOsc’s mul arg in the SynthDef, e.g. say

(
SynthDef(\tone, {|out=0, gate=1, freq=440|
	var env = EnvGen.ar(Env.adsr(releaseTime: 0.1), gate, doneAction:2);
	Out.ar(out, SinOsc.ar(freq!2, mul: env));
}).add;
)

Now the problem of “doubling” the event data - it’s (again) a matter of differentiating Streams and Patterns. Some possibilities:

(1) Data sharing

See the chapter in James’ pattern guide:
http://doc.sccode.org/Tutorials/A-Practical-Guide/PG_06g_Data_Sharing.html

I’d use a Ptpar and the lag key to adjust the correct latency, the delay in the Ptpar ensures correct order of evaluation, with lag (lagtime in seconds) you can compensate, this value (0.025) compensates fine on my Mac using SimpleSynth via midi and IAC.

(
a = Pbind(
	// _ is partial application syntax, you could also write:
	// collect { |x| ~degree = x }
	\degree, Prand([1, 2, 3], inf).collect(~degree = _),
	\dur, Prand([2, 1, 1]/5 , inf).collect(~dur = _),
	\lag, 0.025
);

b = Pbind(
	// takes over the values from other stream when played
	\degree, Pfunc { ~degree },
	\dur, Pfunc { ~dur },
	\amp, 0.5,
	\type, \midi,
	\midiout, m,
)
)

Ptpar([0, a, 0.001, b]).play

(2) Using Pchain with Streams

Maybe a surprising solution (at least for me, first time I’ve done it this way - or I had forgotten it … see Pchain help file, last examples). If you chain the Streams, define the source with a Pstutter, then each random value will be produced twice, for the both streams that need it. ‘lag’ is also taken over, so redefine it in the midi pattern to keep the difference.

(
a = Pbind(
	\degree, Pstutter(2, Pxrand([1, 2, 3], inf)),
	\dur, Pstutter(2, Prand([2, 1, 1]/5 , inf)),
	\lag, 0.025
).asStream;


b = Pbind(
	\lag, 0,
	\amp, 0.5,
	\type, \midi,
	\midiout, m
) <> a
)

Ptpar([0, a, 0.001, b]).play

(3) Using PSdup from miSCellaneous lib

PSx patterns behave like Streams, they remember last items (events), PSdup is just duplicating the data of another PSx pattern, here PS(p). This solution looks very similar to (2), but you don’t have to use Pstutter.

(
p = Pbind(
	\degree, Prand([1, 2, 3], inf),
	\dur, Prand([2, 1, 1]/5 , inf),
	\lag, 0.025
);

a = PS(p);

b = Pbind(
	\lag, 0,
	\amp, 0.5,
	\type, \midi,
	\midiout, m
) <> PSdup(a)
)

Ptpar([0, a, 0.001, b]).play

PSx patterns are admittedly strange - but especially in a case like this they provide a quite straight solution.

… and (2) can be simplified a bit, we can stutter the whole event pattern:

(
a = Pstutter(2, 
	Pbind(
		\degree, Pxrand([1, 2, 3], inf),
		\dur, Prand([2, 1, 1]/5 , inf),
		\lag, 0.025
	)
).asStream;


b = Pbind(
	\lag, 0,
	\amp, 0.5,
	\type, \midi,
	\midiout, m
) <> a
)

Ptpar([0, a, 0.001, b]).play

If you want to set the latency, you’d have to do it in a different way, otherwise latency isn’t set (check with m.latency after evaluating the above line).

Both syntaxes work fine.

m = MIDIOut(0).latency_(0.05);
m.latency;
-> 0.05

n = MIDIOut(0).latency = 0.05;
n.latency;
-> 0.05

any_expression.method_(value) and any_expression.method = (value) compile to the same method call method_.

I think it’s clearer to write setter_ notation; in my first example above, it’s obvious that the return value should be the MIDIOut object, while in the second, it isn’t clear whether the return should be a MIDIOut or 0.05 ((x = 5).postln vs (Point(1, 2).x = 3).postln).

It’s correct that MIDIOut latency and server latency should match.

My preferred solution for the problem of “one pattern, multiple actions” is to make an event type that does both things.

(
Event.addEventType(\midisynth, { |server|
	Event.eventTypes[\note].value(server);
	Event.eventTypes[\midi].value(server);
});
)

MIDIClient.init;
m = MIDIOut(0).latency_(0.2);

p = Pbind(\type, \midisynth, \midiout, m, ...).play;

hjh

2 Likes

That’s indeed true, I must have seen a ghost syntax :slightly_smiling_face:

James’ solution seemed the most straightforward for me and worked perfectly.
Thank you all very much for your replies!

Ouch, I seem to hit a problem using the “addEventType” solution: the Pbind does not seem to send any note off messages. Is this expected? And if so, how can it be solved?

Definitely not expected. Also, not reproducible.

note-off

Pure Data receives note-off messages as “notes” with velocity 0. If SuperCollider were not sending note-off messages, then Pd wouldn’t produce any printed output such as “note: 60 0” – so SuperCollider is definitely sending the releases.

Since it isn’t reproducible in the basic case, I would guess that there might be something wrong with your usage of that composite event type (or perhaps something is overriding the MIDI event logic). But I can’t guess what that might be, since there’s no code example to look at.

hjh

Actually, I was doing a very basic test.
I’ll try again tomorrow to make sure I didn’t do anything stupid.

(
SynthDef(\plucking, {arg amp = 0.1, freq = 440, decay = 5, coef = 0.1, freqScale=1.0;
		var env, snd;
		env = EnvGen.kr(Env.linen(0, decay, 0), doneAction: 2);
		snd = Pluck.ar(
			in: WhiteNoise.ar(amp),
			trig: Impulse.kr(0),
			maxdelaytime: 0.1,
			delaytime: (freq*freqScale).reciprocal,
			decaytime: decay,
			coef: coef);
		Out.ar(0, snd!2);
}).add;

// be sure to first initialize ~midiout to something sensible and ~midichannel to 0

Pbind(\type, \midisynth,
	  \midiout, ~midiout,
	  \chan, ~midichannel,
	  \instrument, \plucking,
	  \midinote, Prand((36..55), inf).trace,
	  \dur, Pseq([Pn(Pgeom(1.0, 0.9, 10), 2), Pn(Pgeom(1.0, 0.9, 20),2)], 1),
	  \amp, Pbrown(0.1, 0.6, 0.1, inf),
	  \legato, 1,
).play; 
)

Prand may produce the same value twice in succession.

And, because \legato is 1, the current note’s note-on in the previous note’s note-off will be sent at exactly the same time.

So you may get a sequence such as:

  • Time A: Note-on 42
  • Time B: Note-on 42, note-off 42
  • Time C: Note-on 53, note-off 42

At B, there could be stuck notes.

Could you try \legato, 0.99 (or Pxrand to guarantee no repeated note numbers)?

hjh

Oh wait, I see the real problem.

if(hasGate and: ...) – and hasGate has been set previously by the \note event type function. The SynthDef doesn’t have a gate input – so this is false and it affects the MIDI event type.

I cheated a bit in there by calling the event type functions in the same identical event object. Probably should copy:

(
Event.addEventType(\midisynth, { |server|
	currentEnvironment.copy.use {
		Event.eventTypes[\note].value(server);
	};
	Event.eventTypes[\midi].value(server);
});
)

I think it’s not strictly necessary to copy the last one. The intent is to isolate side effects; the last one’s side effects are already naturally isolated from the previous ones.

That’s a good catch actually. Take a shortcut in program logic --> future bugs. Always :laughing:

hjh

1 Like

The new version works as expected! Thanks a lot for digging deeper.

Is there any way to make this (or something like this) work also for a Pmono (where I generate a \midinote key to influence the frequency sent to the synth)?

Using the \midisynth event type with Pmono sends midi information, but doesn’t seem to handle the internal synthesis properly.

This seems to do more or less what I hoped for:

	Event.addEventType(\monomidisynth, { | server |
		currentEnvironment.copy.use {
			Event.eventTypes[\monoSet].value(server);
		};
		Event.eventTypes[\midi].value(server);
	});
1 Like