Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keyboard functionality #3

Open
unculcated opened this issue Aug 22, 2024 · 0 comments
Open

Keyboard functionality #3

unculcated opened this issue Aug 22, 2024 · 0 comments

Comments

@unculcated
Copy link

I added keyboard functionality

(
// This is b700ish
// A vague attempt to implement a rough guess of the
// Buchla 700 voice architecture in Supercollider
// Aaron Lanterman, October 21, 2020

// Make sure you also have b700ish_patches.scd file.
// Also, find the line that reads
// "/Users/lanterma/buchla700_supercollider/b700ish_patches.scd".load;
// and change it to an appropriate path on your machine.

// Execute this whole-file code block to start.
// There's a bug where you need to manually select a patch from the patch menu before sound will start.
// The Patch drop-down menu is in the upper right corner; I think any patch will do.
// You can use a MIDI controller or the key buttons in the lower right corner of the GUI.
s.boot;
s.scope;
FreqScope.new;

~trianglePink = Color(255/255,0,170/255,1);
~circleBlue = Color(0,170/255,255/255,1);
~squareOrange = Color(255/255,170/255,0,1);
~squareGreen = Color(0,255/255,170/255,1);

~envTitleStrings = ["Idx1","Idx2","Idx3","Idx4","Idx5","Idx6", "LvlA", "LvlB", "Filt", "Res"];
~envTitleColors = (~trianglePink ! 6) ++ [~squareOrange] ++ [~squareGreen] ++ (Color.black ! 2);
~numberEnvs = ~envTitleStrings.size;

~idx1eno = 0; ~idx2eno = 1; ~idx3eno = 2;
~idx4eno = 3; ~idx5eno = 4; ~idx6eno = 5;
~levelAeno = 6; ~levelBeno = 7;
~filteno = 8; ~reseno = 9;

"/Users/ss/Downloads/b700ish-main/b700ish_patches.scd".load;
if (~currentPatch.isNil,{~currentPatch=0});
~instruments[~currentPatch].value;

~chebyNames = Array.new;
~chebyCoefs = Array.new;
~chebyNames = ~chebyNames.add("Default");
~chebyCoefs = ~chebyCoefs.add([1]);
~chebyNames = ~chebyNames.add("True Triangle ");
~chebyCoefs = ~chebyCoefs.add([1,0] / ((1..32).squared));
~chebyNames = ~chebyNames.add("Square-Compatible Triangle");
~chebyCoefs = ~chebyCoefs.add([1,0,-1,0] / ((1..32).squared));
~chebyNames = ~chebyNames.add("Jimmy Smith (All Positive)");
~chebyCoefs = ~chebyCoefs.add([1,1,1]);
~chebyNames = ~chebyNames.add("Jimmy Smith");
~chebyCoefs = ~chebyCoefs.add([1,1,-1]);
~chebyNames = ~chebyNames.add("Full Tonewheel");
~chebyCoefs = ~chebyCoefs.add([1,1,-1,-1,0,1,0,-1,0,1,0,-1,0,0,0,1]);
~chebyNames = ~chebyNames.add("True Square");
~chebyCoefs = ~chebyCoefs.add([1,0,-1,0] / (1..32));
~chebyNames = ~chebyNames.add("Triangle-Compatible Square");
~chebyCoefs = ~chebyCoefs.add([1,0] / (1..32));
~chebyNames = ~chebyNames.add("Alt Saw");
~chebyCoefs = ~chebyCoefs.add(0.25*[1,-1,-1,1] / (1..32));
~chebyNames = ~chebyNames.add("Alt Impulse Train");
~chebyCoefs = ~chebyCoefs.add(0.1*[1,-1,-1,1]Array.fill(32,1));
~chebyNames = ~chebyNames.add("Alt Sign-Flipping Impulse Train");
~chebyCoefs = ~chebyCoefs.add(0.1
[1,0,-1,0]*Array.fill(32,1));

~wsdefaultsig = Signal.chebyFill(4096, [1], normalize: true, zeroOffset:false);
~wsdefault = ~wsdefaultsig.asWavetableNoWrap;
~wsdefaultbuf = Buffer.loadCollection(s, ~wsdefault);

SynthDef.new(\fbfm, {
arg freq = 220, gate = 0, config = 1, offsets = #[0,0,10,0,0,10,10,10,10,0],
numerators = #[1,1,1,1], denominators = #[1,1,1,1],
cf = 0,
wsAbuf = ~wsdefaultbuf, wsBbuf = ~wsdefaultbuf;

var cfreq = freq;
var po = LocalIn.ar(4);
var freqs = cfreq * numerators / denominators;

var out, oo, fmodinputs, tm2, tm5, wsAoutput, wsBoutput, wsMix;
var levelA, levelB, rawlevelA, rawlevelB;

var acousticVars,rawindexes,max_rawindex,io0,io1,io2,io3,io4,io5,io6;

var envidx1, envidx2, envidx3, envidx4, envidx5, envidx6;
var envfilt, envres, envlevelA, envlevelB;
var filtercontrol, rescontrol;

var envidx1trick = Env.newClear(8);
var envidx2trick  = Env.newClear(8);
var envidx3trick  = Env.newClear(8);
var envidx4trick  = Env.newClear(8);
var envidx5trick  = Env.newClear(8);
var envidx6trick  = Env.newClear(8);
var envfilttrick  = Env.newClear(8);
var envrestrick  = Env.newClear(8);
var envlevelAtrick  = Env.newClear(8);
var envlevelBtrick  = Env.newClear(8);

envidx1 = \env1.kr(envidx1trick.asArray);
envidx2 = \env2.kr(envidx2trick.asArray);
envidx3 = \env3.kr(envidx3trick.asArray);
envidx4 = \env4.kr(envidx4trick.asArray);
envidx5 = \env5.kr(envidx5trick.asArray);
envidx6 = \env6.kr(envidx6trick.asArray);
envlevelA = \envlA.kr(envlevelAtrick.asArray);
envlevelB = \envlB.kr(envlevelBtrick.asArray);
envfilt = \envf.kr(envfilttrick.asArray);
envres = \envr.kr(envrestrick.asArray);

acousticVars = Clip.kr(offsets.lag(0.5) +
   [EnvGen.kr(envidx1,gate), EnvGen.kr(envidx2,gate), EnvGen.kr(envidx3,gate),
    EnvGen.kr(envidx4,gate), EnvGen.kr(envidx5,gate), EnvGen.kr(envidx6,gate),
	EnvGen.kr(envlevelA,gate,doneAction: Done.freeSelf),
	EnvGen.kr(envlevelB,gate,doneAction: Done.freeSelf),
	EnvGen.kr(envfilt,gate), EnvGen.kr(envres,gate)],
	0,10);

filtercontrol = acousticVars[~filteno];
rescontrol = acousticVars[~reseno];
levelA = acousticVars[~levelAeno];
levelB = acousticVars[~levelBeno];

//rawindexes =
//   8*pi*(2**(((127*[acousticVars[~idx1eno],acousticVars[~idx2eno],acousticVars[~idx3eno],
//	                acousticVars[~idx4eno],acousticVars[~idx5eno],acousticVars[~idx6eno]]
 //             /10)-135)/8));

rawindexes = pi*(2**((33/16)-((100-(10*
	[acousticVars[~idx1eno],acousticVars[~idx2eno],acousticVars[~idx3eno],
     acousticVars[~idx4eno],acousticVars[~idx5eno],acousticVars[~idx6eno]]))/8)));

 max_rawindex = pi*(2**(33/16));

// index triangle outputs
 io0 = rawindexes[0] * Select.ar(config,
	    [po[2-1],po[2-1],po[2-1],po[1-1],po[1-1],po[1-1],
		 po[3-1],po[4-1],po[3-1],po[4-1],po[3-1],po[1-1]]);
 io1 = rawindexes[1] * Select.ar(config,
	    [po[4-1],po[2-1],po[3-1],po[3-1],po[2-1],po[4-1],
		 po[2-1],po[2-1],po[2-1],po[3-1],po[2-1],po[2-1]]);

 io3 = rawindexes[3] * Select.ar(config,
	    [po[2-1],po[4-1],po[4-1],po[2-1],po[3-1],po[3-1],
	     po[4-1],po[4-1],po[3-1],po[2-1],po[1-1],po[4-1]]);
 io4 = rawindexes[4] * Select.ar(config,
	    [po[4-1],po[4-1],po[1-1],po[3-1],po[4-1],po[2-1],
		 po[1-1],po[4-1],po[1-1],po[1-1],po[2-1],po[3-1]]);

// index triangles 3 and 6 (on images), i.e. 2 and 5 in code, only feed wavetables
fmodinputs = [Select.ar(config,[io0,io0,io0,io1,io1,io1,
io1,io1,io1,io0,io1,io4]),
Select.ar(config,[Silent.ar,Silent.ar,io1,io4,io3,Silent.ar,
io0,io0,io3,io1,Silent.ar,io0]),
Select.ar(config,[io3,io3,io3,Silent.ar,Silent.ar,io4,
Silent.ar,Silent.ar,Silent.ar,io3,io4,Silent.ar]),
Select.ar(config,[Silent.ar,Silent.ar,io4,Silent.ar,Silent.ar,Silent.ar,
Silent.ar,Silent.ar,Silent.ar,io4,Silent.ar,Silent.ar])];

fmodinputs[0] = fmodinputs[0] +
        Select.ar(config,[Silent.ar,Silent.ar,Silent.ar,Silent.ar,Silent.ar,Silent.ar,
                          io3,io3,Silent.ar,Silent.ar,Silent.ar,Silent.ar]);
fmodinputs[1] = fmodinputs[1] +
        Select.ar(config,[Silent.ar,Silent.ar,Silent.ar,Silent.ar,io4,Silent.ar,
                          Silent.ar,Silent.ar,Silent.ar,Silent.ar,Silent.ar,Silent.ar]);

// oscillator outputs
oo = SinOsc.ar(freqs,fmodinputs);

// index triangles 3 and 6 (on images), i.e. 2 and 5 in code, only feed wavetables
tm2 = Select.ar(config,[io1,io1,Silent.ar,io0,Silent.ar,io0,
	                    Silent.ar,Silent.ar,io0,Silent.ar,io0,io3]);
tm5 = Select.ar(config,[io4,io4,Silent.ar,io3,Silent.ar,io3,
	                    Silent.ar,io4,io4,Silent.ar,io3,io1]);

// Since the output of index triangles 3 and 6 (on images), i.e. 2 and 5 in code,
// doesn't involve any feedback mechanisms, we might as well use oo intead of po.
io2 = ((rawindexes[2] + tm2) / max_rawindex) *
      Select.ar(config,
     [oo[1-1],oo[1-1],oo[1-1],oo[4-1],oo[1-1],oo[2-1],
	  oo[1-1],oo[1-1],oo[1-1],oo[1-1],oo[1-1],oo[1-1]]);

io5 = ((rawindexes[5] + tm5) / max_rawindex) *
      Select.ar(config, [oo[3-1],oo[3-1],oo[3-1],oo[4-1],oo[1-1],oo[4-1],
	 	                 oo[1-1],oo[4-1],oo[4-1],oo[3-1],oo[3-1],oo[1-1]]);

LocalOut.ar(oo);
wsAoutput = Shaper.ar(wsAbuf, io2);
wsBoutput = Shaper.ar(wsBbuf, io5);

//rawlevelA = 8*pi*(2**(((127*levelA/10)-135)/8));
//rawlevelB = 8*pi*(2**(((127*levelB/10)-135)/8));

rawlevelA = pi*(2**((33/16)-((100-(10*levelA))/8)));
rawlevelB = pi*(2**((33/16)-((100-(10*levelB))/8)));

wsAoutput = (rawlevelA / max_rawindex) * wsAoutput;
wsBoutput = (rawlevelB / max_rawindex) * wsBoutput;
wsMix = (wsBoutput * cf) + (wsAoutput * (1 - cf));
out = wsMix;
out = MoogFF.ar(wsMix, 20*(2**filtercontrol), 4 * rescontrol/10);
Out.ar(0, 0.2*[out, out]);

}).add;

///////////////

MIDIClient.init;
MIDIIn.connectAll;

~notes = Array.newClear(128); // array has one slot per possible MIDI note

~startNote = { arg velocity, noteNumber;
~vel = velocity / 127;
~nn = noteNumber;
~genvstr.do({arg item, i;
var realenv = try {item.compile.value;} {arg error; };
if(realenv.isKindOf(Env),
{~genv[i] = realenv;
{~envtitles[i].stringColor = ~darkGreen;}.defer;
},
{~genv[i] = nil;
postln("Charm");
{~envtitles[i].stringColor = Color.red;}.defer;}
);
});
~notes[noteNumber] = Synth.new(\fbfm, [\freq, (noteNumber).midicps, \gate, 1,
\config, ~gconfig,
\offsets, ~goffsets,
\numerators, ~gnumerators,
\denominators, ~gdenominators,
\cf, ~gcf,
\wsAbuf, ~gwsAbuf, \wsBbuf, ~gwsBbuf,
// it seems like SC doesn't want me to pass in ~genvinv all at once,
// as an array; when compiling the synthdef, it complains that
// the "message 'at' is not understood" -- not sure why...
\env1, ~genv[~idx1eno], \env2, ~genv[~idx2eno], \env3, ~genv[~idx3eno],
\env4, ~genv[~idx4eno], \env5, ~genv[~idx5eno], \env6, ~genv[~idx6eno],
\envlA, ~genv[~levelAeno], \envlB, ~genv[~levelBeno],
\envf, ~genv[~filteno], \envr, ~genv[~reseno]
]);
~notes[noteNumber].register;
};

~stopNote = { arg velocity, noteNumber;
~notes[noteNumber].set(\gate, 0);
~notes[noteNumber] = nil;
};

MIDIdef.noteOn(\noteOnTest,{ arg velocity, noteNumber, chan, src;
~startNote.value(velocity, noteNumber)});

MIDIdef.noteOff(\noteOffTest, { arg velocity, noteNumber, chan, src;
~stopNote.value(velocity, noteNumber); });

~updateGUI = {
~cfimages = Array.fill(12,
{arg i;
Image.new("/Users/ss/Downloads/b700ish-main/fm_config_c_"
++ i.asStringToBase(width: 2) ++ ".png"); });

if(w.notNil,
{if(w.isClosed,
{~currentBounds = Rect(0,540,700,600)},
{~currentBounds = w.bounds;
~currentBounds.top = ~currentBounds.top+22;
});},
{~currentBounds = Rect(0,540,700,600)});

Window.closeAll;
~forceOnTop = false;
w = Window("B700ish",~currentBounds,resizable: false).front.alwaysOnTop_(~forceOnTop);

~cfknob = Knob(w,Rect(330,110,32,32))
.action_({arg obj;
~notes.do({arg item;
~gcf = obj.value;
if (item.isPlaying,
{item.set(\cf,~gcf);});
});
})
.value = ~gcf;

StaticText(w, Rect(165,150,160,40)).string_("Frequency Ratios:");

~numtextboxes = Array.fill(4,
{arg i;
var tf = TextField(w,Rect(165+(40*i),182,37,22))
.action_({arg obj;
var vaf = obj.value.asFloat;
if(vaf != 0,
{~gnumerators[i] = vaf;
~notes.do({arg item;
if(item.isPlaying,
{item.seti(\numerators,i,vaf);});
})
});
obj.value = ~gnumerators[i].asString;
});
tf.string = ~gnumerators[i].asString;
tf;
});

~denomtextboxes = Array.fill(4,
{arg i;
var tf = TextField(w,Rect(165+(40*i),207,37,22))
.action_({arg obj;
var vaf = obj.value.asFloat;
if(vaf != 0,
{~gdenominators[i] = vaf;
~notes.do({arg item;
if(item.isPlaying,
{item.seti(\denominators,i,vaf);});
})
});
obj.value = ~gdenominators[i].asString;
});
tf.string = ~gdenominators[i].asString;
tf;
});

w.drawFunc_({~cfimages[~gconfig].tileInRect(Rect(10,182,128,128))});
w.refresh;

StaticText(w, Rect(10,150,160,40)).string_("Config:");

~configselect = PopUpMenu(w, Rect(60, 160, 40, 20));
~configselect.items = Array.series(12);
~configselect.value = ~gconfig;
~configselect.action =
{ arg obj;
~gconfig = obj.value;
w.drawFunc_({~cfimages[~gconfig].tileInRect(Rect(10,182,128,128))});
w.refresh; // need this so new image appears
~notes.do({arg item;
if(item.isPlaying,
{item.set(\config,~gconfig);})
});
};

~setupAbuf = { arg selection;
~wsAsig = Signal.chebyFill(4096, ~chebyCoefs[selection],
normalize: true, zeroOffset:false);
~wsA = ~wsAsig.asWavetableNoWrap;
~gwsAbuf = Buffer.loadCollection(s, ~wsA);
};

~setupBbuf = { arg selection;
~wsBsig = Signal.chebyFill(4096, ~chebyCoefs[selection],
normalize: true, zeroOffset:false);
~wsB = ~wsBsig.asWavetableNoWrap;
~gwsBbuf = Buffer.loadCollection(s, ~wsB);
};

~envtitles = Array.fill(~numberEnvs,
{arg i;
StaticText(w, Rect(5,310+(25*i),40,40)).string_(~envTitleStrings[i] ++ ":").align_(\left)
});

~darkGreen = Color(0,0.5,0,1);
~genv = Array.fill(~numberEnvs);
~envtextboxes = Array.fill(~numberEnvs,
{arg i;
var realenv = nil;
var tf = TextField(w,Rect(40,320+(25*i),300,22))
.action_({arg obj;
var realenv = nil;
~vel = 1; ~nn = 60;
realenv = try {obj.value.compile.value;} {arg error; };
~genvstr[i] = obj.value;
if(realenv.isKindOf(Env),
{~genv[i] = realenv;
~envtitles[i].stringColor = ~darkGreen;},
{~genv[i] = nil;
~envtitles[i].stringColor = Color.red;}
);
});
~genvstr;
tf.value = ~genvstr[i];
~vel = 1; ~nn = 60;
realenv = try {~genvstr[i].compile.value;} {arg error; };
if(realenv.isKindOf(Env),
{~genv[i] = realenv;
~envtitles[i].stringColor = ~darkGreen;
},
{~genv[i] = nil;
~envtitles[i].stringColor = Color.red;}
);
tf;
});
~goffsets[1]; //8.4

~offsetSliders = Array.fill(~numberEnvs,
{arg i;
Slider(w,Rect(10+(32i),6,20,128))
.action_({arg obj;
~goffsets[i] = 10
(obj.value);
~notes.do({arg item;
if (item.isPlaying,
{item.seti(\offsets,i,~goffsets[i]);}
);
});
}).knobColor_(~envTitleColors[i]).value = (~goffsets[i] / 10);
StaticText(w, Rect(0+(32*i),120,40,40)).string_(~envTitleStrings[i]).align_(\center);
});

~setupAbuf.value(~gwsAselect);
~setupBbuf.value(~gwsBselect);

~makeAplots = {
~plotterA = Plotter("",Rect(350,182,190,100),w);
~plotterA.plotColor_(Color.red);
~plotterA.value = ~wsAsig;
~plotterAsinin = Plotter("",Rect(550,182,100,100),w);
~plotterAsinin.plotColor_(Color.red);
// There has to be a more elegant way to do this...
~plotterAsinin.value =
Array.fill(100,{arg n; ~wsAsig[(~wsAsig.size - 1) * (sin(2pin / 100)+1)/2]});
};

~makeBplots = {
~plotterB = Plotter("",Rect(350,310,190,100),w);
~plotterB.plotColor_(Color.green);
~plotterB.value = ~wsBsig;
~plotterBsinin = Plotter("",Rect(550,310,100,100),w);
~plotterBsinin.plotColor_(Color.green);
// There has to be a more elegant way to do this...
~plotterBsinin.value =
Array.fill(100,{arg n; ~wsBsig[(~wsBsig.size - 1) * (sin(2pin / 100)+1)/2]});
};

~makeAplots.value();
~makeBplots.value();

StaticText(w, Rect(350,150,160,40)).string_("WaSh A:");
~wsAselect = PopUpMenu(w, Rect(408, 160, 130,20));
~wsAselect.items = ~chebyNames;
~wsAselect.value = ~gwsAselect;
~wsAselect.action = { arg obj;
~gwsAselect = obj.value;
~setupAbuf.value(~gwsAselect);
~makeAplots.value();
};

StaticText(w, Rect(350,278,160,40)).string_("WaSh B:");
~wsBselect = PopUpMenu(w, Rect(408, 288, 130, 20));
~wsBselect.items = ~chebyNames;
~wsBselect.value = ~gwsBselect;
~wsBselect.action = { arg obj;
~gwsBselect = obj.value;
~setupBbuf.value(~gwsBselect);
~makeBplots.value();
};

StaticText(w, Rect(350,50,160,40)).string_("Patch:");
~patchselect = PopUpMenu(w, Rect(395, 60, 250,20));
~patchselect.items = ~instrumentNames;
~patchselect.value = ~currentPatch;
~patchselect.action = { arg obj;
~currentPatch = obj.value;
~instruments[~currentPatch].value;
~updateGUI.value;
};

~keysrow1 = Array.fill(13, {arg i;
Button(w, Rect(350+(22*i), 430, 20, 40))
.states_([["", Color.white, Color.white]])
.mouseDownAction_({~startNote.value(127, 36+i)})
.mouseLeaveAction_({~stopNote.value(127, 36+i)})
.action_({~stopNote.value(127, 36+i)});
});

~keysrow2 = Array.fill(13, {arg i;
Button(w, Rect(350+(22*i), 472, 20, 40))
.states_([["", Color.white, Color.white]])
.mouseDownAction_({~startNote.value(127, 12+36+i)})
.mouseLeaveAction_({~stopNote.value(127, 12+36+i)})
.action_({~stopNote.value(127, 12+36+i)});
});

~keysrow2 = Array.fill(13, {arg i;
Button(w, Rect(350+(22*i), 514, 20, 40))
.states_([["", Color.white, Color.white]])
.mouseDownAction_({~startNote.value(127, 24+36+i)})
.mouseLeaveAction_({~stopNote.value(127, 24+36+i)})
.action_({~stopNote.value(127, 24+36+i)});
});

~keyboardActiveButton = Button(w, Rect(350, 560, 150, 30))
.states_([
["Activate Keyboard", Color.black, Color.gray],
["Deactivate Keyboard", Color.white, Color.red]
])
.action_({ |butt|
~keyboardActive = butt.value == 1;
});

~keyboardActive = false;
~keyboardActiveButton = Button(w, Rect(350, 560, 150, 30))
.states_([
["Activate Keyboard", Color.black, Color.gray],
["Deactivate Keyboard", Color.white, Color.red]
])
.action_({ |butt|
~keyboardActive = butt.value == 1;
});

~keyboardActive = false;

~keyboardNoteMap = Dictionary[
    $a -> 60, $w -> 61, $s -> 62, $e -> 63, $d -> 64, $f -> 65, $t -> 66,
    $g -> 67, $y -> 68, $h -> 69, $u -> 70, $j -> 71, $k -> 72, $o -> 73,
    $l -> 74, $p -> 75, $; -> 76
];

~keyboardNoteOn = { |char|
    var note = ~keyboardNoteMap[char];
    if(note.notNil, {
        ~startNote.value(127, note);
    });
};

~keyboardNoteOff = { |char|
    var note = ~keyboardNoteMap[char];
    if(note.notNil, {
        ~stopNote.value(0, note);
    });
};

w.view.keyDownAction = { |view, char, modifiers, unicode, keycode|
    if(~keyboardActive, {
        ~keyboardNoteOn.(char);
    });
};

w.view.keyUpAction = { |view, char, modifiers, unicode, keycode|
    if(~keyboardActive, {
        ~keyboardNoteOff.(char);
    });
};

};

~updateGUI.value;
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant