CC messages mysteriously being sent twice

I have written a couple of classes for generating CC from audio input and providing a GUI to control the envelopes.

Class 1 (BasicCCFollower) contains a synth that reads audio and writes a control signal to a bus, and can output a midiCC message based on the control value.

Class 2 (DuoBusCCGen) contains a synth that takes 2 control busses and can output a midiCC message.

The weird thing is that in the CC write part, for Class 2 it is often (though not always) sending 2 CC messages for each value, while this isn’t happening for Class 1. This is happening even though the relevant code is only being evaluated once (at least according to the postln in the same block).

The relevant midi code is identical so I have no idea why the difference. Wondering if it’s something to do with how the code is called / system latency, but have tried changing execution order and wait time for the loop that calls it but no difference.

Any help would be much appreciated!

//levelccgens.sc - classes

BasicCCFollower {

	var s, midiClient, ccChan, ccNum, name, <audioIn, attackTime, releaseTime, maxScale, maxAttack, maxRelease, scale = 15, textLiveCC = "0", <isActive=true, scaleNumBox, scaleKnob, gui, <bus, <audioOut, <>follower, >w, lastVal = 0.0;
	*new { | s, midiClient, ccChan, ccNum, name, audioIn, attackTime, releaseTime, maxScale, maxAttack, maxRelease|
		^super.newCopyArgs(s, midiClient, ccChan, ccNum, name, audioIn, attackTime = 5, releaseTime = 5, maxScale = 40, maxAttack = 10, maxRelease=10 )
	}

	gui {
		Knob.defaultMode = \vert;
		gui = VLayout(
			HLayout(
				VLayout(
					/*textScale = StaticText().string_(textScale),*/

					HLayout(

						Knob().action_({|knob|
							attackTime = knob.value * maxAttack;
							follower.set(\attack, attackTime);
							follower.get(\attack, { arg value; ("attack is now:" + value.round(0.05)).postln; });
							// postln(follower.get(\attack, attackTime));
							// attackTime.string_(knob.value.round(0.01,))
						}).value_(attackTime / maxAttack),
						Knob().action_({|knob|
							releaseTime = knob.value * maxRelease;
							follower.set(\release, releaseTime);
							follower.get(\attack, { arg value; ("release is now:" + value.round(0.05)).postln; });
							// ~k2.string_(knob.value.round(0.01,))
						}).value_(releaseTime / maxRelease),
				), VLayout(
						textLiveCC = StaticText().string_(textLiveCC),
						scaleNumBox = NumberBox().action_({|num|
							scale = num.value.round(0.01);
							// textLiveCC.string = "CC value - " ++ scale;
							scaleKnob.value = scale / maxScale;
						}).value_(scale / maxScale),
						scaleKnob = Knob().action_({|knob|
							scale = (knob.value * maxScale).round(0.01);
							follower.set(\amp, scale);
							// textLiveCC.string = "CC value - " ++ scale;
							scaleNumBox.value = scale;
					}).value_(scale / maxScale),
					),
					Button().states_([[name, Color.green],[name, Color.red]]).action_( {
						isActive = isActive.not;
					})
			))
		);
		^ gui
	}

	setup {
		bus = Bus.control(s, 1);
		follower = SynthDef.new(\follower, {
			arg out;//, audioIn, scale;
			Out.kr(out, EnvDetect.ar(
				SoundIn.ar(\in.ar(audioIn)) * \amp.kr(scale), // Audio In
				\attack.kr(attackTime),// Attack
				\release.kr(releaseTime))); //Release
		}).play(target:s, ///... and play it on the server...
		args: [   //... with some arguments...
			\out, bus.index //, \audioIn, audioIn, \scale, scale // ... including the target bus
		]);
		("setup of "+ name + "complete").postln;

	}

	createFollower {
		follower = SynthDef.new(\follower, {
			arg out, audioIn = audioIn;
			Out.kr(out, EnvDetect.ar(SoundIn.ar(\in.ar(audioIn)),4,5));
		}).play(target:s, ///... and play it on the server...
			args: [   //... with some arguments...
				\out, bus.index // ... including the target bus
		]);

	}

	runFollower {
		bus.get( 	// getting a bus value is asnychronous, so the method takes a callback...
			{
				arg val; // ...whose argument is the bus value.

				var scaled; // the value is in [-1, 1], so we want to rescale it
				scaled = (val * 127).asInteger;

				scaled = if (scaled > 125, { 127 }, { scaled } );
				scaled = if (scaled < 2, { 0 }, { scaled } );


				isActive.if({
					while ({scaled != lastVal}, {
						lastVal = scaled;
						("scaled: " + scaled + "- lastval: " + lastVal).postln;

						midiClient.control(ccChan, ccNum, scaled);
						{
							textLiveCC.string = "CC value - " ++ scaled;
						}.defer
					} )
				} );
			}
		);
	}

}

