Feasibility of utilizing ADC's while a device is running in I2s Slave mode

James_Hicks

New member
Hello, I'm working on a project right now where I have a single teensy 4.0 board generating different audio signals. This device is the I2s master, it's then transferring audio over I2s to another teensy 4.0 in I2s slave mode. This slave teensy is then transferring audio over I2s to a DAC and then to a speaker.

The issue is this: I want the slave Teensy to be able to read in ADC values and change an audio delay time based on those values. When I attempt to do this the audio is garbage. When I just pass the audio without reading ADC values from the slider, the audio is fine.

My idea on how to solve the issue is this: utilize a DMA to transfer ADC values directly to the variable controlling the delay time.

Is this feasible? How might I go about this? What's causing the audio issues?

My setup works generating audio and passing it through the I2s slave all the way to the DAC. However, when I try to read ADC values on the slave device the audio becomes garbage.

For your information the goal is to have modular blocks that take in audio over I2S and manipulate the audio and pass it along to the next modular block.

Source device is shown on the right and slave block is shown on the left.

PXL_20250424_015609303.jpg


Below is the code for the source teensy and the slave teensy.


## SOURCE TEENSY (I2S MASTER)

C++:
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>
#include <Bounce.h>
#include <stdint.h>

AudioSynthWaveform waveform;
AudioOutputI2S i2s;
AudioConnection patchcable1(waveform, 0, i2s, 0);

// buttons to control signal type
Bounce sine_button = Bounce(0, 5);
Bounce triangle_button = Bounce(1, 5);
Bounce saw_button = Bounce(2, 5);

uint32_t frequency = 500;

void setup() {
  AudioMemory(100);

  waveform.amplitude(1.0);
  waveform.frequency(frequency);
  waveform.begin(WAVEFORM_SINE);
  pinMode(0, INPUT_PULLUP);
  pinMode(1, INPUT_PULLUP);
  pinMode(2, INPUT_PULLUP);

  Serial.begin(9600);
  Serial.printf("Source Block Initialized...\n");
}

void loop() {

  //update button states
  sine_button.update();
  triangle_button.update();
  saw_button.update();

  // pot to control the frequency
  float adc_off_A0 = (float)analogRead(A0) / 1023.0;
  waveform.frequency(100.0 + adc_off_A0 * 900.0);

  AudioInterrupts();

  if(sine_button.fallingEdge())
  {
    // play the sine wave
    waveform.begin(WAVEFORM_SINE);
  }
  else if(triangle_button.fallingEdge())
  {
    // play the triangle wave
    waveform.begin(WAVEFORM_TRIANGLE);
  }
  else if(saw_button.fallingEdge())
  {
    // play the saw wave
    waveform.begin(WAVEFORM_SAWTOOTH);
  }

  AudioNoInterrupts();

}

## SLAVE TEENSY (I2S SLAVE)
C++:
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>
#include <Bounce.h>
#include <stdint.h>
#include <imxrt.h>
#include <DMAChannel.h> // maybe we'll just try to transfer ADC using dma

AudioOutputI2Sslave i2s_out;
AudioInputI2Sslave i2s_in;
AudioEffectDelay delay_1;
AudioConnection patchcable1(i2s_in, 0, delay_1, 0);
AudioConnection patchcable6(delay_1, 0, i2s_out, 0);

// I want a dma to transfer ADC data to a variable


void setup() {
  AudioMemory(400);

  Serial.begin(9600);
  Serial.printf("Delay Block Initialized...\n");
}

void loop() {

  AudioNoInterrupts();

  // slider to control the delay time
  float delay_time = (float)analogRead(A0) / 1023.0;

  delay_1.delay(0, delay_time * 500);
 
  AudioInterrupts();

}
 
Before you get into complex ways of reading the ADC, it’s probably worth doing a couple of things.

Firstly, remove the Audio(No)Interrupts() calls - they’re only really needed when you’re trying to synchronise changes to multiple objects, which you’re not doing. In any case, there’s no reason to have them off while you read the ADC.

Secondly, you need to only be changing the delay length when a real slider move has happened. If you write yourself a little sketch to output the delay values, you’ll find they’re jittering all over the place, and that is almost certainly what’s causing the audio to be “garbage”.

Exactly how to determine a “real” move is a bit of a black art: simply averaging the last few readings does reduce noise, but never really eliminates it. I find it’s best to have a “dead zone”, where a change of (averaged) reading within the zone makes no difference, though that does make super-precise settings impossible.

Depending on your use case, you might also want to think about making the setting non-linear. You don’t really need a nominal resolution of 0.5ms for delays of around 500ms, and you might well want finer resolution down at the 0-20ms end.

Please do report back on progress somewhere. I’ve toyed with the idea of a multi-Teensy audio system, but never tried actually making one. I was going to make the downstream Teensy the master, so it could use e.g. AudioInputI2SOct to receive from 4 slaves, but otherwise much the same as your concept.

One last thing … do keep an eye on latency if you make your chain of Teensys much longer!
 
The ResponsiveAnalogRead library works great with this, you get to set a threshold and if the pot movement passes that, you'll get the reading of the pot to use. I think that the "audio garbage" you're getting is more from the jitter of the pot constantly changing the delay time than something else. I use the ADCs when processing audio all the time and no issues whatsoever.
 
Indeed jitter in a delay will create all sorts of artifacts, you really don't want that!
 
Back
Top