ATmega

Touch Synth

This is a piezo-based touch synthesizer that's based around an ATmega328P nano board.

// LOESS-LABS.NET
// Touch synthesizer
// Dylan Barry, 2025, GNU GPLv3

#include <MozziConfigValues.h>
#define MOZZI_AUDIO_CHANNELS MOZZI_STEREO
#define MOZZI_CONTROL_RATE 128 // This is the rate in hz at which the update_control function runs
#include <Mozzi.h>
#include <Oscil.h>
#include <RollingAverage.h>
#include <Smooth.h>
#include <tables/triangle_valve_2048_int8.h>  // Saturated triangle, sounds better than pure sine
#include <tables/triangle_dist_squared_2048_int8.h>  // Distorted triangle, has a buzzy octave sound
#include <tables/triangle_dist_cubed_2048_int8.h>  // Distorted triangle, sounds squareish
#include <tables/saw2048_int8.h>  // Sawtooth
#include <tables/pinknoise8192_int8.h>  // Pink noise 


// These are the variables you might want to mess with to fine tune piezo response.

// These set the threshold for the volume cutoff. Higher value = more cut off
const int SENSITIVITY_A = 150;
const int SENSITIVITY_B = 150;
const int SENSITIVITY_C = 150;
const int SENSITIVITY_D = 150;
// Threshold for the "middle" of the piezo voltages
const int FLIPTHRESH_A = 512;
const int FLIPTHRESH_B = 512;
const int FLIPTHRESH_C = 512;
const int FLIPTHRESH_D = 512;
// Multiplier applied after all the other stuff, bigger number, more gain
const int MULTIPLIER_A = 2;
const int MULTIPLIER_B = 2;
const int MULTIPLIER_C = 2;
const int MULTIPLIER_D = 2;


// Define a global pointer to the current wavetable data
const int8_t* currentWaveformTableA = TRIANGLE_VALVE_2048_DATA;
const int8_t* currentWaveformTableB = TRIANGLE_VALVE_2048_DATA;
const int8_t* currentWaveformTableC = TRIANGLE_VALVE_2048_DATA;
const int8_t* currentWaveformTableD = TRIANGLE_VALVE_2048_DATA;

// Define variables to set the current waveform selection
int waveIndexA = 0; int waveIndexB = 0; int waveIndexC = 0; int waveIndexD = 0;

// Define an array of available wavetable pointers
const int8_t* wavetablePointers[] = {
  TRIANGLE_VALVE_2048_DATA, TRIANGLE_DIST_SQUARED_2048_DATA, TRIANGLE_DIST_CUBED_2048_DATA, SAW2048_DATA, PINKNOISE8192_DATA
};
// Define list length
const int NUM_WAVEFORMS = 5;

// Initialize oscillators using valve triangle wavetable length with pointers to select actual sample selection
Oscil<TRIANGLE_VALVE_2048_NUM_CELLS, MOZZI_AUDIO_RATE> aSin(currentWaveformTableA);
Oscil<TRIANGLE_VALVE_2048_NUM_CELLS, MOZZI_AUDIO_RATE> bSin(currentWaveformTableB);
Oscil<TRIANGLE_VALVE_2048_NUM_CELLS, MOZZI_AUDIO_RATE> cSin(currentWaveformTableC);
Oscil<TRIANGLE_VALVE_2048_NUM_CELLS, MOZZI_AUDIO_RATE> dSin(currentWaveformTableD);

// Smoothers for piezo release reading
Smooth<long> smoothA(0.99f);
Smooth<long> smoothB(0.99f);
Smooth<long> smoothC(0.99f);
Smooth<long> smoothD(0.99f);

// Smooth pot readings for each channel
RollingAverage<int, 16> avgA;
RollingAverage<int, 16> avgB;
RollingAverage<int, 16> avgC;
RollingAverage<int, 16> avgD;

// Amp variables control volume of oscs
int ampA = 0; int ampB = 0; int ampC = 0; int ampD = 0;

// Degree to which modulator wave is bitshifted down
// Higher number = reduced intensity
uint8_t shiftA = 7; uint8_t shiftB = 7; uint8_t shiftC = 7; uint8_t shiftD = 7;

// Numerator and denominator variables for release note of each voice
uint8_t numerA = 1; uint8_t numerB = 1; uint8_t numerC = 1; uint8_t numerD = 1;
uint8_t denomA = 1; uint8_t denomB = 1; uint8_t denomC = 1; uint8_t denomD = 1;

