//thanks to: polyfony.ck (base code), fm2.ck (modulation code) //inspired by VCV Rack, Chuck, Roland JD-XA [0,1] @=> int menuvals[]; 0 => int menuselector => int paramselector; ["DEMOMODE?","Filter?","OSC1 Freq CV","Filter Cutoff CV","Filter Resonance CV","Pulse-Width CV"] @=> string parameterstrings[]; [0.,1.,1.,0.,0.,0.] @=> float parametervals[];//control voltages will go from 0 - 2x for further manipulation Gain CutoffCV; Gain QCV; Gain PWCV; 0 => CutoffCV.gain => QCV.gain => PWCV.gain; // device to open (see: chuck --probe) 0 => int device; MidiIn min; MidiMsg msg; Hid hi; HidMsg kbmsg; ["Greetings,","Good day to you,","It's great to see you","I bid thee well","The Sun doth smile upon thee today","In a Twisted World it is a joy to see thee","I am honored by your presence"] @=> string greetings[]; ["O Great","Sir Fairest","Your Excellency","My Lord","My Dearest","The One and Only","My Liege","King of the Synths","Tiger King"] @=> string titles[]; // try to open MIDI port (see chuck --probe for available devices) if( !min.open( device ) ) me.exit(); <<< greetings[Math.random2(0,greetings.cap()-1)],titles[Math.random2(0,titles.cap()-1)],min.name(),"!" >>>; if( !hi.openKeyboard( device ) ) me.exit(); <<< "I have ensured that" , hi.name() , "will serve as your companion for this long journey. Fare thee well!">>>; <<<"Take to the menu with a knob, or perhaps the arrow keys, if you should find yourself in Need.","">>>; // make our own event class NoteEvent extends Event { int note; int velocity; } // make our own event class KnobEvent extends Event { int id1; int id2; int amount; } NoteEvent on; KnobEvent knob; //OSC2 (aka "LFOs") 1. => float OSC2FINE; SinOsc LFO_sin =>blackhole; TriOsc LFO_tri; //=>blackhole; SqrOsc LFO_sqr; //=>blackhole; Math.random2(330,2000) => float OSC2FREQ => LFO_sin.freq => LFO_tri.freq => LFO_sqr.freq; 0 => LFO_sin.gain => LFO_tri.gain => LFO_sqr.gain; // the base patch PulseOsc osc=> ADSR adsr => LPF filter => Echo delay => NRev r => Gain amp => dac; //adsr.op(-1); adsr.set(50::ms,500::ms,.35,50::ms); Noise aux => filter; 0=>aux.gain; 0=> delay.gain; filter.set(Math.random2f(200,10000),1); 1=> float OSC1FINE; Math.pow(2,Math.random2(4,10)) => float OSC1FREQ => osc.freq; //start osc1 at a random frequency .1 => amp.gain; 0 => delay.mix; -1 => delay.op; .05 => r.mix; //set up modulation (currently only sin modulation) //the program starts with the synth being modulated LFO_sin => Gain Osc1CV => osc; 2 => osc.sync; //set up osc1 self-feedback osc => Gain feedback => osc; 0 => feedback.gain; //set up feedback within delay delay => Gain delay_feedback => delay; 0 => delay_feedback.gain; //OSC2 => PW spork~PW_mod(); fun void PW_mod() { while(true) { while(PWCV.gain()>0) { (Math.fabs(LFO_sin.last())*PWCV.gain())/LFO_sin.gain() => float val; //gain-independent unipolar modulation if(val>1) 1=>val; if(val<0) 0=>val; if (!Math.isnan(val)) val => osc.width; //<<<"pulse width: ", osc.width()>>>; 25::ms => now; } 1::second => now; } } //OSC2 => Filter Cutoff spork~cutoff_mod(); fun void cutoff_mod() { while(true) { while(CutoffCV.gain() >0) { //<<>>; (LFO_sin.last()*CutoffCV.gain()) + filter.freq() => float val; //gain-dependent bipolar modulation if(val<0) 0=> val; if(val>20000) 20000=>val; if (!Math.isnan(val)) val => filter.freq; //<<<"filtermod: ", filter.freq()>>>; 20::ms => now; } 1::second => now; } } //OSC2 => Filter Resonance spork~Q_mod(); fun void Q_mod() { while(true) { while(QCV.gain() >0) { //<<< (Math.fabs(LFO_sin.last())*QCV.gain())>>>; (LFO_sin.last()*QCV.gain()) + filter.Q() => float val; //gain-dependent unipolar modulation if(val<0.35) 0.35=>val; if(val>10) 10=>val; if (!Math.isnan(val)) val => filter.Q; //<<<"Q: ", filter.Q()>>>; 20::ms => now; } 1::second=>now; } } spork ~ self_playing(); fun void self_playing() { while (true) { //while DEMOMODE enabled while(parametervals[0]==1) { Math.random2(0,127) => on.note; <<<"(",on.note,")">>>; on.signal(); Math.random2(25,(Math.random2(50,2500)))::ms=> now; } 1::second=>now; } } //sets paramselector, prints new selection fun void menuselecthandler(int amount) { amount => paramselector; <<>>; } //assuming paramselector & corresponsing parametervals value is updated fun void menuvalhandler(float amount) { //paramselector maps to values in parametervals if(paramselector==0) { set_demomode(amount, 0); } else if(paramselector==1) { set_filter(amount,1); } else if(paramselector==2) { set_osc1cv(amount,2); } else if(paramselector==3) { set_cutoffcv(amount,3); } else if(paramselector==4) { set_qcv(amount,4); } else if(paramselector==5) { set_pwcv(amount,5); } } // handler for MIDI notes fun void handler() { int note; while( true ) { on => now; adsr.keyOn(); on.note => note; Std.mtof( note ) => osc.freq; <<<"(",note,")">>>; } } fun void knob_handler() { float amount; int id1; int id2; while(true) { knob => now; knob.amount => amount; knob.id1 => id1; knob.id2 => id2; //the MIDI protocol uses a combination of two id's to designate unique knobs //in the comments I put down the knobs that each of these map to as labeled on my JD-XA if(id1 ==191) { if (id2 == 12) { //Reverb set_reverb(amount); } else if (id2 == 13) { //Delay set_delay(amount); } else if (id2 == 80) { //TFX 1 SELECT (our SETTINGS/PARAMETERS menu selector) set_parameterselect(amount); } else if (id2 == 81) { //TFX 2 SELECT (currently no functionality) } else if (id2 == 14) { //TFX 1 CTRL (we will use as our CONTROL KNOB for the menu) set_controlknob(amount); } else if (id2 == 82) { //Time [Delay] set_delaytime(amount); } else if (id2 == 83) { //Knob: "Mic Level" (we will use it for delay feedback) set_delayfeedback(amount); } } else if(id1 ==176) { if (id2 == 117) {//mixer amp gain set_ampgain(amount); } else if (id2 == 29) { //mixer osc 1 gain set_osc1gain(amount); } else if (id2 == 30) { //mixer osc 2 gain set_osc2gain(amount); } else if (id2 == 31) { //mixer Aux gain set_auxgain(amount); } else if (id2 == 102) { //filter cutoff freq set_filtercutoff(amount); } else if (id2 == 105) { //filter resonance set_filterresonance(amount); } else if (id2 == 115) { //filter drive (is this diff than filter gain?) set_filtergain(amount); } else if(id2 == 19) { //OSC 1 pitch (in addition to dedicated keyboard) set_osc1freq(amount); } else if(id2 == 22) { //OSC 1 pitch FINE set_osc1fine(amount); } else if(id2 == 20) { //OSC 2 pitch set_osc2freq(amount); } else if(id2 == 23) { //OSC 2 pitch FINE set_osc2fine(amount); } else if (id2 == 25) { //crossmod (i use it for osc1 self-feedback) set_feedbackgain(amount); } else if(id2 == 6) { //mapped to: literally any slider on the whole synth. wow. set_osc1pulsewidth(amount); } } } } //exponential response curve from 0.0002 - 15000 Hz fun void set_osc1freq(float amount) { .0002 * Math.pow(amount,3.75) => OSC1FREQ; OSC1FINE * OSC1FREQ => osc.freq; <<<"O-S-C-1 F-R-E-Q: ",OSC1FREQ,"Hz">>>; } //FINE and COARSE control separate parameters now stored in global variables //each time the freq updates it will reference both of these variables //OSC1FINE scale: 0.5x - 2x (thanks Wolfram Alpha for help with a good algorithm) fun void set_osc1fine(float amount) { 0.502649 * Math.pow(Math.e, 0.0108768*amount) => OSC1FINE; OSC1FINE * OSC1FREQ => osc.freq; <<<"O-S-C-1 F-I-N-E:",OSC1FINE,"x">>>; } fun void set_osc2freq(float amount) { //exponential response curve from 0.0002 - 2790 Hz .0002 * Math.pow(amount,3.35) => OSC2FREQ; OSC2FINE * OSC2FREQ => LFO_sin.freq => LFO_tri.freq => LFO_sqr.freq; <<<"O-S-C-2 F-R-E-Q: ",LFO_sin.freq(),"Hz">>>; } //like osc1fine //OSC2FINE scale: 0.5x - 2x (thanks Wolfram Alpha for help with a good algorithm) fun void set_osc2fine(float amount) { 0.502649 * Math.pow(Math.e, 0.0108768*amount) => OSC2FINE; OSC2FINE * OSC2FREQ => LFO_sin.freq; <<<"O-S-C-2 F-I-N-E:",OSC2FINE,"x">>>; } fun void set_parameterselect(float amount) { ((amount$int/3) % (parametervals.cap())) => int val; menuselecthandler(val); //room for 42 parameters with this granularity } //this knobs controls anything we select in the menu fun void set_controlknob(float amount) { //update the value of the selected parameter amount => parametervals[paramselector] => float val; menuvalhandler(val); } fun void set_reverb(float amount) { amount /127.0 => r.mix; <<<"R*E*V*E*R*B: ",r.mix()*100,"%">>>; } fun void set_delay(float amount) { //scale: 0 - 100 (amount /127.0) => delay.mix; if(amount==0) delay.op(-1); //hopefully this will clean up the signal else delay.op(1); <<<"D*E*L*A*Y: ",delay.mix()*100,"%">>>; } fun void set_delaytime(float amount) { //scale: 0 - 1 seconds (amount /127.0)::second => delay.max => delay.delay; <<<"D*E*L*A*Y T*I*M*E: ",delay.delay(),"samples">>>; } fun void set_delayfeedback(float amount) { //scale: ? risk management should be practiced here //:!!! note that this is a direct & raw feedback if used with delay off; Be warned!!! (amount /127.0) => delay_feedback.gain; <<<"D*E*L*A*Y F*E*E*D*B*A*C*K: ",delay_feedback.gain()*100,"%">>>; } fun void set_osc1gain(float amount) { (amount/127.) => osc.gain; <<<"O-S-C-1: ",osc.gain()*100,"%">>>; } fun void set_osc2gain (float amount) { //exponential response curve from 0 - 300 .025 * Math.pow(amount,1.95) => LFO_sin.gain => LFO_tri.gain => LFO_sqr.gain; <<<"O-S-C-2: ",LFO_sin.gain(),"x">>>; } fun void set_feedbackgain (float amount) { amount => feedback.gain; //safety-reduced modulation gain Maxes out @ 127 <<<"O*S*C*1 F*E*E*D*B*A*C*K: ",feedback.gain(),"%">>>; } fun void set_auxgain (float amount) { //exponential response curve from 0 - 300 amount/(127./2) => aux.gain; <<<"N-O-I-S-E: ",aux.gain(),"x">>>; } fun void set_ampgain(float amount) { (amount /127.0)*.1 => amp.gain;//safety-enhanced gain Maxes out @ 0.1 <<<"A-M-P: ",amp.gain()*100,"%">>>; } fun void set_filtergain(float amount) { (amount/127.) => filter.gain; <<<"F-I-L-T-E-R G-A-I-N: ",filter.gain()*100,"%">>>; } fun void set_filtercutoff (float amount) { //scale: 100 - 10000 Hz //had to limit the range on both ends due to audio glitches, im kind of disappointed by this; what gives? //why is sound getting quieter towards the higher end?! this is unexpected behavior. ((amount /127.0) * 10000)+100=> filter.freq; <<<"F-I-L-T-E-R F-R-E-Q: ",filter.freq(), "Hz">>>; } fun void set_filterresonance(float amount) { //scale: 0 - 10 0.35 +(amount /127.0) * 10 =>filter.Q; <<<"F-I-L-T-E-R R-E-S: ",filter.Q(),"x">>>; } fun void set_osc1pulsewidth(float amount) { //limited range because of strange errors. amount /127.0=>osc.width; <<<"P-U-L-S-E W-I-D-T-H: ",osc.width(),"nanometres">>>; } fun void set_demomode(float amount, int id) { //either YES or NO for demomode if(amount > (127.0/2)) { 1. => parametervals[id]; <<>>; } else { 0. => parametervals[id]; <<>>; } } fun void set_filter(float amount, int id) { //either YES, enable filter, or NO, passthru filter if(amount > (127.0/2)) { filter.op(1); 1. => parametervals[id]; <<>>; } else { filter.op(-1); 0. => parametervals[id]; <<>>; } } fun void set_osc1cv (float amount, int id) { //range is exponential 0 - 100 Math.pow(10,amount /(127.0/2))-1 =>parametervals[id]=>Osc1CV.gain; <<>>; } fun void set_cutoffcv (float amount, int id) { //range is exponential 0 - 100 Math.pow(10,amount /(127.0/2))-1 =>parametervals[id]=>CutoffCV.gain; <<>>; } fun void set_qcv (float amount, int id) { //range is exponential 0 - 100 Math.pow(10,amount /(127.0/2))-1 =>parametervals[id]=>QCV.gain; <<>>; } fun void set_pwcv (float amount, int id) { //range is exponential 0 - 100 Math.pow(10,amount /(127.0/2))-1 => parametervals[id]=>PWCV.gain; <<>>; } //MIDI & knob handlers act on received messages for( 0 => int i; i < 1; i++ ) spork ~ handler(); spork ~ knob_handler(); spork ~ keyboard_receiver(); fun void keyboard_receiver () { 4 => int DOWNCYCLE; 6 => int UPCYCLE; while( true ) { // wait on event hi => now; // get one or more messages while( hi.recv( kbmsg ) ) { // check for action type if( kbmsg.isButtonDown() ) { //<<< "down:", kbmsg.which, "(code)", kbmsg.key, "(usb key)", kbmsg.ascii, "(ascii)" >>>; if(kbmsg.which==203) { //left arrow-key paramselector--; if(paramselector<0) parametervals.cap()-1 => paramselector; ((paramselector) % parametervals.cap()) => int val; menuselecthandler(val); } else if(kbmsg.which==205) {//right arrow-key ((paramselector+1) % parametervals.cap()) => int val; menuselecthandler(val); } else if(kbmsg.which==200) {//up arrow UPCYCLE++; if (UPCYCLE>10) 6=>UPCYCLE; (UPCYCLE*12.7)$int => int temp; //its a broken up arrow okay? menuvalhandler(temp); } else if(kbmsg.which==208) { //down arrow DOWNCYCLE--; if (DOWNCYCLE <0) 5=> DOWNCYCLE; (DOWNCYCLE*12.7)$int => int temp; menuvalhandler(temp); } } else { //<<< "up:", msg.which, "(code)", msg.key, "(usb key)", msg.ascii, "(ascii)" >>>; } } } } //MIDI message catcher. This is the time/load-bearing function of this entire program, so don't knock it! while( true ) { // wait on midi event min => now; // print components of MIDI msg <<>>; while( min.recv( msg ) ) { // catch only noteon if( (msg.data1 & 0xf0) == 0x90 ){ // check velocity if( msg.data3 > 0 ) { // store midi note number msg.data2 => on.note; // store velocity msg.data3 => on.velocity; // signal the event on.signal(); } } //KNOBS (the effects knobs on my JD-XA) //note to self: the settings may be different on different hardware, put a global var for this or something. else if(msg.data1 ==191 || msg.data1 ==176) { msg.data1 => knob.id1; msg.data2 => knob.id2; msg.data3 => knob.amount; knob.signal(); } } }