Teensy 4.0 and UDA1334A DAC Issues

class-ab

New member
Hi all,

I was hoping for some help with a DIY polyphonic synth I am trying to create. I have a fully functional code (see first code block) that works with a Teensy 4.0 as the brains, an Arduino MEGA and Arduino UNO acting as multiplexers over i2c for all the potentiometers (as well as an actual multiplexer connected to the Teensy) and an UDA1334A DAC. The working code (see first code block) does not use the DAC at all, and simply plays the audio back to the computer as a recording device, and this works perfectly. All features of the synth work, including the 8x8 keyboard matrix connected to the Mega. However, if I change 3 lines of code that makes the Teensy ouput over I2S to the DAC, everything stops working!

I have to completely confess that almost all of this code has been written by AI. I am a hardware guy, not software, and cannot do more than very basic coding. The code is nearly 1000 lines long but all the things I think are causing issues (I2S and I2C and serial) are all near the top.

To convert from USB to I2S, I change AudioOutputUSB usb1; to AudioOutputI2S i2s1;, and I also change the patches from the USB to the I2S output. When reuploaded, several things can occur in completely random and untestable steps. These include:

- Arduino MEGA not sending any commands over i2c and serial, becoming virtually 'frozen',
- Teensy misreading i2c from mega and thinking potentiometers are noteOn/Off events,
- Teensy multiplexer constantly updating various VCF controls (again, no order or pattern, seemingly random)

You might notice various lines around the prioity of events, these were suggested to me by AI and I don't think they made any difference at all.
I have included the Teensy, Arduino Mega and Uno code in that order. In my testing, I could change the mega and uno code as much as I wanted yet changing the usb to i2s would always be the issue.

I have also uploaded pictures of the PCB and schematic I used to connect all components together.

TEENSY CODE:

C++:
#include <Wire.h>
#include <SPI.h>
#include <Audio.h>
// =====================================================
// Teensy 3-VCO Synth + MUX + I2C System
// 6 voices × 3 VCOs = 18 waveform generators
// Per-voice VCF and VCA
// =====================================================
#define TEENSY_ADDRESS 8
// =====================================================
// VCO CONTROL MAPPING - EDIT HERE
// =====================================================
// Format: "MUX_X" for local multiplexer channel X (0-15)
//         "UNO_X" for Arduino Uno pot X (0-3)
//         "MEGA_X" for Arduino Mega pot X (0-15)
// VCO 1 Controls
#define VCO1_WAVEFORM "MUX_10"  // Rotary switch channel
#define VCO1_VOLUME "MUX_3"     // Pot channel
#define VCO1_DETUNE "MUX_0"     // Pot channel
#define VCO1_OCTAVE "MUX_13"    // Rotary switch channel for octave selection

// VCO 2 Controls
#define VCO2_WAVEFORM "MUX_11"  // Rotary switch channel
#define VCO2_VOLUME "MUX_4"     // Pot channel
#define VCO2_DETUNE "MUX_1"     // Pot channel
#define VCO2_OCTAVE "MUX_14"    // Rotary switch channel for octave selection

// VCO 3 Controls
#define VCO3_WAVEFORM "MUX_12"  // Rotary switch channel
#define VCO3_VOLUME "MUX_5"     // Pot channel
#define VCO3_DETUNE "MUX_2"     // Pot channel
#define VCO3_OCTAVE "MUX_15"    // Rotary switch channel for octave selection

// VCF (Filter) Controls
#define VCF_CUTOFF "MUX_6"       // Filter cutoff frequency
#define VCF_RESONANCE "MUX_9"    // Filter resonance
#define VCF_ENV_AMOUNT "MEGA_2"  // Filter envelope amount (0-100%)
#define VCF_ATTACK "MUX_7"       // Filter envelope attack
#define VCF_DECAY "MEGA_0"       // Filter envelope decay
#define VCF_SUSTAIN "MUX_8"      // Filter envelope sustain
#define VCF_RELEASE "MEGA_1"     // Filter envelope release

// VCA (Amplifier) Controls
#define VCA_ATTACK "MEGA_4"    // Amplitude envelope attack
#define VCA_DECAY "MEGA_5"     // Amplitude envelope decay
#define VCA_SUSTAIN "MEGA_8"   // Amplitude envelope sustain
#define VCA_RELEASE "MEGA_11"  // Amplitude envelope release

// Reverb Controls
#define REVERB_ROOMSIZE "MEGA_12"  // Reverb room size
#define REVERB_DAMPING "MEGA_13"   // Reverb damping
#define REVERB_MIX "MEGA_14"       // Reverb dry/wet mix

// =====================================================
// MUX Configuration
// =====================================================
const int S0 = 5, S1 = 4, S2 = 3, S3 = 2, SIG_PIN = 14;
// Switch config
const int firstSwitchChannel = 10;
const int numSwitches = 6;
const int totalPositions = 4;
const int switchTolerance = 5;
const int ignoreThreshold = 5;
// Pot config
const int firstPotChannel = 0;
const int numPots = 10;
const int potTolerance = 2;
// Storage
int currentPos[numSwitches] = { 0 };
int lastPotValues[numPots] = { 0 };
// Calibrated switch values
int positions[numSwitches][totalPositions] = {
  { 613, 662, 746, 808 },
  { 411, 464, 558, 643 },
  { 506, 564, 659, 732 },
  { 516, 566, 660, 733 },
  { 462, 513, 610, 690 },
  { 646, 697, 776, 834 }
};

// =====================================================
// I2C Configuration - FIXED CIRCULAR BUFFER
// =====================================================
#define I2C_BUFFER_SIZE 256  // Circular buffer

// Circular buffer for incoming I2C data
volatile byte i2cBuffer[I2C_BUFFER_SIZE];
volatile int i2cWriteIndex = 0;
volatile int i2cReadIndex = 0;

// Storage for UNO and MEGA pot values
int unoPotValues[4] = { 0 };
int megaPotValues[16] = { 0 };

// =====================================================
// Audio System - 18 Waveforms + 6 VCF + 6 VCA + Effects
// =====================================================
AudioSynthWaveform waveform[18];
AudioMixer4 vcoMixer[6];               // One mixer per voice (3 VCOs → 1 mixer)
AudioFilterStateVariable filter[6];    // One filter per voice
AudioEffectEnvelope filterEnv[6];      // Filter envelope per voice
AudioEffectEnvelope ampEnv[6];         // Amplitude envelope per voice
AudioMixer4 voiceMixer1, voiceMixer2;  // Mix 6 voices
AudioMixer4 preEffectsMix;             // Mix before effects
AudioEffectFreeverb reverb1;           // Reverb effect
AudioMixer4 reverbMixer;               // Mix dry + reverb signal
AudioMixer4 finalMix;
AudioOutputUSB usb1;

