class CassetteVoice extends VoiceBankVoice { // override 0.5 => gainAtZeroVelocity; 0.5 => highCutoffSensitivity; -0.3 => lowCutoffSensitivity; 400 => float myLPFCutoff; LPF lpf => adsr; WaveTable osc1 => lpf; WaveTable osc2 => lpf; Noise osc3 => Gain noiseGain => lpf; // osc 1: puls5 down an octave, 37% volume // osc 2: puls1, 39% volume // osc 3: white noise, 22% volume (actually quieter) osc1.init( me.dir() + "puls5_c1_1cycle_441k.wav", 24 ); osc2.init( me.dir() + "puls1_c1_2cycle_441k.wav", 24 ); 0.37 => osc1.gain; 0.39 => osc2.gain; 0.22 => osc3.gain; // all through LPF with start cutoff 0.82, end cutoff 0.17 // do not know what these values mean in Hz! // I think 0.0 is the note freq and 1.0 is 20000Hz // maybe map it through midi notes though 20000 => Std.ftom => float oneCutoff; myFreq => Std.ftom => float zeroCutoff; fun float cutoffToHz( float cutoff ) { Std.scalef( Std.clampf( cutoff, 0, 1 ), 0, 1, zeroCutoff, oneCutoff ) => float cutoffMidi; return Std.mtof( cutoffMidi ); } // 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( 1::ms, 140::ms, 0.0, 0.0::ms ); fun void triggerLPFEnv() { myFreq => Std.ftom => zeroCutoff; 0.17 => float minCutoff; 1.0 => float maxCutoff; minCutoff => float currentCutoff; maxCutoff - minCutoff => float cutoffDiff; // scale freq dif by current velocity myVelocity *=> cutoffDiff; lpfEnv.keyOn( 1 ); 0.5::ms => dur delta; now + lpfEnv.attackTime() + lpfEnv.decayTime() + 2 * delta => time end; while( now < end ) { // set minCutoff + cutoffDiff * lpfEnv.value() => currentCutoff; Math.pow( currentCutoff, 1.5 ) => currentCutoff; currentCutoff + myCutoff => this.cutoffToHz => myLPFCutoff; lpfEnv.value() => noiseGain.gain; // wait delta => now; } } // lpf is a little resonant (0.16/1, so maybe Q = 1.16) with a tiny amount of drive (negligible) 1.16 => lpf.Q; // then ADSR with A = 0ms, D = 99ms, S = 1, R = 240ms 240::ms => rTime; adsr.set( 0::ms, 99::ms, 1.0, rTime ); // analog: randomly alter pitch and cutoff (0 to 1: 0.04) 0.03 => float analog; fun void applyAnalog() { float a; float f; while( true ) { Math.random2f( 1 - analog, 1 + analog ) => a; myLPFCutoff * a => lpf.freq; myFreq * a => f; f / 2 => osc1.freq; f => osc2.freq; // noise has no freq // f => osc3.freq; 1::ms => now; } } spork ~ this.applyAnalog(); // trigger note on fun void noteOn() { adsr.keyOn( 1 ); spork ~ this.triggerLPFEnv(); } // trigger note off fun void noteOff() { adsr.keyOff( 1 ); } } class Cassette extends VoiceBank { // override 8 => numVoices; // voices CassetteVoice myVoices[numVoices]; // assign to superclass v.size( myVoices.size() ); for( int i; i < myVoices.size(); i++ ) { myVoices[i] @=> v[i]; } // and connect init( true ); } Cassette c => LPF l => JCRev rev => dac; 0.6 => c.gain; 0.0 => rev.mix; 15000 => l.freq; // knobs global float gReverb; global float gCutoff; global float gLowpass; fun void ApplyGlobals() { while( true ) { 10::ms => now; gReverb => rev.mix; gLowpass => l.freq; gCutoff => c.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; c.noteOn( m, velocity ); //<<< "on", m, v >>>; } fun void NoteOff( int m ) { spork ~ c.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 >>>; } }