class RhodesVoice extends StereoVoiceBankVoice { // override 0.5 => gainAtZeroVelocity; 0.6 => highCutoffSensitivity; -0.15 => lowCutoffSensitivity; 2 => int unison; 400 => float myLPFCutoff; LPF lpfL => adsrL; LPF lpfR => adsrR; Pan2 pan; pan.left => lpfL; pan.right => lpfR; SqrOsc osc1[unison]; TriOsc osc2[unison]; // osc 1: sqrosc +0c, 34% volume // osc 2: triosc +12+3c, 34% volume for( int i; i < unison; i++ ) { 0.34 / unison => osc1[i].gain; 0.34 / unison => osc2[i].gain; osc1[i] => pan; osc2[i] => pan; } TriOsc panLFO => blackhole; fun void doPanLFO() { while( true ) { 0.28 * panLFO.last() => pan.pan; } } fun void sync() { for( int i; i < unison; i++ ) { // TODO: necessary? 0 => osc1[i].phase => osc2[i].phase; } -0.25 => panLFO.phase; } // map cutoff to cube fun float cutoffToHz( float cutoff ) { return Math.min( Std.scalef( Math.pow( Std.clampf( cutoff, 0, 1 ), 3 ), 0, 1, myFreq * 0.3, 18000 ), 18000 ); } // LPF cutoff envelope is AD with A = 0.92ms, D = 170ms // but actually the max cutoff is 0.17 + (0.82-0.17 * current velocity) ADSR lpfEnv => blackhole; lpfEnv.set( 0::ms, 10000::ms, 0.0, 100::ms ); fun void triggerLPFEnv() { lpfEnv.keyOff( 1 ); 0.088 => float minCutoff; // higher cutoff at higher pitch and at higher velocity Std.scalef( myVelocity, 0, 1, 0.23, 0.46 ) => float maxCutoff; minCutoff => float currentCutoff; maxCutoff - minCutoff => float cutoffDiff; lpfEnv.keyOn( 1 ); 0.5::ms => dur delta; now + lpfEnv.attackTime() + lpfEnv.decayTime() + 2 * delta => time end; while( now < end ) { // set minCutoff + cutoffDiff * Math.pow( lpfEnv.value(), 3 ) => currentCutoff; // Math.pow( currentCutoff, 3 ) => currentCutoff; currentCutoff + myCutoff => this.cutoffToHz => myLPFCutoff; // wait delta => now; } } spork ~ this.triggerLPFEnv() @=> Shred triggerLPFEnvShred; // lpf is a little resonant 2.0 => lpfL.Q => lpfR.Q; // then ADSR on volume 380::ms => rTime; adsrL.set( 0::ms, 330::ms, 0.583, rTime ); adsrR.set( 0::ms, 330::ms, 0.583, rTime ); // analog: randomly alter pitch and cutoff (0 to 1: 1. units?) 0.005 => float analog; float analogs[unison + 1]; fun void calculateAnalog() { for( int i; i < analogs.size(); i++ ) { Math.random2f( 1 - analog, 1 + analog ) => analogs[i]; } } // osc1: freq // osc2: +1 octave + 3 cents fun void applyFreqs() { float a; float f1, f2, f3; while( true ) { Math.min( myLPFCutoff * analogs[unison], 21000 ) => lpfL.freq => lpfR.freq; // myFreq + 1 octave + 3 cents myFreq * 2.003469 => float f1; for( int i; i < unison; i++ ) { myFreq * analogs[i] => osc1[i].freq; f1 * analogs[i] => osc2[i].freq; } 0.5::ms => now; } } spork ~ this.applyFreqs() @=> Shred applyFreqsShred; // trigger note on fun void noteOn() { // TODO is sync necessary? sync(); // key on adsrL.keyOn( 1 ); adsrR.keyOn( 1 ); triggerLPFEnvShred.exit(); spork ~ this.triggerLPFEnv() @=> triggerLPFEnvShred; calculateAnalog(); // try to fix smeariness // TODO necessary? applyFreqsShred.exit(); 1 => myLPFCutoff; spork ~ this.applyFreqs() @=> applyFreqsShred; } // trigger note off fun void noteOff() { adsrL.keyOff( 1 ); adsrR.keyOff( 1 ); } } class Rhodes extends StereoVoiceBank { 4 => numVoices; 1.25 => float distortAmount; // distortion Gain preGainL => AbsDistortion distortL => myGainL; Gain preGainR => AbsDistortion distortR => myGainR; distortAmount => preGainL.gain => preGainR.gain; // create voices and connect to distortion RhodesVoice myVoices[numVoices]; for( int i; i < myVoices.size(); i++ ) { myVoices[i].connect( preGainL, preGainR ); } // assign to superclass v.size( myVoices.size() ); for( int i; i < myVoices.size(); i++ ) { myVoices[i] @=> v[i]; } // don't connect init( false ); } Rhodes r; LPF lpfL => JCRev revL => dac.left; LPF lpfR => JCRev revR => dac.right; 0.6 => r.gain; r.connect( lpfL, lpfR ); 0.05 => revL.mix => revR.mix; 15000 => lpfL.freq => lpfR.freq; // knobs global float gReverb; global float gCutoff; global float gLowpass; fun void ApplyGlobals() { while( true ) { 10::ms => now; gReverb => revL.mix => revR.mix; gLowpass => lpfL.freq => lpfR.freq; gCutoff => r.cutoff; } } spork ~ ApplyGlobals(); // end knobs global Event midiMessage; global int midiCommand; global int midiNote; global int midiVelocity; fun void NoteOn( int m, int v ) { v * 1.0 / 128 => float velocity; r.noteOn( m, velocity ); //<<< "on", m, v >>>; } fun void NoteOff( int m ) { spork ~ r.noteOff( m ); //<<< "off", m >>>; } while( true ) { midiMessage => now; if( midiCommand >= 144 && midiCommand < 160 ) { if( midiVelocity > 0 ) { NoteOn( midiNote, midiVelocity ); } else { NoteOff( midiNote ); } } else if( midiCommand >= 128 && midiCommand < 144 ) { NoteOff( midiNote ); } else { //<<< "unknown midi command:", midiCommand, midiNote, midiVelocity >>>; } }