// Connect VCOs to voice mixers
AudioConnection patchCord1(waveform[0], 0, vcoMixer[0], 0);    // Voice 0, VCO 1
AudioConnection patchCord2(waveform[1], 0, vcoMixer[0], 1);    // Voice 0, VCO 2
AudioConnection patchCord3(waveform[2], 0, vcoMixer[0], 2);    // Voice 0, VCO 3
AudioConnection patchCord4(waveform[3], 0, vcoMixer[1], 0);    // Voice 1, VCO 1
AudioConnection patchCord5(waveform[4], 0, vcoMixer[1], 1);    // Voice 1, VCO 2
AudioConnection patchCord6(waveform[5], 0, vcoMixer[1], 2);    // Voice 1, VCO 3
AudioConnection patchCord7(waveform[6], 0, vcoMixer[2], 0);    // Voice 2, VCO 1
AudioConnection patchCord8(waveform[7], 0, vcoMixer[2], 1);    // Voice 2, VCO 2
AudioConnection patchCord9(waveform[8], 0, vcoMixer[2], 2);    // Voice 2, VCO 3
AudioConnection patchCord10(waveform[9], 0, vcoMixer[3], 0);   // Voice 3, VCO 1
AudioConnection patchCord11(waveform[10], 0, vcoMixer[3], 1);  // Voice 3, VCO 2
AudioConnection patchCord12(waveform[11], 0, vcoMixer[3], 2);  // Voice 3, VCO 3
AudioConnection patchCord13(waveform[12], 0, vcoMixer[4], 0);  // Voice 4, VCO 1
AudioConnection patchCord14(waveform[13], 0, vcoMixer[4], 1);  // Voice 4, VCO 2
AudioConnection patchCord15(waveform[14], 0, vcoMixer[4], 2);  // Voice 4, VCO 3
AudioConnection patchCord16(waveform[15], 0, vcoMixer[5], 0);  // Voice 5, VCO 1
AudioConnection patchCord17(waveform[16], 0, vcoMixer[5], 1);  // Voice 5, VCO 2
AudioConnection patchCord18(waveform[17], 0, vcoMixer[5], 2);  // Voice 5, VCO 3

// Connect VCO mixers to filters
AudioConnection patchCord19(vcoMixer[0], 0, filter[0], 0);
AudioConnection patchCord20(vcoMixer[1], 0, filter[1], 0);
AudioConnection patchCord21(vcoMixer[2], 0, filter[2], 0);
AudioConnection patchCord22(vcoMixer[3], 0, filter[3], 0);
AudioConnection patchCord23(vcoMixer[4], 0, filter[4], 0);
AudioConnection patchCord24(vcoMixer[5], 0, filter[5], 0);

// Connect filters (lowpass output) to amplitude envelopes
AudioConnection patchCord31(filter[0], 0, ampEnv[0], 0);
AudioConnection patchCord32(filter[1], 0, ampEnv[1], 0);
AudioConnection patchCord33(filter[2], 0, ampEnv[2], 0);
AudioConnection patchCord34(filter[3], 0, ampEnv[3], 0);
AudioConnection patchCord35(filter[4], 0, ampEnv[4], 0);
AudioConnection patchCord36(filter[5], 0, ampEnv[5], 0);

// Connect amplitude envelopes to voice mixers
AudioConnection patchCord37(ampEnv[0], 0, voiceMixer1, 0);
AudioConnection patchCord38(ampEnv[1], 0, voiceMixer1, 1);
AudioConnection patchCord39(ampEnv[2], 0, voiceMixer1, 2);
AudioConnection patchCord40(ampEnv[3], 0, voiceMixer1, 3);
AudioConnection patchCord41(ampEnv[4], 0, voiceMixer2, 0);
AudioConnection patchCord42(ampEnv[5], 0, voiceMixer2, 1);

// Connect voice mixers to pre-effects mix
AudioConnection patchCord43(voiceMixer1, 0, preEffectsMix, 0);
AudioConnection patchCord44(voiceMixer2, 0, preEffectsMix, 1);

// Connect pre-effects to reverb
AudioConnection patchCord48(preEffectsMix, 0, reverb1, 0);
AudioConnection patchCord49(preEffectsMix, 0, reverbMixer, 0);  // Dry signal
AudioConnection patchCord50(reverb1, 0, reverbMixer, 1);        // Wet signal

// Connect reverb mixer to final output
AudioConnection patchCord51(reverbMixer, 0, finalMix, 0);
AudioConnection patchCord52(reverbMixer, 0, finalMix, 1);
AudioConnection patchCord53(finalMix, 0, usb1, 0);
AudioConnection patchCord54(finalMix, 0, usb1, 1);

// =====================================================
// VCO State Variables
// =====================================================
struct VCOSettings {
  int waveform;  // WAVEFORM_SINE, etc.
  float volume;  // 0.0 to 1.0
  float detune;  // -1.0 to +1.0 semitones
  int octave;    // Octave shift: 1=-2 octaves, 2=-1 octave, 3=0 octaves, 4=+1 octave
};
VCOSettings vco[3] = {
  { WAVEFORM_SINE, 0.33, 0.0, 3 },  // Default to 0 octave shift (position 3)
  { WAVEFORM_SINE, 0.33, 0.0, 3 },
  { WAVEFORM_SINE, 0.33, 0.0, 3 }
};

// VCF and VCA Settings
struct FilterSettings {
  float cutoff;     // 20.0 to 10000.0 Hz
  float resonance;  // 0.7 to 5.0
  float envAmount;  // 0.0 to 1.0 - how much envelope affects filter
  float attack;     // 0 to 11880 ms
  float decay;      // 0 to 11880 ms
  float sustain;    // 0.0 to 1.0
  float release;    // 0 to 11880 ms
};
FilterSettings vcfSettings = { 1000.0, 0.7, 0.0, 10.0, 100.0, 0.5, 200.0 };

// Manual filter envelope state (since audio envelope doesn't work for this)
struct FilterEnvState {
  bool active;
  unsigned long noteOnTime;
  unsigned long noteOffTime;
  bool released;
  float lastValue;
};
FilterEnvState filterEnvState[6];

struct AmplifierSettings {
  float attack;   // 0 to 11880 ms
  float decay;    // 0 to 11880 ms
  float sustain;  // 0.0 to 1.0
  float release;  // 0 to 11880 ms
};
AmplifierSettings vcaSettings = { 10.0, 100.0, 0.8, 200.0 };

// Reverb Settings
struct ReverbSettings {
  float roomsize;  // 0.0 to 1.0
  float damping;   // 0.0 to 1.0
  float mix;       // 0.0 to 1.0 (dry/wet)
};
ReverbSettings reverbSettings = { 0.5, 0.5, 0.3 };

float activeFreq[6] = { 0.0 };  // Base frequency for each of 6 voices

