// Acid synth firmware for Sitka Instruments WS-1.0 // by Oleksiy Hrachov // // Although the code designed to work on Sitka Instruments WS-1.0 synth, it should // be pretty easy to adapt to run on other arduino/mozzi-based setups // // This code is licenced under GPL v3 or later // todo: // debug accent handling that overloads the filter, also accent can be too short or too long if it's relying on midi note // add overdrive // frequency from CV (noteFreq) could be Q16n16 for consistency #include #include #include #include #include #include #include #include #include #include #include #include "config.h" //Band limited SQUARE oscilator tables #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include Oscil aSq90(SQUARE_MAX_90_AT_16384_512_DATA); Oscil aSq101(SQUARE_MAX_101_AT_16384_512_DATA); Oscil aSq122(SQUARE_MAX_122_AT_16384_512_DATA); Oscil aSq138(SQUARE_MAX_138_AT_16384_512_DATA); Oscil aSq154(SQUARE_MAX_154_AT_16384_512_DATA); Oscil aSq174(SQUARE_MAX_174_AT_16384_512_DATA); Oscil aSq210(SQUARE_MAX_210_AT_16384_512_DATA); Oscil aSq264(SQUARE_MAX_264_AT_16384_512_DATA); Oscil aSq327(SQUARE_MAX_327_AT_16384_512_DATA); Oscil aSq431(SQUARE_MAX_431_AT_16384_512_DATA); Oscil aSq546(SQUARE_MAX_546_AT_16384_512_DATA); Oscil aSq744(SQUARE_MAX_744_AT_16384_512_DATA); Oscil aSq910(SQUARE_MAX_910_AT_16384_512_DATA); Oscil aSq1170(SQUARE_MAX_1170_AT_16384_512_DATA); Oscil aSq1638(SQUARE_MAX_1638_AT_16384_512_DATA); Oscil aSq2730(SQUARE_MAX_2730_AT_16384_512_DATA); Oscil aSq8192(SQUARE_MAX_8192_AT_16384_512_DATA); MetaOscil aSquare {&aSq90, &aSq101, &aSq122, &aSq138, &aSq154, &aSq174, &aSq210, &aSq264, &aSq327, &aSq431, &aSq546, &aSq744, &aSq1170, &aSq1638, &aSq2730, &aSq8192}; //Band limited SAW oscilator tables #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include Oscil aSaw138(SAW_MAX_138_AT_16384_512_DATA); Oscil aSaw154(SAW_MAX_154_AT_16384_512_DATA); Oscil aSaw182(SAW_MAX_182_AT_16384_512_DATA); Oscil aSaw240(SAW_MAX_240_AT_16384_512_DATA); Oscil aSaw292(SAW_MAX_292_AT_16384_512_DATA); Oscil aSaw341(SAW_MAX_341_AT_16384_512_DATA); Oscil aSaw390(SAW_MAX_390_AT_16384_512_DATA); Oscil aSaw431(SAW_MAX_431_AT_16384_512_DATA); Oscil aSaw512(SAW_MAX_512_AT_16384_512_DATA); Oscil aSaw630(SAW_MAX_630_AT_16384_512_DATA); Oscil aSaw744(SAW_MAX_744_AT_16384_512_DATA); Oscil aSaw910(SAW_MAX_910_AT_16384_512_DATA); Oscil aSaw1170(SAW_MAX_1170_AT_16384_512_DATA); Oscil aSaw1638(SAW_MAX_1638_AT_16384_512_DATA); Oscil aSaw2730(SAW_MAX_2730_AT_16384_512_DATA); Oscil aSaw4096(SAW_MAX_4096_AT_16384_512_DATA); Oscil aSaw8192(SAW_MAX_8192_AT_16384_512_DATA); MetaOscil aSaw {&aSaw138, &aSaw154, &aSaw182, &aSaw240, &aSaw292, &aSaw341, &aSaw390, &aSaw431, &aSaw512, &aSaw630, &aSaw744, &aSaw1170, &aSaw1638, &aSaw2730, &aSaw4096, &aSaw8192}; IntMap kMapNote( 0, 4095, 24 * pitchSubSteps, 84 * pitchSubSteps ); IntMap kMapCutoff( 0, 1023, 26, 100 ); IntMap kMapEnvMod( 0, 1023, 0, 140 ); IntMap kMapResonance( 0, 1023, 0, 255 ); IntMap kMapDrive( 0, 1023, 0, 400 ); IntMap kMapAttack( 0, 1023, 1, 100 ); IntMap kMapDecay( 0, 1023, 100, 1500 ); IntMap kMapAccentLevel( 0, 1023, 0, 128 ); Ead kEnvelope(MOZZI_CONTROL_RATE); LowPassFilter lpfr; Portamento kGlide; //Global variables byte gain; bool MIDINotePlaying; bool MIDINoteFaded = true; bool gateIsHigh = false; float noteFreq; bool envSwitchVal; bool droneSwitchVal; byte MIDIChannel; byte memCode = "a"; //a = MIDI Channel stored in 1023 int driveAmount; byte notePlaying; bool accent = false; void MIDINoteOn(byte channel, byte note, byte velocity) { notePlaying = note; accent = false; //reset if the previous note was accented if (velocity >= 127) { accent = true; } if (!MIDINotePlaying || !droneSwitchVal) { kGlide.setTime(2); //4 at 256, 2 at 512 control rate to avoid portamento glitches caused by 0 } else { kGlide.setTime(GLIDE_TIME); } kGlide.start(note); if (!MIDINotePlaying || !droneSwitchVal) { kEnvelope.start(); } MIDINotePlaying = true; MIDINoteFaded = false; digitalWrite(LED, LOW); } void MIDINoteOff(byte channel, byte note, byte velocity) { if (note == notePlaying || !droneSwitchVal) { MIDINotePlaying = false; digitalWrite(LED, HIGH); } } long softClip(long input) { int threshold = 24000; if (input < -threshold) { return -threshold + (input + threshold) / 4; } else if (input > threshold) { return threshold + (input - threshold) / 4; } else { return input; } } void setup(){ pinMode(LED, OUTPUT); pinMode(GateIn, INPUT_PULLUP); pinMode(EnvSwitch, INPUT_PULLUP); pinMode(DroneSwitch, INPUT_PULLUP); //MIDI Channel setup if(!digitalRead(GateIn)) { //MIDI setup mode, first knob you rotate = MIDI channel int knob1Orig = analogRead(Knob1); int knob2Orig = analogRead(Knob2); int knob3Orig = analogRead(Knob3); int knob4Orig = analogRead(Knob4); int knobAOrig = analogRead(KnobA); int knobDROrig = analogRead(KnobDR); int knobSOrig = analogRead(KnobS); int threshhold = 50; //To reduce noise from the input while(true) { if(abs(analogRead(Knob1) - knob1Orig) > threshhold) { MIDIChannel = 1; break; } else if (abs(analogRead(Knob2) - knob2Orig) > threshhold) { MIDIChannel = 2; break; } else if (abs(analogRead(Knob3) - knob3Orig) > threshhold) { MIDIChannel = 3; break; } else if (abs(analogRead(Knob4) - knob4Orig) > threshhold) { MIDIChannel = 4; break; } else if (abs(analogRead(KnobA) - knobAOrig) > threshhold) { MIDIChannel = 5; break; } else if (abs(analogRead(KnobDR) - knobDROrig) > threshhold) { MIDIChannel = 6; break; } else if (abs(analogRead(KnobS) - knobSOrig) > threshhold) { MIDIChannel = 7; break; } digitalWrite(LED, HIGH); //slow blinking to indicate the setup mode delay(500); digitalWrite(LED, LOW); delay(500); } EEPROM.write(0, MIDIChannel); EEPROM.write(1023, memCode); digitalWrite(LED, LOW); delay(150); for(int i = 0; i < MIDIChannel; i ++) { //fast blink to indicate saved MIDI channel digitalWrite(LED, HIGH); delay(150); digitalWrite(LED, LOW); delay(150); } delay(500); } else if (EEPROM.read(1023) == memCode) { MIDIChannel = EEPROM.read(0); } else { //Save default MIDI Channel to EEPROM if it wasn't set MIDIChannel = 1; //channel 1 by default EEPROM.write(0, MIDIChannel); EEPROM.write(1023, memCode); } disconnectDigitalIn(Knob1); disconnectDigitalIn(Knob2); disconnectDigitalIn(Knob3); disconnectDigitalIn(Knob4); disconnectDigitalIn(KnobA); disconnectDigitalIn(KnobDR); disconnectDigitalIn(KnobS); disconnectDigitalIn(CVIn); MIDI.setHandleNoteOn(MIDINoteOn); MIDI.setHandleNoteOff(MIDINoteOff); MIDI.begin(MIDIChannel); aSquare.setCutoffFreqs(90, 101, 122, 138, 154, 174, 210, 264, 327, 431, 546, 744, 1170, 1638, 2730, 8192); aSaw.setCutoffFreqs(138, 154, 182, 240, 292, 341, 390, 431, 512, 630, 744, 1170, 1638, 2730, 4096, 8192); startMozzi(); digitalWrite(LED, HIGH); } void updateControl(){ for(int i=0; i<10; i++) { // Hacky way to drain the buffer faster MIDI.read(); } //Get Control Values int knob1Val = mozziAnalogRead(Knob1); int knob2Val = mozziAnalogRead(Knob2); int knob3Val = mozziAnalogRead(Knob3); int knob4Val = mozziAnalogRead(Knob4); int knobAVal = mozziAnalogRead(KnobA); int knobDRVal = mozziAnalogRead(KnobDR); int knobSVal = mozziAnalogRead(KnobS); droneSwitchVal = digitalRead(DroneSwitch); envSwitchVal = digitalRead(EnvSwitch); bool gateInVal = !digitalRead(GateIn); int CVInVal = mozziAnalogRead(CVIn); //Remap the values and assign to parameters int accentLevel = kMapAccentLevel(knobSVal); int baseCutoff = kMapCutoff(knob1Val); int cutoff = accent? (baseCutoff + (((long)accentLevel * (100 - baseCutoff)) >> 7)) : baseCutoff; int resonance = accent? (255 - (((long)(128 - accentLevel) * (255 - kMapResonance(knob2Val))) >> 7)) : kMapResonance(knob2Val); int envMod = kMapEnvMod(knob3Val); driveAmount = kMapDrive(knob4Val); //Set pitch and play notes on trigger if (MIDINoteFaded) { noteFreq = mtof((float) kMapNote(CVInVal << 2) / pitchSubSteps); digitalWrite(LED, !gateInVal); if (gateInVal && !gateIsHigh) { gateIsHigh = true; kEnvelope.start(); } else if (!gateInVal && gateIsHigh) { gateIsHigh = false; } } //Update Envelope Settings int attackTime = kMapAttack(knobAVal); int baseDecay = kMapDecay(knobDRVal); int decayTime = accent? (100 + (((long)(128 - accentLevel) * (baseDecay - 100)) >> 7)) : baseDecay; kEnvelope.setDecay(decayTime); kEnvelope.setAttack(accent? 1 : attackTime); //Calculate makeup Gain //makeupGain = ((long)256 * 127) / (127 + driveAmount); //Set Oscilator Frequency Q16n16 oscFreq; if (!MIDINoteFaded) { oscFreq = kGlide.next(); aSquare.setFreq_Q16n16(oscFreq); aSaw.setFreq_Q16n16(oscFreq); } else { aSquare.setFreq(noteFreq); aSaw.setFreq(noteFreq); } int env = kEnvelope.next(); if (!MIDINotePlaying && !MIDINoteFaded && env == 0) { MIDINoteFaded = true; accent = false; } gain = accent? constrain(env + (((long)env * accentLevel) >> 7), 0, 255) : env; //needs finetuning lpfr.setCutoffFreqAndResonance(constrain(cutoff + (((long)env * envMod * cutoff)>>16), 0, 255), resonance); } AudioOutput updateAudio(){ long signal; if(envSwitchVal) { signal = (aSquare.next() * 3) >> 2; //tricky way to set 0.75 gain to ~ match saw level } else { signal = aSaw.next(); } signal = signal >> 2; //lowering the gain before filter to avoid filter overflow bug signal = lpfr.next(signal); //filter signal = (signal * gain) << 2; //envelope //signal = softClip((signal * (127 + driveAmount)) >> 9); //overdrive //signal = (signal * makeupGain) >> 7; //makeup gain compensation return MonoOutput::fromNBit(17, signal).clip(); } void loop(){ audioHook(); }