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();
}
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).