// =====================================================
// Timing
// =====================================================
unsigned long lastMuxTime = 0;
const unsigned long MUX_INTERVAL = 5;  // Read one MUX channel every 5ms
int currentMuxChannel = 0;             // Track which channel to read next
unsigned long lastFilterUpdate = 0;
const unsigned long FILTER_UPDATE_INTERVAL = 10;  // Update filter envelopes every 10ms

// =====================================================
// Circular Buffer Helper Functions
// =====================================================
int getBufferedByteCount() {
  int write = i2cWriteIndex;
  int read = i2cReadIndex;

  if (write >= read) {
    return write - read;
  } else {
    return I2C_BUFFER_SIZE - read + write;
  }
}

byte readFromBuffer() {
  if (i2cReadIndex == i2cWriteIndex) {
    return 0;  // Buffer empty
  }

  byte b = i2cBuffer[i2cReadIndex];
  i2cReadIndex = (i2cReadIndex + 1) % I2C_BUFFER_SIZE;
  return b;
}

// =====================================================
// Setup
// =====================================================
void setup() {
  Serial.begin(115200);
  delay(500);
  Serial.println("Teensy 3-VCO Synth with per-voice VCF & VCA + Effects");
  pinMode(S0, OUTPUT);
  pinMode(S1, OUTPUT);
  pinMode(S2, OUTPUT);
  pinMode(S3, OUTPUT);
  AudioMemory(300);
  NVIC_SET_PRIORITY(IRQ_LPI2C1, 32);
  NVIC_SET_PRIORITY(IRQ_SAI1, 192);  // Lower priority than I2C
  NVIC_SET_PRIORITY(IRQ_SAI2, 192);  // (Teensy 4.x uses SAI1 or SAI2 depending on output)


  // Initialize all 18 waveforms
  for (int i = 0; i < 18; i++) {
    waveform[i].begin(WAVEFORM_SINE);
    waveform[i].amplitude(1.0);  // Set to full, VCA will control amplitude
  }

  // Set VCO mixer gains (3 VCOs per voice)
  for (int i = 0; i < 6; i++) {
    vcoMixer[i].gain(0, vco[0].volume);
    vcoMixer[i].gain(1, vco[1].volume);
    vcoMixer[i].gain(2, vco[2].volume);
    vcoMixer[i].gain(3, 0.0);  // Unused channel
  }

  // Initialize filters and envelopes for each voice
  for (int i = 0; i < 6; i++) {
    // Filter setup
    filter[i].frequency(vcfSettings.cutoff);
    filter[i].resonance(vcfSettings.resonance);

    // Initialize filter envelope state
    filterEnvState[i].active = false;
    filterEnvState[i].released = false;
    filterEnvState[i].lastValue = 0.0;

    // Filter envelope objects for timing
    filterEnv[i].attack(vcfSettings.attack);
    filterEnv[i].decay(vcfSettings.decay);
    filterEnv[i].sustain(vcfSettings.sustain);
    filterEnv[i].release(vcfSettings.release);

    // Amplitude envelope setup
    ampEnv[i].attack(vcaSettings.attack);
    ampEnv[i].decay(vcaSettings.decay);
    ampEnv[i].sustain(vcaSettings.sustain);
    ampEnv[i].release(vcaSettings.release);
  }

  // Set voice mixer gains
  for (int i = 0; i < 4; i++) {
    voiceMixer1.gain(i, 0.25);
    voiceMixer2.gain(i, 0.25);
  }

  // Pre-effects mixer gains
  preEffectsMix.gain(0, 0.5);
  preEffectsMix.gain(1, 0.5);

  // Initialize reverb
  reverb1.roomsize(reverbSettings.roomsize);
  reverb1.damping(reverbSettings.damping);
  reverbMixer.gain(0, 1.0 - reverbSettings.mix);  // Dry
  reverbMixer.gain(1, reverbSettings.mix);        // Wet

  finalMix.gain(0, 0.5);
  finalMix.gain(1, 0.5);

  Wire.begin(TEENSY_ADDRESS);
  Wire.onReceive(receiveEvent);

  Serial.println("Audio system initialized");

  // Initial read of all controls at bootup
  Serial.println("Reading initial control positions...");
  delay(100);  // Give I2C time to stabilize

  // Read all MUX controls
  for (int i = 0; i < numSwitches; i++) {
    readMuxSwitch(firstSwitchChannel + i, i);
  }
  for (int i = 0; i < numPots; i++) {
    readMuxPot(firstPotChannel + i, i);
  }

  // Force update all parameters with initial values
  updateVCOParameters();
  updateVCFParameters();
  updateVCAParameters();
  updateReverbParameters();

  Serial.println("Initial control read complete - synth ready");
}

// =====================================================
// Main Loop
// =====================================================
void loop() {
  unsigned long now = millis();

  // Process I2C buffer - MOVED TO MAIN LOOP
  while (getBufferedByteCount() >= 3) {
    byte b0 = readFromBuffer();
    byte b1 = readFromBuffer();
    byte b2 = readFromBuffer();

    if (b0 == 0 || b0 == 1) {
      // Note event
      handleNoteEvent(b0, b1, b2);
    } else {
      // Pot messages
      int index = b0;
      int value = (b1 << 8) | b2;

      // Invert potentiometer values from UNO and MEGA
      value = 1023 - value;

      if (index >= 100) {
        // MEGA pot (100-115)
        int megaIndex = index - 100;
        if (megaIndex < 16) {
          megaPotValues[megaIndex] = value;
        }
      } else {
        // UNO pot (0-3)
        if (index < 4) {
          unoPotValues[index] = value;
        }
      }
    }
  }

  // Update filter envelopes
  if (now - lastFilterUpdate >= FILTER_UPDATE_INTERVAL) {
    lastFilterUpdate = now;
    updateFilterEnvelopes();
  }

  // Read ONE MUX channel at a time to avoid blocking I2C
  if (now - lastMuxTime >= MUX_INTERVAL) {
    lastMuxTime = now;

    // Read one switch or pot per iteration
    if (currentMuxChannel < numSwitches) {
      // Read a switch
      readMuxSwitch(firstSwitchChannel + currentMuxChannel, currentMuxChannel);
    } else if (currentMuxChannel < numSwitches + numPots) {
      // Read a pot
      int potIndex = currentMuxChannel - numSwitches;
      readMuxPot(firstPotChannel + potIndex, potIndex);
    }

    // Move to next channel
    currentMuxChannel++;

    // If we've read all channels, update parameters and restart
    if (currentMuxChannel >= numSwitches + numPots) {
      currentMuxChannel = 0;

      // Update all parameters after reading all inputs
      updateVCOParameters();
      updateVCFParameters();
      updateVCAParameters();
      updateReverbParameters();
    }
  }
}