// Variables to store cached ADC readings
int ampReadA, ampReadB, ampReadC, ampReadD;
bool shiftModeOn;  // Stores cached state of Pin 13

// Define rows and columns
const byte ROWS = 4;
const byte COLS = 3;

// Pin definitions
byte rowPins[ROWS] = { 2, 3, 4, 5 };  // Rows are OUTPUTS (D2-D5)
byte colPins[COLS] = { 6, 7, 8 };     // Columns are INPUTS (D6-D8)

// Global array to store the current debounced state of all 12 keys
bool keyStates[ROWS * COLS] = { false };
bool prevKeyStates[ROWS * COLS] = { false };
unsigned long lastDebounceTime[ROWS * COLS] = { 0 };
const unsigned long DEBOUNCE_DELAY = 50;

// Scans the keypad matrix. Updates the global keyStates array 
void scanKeypad() {
  unsigned long currentTime = millis();
  int keyIndex = 0;

  for (byte i = 0; i < ROWS; i++) {
    digitalWrite(rowPins[i], HIGH);
  }

  for (byte c = 0; c < COLS; c++) {
    for (byte r = 0; r < ROWS; r++) {
      
      digitalWrite(rowPins[r], LOW);
      int pinState = digitalRead(colPins[c]);
      bool rawPressed = (pinState == LOW);

      if (rawPressed != keyStates[keyIndex]) {
        if ((currentTime - lastDebounceTime[keyIndex]) > DEBOUNCE_DELAY) {
          keyStates[keyIndex] = rawPressed;
          lastDebounceTime[keyIndex] = currentTime;
        }
      }

      digitalWrite(rowPins[r], HIGH);
      keyIndex++;
    }
  }
}

// Analog Pin Definitions
const int AMP_A_PIN = 0; // Piezo preamp out for key A
const int AMP_B_PIN = 1; // Piezo preamp out for key B
const int AMP_C_PIN = 2; // Piezo preamp out for key C
const int AMP_D_PIN = 3; // Piezo preamp out for key D
const int FREQ_A_PIN = 4; // Pot for key A
const int FREQ_B_PIN = 5; // Pot for key B
const int FREQ_C_PIN = 6; // Pot for key C
const int FREQ_D_PIN = 7; // Pot for key D


// Timing variables for aftertouch
unsigned long previousMillisA = 0;
unsigned long timeAboveThresholdA = 0;
bool wasAboveThresholdA = false;

unsigned long previousMillisB = 0;
unsigned long timeAboveThresholdB = 0;
bool wasAboveThresholdB = false;

unsigned long previousMillisC = 0;
unsigned long timeAboveThresholdC = 0;
bool wasAboveThresholdC = false;

unsigned long previousMillisD = 0;
unsigned long timeAboveThresholdD = 0;
bool wasAboveThresholdD = false;


// Calculates amplitude by applying a dead zone to an ADC reading.
int calculateDeadZoneAmplitude(int reading, int sensitivitySetting) {
  const int threshold = (sensitivitySetting >> 3) + 1;
  int centered_val = abs(reading - 512);
  if (centered_val <= threshold) {
    return 0;
  } else {
    // Scale the amplitude back up based on the distance from the threshold
    return (centered_val - threshold) * 2;
  }
}

// Aftertouch, so basically when the piezo crosses 0 an goes negative,
// the signal gets smoothed, which acts like a low pass kind of, so
// you get a release on the signal, but the initial press of the piezo
// is unaffacted, so you're still in control. It takes timing into account
// though, so short presses have short/no release time.
int dynamicAmplitude(
  int rawRead,
  int threshold,
  bool greaterThanThreshold,
  Smooth<long> *smoothObject,
  unsigned long *previousMillisPtr,
  unsigned long *timeAboveThresholdPtr,
  bool *wasAboveThresholdPtr) {
  unsigned long currentMillis = millis();  // Get the current time

  // Check the condition (either > threshold OR < threshold)
  bool isAboveCondition;
  if (greaterThanThreshold) {
    isAboveCondition = rawRead > threshold;
  } else {
    isAboveCondition = rawRead < threshold;
  }

  // Check if the condition has changed from OFF to ON (RISING EDGE)
  if (isAboveCondition && !(*wasAboveThresholdPtr)) {
    *previousMillisPtr = currentMillis;  // Start the timer
    *timeAboveThresholdPtr = 0;          // Reset the stored time on start
    smoothObject->setSmoothness(0.10f);  // Set to fast smoothing so the fade in isn't sloppy
  }
  // Check if the condition has changed from ON to OFF (FALLING EDGE)
  else if (!isAboveCondition && (*wasAboveThresholdPtr)) {
    // Calculate the total duration the signal was ON
    *timeAboveThresholdPtr = currentMillis - *previousMillisPtr;
    *previousMillisPtr = 0;  // Reset timer for the next cycle
  }

  // Store current state for the next control cycle
  *wasAboveThresholdPtr = isAboveCondition;

  // Only apply the smoothing when the condition is OFF (inverted state)
  if (!isAboveCondition) {
    // Maps time (0ms to 100ms) to smoothing amount (0.10f to 0.98f).
    // Longer ON time results in more smoothing (longer tail)
    const unsigned long MAX_TIME_MS = 100UL;
    float timeNormalized = (float)min(*timeAboveThresholdPtr, MAX_TIME_MS) / (float)MAX_TIME_MS;
    float newSmoothCoeff = 0.10f + (0.88f * timeNormalized);
    smoothObject->setSmoothness(newSmoothCoeff);
  }

  // Return the smoothed reading
  return smoothObject->next(rawRead);
}



