From a1138ef1c523d3046df0ef20775519e5b7d51c47 Mon Sep 17 00:00:00 2001 From: fakebitpolytechnic Date: Mon, 29 Jul 2013 13:52:59 +0100 Subject: Working 2-note FM polyphony (via EAD env rather than ADSR) --- .../CheapSynth_FM4osc2Poly_v0_5_6.ino | 421 +++++++++++++++++++++ 1 file changed, 421 insertions(+) create mode 100644 CheapSynth_FM4osc2Poly_v0_5_6/CheapSynth_FM4osc2Poly_v0_5_6.ino diff --git a/CheapSynth_FM4osc2Poly_v0_5_6/CheapSynth_FM4osc2Poly_v0_5_6.ino b/CheapSynth_FM4osc2Poly_v0_5_6/CheapSynth_FM4osc2Poly_v0_5_6.ino new file mode 100644 index 0000000..5c776b0 --- /dev/null +++ b/CheapSynth_FM4osc2Poly_v0_5_6/CheapSynth_FM4osc2Poly_v0_5_6.ino @@ -0,0 +1,421 @@ + +/* + * FM+LFO notes from Mozzi in response to MIDI input, + * using the standard Arduino MIDI library: + * http://playground.arduino.cc/Main/MIDILibrary + * + * Mozzi help/discussion/announcements: + * https://groups.google.com/forum/#!forum/mozzi-users + * + * Massively based on examples by Tim Barrass 2013nextenv + * This example code is in the public domain. + * + * sgreen/dpape/dgreen - modified to bring together harmonic FM, LFO, portamento, + * mono note priority, & Rock Band keytar controls - http://www.cheapsynth.com/ + * + * Audio output from pin 9 (pwm) + * Midi plug pin 2 (centre) to Arduino gnd, pin 5 to RX (0) + * http://www.philrees.co.uk/midiplug.htm + * + On Rock Band keytar, switch between patch modes using the big transparent button above +the LEDs, as follows... + +Mode 0 - monophonic FM lead sound with variable modulation harmonics, portamento, modulation depth +on modulation strip, low-frequency oscillator (LFO) speed on pitch bend strip (ie button held down) + +Mode 1 - (2-note polyphonic) Squarewave added to squared-off sinewave at variable harmonics, +pitch bend strip adjusts "duty cycle", ie how much the squared sine is On vs Off + +Mode 2 - (2-note polyphonic) Actually sawtooth wave, pitch bend strip gives chorus-y effect by +variably detuning one of the current notes + +Mode 3 - (2-note "polyphonic") Squarewave XOR'd with sinewaves at variable harmonics, similar fake-LFO +detuning on pitch bend strip, massive upwards zoom on both notes when no portamento! + +Mode 4 - intentionally left blank so you know where you are + +Standard controls: +Note decay length on forward arrow to the right of big transparent Mode button (currently applies +to less computationally demanding envelopes in Mode 0 and 2), toggles between short/medium/long + +Different modulation freq harmonics on the right-hand corner up and down keys between octave up +and octave down (all modes except Mode 2). + +Portamento speed on Back arrow to the left of big transparent Mode button, toggles between no portamento +(ie 2-note polyphony in modes that support it), 10ms fast glide between notes, 400ms slow glide. +*/ + +#include +#include +#include /oscSaw/ oscillator template +#include // for envelope +#include // table for Oscils to play +#include // table for Oscils to play +#include // NB portamento requires table > 512 size? +#include +#include +#include +#include +#include +#include + +// use #define for CONTROL_RATE, not a constant +#define CONTROL_RATE 128 // powers of 2 please + +// Mozzi example uses COS waves for carrier and modulator +// DP: "gets brutal very fast" (esp in higher octaves) if you use a saw/square carrier +// gets subtly more brutal if you use smaller wavetables (fewer samples per cycle) +Oscil oscCarrier(SMOOTHSQUARE8192_DATA); // huge file :( +Oscil oscSquare2(SMOOTHSQUARE8192_DATA); // huge file :( +Oscil oscModulator(COS2048_DATA); +Oscil oscModulator2(COS2048_DATA); +Oscil oscLFO(COS2048_DATA); +Oscil oscSaw1(SAW1024_DATA); +Oscil oscSaw2(SAW1024_DATA); + +// envelope generator +ADSR envelope; +ADSR envelope2; +Ead kEnvelopeGuitar1(CONTROL_RATE); +Ead kEnvelopeGuitar2(CONTROL_RATE); + +Portamento aPortamento; +//LowPassFilter lpf; // LPF left over from earlier versions of code, may reinstate! +//int crushCtrl = 0; // ditto bitcrush + +#define LED 13 // to see if MIDI is being recieved + +long vibrato=0; +long vibrato2=0; +int gainGuitar1; +int gainGuitar2; +int LFO; +unsigned int carrierXmodDepth1; +unsigned int carrierXmodDepth2; + +// initialise globals (some eg amplitude no longer used) +int maxmode=4; +float carrierFreq = 10.f; +float carrier2 = 10.f; +float modFreq = 10.f; +float modFreq2 = 10.f; +float modDepth = 0; +float amplitude = 0.f; +float modOffset =1; +byte lastnote1=0; +byte lastnote2=0; +int nextenv=1; +byte portSpeed=0; +byte noteLength=1; +float shifted=0.1f; +byte mode=0; +unsigned long harmonic; +int squaredsin1; +int squaredsin2; +int bitmask=64; +int multiplier; + +float modOffsets[] = { +4,3.5,3,2.5,2,1.5,1,0.6666666,0.5,0.4,0.3333333,0.2857,0.25,0,0,0,0,0,0, +}; // harmonic ratios corresponding to DP's preferred intervals of 7, 12, 7, 19, 24, 0, 12, -12, etc + +void setup() { + pinMode(LED, OUTPUT); + + // Initiate MIDI communications, listen to all channels + MIDI.begin(MIDI_CHANNEL_OMNI); + + // Connect the HandleNoteOn function to the library, so it is called upon reception of a NoteOn. + MIDI.setHandleNoteOn(HandleNoteOn); // Put only the name of the function + MIDI.setHandleNoteOff(HandleNoteOff); // Put only the name of the function + MIDI.setHandleControlChange(HandleControlChange); + MIDI.setHandlePitchBend(HandlePitchBend); // Put only the name of the function + MIDI.setHandleProgramChange(HandleProgramChange); + MIDI.setHandleContinue(HandleContinue); + MIDI.setHandleStop(HandleStop); + MIDI.setHandleStart(HandleStart); + + envelope.setADLevels(127,100); + envelope.setTimes(20,20,20000,850); // 20000 is so the note will sustain 20 seconds unless a noteOff comes + envelope2.setADLevels(127,100); + envelope2.setTimes(20,20,20000,850); // 20000 is so the note will sustain 20 seconds unless a noteOff comes + + kEnvelopeGuitar1.set(20, 4000); + kEnvelopeGuitar2.set(20, 4000); + + aPortamento.setTime(0u); +// lpf.setResonance(200); +// lpf.setCutoffFreq(255); + oscLFO.setFreq(10); // default LFO frequency + + startMozzi(CONTROL_RATE); +} + + +void UpdateFreqs() +{ + oscCarrier.setFreq(carrierFreq); + oscSaw1.setFreq(carrierFreq); + carrierXmodDepth1=carrierFreq * modDepth; + modFreq=(carrierFreq * modOffset / (64/bitmask) ); + + oscSquare2.setFreq(carrier2); + oscSaw2.setFreq(carrier2 * (1.0f + ((shifted -0.1f) / 384)) ); + carrierXmodDepth2=carrier2 * modDepth; + modFreq2=(carrier2 * modOffset / (64/bitmask) ); + + oscModulator.setFreq( modFreq ); + oscModulator2.setFreq( modFreq2 ); +} + +void HandleProgramChange (byte channel, byte number) +{ + modOffset = modOffsets[number%15]; + UpdateFreqs(); +// modFreq=(carrierFreq * modOffset / (64/bitmask) ); +//dreadful hack as ProgramChange returns absolute prog number 0-127, "gets stuck" at upper/lower ends +} + +void HandleNoteOn(byte channel, byte note, byte velocity) { + aPortamento.start(note); +// carrierFreq=(mtof(note)); // simple but less accurate frequency +// osc.setFreq_Q16n16(Q16n16_mtof(Q8n0_to_Q16n16(note))); // accurate frequency +if ( portSpeed) + {nextenv=1; lastnote2=999;} + + if (nextenv==1) + { + lastnote1=note; + envelope.noteOn(); +kEnvelopeGuitar1.start(); + digitalWrite(LED,HIGH); + nextenv=2; // send next note to other envelope, unless overridden in the meantime + + carrierFreq = Q16n16_to_float (Q16n16_mtof(Q8n0_to_Q16n16(lastnote1))); + UpdateFreqs(); + } + else + { + lastnote2=note; + envelope2.noteOn(); +kEnvelopeGuitar2.start(); + digitalWrite(LED,HIGH); + nextenv=1; // send next note to other envelope, unless overridden in the meantime + + carrier2 = Q16n16_to_float (Q16n16_mtof(Q8n0_to_Q16n16(lastnote2))); + UpdateFreqs(); + } + +} + +void HandleStop () { //this is the Back button on the Xbox keyboard + portSpeed++; + {if (portSpeed>2) portSpeed = 0;} + aPortamento.setTime( (portSpeed*portSpeed*100) ); // values of 0,100,400 milliseconds? (next 900) +} + +void HandleStart () { //this is the "Start" button on the Xbox keyboard + noteLength=(noteLength+1)%3; // loop from 0-2 + kEnvelopeGuitar1.set(20, (noteLength+1)*(noteLength+1)*(noteLength+1)*500 ); + kEnvelopeGuitar2.set(20, (noteLength+1)*(noteLength+1)*(noteLength+1)*500 ); + //cube into values around 500, 4000, 13500 +} + +void HandleContinue () { // the big round button on all keytars + mode++; + {if (mode>maxmode) mode = 0;} //currently has a silent mode at the end so you know where you are : ) +} + +void HandleNoteOff(byte channel, byte note, byte velocity) { + digitalWrite(LED,LOW); // may as well switch LED off + + if ( (note == lastnote1) ) //kill envelope if prev note released + { + envelope.noteOff(); + if (gainGuitar2>0) {nextenv=1;} +} + + if ( (note == lastnote2) ) + {envelope2.noteOff(); + if (gainGuitar1>0) {nextenv=2;} + } + +} + + +void HandlePitchBend (byte channel, int bend) +{ +//bend value from +/-8192, translate to 0.1-8 Hz? +shifted= float ((bend+8500) /2048.f ) +0.1f; +if (mode==0) {oscLFO.setFreq(shifted); } +if (mode==1) + { + bitmask= 1 << (int (shifted)); multiplier=7-(int (shifted)); + UpdateFreqs(); + } + else {bitmask=64;} + +if (mode==2) { oscSaw2.setFreq(carrier2 * (1.0f + ((shifted -0.1f) / 384)) ); } +} + +void HandleControlChange (byte channel, byte number, byte value) +{ +// http://www.indiana.edu/~emusic/cntrlnumb.html + +/* +Serial.end(); // sample debug code to check values through (noisy) terminal interface +Serial.begin(38400); +Serial.print(" number:"); +Serial.print(number); +Serial.print(" value:"); +Serial.println( value ); +Serial.println(); +Serial.end(); +Serial.begin(31250); +*/ + + switch(number) { +// case 3: // could trap different controller IDs here + + case 1: // modulation wheel + //lpf.setResonance(value*2); + + //WORKING MODDEPTH CONTROLLER + float divided= float (value /46.f ); // values=0-127, map to 0-2.75 + modDepth = (divided * divided); // square to 0-8 + if (modDepth>7.5) {modDepth=7.5;} // sanity check (prob unnecessary) + if (modDepth<0.2) {modDepth=0;} // easier to get pure tone + UpdateFreqs(); + break; + } +} + +void updateControl(){ + MIDI.read(); + envelope.update(); + envelope2.update(); + + gainGuitar1 = kEnvelopeGuitar1.next(); + gainGuitar2 = kEnvelopeGuitar2.next(); + if(gainGuitar1 > 120) gainGuitar1 = 120; + if(gainGuitar2 > 120) gainGuitar2 = 120; + +if ( portSpeed ) + { + carrierFreq = Q16n16_to_float ( aPortamento.next() ); //NB presumably returns frequency as Q16n16 + UpdateFreqs(); + } + +if (mode==3) + { + modFreq *= 1.0f +(shifted / 384); // generates fake LFO via detuning mod osc + modFreq2*= 1.0f +(shifted / 384); // generates fake LFO via detuning mod osc +// modFreq2*= 1 + (shifted -0.1f) / 384); // generates fake LFO via detuning mod osc + oscModulator.setFreq( modFreq ); + oscModulator2.setFreq( modFreq2 ); + } + +} + +// strip the low bits off! - not used in current implementation +int bitCrush(int x, int a) +{ + return (x>>a)<>7 ; +vibrato*= carrierXmodDepth1; +vibrato2 = ( LFO * oscModulator2.next() ) >>7 ; +vibrato2*= carrierXmodDepth2; +return (int) ( + (oscCarrier.phMod( vibrato >>3 ) * gainGuitar1 ) + + (oscSquare2.phMod( vibrato2>>3 ) * gainGuitar2 ) + ) >> 9; +// >> 9 = 9 fast binary divisions by 2 = divide by 512 + break; + + case 1: // ADDING square + "converted" sine +//squaredsin1= ( (oscModulator.next() & 0b0000000001000000)-1 ) <<1; +squaredsin1= ( (oscModulator.next() & bitmask) ) <>1 )*envelope.next() ) ++ ( ( (oscSquare2.next() + squaredsin2 )>>1 )*envelope2.next() ) +) >> 8; + break; + + case 2: // 2 tri poly +return (int)( + ( oscSaw1.next() *gainGuitar1 ) + + ( oscSaw2.next() *gainGuitar2 ) + ) >> 9; + break; + + case 3: // STARTED AS 2 sq poly XORd with respective sines, now big whoop +return (int)( + ( (oscCarrier.next()^oscModulator.next()) * envelope.next() ) + + ( (oscSquare2.next()^oscModulator2.next()) * envelope2.next() ) + ) >> 8; +//return (int) ( (oscCarrier.next() | ( (oscModulator.next() * int (modDepth+1))>>3 ) ) * (envelope.next() ) ) >> 7; + break; + + case 13: // 2 sq poly ORd with respective sines +return (int) ( ( (oscCarrier.next()|oscModulator.next() ) * envelope.next()) + ( (oscSquare2.next()|oscLFO.next() ) * envelope2.next()) ) >> 8; +//return (int) ( (oscCarrier.next() | ( (oscModulator.next() * int (modDepth+1))>>3 ) ) * (envelope.next() ) ) >> 7; + break; + + + case 6: // 2 sq poly +return (int) ( (oscCarrier.next() * envelope.next()) + (oscSquare2.next() * envelope2.next()) ) >> 8; +//return (int) ( (oscCarrier.next() | ( (oscModulator.next() * int (modDepth+1))>>3 ) ) * (envelope.next() ) ) >> 7; + break; + +//case 5 intentionally left blank + +/* + case 6: // ADDING squares +return (int) ( (oscCarrier.next() + oscSquare2.next() ) * (envelope.next() ) ) >> 8; +//return (int) ( (oscCarrier.next() | ( (oscModulator.next() * int (modDepth+1))>>3 ) ) * (envelope.next() ) ) >> 7; + break; +*/ +// case 1: // AND with fake LFO +//all this 'harmonic' stuff a failed attempt to use the same LFO as FM, causes massive beat f/x? +//harmonic = ( oscLFO.next() * oscModulator.next() ) >> 7; +//harmonic = ( harmonic * int (modDepth+1) ) >>3 ; +//return (int) ( (oscCarrier.next() ^ ( harmonic )) * (envelope.next() ) ) >> 7; +//return (int) ( (oscCarrier.next() & ( (oscModulator.next() * int (modDepth+1))>>3 ) ) * (envelope.next() ) ) >> 7; +// break; + +/* case 3: // OR with fake LFO +//int harmonic = ( oscLFO.next() * oscModulator.next() ) >>7; +//return (int) ( (oscCarrier.next() | ( (oscModulator.next() * int (modDepth+1))>>3 ) ) * (envelope.next() ) ) >> 7; +// break; + case 4: // XOR with fake LFO +//int harmonic = ( oscLFO.next() * oscModulator.next() ) >>7; +return (int) ( (oscCarrier.next() ^ ( (oscModulator.next() * int (modDepth+1))>>3 ) ) * (envelope.next() ) ) >> 7; + break; + */ + } + +// OLD CODE FROM BITCRUNCHED SINEWAVE, DIFF WAYS OF DOING THAT +// int x = (envelope.next() * lpf.next(osc.next()))>>8; +// x = bitCrush(x, crushCtrl>>4); +// x = (x * crushCtrl)>>4; // simple gain +// x = bitCrush(x, 6); +// x = lpf.next(x); +// return x; +} + + +void loop() { + audioHook(); // required here +} + -- cgit v1.3