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;