// Button logic for each voice
// top button adds +1 to numerator, bottom button as +1 to denominator
// if mod button is pressed, they subtract by 1 instead. If all three
// are pressed at once, the ratio is reset to 1:1. It is 15 limit so it
// stops at 15.
// When the middle key is pressed, it switches to the bitwise mode, so
// the numerator button makes the mod signal stronger, denominator key
// makes it weaker. When it is all the way down the effect is off.
// Lastly, when you hold down the middle key and use the numerator/denom
// key, it cycles between the 5 waveforms.
void voice(uint8_t *numerPtr, uint8_t *denomPtr, uint8_t *shiftPtr, int *wavePtr, bool shiftMode, int numerKey, int denomKey, int modKey) {

  // Logic to "reset" JI by pressing the mod key + numerator and denominator keys simultaneously.
  bool resetPressed = keyStates[numerKey] && keyStates[denomKey] && keyStates[modKey];
  bool prevResetPressed = prevKeyStates[numerKey] && prevKeyStates[denomKey] && prevKeyStates[modKey];
  bool resetRisingEdge = resetPressed && !prevResetPressed;

  if (resetRisingEdge) {
    *numerPtr = 1;
    *denomPtr = 1;
    return;  // Exit the function after reset
  }

  // Check for the rising edge of the numer and denom
  bool denomRisingEdge = keyStates[denomKey] && !prevKeyStates[denomKey];
  bool numerRisingEdge = keyStates[numerKey] && !prevKeyStates[numerKey];


  // Denominator logic (prioritized if both numer/denom single keys are pressed)
  if (denomRisingEdge) {
    if (keyStates[5]){
      *wavePtr = (*wavePtr - 1 + NUM_WAVEFORMS) % NUM_WAVEFORMS;
    }
    if (shiftMode & !keyStates[5]) {  // Use the cached shiftMode value
      *shiftPtr = *shiftPtr + 2;
      if (*shiftPtr >= 7) { *shiftPtr = 7; }

    } else {
      if (keyStates[modKey]& !keyStates[5]) {
        (*denomPtr)--;
        if (*denomPtr <= 1) { *denomPtr = 1; }
      } else if (!keyStates[5]) {
        (*denomPtr)++;
        if (*denomPtr >= 15) { *denomPtr = 15; }
      }
    }
  }
  // Numerator logic
  else if (numerRisingEdge) {
    if (keyStates[5]){
      *wavePtr = (*wavePtr + 1) % NUM_WAVEFORMS;
    }
    if (shiftMode & !keyStates[5]) {  // Use the cached shiftMode value
      *shiftPtr = *shiftPtr - 2;
      if (*shiftPtr <= 0) { *shiftPtr = 0; }
    } else {
      if (keyStates[modKey]& !keyStates[5]) {
        (*numerPtr)--;
        if (*numerPtr <= 1) { *numerPtr = 1; }
      } else if (!keyStates[5]) {
        (*numerPtr)++;
        if (*numerPtr >= 15) { *numerPtr = 15; }
      }
    }
  }
}

void setup() {
  // Keypad pin setup
  for (byte i = 0; i < ROWS; i++) {
    pinMode(rowPins[i], OUTPUT);
    digitalWrite(rowPins[i], HIGH);
  }
  for (byte i = 0; i < COLS; i++) {
    pinMode(colPins[i], INPUT_PULLUP);
  }
  // Pin 13 setup 
  pinMode(13, OUTPUT);
  digitalWrite(13, LOW);

  startMozzi();
}