// =====================================================
// I2C Interrupt Handler - FAST VERSION
// =====================================================
void receiveEvent(int numBytes) {
  // Simply read all bytes into circular buffer - KEEP IT FAST!
  while (Wire.available()) {
    byte b = Wire.read();

    int nextWrite = (i2cWriteIndex + 1) % I2C_BUFFER_SIZE;

    // Only write if buffer isn't full
    if (nextWrite != i2cReadIndex) {
      i2cBuffer[i2cWriteIndex] = b;
      i2cWriteIndex = nextWrite;
    }
    // If buffer is full, byte is dropped (but doesn't hang)
  }
}

// =====================================================
// MUX Helpers
// =====================================================
void setMuxChannel(int ch) {
  digitalWrite(S0, bitRead(ch, 0));
  digitalWrite(S1, bitRead(ch, 1));
  digitalWrite(S2, bitRead(ch, 2));
  digitalWrite(S3, bitRead(ch, 3));
  delayMicroseconds(5);
}

void readMuxSwitch(int channel, int switchIndex) {
  setMuxChannel(channel);
  int val = analogRead(SIG_PIN);
  if (val <= ignoreThreshold) return;
  for (int p = 0; p < totalPositions; p++) {
    int target = positions[switchIndex][p];
    if (val >= target - switchTolerance && val <= target + switchTolerance) {
      if (currentPos[switchIndex] != p + 1) {
        currentPos[switchIndex] = p + 1;
        Serial.printf("[MUX] Switch %d → pos %d (raw %d)\n",
                      switchIndex + 1, currentPos[switchIndex], val);
      }
      return;
    }
  }
}

void readMuxPot(int channel, int potIndex) {
  setMuxChannel(channel);
  int val = analogRead(SIG_PIN);
  // Invert potentiometer: turn up = increase value
  val = 1023 - val;

  if (abs(val - lastPotValues[potIndex]) > potTolerance) {
    lastPotValues[potIndex] = val;
  }
}

// =====================================================
// Filter Envelope Update (Manual ADSR)
// =====================================================
float calculateFilterEnvelope(int voice, unsigned long now) {
  if (!filterEnvState[voice].active) return 0.0;

  if (!filterEnvState[voice].released) {
    // Note is held - calculate attack/decay/sustain
    unsigned long elapsed = now - filterEnvState[voice].noteOnTime;

    if (elapsed < vcfSettings.attack) {
      // Attack phase
      return (float)elapsed / vcfSettings.attack;
    } else if (elapsed < vcfSettings.attack + vcfSettings.decay) {
      // Decay phase
      unsigned long decayElapsed = elapsed - vcfSettings.attack;
      float decayProgress = (float)decayElapsed / vcfSettings.decay;
      return 1.0 - (decayProgress * (1.0 - vcfSettings.sustain));
    } else {
      // Sustain phase
      return vcfSettings.sustain;
    }
  } else {
    // Note released - calculate release
    unsigned long elapsed = now - filterEnvState[voice].noteOffTime;

    if (elapsed < vcfSettings.release) {
      // Release phase
      float releaseProgress = (float)elapsed / vcfSettings.release;
      return filterEnvState[voice].lastValue * (1.0 - releaseProgress);
    } else {
      // Envelope finished
      filterEnvState[voice].active = false;
      return 0.0;
    }
  }
}

void updateFilterEnvelopes() {
  unsigned long now = millis();

  for (int v = 0; v < 6; v++) {
    if (filterEnvState[v].active || vcfSettings.envAmount > 0.01) {
      float envValue = calculateFilterEnvelope(v, now);

      // Calculate modulated cutoff frequency
      // envAmount controls how much the envelope affects the filter (0 to 4 octaves)
      float octaveShift = envValue * vcfSettings.envAmount * 4.0;  // 0 to 4 octaves
      float modulatedCutoff = vcfSettings.cutoff * powf(2.0, octaveShift);

      // Clamp to reasonable range
      modulatedCutoff = constrain(modulatedCutoff, 20.0, 12000.0);

      // Update filter frequency
      filter[v].frequency(modulatedCutoff);
    }
  }
}

// =====================================================
// VCO Parameter Update
// =====================================================
void updateVCOParameters() {
  bool changed = false;

  // VCO 1
  int vco1Wave = getControlValue(VCO1_WAVEFORM, true);
  int vco1Vol = getControlValue(VCO1_VOLUME, false);
  int vco1Det = getControlValue(VCO1_DETUNE, false);
  int vco1Oct = getControlValue(VCO1_OCTAVE, true);

  int newWave1 = mapWaveform(vco1Wave);
  if (newWave1 != vco[0].waveform) {
    vco[0].waveform = newWave1;
    changed = true;
    Serial.printf("[VCO1] Waveform → %d\n", newWave1);
  }

  float newVol1 = vco1Vol / 1023.0;
  if (abs(newVol1 - vco[0].volume) > 0.01) {
    vco[0].volume = newVol1;
    for (int v = 0; v < 6; v++) {
      vcoMixer[v].gain(0, newVol1);
    }
    changed = true;
  }

  float newDet1 = (vco1Det - 512) / 512.0;  // -1.0 to +1.0 semitones
  if (abs(newDet1 - vco[0].detune) > 0.01) {
    vco[0].detune = newDet1;
    changed = true;
  }

  if (vco1Oct != vco[0].octave && vco1Oct >= 1 && vco1Oct <= 4) {
    vco[0].octave = vco1Oct;
    changed = true;
    Serial.printf("[VCO1] Octave → %d (%d octaves)\n", vco1Oct, vco1Oct - 3);
  }

  // VCO 2
  int vco2Wave = getControlValue(VCO2_WAVEFORM, true);
  int vco2Vol = getControlValue(VCO2_VOLUME, false);
  int vco2Det = getControlValue(VCO2_DETUNE, false);
  int vco2Oct = getControlValue(VCO2_OCTAVE, true);

  int newWave2 = mapWaveform(vco2Wave);
  if (newWave2 != vco[1].waveform) {
    vco[1].waveform = newWave2;
    changed = true;
    Serial.printf("[VCO2] Waveform → %d\n", newWave2);
  }

  float newVol2 = vco2Vol / 1023.0;
  if (abs(newVol2 - vco[1].volume) > 0.01) {
    vco[1].volume = newVol2;
    for (int v = 0; v < 6; v++) {
      vcoMixer[v].gain(1, newVol2);
    }
    changed = true;
  }

  float newDet2 = (vco2Det - 512) / 512.0;
  if (abs(newDet2 - vco[1].detune) > 0.01) {
    vco[1].detune = newDet2;
    changed = true;
  }

  if (vco2Oct != vco[1].octave && vco2Oct >= 1 && vco2Oct <= 4) {
    vco[1].octave = vco2Oct;
    changed = true;
    Serial.printf("[VCO2] Octave → %d (%d octaves)\n", vco2Oct, vco2Oct - 3);
  }

  // VCO 3
  int vco3Wave = getControlValue(VCO3_WAVEFORM, true);
  int vco3Vol = getControlValue(VCO3_VOLUME, false);
  int vco3Det = getControlValue(VCO3_DETUNE, false);
  int vco3Oct = getControlValue(VCO3_OCTAVE, true);

  int newWave3 = mapWaveform(vco3Wave);
  if (newWave3 != vco[2].waveform) {
    vco[2].waveform = newWave3;
    changed = true;
    Serial.printf("[VCO3] Waveform → %d\n", newWave3);
  }

  float newVol3 = vco3Vol / 1023.0;
  if (abs(newVol3 - vco[2].volume) > 0.01) {
    vco[2].volume = newVol3;
    for (int v = 0; v < 6; v++) {
      vcoMixer[v].gain(2, newVol3);
    }
    changed = true;
  }

  float newDet3 = (vco3Det - 512) / 512.0;
  if (abs(newDet3 - vco[2].detune) > 0.01) {
    vco[2].detune = newDet3;
    changed = true;
  }

  if (vco3Oct != vco[2].octave && vco3Oct >= 1 && vco3Oct <= 4) {
    vco[2].octave = vco3Oct;
    changed = true;
    Serial.printf("[VCO3] Octave → %d (%d octaves)\n", vco3Oct, vco3Oct - 3);
  }

  // Update active voices if any parameter changed
  if (changed) {
    updateActiveVoices();
  }
}

