From 336dd2e3b6b56b15d8c165ae870b1318642e1d5f Mon Sep 17 00:00:00 2001 From: Oleksiy H Date: Sun, 6 Sep 2026 19:28:01 +0300 Subject: [PATCH] added WIP Acid --- Acid/Acid.ino | 238 ++++++++++++++++++++++++++++++++++++++++++++++++++ Acid/config.h | 24 +++++ 2 files changed, 262 insertions(+) create mode 100644 Acid/Acid.ino create mode 100644 Acid/config.h diff --git a/Acid/Acid.ino b/Acid/Acid.ino new file mode 100644 index 0000000..76e9302 --- /dev/null +++ b/Acid/Acid.ino @@ -0,0 +1,238 @@ +// 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 + +#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 kMapLFOSpeed( 0, 1023, 500, 20000 ); +IntMap kMapFreqMod( 0, 1023, 0, 1023 ); +IntMap kMapResonance( 0, 1023, 200, 1 ); +IntMap kMapCutoff( 0, 1023, 60, 3600 ); +IntMap kMapAttack( 0, 1023, 0, 80 ); +IntMap kMapDecayRelease( 0, 1023, 8, 160 ); +IntMap kMapSustain( 0, 1023, 0, 255 ); + +ADSR envelope; +StateVariable lpf; + +//Global variables +byte gain; +bool MIDINotePlaying; +bool gateIsHigh = false; +float noteFreq; +bool envSwitchVal; + +void MIDINoteOn(byte channel, byte note, byte velocity) { + noteFreq = mtof((int) note); + envelope.noteOn(); + MIDINotePlaying = true; + digitalWrite(LED, LOW); +} + +void MIDINoteOff(byte channel, byte note, byte velocity) { + envelope.noteOff(); + digitalWrite(LED, HIGH); +} + +long softClip(long input) { + int threshold = 2048; + 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.setHandleNoteOn(MIDINoteOn); + MIDI.setHandleNoteOff(MIDINoteOff); + MIDI.begin(MIDI_CHANNEL); + + 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(); + + envelope.setAttackLevel(255); + + 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); + bool droneSwitchVal = digitalRead(DroneSwitch); + envSwitchVal = digitalRead(EnvSwitch); + bool gateInVal = !digitalRead(GateIn); + int CVInVal = mozziAnalogRead(CVIn); + + //Remap the values and assign to parameter + int resonance = kMapResonance(knob3Val); + int cutoff = kMapCutoff(knob4Val); + + //Set pitch and play notes on trigger + if (!MIDINotePlaying) { + noteFreq = mtof((float) kMapNote(CVInVal << 2) / pitchSubSteps); + digitalWrite(LED, !gateInVal); + if (gateInVal && !gateIsHigh) { + gateIsHigh = true; + envelope.noteOn(); + } else if (!gateInVal && gateIsHigh) { + gateIsHigh = false; + envelope.noteOff(); + } + } else if (MIDINotePlaying && !envelope.playing()) { + MIDINotePlaying = false; + } + + //Update Filter settings + lpf.setResonance(resonance); + //lpf.setCentreFreq(cutoff); + + //Update Envelope Settings + int attackTime = kMapAttack(knobAVal); + int decayReleaseTime = kMapDecayRelease(knobDRVal); + int sustainLevel = kMapSustain(knobSVal); + envelope.setDecayLevel(sustainLevel); + envelope.setTimes(attackTime, decayReleaseTime, 30000, decayReleaseTime); //30000 is so the note will sustain 30 seconds unless a noteOff comes + + //Set Oscilator Frequency + float oscFreq = noteFreq; + aSquare.setFreq(oscFreq); + aSaw.setFreq(oscFreq); + + envelope.update(); + + int env = envelope.next(); + + if(!droneSwitchVal) { + gain = env; + } else { + gain = 255; + } + lpf.setCentreFreq(60 + env * 14); + +} + +AudioOutput updateAudio(){ + long signal; + if(envSwitchVal) { + signal = aSquare.next() * 0.3; + } else { + signal = aSaw.next() * 1.2; + } + signal = lpf.next(signal); //filter + signal = (signal * gain); //envelope + //signal = softClip((signal * (127 + driveAmount)) >> 8); //overdrive + return MonoOutput::fromNBit(17, signal).clip(); +} + +void loop(){ + audioHook(); +} \ No newline at end of file diff --git a/Acid/config.h b/Acid/config.h new file mode 100644 index 0000000..5d4097d --- /dev/null +++ b/Acid/config.h @@ -0,0 +1,24 @@ +//Settings +const int pitchSubSteps = 16; //set how many steps are there between semitones. set to 1 to quantize to semitones +const int driveAmount = 350; +const bool oversamplingEnabled = false; //makes V/OCT tracking more precise, but adds a little portamento +#define MIDI_CHANNEL 2 //MIDI_CHANNEL_OMNI +#define MOZZI_CONTROL_RATE 256 + +//Hardware Definitions +#define Knob1 A6 //Intensity +#define Knob2 A4 //Modulator frequency ratio/Harmonics +#define Knob3 A2 //LFO Frequency +#define Knob4 A0 //LFO Shape +#define KnobA A5 //Attack +#define KnobDR A3 //Decay and Release +#define KnobS A1 //Sustain +#define CVIn A7 //CV input and Pitch knob +#define GateIn 10 +#define EnvSwitch 11 +#define DroneSwitch 12 +#define LED 5 + +#define MOZZI_ANALOG_READ_RESOLUTION 10 + +MIDI_CREATE_DEFAULT_INSTANCE(); \ No newline at end of file