From 2c55dd46a6191c4c6820975a691bc5637ef15488 Mon Sep 17 00:00:00 2001 From: Oleksiy H Date: Sat, 4 Oct 2025 23:16:21 +0300 Subject: [PATCH] First commit. FLMNG and DubSiren are already here --- .gitignore | 1 + DubSiren/DubSiren.ino | 246 ++++++++++++++++++++++++++++++++++++++++++ FLMNG/FLMNG.ino | 238 ++++++++++++++++++++++++++++++++++++++++ README.md | 8 +- 4 files changed, 492 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 DubSiren/DubSiren.ino create mode 100644 FLMNG/FLMNG.ino diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1addd27 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*/build/arduino.avr.nano diff --git a/DubSiren/DubSiren.ino b/DubSiren/DubSiren.ino new file mode 100644 index 0000000..4a11a63 --- /dev/null +++ b/DubSiren/DubSiren.ino @@ -0,0 +1,246 @@ +// DubSiren 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: +//Transform Flamingo into DubSiren :) +//✅ Square oscilator +//✅ Square/Sine pitch LFO on switch +//✅ Filter +//⬜ Delay? + +#include +#include +#include +#include +#include +#include +//#include +#include +#include +#include +#include +#include +#include + +//Band limited oscilator tables for aliasing-free Meta Oscilator +#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}; + + +//Settings +const int pitchSubSteps = 32; //set how many steps are there between semitones. set to 1 to quantize to semitones +const int driveAmount = 450; +#define MIDI_CHANNEL MIDI_CHANNEL_OMNI +#define MOZZI_CONTROL_RATE 1024 + +//Hardware Definitions +#define Knob1 A6 //LFO Frequency +#define Knob2 A4 //LFO Intensity +#define Knob3 A2 //LPF Resonance +#define Knob4 A0 //LPF Cutoff +#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(); + +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 ); + +//Oscil aSquare(SQUARE_ANALOGUE512_DATA); +Oscil kSquareLFO(SQUARE_NO_ALIAS_2048_DATA); +Oscil kSineLFO(SIN2048_DATA); + +ADSR envelope; +StateVariable lpf; +OverSample kOverSamplePitch; + +//Global variables +byte gain; +bool MIDINotePlaying; +bool gateIsHigh = false; +float noteFreq; + +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); + + startMozzi(); + + envelope.setAttackLevel(255); + + digitalWrite(LED, HIGH); +} + +void updateControl(){ + //Get Control Values + int CVInVal = mozziAnalogRead(CVIn); + 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); + bool envSwitchVal = digitalRead(EnvSwitch); + bool gateInVal = !digitalRead(GateIn); + + //Remap the values and assign to parameter + float LFOSpeed = kMapLFOSpeed(knob1Val) / 100; + float expLFOSpeed = (float) LFOSpeed * LFOSpeed / 800000; + float freqMod = (float) kMapFreqMod(knob2Val) / 1023; + float modSpeed = LFOSpeed; + int resonance = kMapResonance(knob3Val); + int cutoff = kMapCutoff(knob4Val); + + //Set pitch and play notes on trigger + if (!MIDINotePlaying) { + int oversampledCVInVal = kOverSamplePitch.next(CVInVal); + noteFreq = mtof((float) kMapNote(oversampledCVInVal) / pitchSubSteps); + //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 + + //LFO stuff + //Set oscillator frequencies + kSquareLFO.setFreq(modSpeed); + kSineLFO.setFreq(modSpeed); + int LFOValue; + if(envSwitchVal) { + LFOValue = kSquareLFO.next()-127; //-109 to bring the rangt to ~ 0-192 + } else { + LFOValue = kSineLFO.next()-127; + } + + //Set Oscilator Frequency + float oscFreq = noteFreq - noteFreq * LFOValue * freqMod * 3 / 256; + //UFix<16,16> oscFreq = + aSquare.setFreq(oscFreq); + + envelope.update(); + + int env = envelope.next(); + + if(!droneSwitchVal) { + gain = env; + } else { + gain = 255; + } + + MIDI.read(); +} + +AudioOutput updateAudio(){ + long signal = aSquare.next(); + 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/FLMNG/FLMNG.ino b/FLMNG/FLMNG.ino new file mode 100644 index 0000000..c3d5760 --- /dev/null +++ b/FLMNG/FLMNG.ino @@ -0,0 +1,238 @@ +// 2 Operator FM Synth firmware for Sitka Instruments WS-1.0 +// by Oleksiy Hrachov +// +// Code is partly based on Knob_LightLevel_x2_FMSynth example from Mozzi Library +// +// 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: +//test how accurate and fast oversampling is +//smooth harmonics knob? +//rework LFO/Intensity so when LFO rate is 0, intensidy doesn't depend on its phase + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +//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 1 //MIDI_CHANNEL_OMNI +#define MOZZI_CONTROL_RATE 1024 + +//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(); + +IntMap kMapCarrierNote( 0, 4095, 24 * pitchSubSteps, 84 * pitchSubSteps ); +IntMap kMapIntensity( 0, 1023, 10, 350 ); +IntMap kMapHarmonics( 0, 1023, 1, 40 ); +IntMap kMapLFOSpeed( 0, 1023, 1, 10000 ); +IntMap kMapAttack( 0, 1023, 0, 80 ); +IntMap kMapDecayRelease( 0, 1023, 8, 160 ); +IntMap kMapSustain( 0, 1023, 0, 255 ); + +Oscil aCarrier(SIN2048_DATA); +Oscil aModulator(SIN2048_DATA); +Oscil kSineLFO(SIN2048_DATA); +Oscil kSquareLFO(SQUARE_NO_ALIAS_2048_DATA); +Oscil kSawLFO(SAW2048_DATA); +Oscil kNoiseLFO(WHITENOISE8192_DATA); +ADSR envelope; +Smooth aSmoothIntensity(0.95f); +OverSample kOverSamplePitch; + +//Global variables +byte gain; +bool MIDINotePlaying; +bool gateIsHigh = false; +float carrierFreq; +long FMIntensity; // carries control info from updateControl to updateAudio + +void MIDINoteOn(byte channel, byte note, byte velocity) { + carrierFreq = 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); + + startMozzi(); + + envelope.setAttackLevel(255); + + digitalWrite(LED, HIGH); +} + +void updateControl(){ + //Get Control Values + int CVInVal = mozziAnalogRead(CVIn); + 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); + bool envSwitchVal = digitalRead(EnvSwitch); + bool gateInVal = !digitalRead(GateIn); + + //Remap the values and assign to parameter + int intensity = kMapIntensity(knob1Val); + int harmonics = kMapHarmonics(knob2Val); + int LFOSpeed = kMapLFOSpeed(knob3Val); + float expLFOSpeed = (float) LFOSpeed * LFOSpeed / 400000; + float modSpeed = expLFOSpeed; + + //Set pitch and play notes on trigger + if (!MIDINotePlaying) { + if (oversamplingEnabled) { + int oversampledCVInVal = kOverSamplePitch.next(CVInVal); + carrierFreq = mtof((float) kMapCarrierNote(oversampledCVInVal) / pitchSubSteps); + } else { + carrierFreq = mtof((float) kMapCarrierNote(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 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 + + //Calculate the modulation frequency + int FMmodFreq = carrierFreq * harmonics; + + //Set oscillator frequencies + aCarrier.setFreq(carrierFreq); + aModulator.setFreq(FMmodFreq); + kSineLFO.setFreq(modSpeed); + kSquareLFO.setFreq(modSpeed); + kSawLFO.setFreq(modSpeed); + kNoiseLFO.setFreq(modSpeed/4096); + + envelope.update(); + + int sineLFOLevel; + int squareLFOLevel; + int sawLFOLevel; + int noiseLFOLevel; + if (knob4Val < 255) { + sineLFOLevel = 255 - knob4Val; + squareLFOLevel = knob4Val; + sawLFOLevel = 0; + noiseLFOLevel = 0; + } else if (knob4Val < 511) { + sineLFOLevel = 0; + squareLFOLevel = 511 - knob4Val; + sawLFOLevel = knob4Val - 255; + noiseLFOLevel = 0; + } else if (knob4Val < 767) { + sineLFOLevel = 0; + squareLFOLevel = 0; + sawLFOLevel = 767 - knob4Val; + noiseLFOLevel = knob4Val - 511; + } else { + sineLFOLevel = 0; + squareLFOLevel = 0; + sawLFOLevel = 0; + noiseLFOLevel = 255; + } + + int shapedLFO = (kSineLFO.next() * sineLFOLevel + + kSquareLFO.next() * squareLFOLevel + + kSawLFO.next() * sawLFOLevel + + kNoiseLFO.next() * noiseLFOLevel)>>8; + + int env = envelope.next(); + int intensityEnv; + if(envSwitchVal) { + intensityEnv = env*3; + } else { + intensityEnv = 1; + } + FMIntensity = ((long)(intensity + intensityEnv) * (shapedLFO+128))>>8; //(shapedLFO+128))>>16; //(kSineLFO.next()+128))>>8; + + if(!droneSwitchVal) { + gain = env; + } else { + gain = 255; + } + + MIDI.read(); +} + +AudioOutput updateAudio(){ + long modulation = aSmoothIntensity.next(FMIntensity) * aModulator.next(); + long signal = (aCarrier.phMod(modulation) * gain) >> 8; //envelope + signal = softClip((signal * (127 + driveAmount)) >> 8); //overdrive + return MonoOutput::from8Bit(signal); +} + +void loop(){ + audioHook(); +} \ No newline at end of file diff --git a/README.md b/README.md index 99dfa08..d8c824b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ # WS-1.0-Firmwares -Collection of firmwares for WS-1.0 Arduino-based Synthesizer \ No newline at end of file +Collection of firmwares for WS-1.0 Arduino-based Synthesizer + +## FLMNG +2-operator FM synth with LFO mapped to FM intensity + +## DubSiren +Simple synth for creating dub-style sirens: square wave oscillator, sine or square wave LFO mapped to oscillator pitch, resonant filter.