// =====================================================
// VCF Parameter Update
// =====================================================
void updateVCFParameters() {
  bool changed = false;

  // Cutoff: 20 Hz to 10000 Hz (exponential mapping)
  int cutoffVal = getControlValue(VCF_CUTOFF, false);
  float newCutoff = 20.0 * powf(500.0, cutoffVal / 1023.0);  // 20 Hz to 10 kHz
  if (abs(newCutoff - vcfSettings.cutoff) > 10.0) {
    vcfSettings.cutoff = newCutoff;
    changed = true;
  }

  // Resonance: 0.7 to 5.0
  int resVal = getControlValue(VCF_RESONANCE, false);
  float newRes = 0.7 + (resVal / 1023.0) * 4.3;
  if (abs(newRes - vcfSettings.resonance) > 0.05) {
    vcfSettings.resonance = newRes;
    for (int i = 0; i < 6; i++) {
      filter[i].resonance(newRes);
    }
    changed = true;
  }

  // Envelope amount: 0.0 to 1.0
  int envAmtVal = getControlValue(VCF_ENV_AMOUNT, false);
  float newEnvAmt = envAmtVal / 1023.0;
  if (abs(newEnvAmt - vcfSettings.envAmount) > 0.01) {
    vcfSettings.envAmount = newEnvAmt;
    changed = true;
  }

  // Filter envelope ADSR
  int attackVal = getControlValue(VCF_ATTACK, false);
  float newAttack = (attackVal / 1023.0) * 2000.0;  // 0 to 2000 ms
  if (abs(newAttack - vcfSettings.attack) > 5.0) {
    vcfSettings.attack = newAttack;
    for (int i = 0; i < 6; i++) {
      filterEnv[i].attack(newAttack);
    }
    changed = true;
  }

  int decayVal = getControlValue(VCF_DECAY, false);
  float newDecay = (decayVal / 1023.0) * 2000.0;  // 0 to 2000 ms
  if (abs(newDecay - vcfSettings.decay) > 5.0) {
    vcfSettings.decay = newDecay;
    for (int i = 0; i < 6; i++) {
      filterEnv[i].decay(newDecay);
    }
    changed = true;
  }

  int sustainVal = getControlValue(VCF_SUSTAIN, false);
  float newSustain = sustainVal / 1023.0;  // 0.0 to 1.0
  if (abs(newSustain - vcfSettings.sustain) > 0.01) {
    vcfSettings.sustain = newSustain;
    for (int i = 0; i < 6; i++) {
      filterEnv[i].sustain(newSustain);
    }
    changed = true;
  }

  int releaseVal = getControlValue(VCF_RELEASE, false);
  float newRelease = (releaseVal / 1023.0) * 3000.0;  // 0 to 3000 ms
  if (abs(newRelease - vcfSettings.release) > 5.0) {
    vcfSettings.release = newRelease;
    for (int i = 0; i < 6; i++) {
      filterEnv[i].release(newRelease);
    }
    changed = true;
  }

  if (changed) {
    Serial.println("[VCF] Parameters updated");
  }
}

// =====================================================
// VCA Parameter Update
// =====================================================
void updateVCAParameters() {
  bool changed = false;

  // Amplitude envelope ADSR
  int attackVal = getControlValue(VCA_ATTACK, false);
  float newAttack = (attackVal / 1023.0) * 2000.0;  // 0 to 2000 ms
  if (abs(newAttack - vcaSettings.attack) > 5.0) {
    vcaSettings.attack = newAttack;
    for (int i = 0; i < 6; i++) {
      ampEnv[i].attack(newAttack);
    }
    changed = true;
  }

  int decayVal = getControlValue(VCA_DECAY, false);
  float newDecay = (decayVal / 1023.0) * 2000.0;  // 0 to 2000 ms
  if (abs(newDecay - vcaSettings.decay) > 5.0) {
    vcaSettings.decay = newDecay;
    for (int i = 0; i < 6; i++) {
      ampEnv[i].decay(newDecay);
    }
    changed = true;
  }

  int sustainVal = getControlValue(VCA_SUSTAIN, false);
  float newSustain = sustainVal / 1023.0;  // 0.0 to 1.0
  if (abs(newSustain - vcaSettings.sustain) > 0.01) {
    vcaSettings.sustain = newSustain;
    for (int i = 0; i < 6; i++) {
      ampEnv[i].sustain(newSustain);
    }
    changed = true;
  }

  int releaseVal = getControlValue(VCA_RELEASE, false);
  float newRelease = (releaseVal / 1023.0) * 3000.0;  // 0 to 3000 ms
  if (abs(newRelease - vcaSettings.release) > 5.0) {
    vcaSettings.release = newRelease;
    for (int i = 0; i < 6; i++) {
      ampEnv[i].release(newRelease);
    }
    changed = true;
  }

  if (changed) {
    Serial.println("[VCA] Parameters updated");
  }
}

// =====================================================
// Reverb Parameter Update
// =====================================================
void updateReverbParameters() {
  bool changed = false;

  // Reverb room size: 0.0 to 1.0
  int roomVal = getControlValue(REVERB_ROOMSIZE, false);
  float newRoom = roomVal / 1023.0;
  if (abs(newRoom - reverbSettings.roomsize) > 0.01) {
    reverbSettings.roomsize = newRoom;
    reverb1.roomsize(newRoom);
    changed = true;
  }

  // Reverb damping: 0.0 to 1.0
  int dampVal = getControlValue(REVERB_DAMPING, false);
  float newDamp = dampVal / 1023.0;
  if (abs(newDamp - reverbSettings.damping) > 0.01) {
    reverbSettings.damping = newDamp;
    reverb1.damping(newDamp);
    changed = true;
  }

  // Reverb mix: 0.0 to 1.0
  int mixVal = getControlValue(REVERB_MIX, false);
  float newMix = mixVal / 1023.0;
  if (abs(newMix - reverbSettings.mix) > 0.01) {
    reverbSettings.mix = newMix;
    reverbMixer.gain(0, 1.0 - newMix);  // Dry
    reverbMixer.gain(1, newMix);        // Wet
    changed = true;
  }

  if (changed) {
    Serial.println("[REVERB] Parameters updated");
  }
}