void updateControl() {
  scanKeypad();
  // Key 5 controls the state of pin 13 (which acts as a shift/mode button)
  bool toggleRisingEdge = keyStates[5] && !prevKeyStates[5];

  if (toggleRisingEdge) {
    // Toggle the state of pin 13 (built in LED)
    if (digitalRead(13) == LOW) {
      digitalWrite(13, HIGH);
    } else {
      digitalWrite(13, LOW);
    }
  }

  // Update waveforms
  currentWaveformTableA = wavetablePointers[waveIndexA];
  currentWaveformTableB = wavetablePointers[waveIndexB];
  currentWaveformTableC = wavetablePointers[waveIndexC];
  currentWaveformTableD = wavetablePointers[waveIndexD];

  aSin.setTable(currentWaveformTableA);
  bSin.setTable(currentWaveformTableB);
  cSin.setTable(currentWaveformTableC);
  dSin.setTable(currentWaveformTableD);

  shiftModeOn = digitalRead(13) == HIGH;

  // Read fundamental frequencies from analog inputs
  int fundamentalA = avgA.next(mozziAnalogRead(FREQ_A_PIN));
  int fundamentalB = avgB.next(mozziAnalogRead(FREQ_B_PIN));
  int fundamentalC = avgC.next(mozziAnalogRead(FREQ_C_PIN));
  int fundamentalD = avgD.next(mozziAnalogRead(FREQ_D_PIN));

  // Read all AMP ADC values once
  int rawA = mozziAnalogRead(AMP_A_PIN);
  int rawB = mozziAnalogRead(AMP_B_PIN);
  int rawC = mozziAnalogRead(AMP_C_PIN);
  int rawD = mozziAnalogRead(AMP_D_PIN);

  // Dynamic amplitude function calls. Use "true" for normal piezo, "false" for inverted
  ampReadA = dynamicAmplitude(
    rawA, FLIPTHRESH_A, true,
    &smoothA, &previousMillisA, &timeAboveThresholdA, &wasAboveThresholdA);

  ampReadB = dynamicAmplitude(
    rawB, FLIPTHRESH_B, true,
    &smoothB, &previousMillisB, &timeAboveThresholdB, &wasAboveThresholdB);

  ampReadC = dynamicAmplitude(
    rawC, FLIPTHRESH_C, true,
    &smoothC, &previousMillisC, &timeAboveThresholdC, &wasAboveThresholdC);

  // Channel D: Inverted. Lowered threshold for reliable trigger.
  ampReadD = dynamicAmplitude(
    rawD, FLIPTHRESH_D, true,
    &smoothD, &previousMillisD, &timeAboveThresholdD, &wasAboveThresholdD);


  // Apply voice function with rising edge detection
  voice(&numerA, &denomA, &shiftA, &waveIndexA, shiftModeOn, 0, 1, 2);
  voice(&numerB, &denomB, &shiftB, &waveIndexB, shiftModeOn, 4, 3, 2);
  voice(&numerC, &denomC, &shiftC, &waveIndexC, shiftModeOn, 6, 8, 9);
  voice(&numerD, &denomD, &shiftD, &waveIndexD, shiftModeOn, 11, 10, 9);

  // Calculate just intervals
  float ratioA = ((float)numerA / (float)denomA);
  float ratioB = ((float)numerB / (float)denomB);
  float ratioC = ((float)numerC / (float)denomC);
  float ratioD = ((float)numerD / (float)denomD);

  // Frequency calculation using pots and ratios. Use "<" symbol for normal piezo, ">" for inverted
  int freqA; int freqB; int freqC; int freqD;
  if (ampReadA < FLIPTHRESH_A) { freqA = fundamentalA * ratioA; }
  else {freqA = fundamentalA;}

  if (ampReadB < FLIPTHRESH_B) { freqB = fundamentalB * ratioB; }
  else {freqB = fundamentalB;}

  if (ampReadC < FLIPTHRESH_C) { freqC = fundamentalC * ratioC; }
  else {freqC = fundamentalC;}

  if (ampReadD < FLIPTHRESH_D) { freqD = fundamentalD * ratioD; }
  else {freqD = fundamentalD;}

  // Update oscillator frequencies
  aSin.setFreq(freqA); bSin.setFreq(freqB); cSin.setFreq(freqC); dSin.setFreq(freqD);


  // Calculate voice volumes
  ampA = calculateDeadZoneAmplitude(ampReadA, SENSITIVITY_A) * MULTIPLIER_A;
  ampB = calculateDeadZoneAmplitude(ampReadB, SENSITIVITY_B) * MULTIPLIER_B;
  ampC = calculateDeadZoneAmplitude(ampReadC, SENSITIVITY_C) * MULTIPLIER_C;
  ampD = calculateDeadZoneAmplitude(ampReadD, SENSITIVITY_D) * MULTIPLIER_D;

  // Clamp voice volumes to minimize clipping
  const int MAX_AMP = 1000;
  ampA = min(ampA, MAX_AMP);
  ampB = min(ampB, MAX_AMP);
  ampC = min(ampC, MAX_AMP);
  ampD = min(ampD, MAX_AMP);

  // Store current key states for next control cycle's rising edge detection
  for (int i = 0; i < ROWS * COLS; i++) {
    prevKeyStates[i] = keyStates[i];
  }
}