DuoBusCCGen {

	var s, midiClient, ccChan, ccNum, name, <>bus1, <>bus2, scale = 0.5, maxScale = 1.0, textLiveCC = "0", <isActive=true, scaleNumBox, scaleKnob, gui, <bus,  >w, lastVal = 0.0, <>controller, menu, polarity = true, blend=0.5, blendKnob, blendNumBox;

	*new { | s, midiClient, ccChan, ccNum, name, bus1, bus2 |
		^super.newCopyArgs(s, midiClient, ccChan, ccNum, name, bus1, bus2 )
	}

	gui {
		Knob.defaultMode = \vert;
		gui = VLayout(
			HLayout(
				VLayout(
					/*textScale = StaticText().string_(textScale),*/



					menu = PopUpMenu(w, Rect(10, 10, 180, 20)).items_([
						"Default proportional blend",
						"Inverted proportional blend",
						"MACD",
						"Inverted MACD"
					]).action_({|menu|
						switch(menu.value,
							0, // Default proportional blend
							{
								controller.set(\center,0.0);
								controller.set(\bus1Polarity,1.0);
								controller.set(\bus2Polarity,1.0);
							},
							1, // Inverted proportional blend
							{
								controller.set(\center,1.0);
								controller.set(\bus1Polarity,-1.0);
								controller.set(\bus2Polarity,-1.0);
							},
							2, // MACD
							{
								controller.set(\center,0.5);
								controller.set(\bus1Polarity,1.0);
								controller.set(\bus2Polarity,-1.0);
							},
							3, // Inverted MACD
							{
								controller.set(\center,0.5);
								controller.set(\bus1Polarity,-1.0);
								controller.set(\bus2Polarity, 1.0);
						})
					}),

					HLayout(textLiveCC = StaticText().string_(textLiveCC),
						blendNumBox = NumberBox().action_({|num|
							blend = num.value.round(0.01);
							// textLiveCC.string = "CC value - " ++ scale;
							blendKnob.value = blend;
						}).value_(blend)
					),

					blendKnob = Knob().action_({|knob|
							blend = (knob.value * maxScale).round(0.01);
							controller.set(\busMix, blend);
							// textLiveCC.string = "CC value - " ++ scale;
							blendNumBox.value = blend;
					}).value_(blend),

					/*scaleKnob = Knob().action_({|knob|
						scale = (knob.value * maxScale).round(0.01);
						controller.set(\scale, scale);
						// textLiveCC.string = "CC value - " ++ scale;
						scaleNumBox.value = scale;
					}).value_(scale / maxScale),*/

					Button().states_([[name, Color.green],[name, Color.red]]).action_( {
						isActive = isActive.not;


					})
			))
		);
		^ gui
	}

	createController {
		bus = Bus.control(s, 1);
		controller = SynthDef.new(\ccFunc, {
			arg out;
			Out.kr(out, \center.kr(0.0) + (\staticScalar.kr(0.5) * \dynamicScalar.kr(1.0) * (
				(In.kr(bus1,1) * (1 - \busMix.kr(0.5)) * \bus1Polarity.kr(1.0) )
				+ (In.kr(bus2,1) * \busMix.kr(0.5) *\bus2Polarity.kr(1.0) )
			))
			) ;
			//MACD - bipolar - center = 0.5
			//Simple added averages - unipolar center = 0
			// Out.kr(out, In.kr(bus1,1)*0.5 - In.kr(bus2,1)*0.5) ;
		}).play(target:s, ///... and play it on the server...
			args: [   //... with some arguments...
				\out, bus.index // ... including the target bus
		]);

	}

	runController {
		bus.get(
			// getting a bus value is asnychronous, so the method takes a callback...
			{
				arg val; // ...whose argument is the bus value.

				var scaled; // the value is in [-1, 1], so we want to rescale it
				scaled = (val * 127).asInteger;

				scaled = if (scaled > 125, { 127 }, { scaled } );
				scaled = if (scaled < 2, { 0 }, { scaled } );


				isActive.if({
					while ({scaled != lastVal}, {
						lastVal = scaled;
						("scaled: " + scaled + "- lastval: " + lastVal).postln;

						midiClient.control(ccChan, ccNum, scaled);
						{
							textLiveCC.string = "CC value - " ++ scaled;
						}.defer
					} )
				} );
			}
		);
	}

}
/////////////////////////////// - live scd file