// =====================================================
// Control Value Getter
// =====================================================
int getControlValue(const char* control, bool isSwitch) {
  if (strncmp(control, "MUX_", 4) == 0) {
    int channel = atoi(control + 4);
    if (isSwitch) {
      int switchIndex = channel - firstSwitchChannel;
      if (switchIndex >= 0 && switchIndex < numSwitches) {
        return currentPos[switchIndex];
      }
    } else {
      int potIndex = channel - firstPotChannel;
      if (potIndex >= 0 && potIndex < numPots) {
        return lastPotValues[potIndex];
      }
    }
  } else if (strncmp(control, "UNO_", 4) == 0) {
    int index = atoi(control + 4);
    if (index >= 0 && index < 4) {
      return unoPotValues[index];
    }
  } else if (strncmp(control, "MEGA_", 5) == 0) {
    int index = atoi(control + 5);
    if (index >= 0 && index < 16) {
      return megaPotValues[index];
    }
  }
  return 0;
}

// =====================================================
// Waveform Mapper
// =====================================================
int mapWaveform(int position) {
  switch (position) {
    case 1: return WAVEFORM_SINE;
    case 2: return WAVEFORM_SAWTOOTH;
    case 3: return WAVEFORM_SQUARE;
    case 4: return WAVEFORM_TRIANGLE;
    default: return WAVEFORM_SINE;
  }
}

// =====================================================
// Synth Voice Handling
// =====================================================
void handleNoteEvent(byte cmd, byte note, byte vel) {
  float baseFreq = 440.0 * powf(2.0, (note - 69) / 12.0);

  if (cmd == 1) {  // NoteOn
    Serial.println("NOTE RECEIVED");

    // First, try to find a completely free voice (not playing or releasing)
    int freeVoice = -1;
    for (int v = 0; v < 6; v++) {
      if (activeFreq[v] == 0.0 && !ampEnv[v].isActive()) {
        freeVoice = v;
        break;
      }
    }

    // If no completely free voice, find one that's not actively held (can be releasing)
    if (freeVoice == -1) {
      for (int v = 0; v < 6; v++) {
        if (activeFreq[v] == 0.0) {
          freeVoice = v;
          break;
        }
      }
    }

    // If still no free voice, steal the oldest voice
    if (freeVoice == -1) {
      freeVoice = 0;  // Simple voice stealing - take voice 0
    }

    if (freeVoice != -1) {
      int v = freeVoice;

      // Set frequency immediately
      activeFreq[v] = baseFreq;

      // Set all 3 VCOs for this voice
      for (int vco_num = 0; vco_num < 3; vco_num++) {
        int wfIndex = v * 3 + vco_num;

        // Calculate frequency with octave shift and detune
        int octaveShift = vco[vco_num].octave - 3;  // Convert 1-4 to -2,-1,0,+1
        float octaveMultiplier = powf(2.0, octaveShift);
        float detunedFreq = activeFreq[v] * octaveMultiplier * powf(2.0, vco[vco_num].detune / 12.0);

        waveform[wfIndex].begin(vco[vco_num].waveform);
        waveform[wfIndex].frequency(detunedFreq);
        waveform[wfIndex].amplitude(1.0);  // Full amplitude, VCA controls volume
      }

      // Trigger filter and amplitude envelopes for this voice
      filterEnvState[v].active = true;
      filterEnvState[v].noteOnTime = millis();
      filterEnvState[v].released = false;
      filterEnvState[v].lastValue = 0.0;

      filterEnv[v].noteOn();
      ampEnv[v].noteOn();
    }
  } else {  // NoteOff
    for (int v = 0; v < 6; v++) {
      if (fabs(activeFreq[v] - baseFreq) < 1.0) {
        activeFreq[v] = 0.0;

        // Trigger release phase of envelopes
        unsigned long now = millis();
        filterEnvState[v].noteOffTime = now;
        filterEnvState[v].released = true;
        filterEnvState[v].lastValue = calculateFilterEnvelope(v, now);

        filterEnv[v].noteOff();
        ampEnv[v].noteOff();

        break;  // Only release one voice per note-off
      }
    }
  }
}

// =====================================================
// Update Active Voices (for detune/octave changes)
// =====================================================
void updateActiveVoices() {
  for (int v = 0; v < 6; v++) {
    if (activeFreq[v] > 0.0) {
      for (int vco_num = 0; vco_num < 3; vco_num++) {
        int wfIndex = v * 3 + vco_num;

        // Calculate frequency with octave shift and detune
        int octaveShift = vco[vco_num].octave - 3;  // Convert 1-4 to -2,-1,0,+1
        float octaveMultiplier = powf(2.0, octaveShift);
        float detunedFreq = activeFreq[v] * octaveMultiplier * powf(2.0, vco[vco_num].detune / 12.0);

        waveform[wfIndex].frequency(detunedFreq);
        waveform[wfIndex].begin(vco[vco_num].waveform);
      }
    }
  }
}

Arduino MEGA code:

Code:
#include <Wire.h>

// =====================================================
// Combined MEGA code: Keyboard Scanner + 16 Pots
// =====================================================
#define TEENSY_ADDR 8

// ---------- Keyboard Matrix ----------
const byte rowPins[8] = {23, 22, 25, 24, 27, 26, 29, 28};     
const byte colPins[8] = {10, 11, 12, 13, 51, 50, 53, 52};     
bool keyState[8][8];
bool lastKeyState[8][8];

// ---------- Potentiometers ----------
#define NUM_POTS 16
const int potPins[NUM_POTS] = {A0, A1, A2, A3, A4, A5, A6, A7,
                               A8, A9, A10, A11, A12, A13, A14, A15};
int lastPotValues[NUM_POTS];
const int potTolerance = 5;
const int POT_SEND_INTERVAL = 20; // ms
unsigned long lastPotSend = 0;

// I2C error tracking and rate limiting
unsigned long i2cErrorCount = 0;
unsigned long lastErrorReport = 0;
unsigned long lastI2CSend = 0;
const unsigned long I2C_MIN_INTERVAL = 2; // Minimum 2ms between I2C transmissions