AudioOutput updateAudio() {
  //8 bit oscillator outputs
  int sigA_raw = aSin.next();
  int sigB_raw = bSin.next();
  int sigC_raw = cSin.next();
  int sigD_raw = dSin.next();

  // Chaos bitwise OR signals
  int sigA_chaos = sigA_raw | (sigD_raw >> shiftA);
  int sigB_chaos = sigB_raw | (sigA_raw >> shiftB);
  int sigC_chaos = sigC_raw | (sigB_raw >> shiftC);
  int sigD_chaos = sigD_raw | (sigC_raw >> shiftD);

  // Higher bit versions for hi-fi
  long sigA; long sigB; long sigC; long sigD;

  // VCA/Selecting chaos vs normal
  sigA = (long)(shiftA == 7 ? sigA_raw : sigA_chaos) * ampA;
  sigB = (long)(shiftB == 7 ? sigB_raw : sigB_chaos) * ampB;
  sigC = (long)(shiftC == 7 ? sigC_raw : sigC_chaos) * ampC;
  sigD = (long)(shiftD == 7 ? sigD_raw : sigD_chaos) * ampD;

  // Sum LR
  long sumLeft = sigA + sigB;
  long sumRight = sigC + sigD;

  int left_out = (int)(sumLeft >> 3);
  int right_out = (int)(sumRight >> 3);

  return StereoOutput::from16Bit(left_out, right_out);
}

void loop() {
  audioHook();
}

Polyphonic Light Theremin

This is a photoresistor-based gestural light synthesizer that's based around an ATmega328P nano.

// BUILD AT YOUR OWN RISK, USE
// PROPER INPUT PROTECTION
// CIRCUITRY WHEN NECESSARY.
//
// LOESS-LABS.NET
//
// Dylan Barry, 2024, GNU GPLv3
#include <MozziGuts.h>
#include <Oscil.h>
#include <tables/triangle_valve_2048_int8.h>
#include <ResonantFilter.h> // subsonic filter
#include <RollingAverage.h>
#define CONTROL_RATE 256 // sets CV resolution

Oscil <TRIANGLE_VALVE_2048_NUM_CELLS, AUDIO_RATE> aOsc(TRIANGLE_VALVE_2048_DATA);
Oscil <TRIANGLE_VALVE_2048_NUM_CELLS, AUDIO_RATE> bOsc(TRIANGLE_VALVE_2048_DATA);
LowPassFilter aFilt;
LowPassFilter bFilt;
RollingAverage <int, 64> aAveragePitch;
RollingAverage <int, 64> bAveragePitch;
RollingAverage <int, 32> avgHarm;


// Pin Mappings.
const int aLEDPin = 2;
const int bLEDPin = 3;
const int aLDRPin = 6;
const int bLDRPin = 7;
const int aOscFreqPin = 1;
const int bOscFreqPin = 5;
const int filterFreqPin = 4;
const int fmPin = 0;
const int bendPin = 3;
const int harmonicSelectPin = 2;

// Create global variables.
int aGain, bGain;
int aLDRPrevious, bLDRPrevious;
unsigned long aMillisPrevious, bMillisPrevious;
int aCount, bCount;
int aHarm = 2;
int bHarm = 2;
int fm_intensity;
int aBoom, bBoom;
int aIndex, bIndex;
int bendFactor;

int table0[] = {2, 3};
int table1[] = {2, 4};
int table2[] = {2, 4};
int table3[] = {2, 5};