r = Routine {
	s = Server.default;
	s.options.numInputBusChannels = 20;
	s.options.numOutputBusChannels = 2;
	s.boot;

	s.sync; // wait for the server to be ready
	MIDIClient.init; // initialize MIDI
	m = MIDIOut(0);  // get the first output device


	//create classes
	~m1 = BasicCCFollower(s, m, 1, 30, "line 1", 0);
	~m2 = BasicCCFollower(s, m, 1, 31, "line 2", 0);

	//add classes to array
	l = Array.with(
		~m1,
		~m2
	);
	
//setup from array
	l = l.do({ arg item, i; item.setup });

	~e1 = DuoBusCCGen(s, m, 2, 10, "10 : 1-2", ~m1.bus, ~m2.bus);
	~e2 = DuoBusCCGen(s, m, 2, 11, "11 : 1-2", ~m1.bus, ~m2.bus);



	e = Array.with(
		~e1,
		~e2
	);

	e = e.do({ arg item, i; item.createController });
	
	//make gui
	w = Window.new("Envelope followers").layout_(
		VLayout(
		HLayout(
			*l.collect({ arg item, i; item.gui })

		),
		HLayout(
			*e.collect({ arg item, i; item.gui })
		))
	).front;

	s.meter;

	q = Routine({
		l.do({ arg item, i; item.runFollower });
                e.do({ arg item, i; item.runController });
		0.05.wait;
	}).loop.play;
	//end of routine
}.play

// play routine to initialise everything above, then call next to actually run
r.next; 

Tricky… I don’t immediately see the cause of duplicated messages either.

The first troubleshooting technique I would try is to simplify the scenario.

Does it happen when running only one DuoBusCCGen (instead of two)? If the problem never happens with only one, but does happen with two, then the issue must be an interaction between the two instances. But if it happens with one, then you can simplify the test case.

Does it happen if you replace the two BasicCCFollower data sources with separate control buses? You could even set them to constant values. It isn’t important, when testing DuoBusCCGen, that the data come from the envelope follower specifically. (And again, this could be a useful differential test. If the issue happens only when BasicCCFollower is involved, then we would look for some confusion between the instances – maybe one is responding inappropriately to a server reply meant for another. But, if the issue happens with simple buses as well, then you can simplify the test.)

I would also slow down the loop so that you can observe every interaction discretely. 50ms, I could easily imagine messages crossing. But if it’s 1 or 2 seconds between cycles, if the problem still happens, then it narrows down the field of possible causes.

I did notice a separate thing in DuoBusCCGen.

