Teensy 4.1 + I2S access


New member
I'm fairly experienced with microcontrollers and electronics. I've built a digital synth in the past using AVR's, ESP's and STM's, using both DAC's and I²S codecs. I decided to buy a teensy for it's a microcontroller in the arduino environment and it has the peripherals I need (GPIO's and I²S). My basic setup only uses a µCtrlr and a PCM5102, that only has 3 pins: Bit clock, word select and data in. I've spent my entire afternoon trying to understand how to start and I'm baffled I'm just as confused as before (maybe because I only use C in my projects?):

1) I first just tried to use the I²S library but it doesn't work. I don't understand why but it's not an option, even though Teensy is an arduino compatible board (?).
2) I tried then google and documentation, but it is all very confusing. I cannot find a library to get access to I²S that would substitute I²S. How can you access the peripheral as it is and run my old synth codes? There is audio, audiostream, audiocontrol and output_i2s. Where do I get the access to the I²S ports? There is no example showing that, no tutorial. Everything includes the shields, which I don't want to use and I didn't know they were necessary. Should I buy one?
3) Tried downloading example codes, and connecting the correct pins. Not a single one worked. flat line on the PCM output.

How can I communicate with the I²S codec? I just want to pipe some data to it. Could be using some abstraction that used DMA or just direct access, I don't mind, I just want to be able to send some bytes to the codec without declaring a lot of stuff that has nothing to do with the codec, just to find nothing addressing the I²S itself and nothing coming out of the output.
What am I missing here? Sorry for my writing, english is not my first language and I'm very tired after all the debugging.
You don't need the shields, you just have to connect the appropriate pins.

For example, I use this code with an I2S board I bought on Amazon:

The pins that must be connected are:

  • LRCLK: Pin 20/A6
  • BCLK: Pin 21/A7
  • DIN: Pin 7
  • Ground: Ground
  • VIN: 3.3v or VIN
  • GAIN: See documentation

  Demo of the audio sweep function.
  The user specifies the amplitude,
  start and end frequencies (which can sweep up or down)
  and the length of time of the sweep.

  Modified to eliminate the audio shield, and use Max98357A mono I2S chip.

  Pins:        Teensy 4.0    Teensy 3.x

  LRCLK:    Pin 20/A6    Pin 23/A9
  BCLK:        Pin 21/A7    Pin 9
  DIN:        Pin 7        Pin 22/A8
  Gain:        see below    see below
  Shutdown:    N/C        N/C
  Ground:    Ground        Ground
  VIN:        5v        5v

  Other I2S pins not used by the Max98357A device:

  MCLK        Pin 23        Pin 11
  DOUT        Pin 8        Pin 13

  Gain setting:

  15dB    if a 100K resistor is connected between GAIN and GND
  12dB    if GAIN is connected directly to GND
   9dB    if GAIN is not connected to anything (this is the default)
   6dB    if GAIN is conneted directly to Vin
   3dB    if a 100K resistor is connected between GAIN and Vin.

  SD setting (documentation from the Adafruit board)

  This pin is used for shutdown mode but is also used for setting which channel
  is output. It's a little confusing but essentially:

  * If SD is connected to ground directly (voltage is under 0.16V) then the amp
    is shut down

  * If the voltage on SD is between 0.16V and 0.77V then the output is (Left +
    Right)/2, that is the stereo average.

  * If the voltage on SD is between 0.77V and 1.4V then the output is just the
    Right channel

  * If the voltage on SD is higher than 1.4V then the output is the Left

    This is compounded by an internal 100K pulldown resistor on SD so you need
    to use a pullup resistor on SD to balance out the 100K internal pulldown.

  Or alternatively, use the HiLetgo PCM5102 I2S IIS Lossless Digital Audio DAC
  Decoder which provides stereo output:
  https://smile.amazon.com/gp/product/B07Q9K5MT8/ref=ppx_yo_dt_b_asin_title_o00_s00?ie=UTF8&psc=1  */

#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>

// GUItool: begin automatically generated code (edited by meissner afterwards).
AudioSynthToneSweep    tonesweep;        //xy=99,198
AudioMixer4        mixer2;            //xy=280,253
AudioMixer4        mixer1;            //xy=280,175
AudioOutputI2S        i2s;            //xy=452,189

AudioConnection        patchCord1(tonesweep, 0, mixer1, 0);
AudioConnection        patchCord2(tonesweep, 0, mixer2, 0);
AudioConnection        patchCord3(mixer2,    0, i2s,    1);
AudioConnection        patchCord4(mixer1,    0, i2s,    0);
// GUItool: end automatically generated code