// =====================================================
// Setup
// =====================================================
void setup() {
  Wire.begin(); // MEGA as I2C master
  Wire.setTimeout(10000); // 10ms timeout (increased from 5ms)
  Wire.setClock(50000); // Explicitly set to 100kHz (standard mode)
 
  Serial.begin(115200);
  Serial.println("MEGA Keyboard + Pot controller ready");
 
  // Keyboard pin setup
  for (int c = 0; c < 8; c++) {
    pinMode(colPins[c], OUTPUT);
    digitalWrite(colPins[c], HIGH);
  }
  for (int r = 0; r < 8; r++) {
    pinMode(rowPins[r], INPUT_PULLUP);
  }
 
  // Initialize arrays
  memset(keyState, 0, sizeof(keyState));
  memset(lastKeyState, 0, sizeof(lastKeyState));
  delay(1000); // Give Teensy time to initialize
  // Initialize pot readings
  for (int i = 0; i < NUM_POTS; i++) {
    lastPotValues[i] = analogRead(potPins[i]);
  }
 
 
}

// =====================================================
// Main Loop
// =====================================================
void loop() {
  scanKeyboard();
  reportKeyChanges();
 
  unsigned long now = millis();
  if (now - lastPotSend >= POT_SEND_INTERVAL) {
    lastPotSend = now;
    readPots();
  }
 
  // Report I2C errors periodically
  if (i2cErrorCount > 0 && now - lastErrorReport > 5000) {
    Serial.print("I2C errors: ");
    Serial.println(i2cErrorCount);
    lastErrorReport = now;
  }
}

// =====================================================
// --- Keyboard Scanning ---
// =====================================================
void scanKeyboard() {
  for (int c = 0; c < 8; c++) {
    digitalWrite(colPins[c], LOW);
    delayMicroseconds(30);
    for (int r = 0; r < 8; r++) {
      keyState[r][c] = (digitalRead(rowPins[r]) == LOW);
    }
    digitalWrite(colPins[c], HIGH);
  }
}

void reportKeyChanges() {
  for (int r = 0; r < 8; r++) {
    for (int c = 0; c < 8; c++) {
      if (keyState[r][c] != lastKeyState[r][c]) {
        byte note = c * 8 + r + 36;
        byte cmd = keyState[r][c] ? 1 : 0;
        byte vel = keyState[r][c] ? 100 : 0;
        
        // Wait for minimum interval between I2C sends
        waitForI2CReady();
        
        sendNoteEvent(cmd, note, vel);
        lastKeyState[r][c] = keyState[r][c];
        Serial.print(cmd ? "NoteOn " : "NoteOff ");
        Serial.println(note);
      }
    }
  }
}

// =====================================================
// --- Potentiometer Reading ---
// =====================================================
void readPots() {
  for (int i = 0; i < NUM_POTS; i++) {
    int raw = analogRead(potPins[i]);
    if (abs(raw - lastPotValues[i]) > potTolerance) {
      lastPotValues[i] = raw;
      
      // Wait for minimum interval between I2C sends
      waitForI2CReady();
      
      sendPot(i + 100, raw);
    }
  }
}

// =====================================================
// --- I2C Helpers ---
// =====================================================
void waitForI2CReady() {
  unsigned long now = micros();
  unsigned long elapsed = now - lastI2CSend;
 
  // If less than minimum interval has passed, wait
  if (elapsed < (I2C_MIN_INTERVAL * 1000)) {
    delayMicroseconds((I2C_MIN_INTERVAL * 1000) - elapsed);
  }
}

bool safeI2CSend(byte* data, int len) {
  Wire.beginTransmission(TEENSY_ADDR);
 
  for (int i = 0; i < len; i++) {
    Wire.write(data[i]);
  }
 
  byte error = Wire.endTransmission(true); // true = send stop bit
  lastI2CSend = micros();
 
  if (error != 0) {
    i2cErrorCount++;
    
    // Try to recover the bus
    Wire.end();
    delayMicroseconds(100);
    Wire.begin();
    Wire.setTimeout(10000);
    Wire.setClock(50000);
    
    return false;
  }
 
  return true;
}

void sendNoteEvent(byte cmd, byte note, byte vel) {
  byte data[3] = {cmd, note, vel};
 
  if (safeI2CSend(data, 3)) {
    Serial.println("SEND NOTE");
  } else {
    Serial.print("NOTE I2C ERROR: ");
    Serial.println(Wire.getWriteError());
  }
}

void sendPot(int index, int value) {
  byte data[3] = {(byte)index, highByte(value), lowByte(value)};
 
  if (safeI2CSend(data, 3)) {
    Serial.println("SEND POT");
  } else {
    Serial.print("POT I2C ERROR: ");
    Serial.println(Wire.getWriteError());
  }
}

Arduino UNO code:

Code:
#include <Wire.h>

#define TEENSY_ADDRESS 8
const int potPins[4] = {A0, A1, A2, A3};
int lastPot[4];
const int potTolerance = 2;
const int SEND_INTERVAL = 20;
unsigned long lastSendTime = 0;

void setup() {
  Wire.begin();  // I2C master

  // Read initial values and send all pots at startup
  for (int i = 0; i < 4; i++) {
    lastPot[i] = analogRead(potPins[i]);
    sendPot(i, lastPot[i]); // send numeric packet
  }
}

void loop() {
  unsigned long now = millis();
  if (now - lastSendTime < SEND_INTERVAL) return;
  lastSendTime = now;

  for (int i = 0; i < 4; i++) {
    int raw = analogRead(potPins[i]);
    if (abs(raw - lastPot[i]) > potTolerance) {
      lastPot[i] = raw;
      sendPot(i, raw);
    }
  }
}

void sendPot(int index, int value) {
  Wire.beginTransmission(TEENSY_ADDRESS);
  Wire.write(index);           // 1 byte: pot index 0–3
  Wire.write(highByte(value)); // 1 byte: high byte
  Wire.write(lowByte(value));  // 1 byte: low byte
  Wire.endTransmission();
}

I have browsed this forum for about a week now, and can't really find any similar problems nor solutions. There was a few mentions of USB audio causing issues, but it has worked perfectly fine with me.
This is my first post so please feel free to educate me on proper etiqute and rules!

Thank you to anyone who takes interest in this and spends some time head-butting the wall with me.
Then again, there will probably be some massive oversight that I completely missed.
I have done a decent bit of testing with this thing, so I should mention that:
- With just playing a sine wave from the Teensy to the DAC, it works perfectly fine (aka something to do with the big code)
- All components have been tested on their own and they work fine

Thank you again in advance for helping me with this!
 

Attachments

  • EasyEDA gerbal.zip
    110.9 KB · Views: 12
  • SCH_Schematic1_2_2025-11-21.pdf
    359.2 KB · Views: 14
This is wild guess but these NVIC_SET_PRIORITY calls look suspicious to me and I would comment them out and see if that helps.
 
Hey Tomas,

Thanks for having a look!

I did try removing the SET_PRIORITY calls a few times with various different combinations, and unfortunately to no avail. This whole issue also occurred before they were in the code at all...
 