int table4[] = {2, 3};
int table5[] = {2, 3};
int table6[] = {2, 4};
int table7[] = {2, 4};
int table8[] = {2, 4};

void setup() {
  startMozzi(CONTROL_RATE);
  pinMode(aLEDPin, OUTPUT);
  pinMode(bLEDPin, OUTPUT);
}

void updateControl() {
  // aIndex/bIndex selects the value from the table
  // tableSelect selects the table
  int tableSelect = map(avgHarm.next(mozziAnalogRead(harmonicSelectPin)), 0, 1023, 0, 3);
  // Array of pointers to the tables in order to efficiently assign values
  int* tables[] = {table0, table1, table2, table3, table4, table5, table6, table7, table8};
  int aHarm = tables[tableSelect][aIndex];
  int bHarm = tables[tableSelect + 4][bIndex];

  // Map various variables.
  int aLDR = map(mozziAnalogRead(aLDRPin), 0, 512, 255, 0);
  int bLDR = map(mozziAnalogRead(bLDRPin), 0, 512, 255, 0);
  int aL = map(aLDR, 0, 255, 6, 0);
  int bL = map(bLDR, 0, 255, 6, 0);
  int filterFreq = map(mozziAnalogRead(filterFreqPin), 0, 1023, 0, 20);
  
  // Check for rapid change in LDR reading, flip flop accordingly.
  if (millis() - aMillisPrevious >= 300) {
    if (aLDRPrevious - aLDR>100){
      aCount = aCount + 1;
      if ((aCount % 2) == 0){aBoom = 1; aIndex = 0; digitalWrite(aLEDPin, LOW);}
      else{aBoom = 1; aIndex = 1; digitalWrite(aLEDPin, HIGH);}
    }
    else {aBoom = 0;}
    aLDRPrevious = aLDR;
    aMillisPrevious = millis();
  }
  if (millis() - bMillisPrevious >= 300) {
    if (bLDRPrevious - bLDR>100){
      bCount = bCount + 1;
      if ((bCount % 2) == 0){bBoom = 1; bIndex = 0; digitalWrite(bLEDPin, LOW);}
      else{bBoom = 1; bIndex = 1; digitalWrite(bLEDPin, HIGH);}
    }
    else {bBoom = 0;}
    bLDRPrevious = bLDR;
    bMillisPrevious = millis();
  }

  int bendRead = map(mozziAnalogRead(bendPin), 0, 1023, 0, 512);
  
  if (bendRead < 25){bendFactor = 0;}
  else {bendFactor = bendRead;}
  int aBend = map(aLDR, 0, 255, 0, bendFactor);
  int bBend = map(bLDR, 0, 255, 0, bendFactor);
  // Set Oscillator Frequencies.
  aOsc.setFreq((aAveragePitch.next((mozziAnalogRead(aOscFreqPin) >> 1)+aBend))*aHarm);
  bOsc.setFreq((bAveragePitch.next((mozziAnalogRead(bOscFreqPin) >> 1)+bBend))*bHarm);

  // Set Subsonic Filter Frequency and Resonance.
  aFilt.setCutoffFreqAndResonance((filterFreq + aL), 480);
  bFilt.setCutoffFreqAndResonance((filterFreq + bL + 1), 480);

  // Set FM Intensity.
  fm_intensity = map(mozziAnalogRead(fmPin), 0, 1023, 0, 63);

  // Input signal into Subsonic Filters.
  if (aBoom == 1) {aGain = (int) (aFilt.next(1023));}
  else {aGain = (int) (aFilt.next(aLDR));}
  if (bBoom == 1) {bGain = (int) (bFilt.next(1023));}
  else {bGain = (int) (bFilt.next(bLDR));}

}

AudioOutput_t updateAudio() {
  // Gate and limiters, to avoid clipping.
  if (aGain < 1){aGain = 0;}
  if (aGain > 126){aGain = 126;}
  if (bGain < 1){bGain = 0;}
  if (bGain > 126){bGain = 126;}

  // FM Stuff.
  int aVoice = (aGain * (aOsc.phMod(fm_intensity * ((bGain * bOsc.next())>>5))));
  int bVoice = (bGain * (bOsc.phMod(fm_intensity * (aVoice>>4))));
  
  return MonoOutput::from16Bit(aVoice + bVoice);
}
void loop() {
  audioHook();
}

If you have any questions about your build, issues, etc. just email me (uvknhn@tutanota.com).