const float    t_ampx    = 0.8;
const int    t_lox    = 10;
const int    t_hix    = 22000;
const float    t_timex    = 10;        // Length of time for the sweep in seconds

// Do a sweep in both directions, enabling or disabling the left/right speakers
void do_sweep (int i)
  int do_left  = (i & 1) != 0;
  int do_right = (i & 2) != 0;
  float gain   = (do_left && do_right) ? 0.5f : 1.0f;

  Serial.printf ("Sweep up,   left = %c, right = %c\n",
         (do_left)  ? 'Y' : 'N',
         (do_right) ? 'Y' : 'N');

  mixer1.gain (0, do_left  ? gain : 0.0f);
  mixer2.gain (0, do_right ? gain : 0.0f);

  if (!tonesweep.play (t_ampx, t_lox, t_hix, t_timex)) {
    Serial.println ("ToneSweep - play failed");
    while (1)

  // wait for the sweep to end
  while (tonesweep.isPlaying ())

  // and now reverse the sweep
  Serial.printf ("Sweep down, left = %c, right = %c\n",
         (do_left)  ? 'Y' : 'N',
         (do_right) ? 'Y' : 'N');

  if (!tonesweep.play (t_ampx, t_hix, t_lox, t_timex)) {
    Serial.println("ToneSweep - play failed");
    while (1)

  // wait for the sweep to end
  while (tonesweep.isPlaying ())

  Serial.println ("Sweep done");

void setup(void)
  // Wait for at least 3 seconds for the USB serial connection
  Serial.begin (9600);
  while (!Serial && millis () < 3000)

  AudioMemory (8);
  Serial.println ("setup done");

  for (int i = 1; i <= 3; i++)
    do_sweep (i);

  Serial.println ("Done");

void loop (void)
There is audio, audiostream, audiocontrol and output_i2s. Where do I get the access to the I²S ports? There is no example showing that, no tutorial. Everything includes the shields, which I don't want to use and I didn't know they were necessary. Should I buy one?
If you're generating samples from code somewhere outside of the Audio library, you probably want to use play queues like so:
#include <Audio.h>

AudioPlayQueue           leftqueue;
AudioPlayQueue           rightqueue;
AudioOutputI2S           i2s1;
AudioConnection          patchCord1(leftqueue, 0, i2s1, 0);
AudioConnection          patchCord2(rightqueue, 0, i2s1, 1);

leftqueue would feed the left output channel, rightqueue would feed the right output channel (unless I mixed them up, in which case switch the last numbers around in the patchCord definitions).
There's three ways to push samples into play queues:
- "leftqueue.play($value)" would push a single audio sample of $value into the queue
- "leftqueue.play($ptr, $count)" would copy $count samples from the buffer pointed to by $ptr
- you can call "leftqueue.getBuffer()" which will return an audio buffer, you fill it with AUDIO_BLOCK_SAMPLES number of samples, then call "leftqueue.playBuffer()" to queue them.

(Obviously whichever method you used, you would have to do it for both leftqueue and rightqueue to fill both channels.)
Don't forget: in setup() you will need to call AudioMemory() to give the audio system some buffers to work with. I recommend starting with a decent amount like 64, you can trim it down later once you get things working.

Note that the audio system ONLY operates at 44100Hz. This should be OK if your source material is mostly from CDs...
Thank you for you help MichaelMeissner. It took me quite a while to get the code, as I'm not so used to this level of abstraction.

jmarsh I cannot thank you enough for your help. This is what I was looking for! A simple way to load stuff into my I²S.
I was used to work on periodic interrupts, and a frequency of 44100Hz kinda makes it hard to get a timer period, so I've set the interrupt for 10ms, and in this 10ms I have all the time in the world to load 441 samples on each channel with the leftqueue.play($ptr, $count) method. Now I can just re-implement all of my previous algorithms, but with a much powerful hardware. Thank you so much.

Now I feel stupid I gotta say. I want to study everything that I can about this library but I tried reading the headers and it was overwhelming. Is there a "normal" way to study the Audio library and all its capabilities? Like text documentation? The Teensy page leads you to the audio application, but there is no traditional documentation there, only the graphical interface for the audio blocks.
Now I have two questions so I can go back to coding:

1) Is there a way to change the sample frequency to anything else?
2) Where can I learn more about the AudioMemory function and how to calculate the needed memory?

Thank both of you guys again. Amazing to be able to use this hardware now. I'm just used to more traditional documentation.