What I would do is a classic "divide and conquer" approach. If that is timing/overload issue (not keeping up with everything), I would lower number of oscillators/filters and see if that helps. I would comment out selectively parts of the code and try things one by one, not everything at once.
 
I don't think the Mega and Uno should both be I2C masters on the same bus...
I'm reasonably sure they can both work in slave mode; I would suggest doing that and having the Teensy act as the master, polling them for updates when it wants to.
 
What I would do is a classic "divide and conquer" approach. If that is timing/overload issue (not keeping up with everything), I would lower number of oscillators/filters and see if that helps. I would comment out selectively parts of the code and try things one by one, not everything at once.
That sounds like a great idea, I will try this over the next few days, cheers!
I don't think the Mega and Uno should both be I2C masters on the same bus...
I'm reasonably sure they can both work in slave mode; I would suggest doing that and having the Teensy act as the master, polling them for updates when it wants to.
Whoops, I uploaded the wrong code there, only the Mega was supposed to be the master.
However, when I used both as masters it actually worked fine with the USB connection. I will fix that up too.
 
After some more testing, I reduced my code to the extreme basic, simply taking the keyboard commands and creating sinewaves. Even this code, only 70-80 lines each, still demonstrates the exact same issues as before. As this code works when recieving MIDI commands, I think it must be an i2c and i2s clash.

If anyone has had any experience with an issue like this before, some pointers would be greatly appreciated!

Teensy 4.0:

C++:
#include <Wire.h>
#include <Audio.h>


#define MY_ADDR 0x10

AudioSynthWaveform waveform[8];
AudioMixer4 mixer1, mixer2, finalMix;
AudioOutputI2S i2s1;

AudioConnection patchCord1(waveform[0], 0, mixer1, 0);
AudioConnection patchCord2(waveform[1], 0, mixer1, 1);
AudioConnection patchCord3(waveform[2], 0, mixer1, 2);
AudioConnection patchCord4(waveform[3], 0, mixer1, 3);
AudioConnection patchCord5(waveform[4], 0, mixer2, 0);
AudioConnection patchCord6(waveform[5], 0, mixer2, 1);
AudioConnection patchCord7(waveform[6], 0, mixer2, 2);
AudioConnection patchCord8(waveform[7], 0, mixer2, 3);
AudioConnection patchCord9(mixer1, 0, finalMix, 0);
AudioConnection patchCord10(mixer2, 0, finalMix, 1);
AudioConnection patchCord11(finalMix, 0, i2s1, 0);
AudioConnection patchCord12(finalMix, 0, i2s1, 1);

float activeFreq[8];

void setup() {
  Serial.begin(115200);
  AudioMemory(30);

  for (int i = 0; i < 8; i++) {
    waveform[i].begin(WAVEFORM_SINE);
    waveform[i].amplitude(0.0);
    activeFreq[i] = 0.0;
  }

  for (int i=0; i<4; i++) {
    mixer1.gain(i, 0.5);
    mixer2.gain(i, 0.5);
  }
  finalMix.gain(0, 0.5);
  finalMix.gain(1, 0.5);

  Wire.begin(MY_ADDR);
  Wire.onReceive(receiveEvent);

  Serial.println("Teensy synth ready.");
}

void loop() {
  // idle, audio engine runs in background
}

void receiveEvent(int howMany) {
  if (howMany < 3) return;
  byte cmd = Wire.read();
  byte note = Wire.read();
  byte vel = Wire.read();

  float freq = 440.0 * powf(2.0, (note - 69) / 12.0);

  Serial.print("I2C event: cmd=");
  Serial.print(cmd);
  Serial.print(" note=");
  Serial.print(note);
  Serial.print(" vel=");
  Serial.print(vel);
  Serial.print(" freq=");
  Serial.println(freq);

  if (cmd == 1) { // NoteOn
    for (int i=0; i<8; i++) {
      if (activeFreq[i] == 0.0) {
        waveform[i].frequency(freq);
        waveform[i].amplitude(vel / 127.0);
        activeFreq[i] = freq;
        break;
      }
    }
  } else { // NoteOff
    for (int i=0; i<8; i++) {
      if (fabs(activeFreq[i] - freq) < 1.0) {
        waveform[i].amplitude(0.0);
        activeFreq[i] = 0.0;
      }
    }
  }
}

Arduino Mega (the uno was removed):

C++:
#include <Wire.h>

// Teensy I2C address
#define TEENSY_ADDR 0x10

// Example key scanning setup
const byte rowPins[8] = {23, 22, 25, 24, 27, 26, 29, 28};     
const byte colPins[8] = {10, 11, 12, 13, 51, 50, 53, 52};

bool keyState[8][8];
bool lastKeyState[8][8];

void setup() {
  // disable internal pull-ups (Mega safe for 3.3V bus)
  pinMode(20, INPUT);
  pinMode(21, INPUT);

  Wire.begin(); // Mega as I2C master

  // initialize keyboard matrix pins
  for (int c = 0; c < 8; c++) pinMode(colPins[c], OUTPUT);
  for (int r = 0; r < 8; r++) pinMode(rowPins[r], INPUT_PULLUP);

  memset(keyState, 0, sizeof(keyState));
  memset(lastKeyState, 0, sizeof(lastKeyState));

  Serial.begin(115200);
  Serial.println("Mega master ready");
}

void scanMatrix() {
  for (int c = 0; c < 8; c++) {
    digitalWrite(colPins[c], LOW); // drive column
    delayMicroseconds(30);
    for (int r = 0; r < 8; r++) {
      keyState[r][c] = (digitalRead(rowPins[r]) == LOW);
    }
    digitalWrite(colPins[c], HIGH);
  }
}

void reportChanges() {
  for (int r = 0; r < 8; r++) {
    for (int c = 0; c < 8; c++) {
      if (keyState[r][c] != lastKeyState[r][c]) {
        byte note = c * 8 + r + 36; // example MIDI mapping

        byte cmd = keyState[r][c] ? 1 : 0; // 1 = NoteOn, 0 = NoteOff
        byte vel = keyState[r][c] ? 100 : 0;

        Wire.beginTransmission(TEENSY_ADDR);
        Wire.write(cmd);
        Wire.write(note);
        Wire.write(vel);
        Wire.endTransmission();

        lastKeyState[r][c] = keyState[r][c];

        Serial.print("Sent ");
        Serial.print(cmd ? "NoteOn " : "NoteOff ");
        Serial.println(note);
      }
    }
  }
}

void loop() {
  scanMatrix();
  reportChanges();
}
 
Can you use i2s on its own as test to see if you have audio, just a simple sketch that outputs a tone to the i2s device? You might have more luck with the pcm1502 than the uda1334 as an i2s device, in my last build I had to replace the uda1334 with a pcm1502 i2s device to get the audio to work properly with my spi screen and mux inputs, it was so much better with the change.
 
Back
Top