controller = SynthDef.new(\ccFunc, {
	arg out;
	Out.kr(out, \center.kr(0.0) + (\staticScalar.kr(0.5) * \dynamicScalar.kr(1.0) * (
		(In.kr(bus1,1) * (1 - \busMix.kr(0.5)) * \bus1Polarity.kr(1.0) )
		+ (In.kr(bus2,1) * \busMix.kr(0.5) *\bus2Polarity.kr(1.0) )
	))
	) ;

This is hard coding a specific bus1 and bus2 into a general SynthDef – same SynthDef name for all DuoBusCCGen instances, but different buses. Better practice would be to pass in the bus numbers as arguments:

controller = SynthDef.new(\ccFunc, {
	arg out, bus1, bus2;
	Out.kr(out, \center.kr(0.0) + (\staticScalar.kr(0.5) * \dynamicScalar.kr(1.0) * (
		(In.kr(bus1,1) * (1 - \busMix.kr(0.5)) * \bus1Polarity.kr(1.0) )
		+ (In.kr(bus2,1) * \busMix.kr(0.5) *\bus2Polarity.kr(1.0) )
	))
	) ;
}).play(target:s, ///... and play it on the server...
	args: [   //... with some arguments...
		\out, bus.index,
		bus1: bus1, bus2: bus2
	]
);

This may or may not cause a real problem because you’re always rebuilding the SynthDef before playing it, but I think it’s a better practice, if something will be different within different instances of the same SynthDef, to pass them as arguments.

Also… why is runController using while?

				isActive.if({
					while ({scaled != lastVal}, {

There is no separate else for isActive, so this could be:

if(isActive and: { scaled != lastVal }) {
...
}

In theory the while here should be running only once, but if a looping construction isn’t needed, I’d say don’t use it.

hjh

1 Like

Thanks for the review. I’ve implemented your suggestions - and was happy to find the and operator as couldn’t find this in docs before.

However, now I’ve tried with just one DuoBusCCGen using 2 simple buses (a constant and a slow lfo) - and no joy, tried at 1s, 5s etc. Just to clarify, for each posting of new cc value in postln 2 cc messages are sent (with very small time difference between, MOST of the time but not always). IT’S SO WEIRD!

I can’t see the issue in the SC code. Agreed, weird.

Which OS are you using? (The C++ implementation uses CoreMIDI on Mac, PortMIDI in Windows and ALSA MIDI in Linux, i.e. different functions per OS. It’s possible that one OS could have an outgoing MIDI bug that the others don’t have.)

Also, what is receiving the messages and how are you detecting the doubled messages? That is, there’s one other possible explanation: maybe SC really is sending only once, but the receiving device/app might be responding twice. That seems odd because the envelope follower class is fine, but the two classes use different channels and different CC numbers. When things are weird, we have no choice but to eliminate all experimental variables.

hjh

For what it’s worth – since I couldn’t see the problem, I thought, maybe I should try it myself.

To verify the outgoing MIDI, I used Pure Data. (A separate process strikes me as a cleaner pipeline for testing.)

pd-midi-cc-post-with-delta

That’s in Linux, so I added a var deviceI; and:

	deviceI = MIDIClient.destinations.detectIndex { |ep| ep.device.containsi("pure data") };
	if(deviceI.notNil) {
		m.connect(deviceI);
	};

… to the test script. (MIDIOut connect is different in Linux, compared to the other platforms. Actually, to repeat this test in your environment, you would need to use [Mac] the IAC MIDI bus or [Windows] the free loopMIDI app.)

I also simplified by:

	~m1 = Bus.control(s, 1).set(1);
	~m2 = Bus.control(s, 1).set(2);

	~e1 = DuoBusCCGen(s, m, 2, 10, "10 : 1-2", ~m1/*.bus*/, ~m2/*.bus*/);

… and dropping l stuff from the GUI and loop.

Moving the knob, I get:

// SC post window:

scaled:  112 - lastval:  112
scaled:  90 - lastval:  90
scaled:  73 - lastval:  73
scaled:  97 - lastval:  97

// Pd post window:

print: time 510.839 CC 10 val 112
print: time 487.619 CC 10 val 90
print: time 510.839 CC 10 val 73
print: time 487.619 CC 10 val 97

(I had changed the routine to a half second wait time; Pd is reporting about 500 ms per cycle, which is correct, though I don’t know where the +/-10 ms is coming from.)

Anyway… no duplicated messages on the Pd side. That tells us there’s no problem in your SC code logic, and no bugs in SC_AlsaMIDI.cpp that would cause duplicated messages in Linux.

Remaining possible causes might be: Maybe the device/code that you’re using to print the message is is responding twice. Or, maybe the SC MIDI backend in your OS has a bug (though this is unlikely – the C++ here is pretty stable – if there were a bug here, a lot of users would be reporting it).

Pure Data is free, so you can try the test patch for yourself – save this into a text file with a .pd extension, and you can open it in Pd. (The sc forum mysteriously disallows text files to be uploaded :face_with_raised_eyebrow: so I have to do it this way…)

#N canvas 744 129 450 300 12;
#X obj 84 40 ctlin, f 34;
#X obj 84 166 pack 0 0 0, f 17;
#X obj 84 90 timer;
#X obj 84 65 t b b f, f 9;
#X obj 84 216 print;
#X msg 84 191 time \$1 CC \$3 val \$2;
#X connect 0 0 3 0;
#X connect 0 1 1 2;
#X connect 1 0 5 0;
#X connect 2 0 1 0;
#X connect 3 0 2 0;
#X connect 3 1 2 1;
#X connect 3 2 1 1;
#X connect 5 0 4 0;

hjh

1 Like

Right, I’ve come back to the project after a few days away and now the issue is just not happening.

Very strange, but I have noticed a few other miscellaneous mystery bugs that disappeared after a reboot, so I’m going to put it down as a quirk that I have to live with in the development process.

Thank you for your help in going through my project, even though the result isn’t that satisfying I really appreciate the support and the other code review things were helpful.

And for the record I am using midi monitor on a mac for my midi monitoring, which I find pretty stable so I’m discounting it coming from that side.

Merry xmas