Teensy 4.1 is an SBC programmed as bare-metal from the Arduino environment. There are many packages which enable most of the desired functionalities.

Apart from programming in the Arduino IDE, code can also be compiled using gcc for AVR and uploaded using a command line tool. This would be used if code is generated by CI.

Audio:

Networking:

Hardware:

Experiences so far:

  • stable performance of synth to I2S with various settings (44.1/48/96 kHz, block size 128/64/16/8)
  • I2S input/output tests with WM8731 are not always reproducible, because chip initialization via I2C is unreliable – it does seem to work in principle, though.
  • USB input of official library is glitchy
  • TDM input and output works in 24/32 bit resolution.
  • 8-channel USB input and output (@ 48kHz, 24/32 bit) works flawlessly with the teensy-4-usbAudio](https://github.com/grrrr/teensy-4-usbAudio) library.

Todo:

  • 8-channel AES67 input and output should work

Examples:

I2S output tests

The tests are run using a Hifiberry-DAC-style passive I2S card ("PirateAudio") with Line Out (see pinout).

// i2sout.ino
#include <Audio.h>

AudioSynthWaveformSine waveform;
AudioOutputI2S i2sout;
AudioConnection patchCord1(waveform, 0, i2sout, 0), patchCord2(waveform, 0, i2sout, 1);

void setup() {
    AudioMemory(12);
    waveform.frequency(100);
    waveform.amplitude(0.5);
}

void loop() {}

This also runs cleanly with AUDIO_SAMPLE_RATE_EXACT set to 48000/96000 and/or AUDIO_BLOCK_SAMPLES set to 16. These settings can be conveniently made in the Tools menu for "Audio block size" and "Audio sample rate", provided the IDE config modifications have been made according to this.

Also working is the 32-bit float version using OpenAudio_ArduinoLibrary:

// i2sout_f32.ino
#include <OpenAudio_ArduinoLibrary.h>

AudioSynthWaveformSine_F32 waveform;
AudioOutputI2S_F32 i2sout;
AudioConnection_F32 patchCord1(waveform, 0, i2sout, 0), patchCord2(waveform, 0, i2sout, 1);

void setup() {
    AudioMemory_F32(12);
    waveform.frequency(100);
    waveform.amplitude(0.5);
}

void loop() {}

This also runs cleanly with AUDIO_SAMPLE_RATE_EXACT set to 48000/96000/192000 and/or AUDIO_BLOCK_SAMPLES set to 16/8/4.

Note: When compiling for small block sizes, there are a lot of compilation warnings, because much of the code is not able to handle these sizes. If these objects are not used, this should not pose a problem.

DSP load with OpenAudio_ArduinoLibrary:

  • sample rate 48kHz / block size 64: 0.66%
  • sample rate 48kHz / block size 16: 0.86%
  • sample rate 48kHz / block size 4: 1.66%
  • sample rate 96kHz / block size 4: 3.33%
  • sample rate 192kHz / block size 4: 6.66%

DAC resolution / Noise:

Judging from the code, AudioOutputI2S_F32 does make use of 32-bit format. From the audio output this is not verifiable, since the noise floor of the I2S card is in the region of -97 dB FS, effectively dithering 16-bit output.

Codec needing I2C initialization:

We also test with a Audioinjector Zero card (see pinout) with a WM8731 codec using I2C communication.

In order to control the chip with extended functionality, we use the AudioControlWM8731_F32 class from the hexefx_audiolib_F32 library.

The chip has well-documented problems in I2C communication when clock lines (like I2S BCLK or I2C SCL) are "ringing", i.e., show overshoot due to high bandwidth. This can be improved by inserting resistors (e.g, 220Ω) in series on the clock lines.

// i2sout_zero_f32.ino
#include <OpenAudio_ArduinoLibrary.h>
#include <hexefx_audiolib_F32.h>

AudioSynthWaveformSine_F32 waveform;
AudioOutputI2S_F32 i2sout;
AudioConnection_F32 patchCord1(waveform, 0, i2sout, 0), patchCord2(waveform, 0, i2sout, 1);
AudioControlWM8731_F32 codec;

static bool ok = false;

void setup() {
    delay(1000); // allow codec to awake
    ok = codec.enable();
    if(ok) {
        AudioMemory_F32(12);
        codec.volume(1);

        waveform.frequency(100);
        waveform.amplitude(0.5);
    }
}

void loop() {
    if(ok) {
        printf("Sample rate: %.0f / Block size: %.0f\n", float(AUDIO_SAMPLE_RATE_EXACT), float(AUDIO_BLOCK_SAMPLES));

    printf("DSP usage: %.2f%% (%.2f%% max)\n", float(AudioProcessorUsage()), float(AudioProcessorUsageMax()));
    }
    else
        printf("Codec init FAILED\n");

    delay(1000);
}

This works also well with 48k sample rate and a block size of 16 (0.86% DSP load).

I2S thru tests

The WM8731 codec has an ADC that can be used for input-output tests.

#include <OpenAudio_ArduinoLibrary.h>
#include <hexefx_audiolib_F32.h>
#include <cmath>

AudioInputI2S_F32 i2sin;
AudioOutputI2S_F32 i2sout;
AudioConnection_F32 patchCord1(i2sin, 0, i2sout, 0), patchCord2(i2sin, 1, i2sout, 1);
AudioAnalyzeRMS_F32 rms1, rms2;
AudioConnection_F32 patchCord3(i2sin, 0, rms1, 0), patchCord4(i2sin, 1, rms2, 0);
AudioControlWM8731_F32 codec;

static bool ok = false;

void setup() {
    delay(1000); // allow codec to awake
    ok = codec.enable();
    if(ok) {
        AudioMemory_F32(64);
        codec.volume(1);
        codec.inputSelect(AUDIO_INPUT_LINEIN);
        codec.lineIn_mute(false);
        codec.inputLevel(0.63);
        codec.hp_filter(true);
    }
}

void loop() {
    if(ok) {
        printf("Sample rate: %.0f / Block size: %.0f\n", float(AUDIO_SAMPLE_RATE_EXACT), float(AUDIO_BLOCK_SAMPLES));
        printf("DSP usage: %.2f%% (%.2f%% max)\n", float(AudioProcessorUsage()), float(AudioProcessorUsageMax()));

        if(rms1.available() && rms2.available()) {
            float rms1_dB = log10(rms1.read())*20;
            float rms2_dB = log10(rms2.read())*20;
            printf("Input RMS: %.1f dB / %.1f dB\n", rms1_dB, rms2_dB);
        }
    }
    else
        printf("Codec init FAILED\n");  

    delay(1000);
}

This works out of the box for the default 44.1 kHz sample rate and block size 128. DSP load is 0.64%, input-output latency is 300 samples.

For 48 or 96kHz, control_WM8731_F32.cpp in the hexefx_audiolib_F32 library must be patched for the correct initialization of the WM8731 chip. In the enable method, we need the following for the WM8731_REG_SAMPLING register:

    write(WM8731_REG_SAMPLING,
        WM8731_BITS_USB_NORMAL(0)   |   // normal mode
        WM8731_BITS_BOSR(0)         |   // 256*fs
    ( (AUDIO_SAMPLE_RATE_EXACT == 48000 || AUDIO_SAMPLE_RATE_EXACT == 96000)
        ?
        WM8731_BITS_SR(0)           // n*48kHz
        :
        WM8731_BITS_SR(8)           // n*44.1kHz
    ) |
    ( (AUDIO_SAMPLE_RATE_EXACT == 88200 || AUDIO_SAMPLE_RATE_EXACT == 96000)
        ?
        WM8731_BITS_CLKIDIV2(1)     |   // MCLK/2
        WM8731_BITS_CLKODIV2(1)
        :
        WM8731_BITS_CLKIDIV2(0)     |   // MCLK/1
        WM8731_BITS_CLKODIV2(0)
    );

There is a problem in AudioAnalyzeRMS_F32, currently inhibiting block sizes other than 128.

Without the RMS calculation, with block size 16 at 48kHz, latency is 77 samples. With block size 8, latency is 60 samples, or 1.25 ms. At 96 kHz and block size 8, the latency is 106 samples, or 1.1 ms.

I2S input from AudioInputI2S_F32 with block size 4 doesn't seem to be possible, most probably because of the DMA transfers.

TDM I/O tests

The tests are run using the Teensy Audio TLV320AIC3104 8x8 CODEC Board with 4 stereo codecs on the TDM bus. It is easy to set up with the provided driver.

16-bit mode:

#include "output_tdmA.h"
#include "input_tdmA.h"
#include <Audio.h>
#include <Wire.h>
#include "control_tlv320aic3104.h"

#define CODECS 4
#define AUDIO_BLOCKS (CODECS * 8 + 4) // // 8 = 2 * (2 + 2) per CODEC + processing

AudioInputTDM_A          tdm_in;
AudioOutputTDM_A         tdm_out;
AudioConnection          patchCord(tdm_in, 0, tdm_out, 0);
AudioControlTLV320AIC3104 aic(CODECS, true, AICMODE_TDM);

void setup() 
{
  AudioMemory(AUDIO_BLOCKS);
  delay(100); 

  Wire.begin();
  Wire.setClock(400000);
  aic.setVerbose(2); // for hardware validation, not required for production
  int8_t testCodec = AIC_ALL_CODECS;
  if (CODECS == 1)
    testCodec = 0;

  aic.begin();

  // when issued before enable() all inputs are affected
  aic.inputMode(AIC_DIFF); // or AIC_DIFF
  if(!aic.enable(testCodec)) // After enable() DAC and ADC are muted
    printf("Failed to initialise codec\n");

  // After enable() DAC and ADC are muted. Codecs to change need to be explicitly specified.
  aic.volume(1, -1, testCodec);  // muted on startup
  aic.inputLevel(0, -1, testCodec); // level in dB: 0 = line level, ~-50 = mic level

  delay(100);
}

void loop() 
{
  printf("audioProc %2.1f%%, audioMem %i\n", AudioProcessorUsage(), AudioMemoryUsage());
  delay(1000);
}

The measured input/output latency at 48 kHz with block size 8 is 46 samples, with block size 4 it is 34 samples. Although the codec and the driver software do support 96 kHz sample rate in principle, this seems to be currently faulty.

32-bit mode:

This is possible with the repo after about October 2025.

#include "OpenAudio_ArduinoLibrary.h"
#include "AudioStream_F32.h"
#include "output_tdm32.h"
#include "input_tdm32.h"
#include <Wire.h>
#include "control_tlv320aic3104.h"

#define SAMPLEWIDTH 32
#define CODECS 4
#define AUDIO_BLOCKS (CODECS * 8 + 4) // // 8 = 2 * (2 + 2) per CODEC + processing
#define RST_PIN 22 

AudioInputTDM_32       tdm_in(SAMPLEWIDTH); 
AudioOutputTDM_32      tdm_out(SAMPLEWIDTH);
AudioAnalyzePeak_F32   peakRead0, peakRead1; 

AudioConnection_F32    patchCord1(tdm_in, 0, peakRead0, 0);
AudioConnection_F32    patchCord2(tdm_in, 1, peakRead1, 0);

AudioConnection_F32    patchCord3(tdm_in, 0, tdm_out, 0);
AudioConnection_F32    patchCord4(tdm_in, 1, tdm_out, 1);


AudioControlTLV320AIC3104 aic(CODECS, true, AICMODE_TDM, AUDIO_SAMPLE_RATE, SAMPLEWIDTH);

int muxesFound = 0;
void setup() 
{
  AudioMemory_F32(AUDIO_BLOCKS);
  Serial.begin(115200);
  delay(1000); 

  Wire.begin();
  Wire.setClock(400000);
  aic.setVerbose(2); // for hardware validation, not required for production
  int8_t testCodec = AIC_ALL_CODECS;
  if (CODECS == 1)
    testCodec = 0;

  muxesFound = aic.begin();
  Serial.printf("Muxes found %i\n", muxesFound);
  aic.listMuxes();  

  // when issued before enable() all inputs are affected
  aic.inputMode(AIC_DIFF); // AIC_SINGLE or AIC_DIFF
  //aic.setHPF(AIC_HPF_0045); // default value

  if(!aic.enable(testCodec)) // After enable() DAC and ADC are muted
  Serial.println("Failed to initialise codec");

  // After enable() DAC and ADC are muted. Codecs to change need to be explicitly specified.
  aic.volume(1, -1, testCodec);  // muted on startup
  aic.inputLevel(0, -1, testCodec); // level in dB: 0 = line level, ~-50 = mic level

  delay(100);
  Serial.println("Done setup");
}

uint32_t vTimer;
#define PROCESS_EVERY 1000

void loop() 
{
  if(millis() > vTimer + PROCESS_EVERY)
  {
    vTimer = millis();
    Serial.printf("T %l3i: %1.3f %1.3f - ", millis()/1000, peakRead0.read(), peakRead1.read());
    Serial.printf("audioProc %2.1f%%, audioMem %i\n", AudioProcessorUsage(), AudioMemoryUsage_F32());
  }
}

USB audio I/O tests

USB multichannel works stably with the patches in https://github.com/grrrr/teensy-4-usbAudio, also merged into the PRJC_cores/usb_multichannel branch.

USB/TDM I/O

This is a 16-bit test with 8-channel USB in to TDM out and TDM in to USB out.

#include <Audio.h>
#include <Wire.h>
#include "control_tlv320aic3104.h"

#define CODECS 4
#define RST_PIN 22 

#define AUDIO_kHz ((int) AUDIO_SAMPLE_RATE / 1000)
#define AUDIO_CHANNELS USB_AUDIO_NO_CHANNELS_480

// Changing string in descriptor keeps Windows 10 happier
extern "C"
{
    struct usb_string_descriptor_struct
    {
        uint8_t bLength;
        uint8_t bDescriptorType;
        uint16_t wString[6+1+1+2+1];
    };

  usb_string_descriptor_struct usb_string_serial_number={
    2+(6+1+1+2+1)*2,3,
    {'A','u','d','i','o','-','0'+AUDIO_CHANNELS,'/','0'+(AUDIO_kHz / 10),'0' + (AUDIO_kHz % 10),'B'}
  };
}

AudioSynthWaveform       wav1,wav2,wav3,wav4,wav5,wav6,wav7,wav8;
AudioMixer4              mixer1,mixer2,mixer3;

AudioInputUSBOct         usb_oct_in;
AudioOutputUSBOct        usb_oct_out;

AudioInputTDM          tdm_in;
AudioOutputTDM         tdm_out;

AudioConnection          patchCord1(wav1, 0, usb_oct_out, 0);
AudioConnection          patchCord2(wav1, 0, tdm_out, 4);
AudioConnection          patchCord3(wav2, 0, usb_oct_out, 1);
AudioConnection          patchCord4(wav3, 0, usb_oct_out, 2);
AudioConnection          patchCord5(wav4, 0, usb_oct_out, 3);
AudioConnection          patchCord6(wav5, 0, usb_oct_out, 4);
AudioConnection          patchCord7(wav6, 0, usb_oct_out, 5);
AudioConnection          patchCord8(wav7, 0, usb_oct_out, 6);
AudioConnection          patchCord9(wav8, 0, usb_oct_out, 7);
AudioConnection          patchCord10(usb_oct_in, 0, mixer1, 0);
AudioConnection          patchCord11(usb_oct_in, 1, mixer1, 1);
AudioConnection          patchCord12(usb_oct_in, 2, mixer1, 2);
AudioConnection          patchCord13(usb_oct_in, 3, mixer1, 3);
AudioConnection          patchCord14(usb_oct_in, 4, mixer2, 0);
AudioConnection          patchCord15(usb_oct_in, 5, mixer2, 1);
AudioConnection          patchCord16(usb_oct_in, 6, mixer2, 2);
AudioConnection          patchCord17(usb_oct_in, 7, mixer2, 3);
AudioConnection          patchCord18(mixer1, 0, mixer3, 0);
AudioConnection          patchCord19(mixer2, 0, mixer3, 1);
AudioConnection          patchCord20(mixer3, 0, tdm_out, 3);

AudioControlTLV320AIC3104 aic(CODECS, true, AICMODE_TDM);

AudioSynthWaveform* wavs[] = {&wav1, &wav2, &wav3, &wav4, &wav5, &wav6, &wav7, &wav8};
AudioMixer4* mixers[] = {&mixer1,&mixer2};

int muxesFound = 0;

void setup() 
{
  AudioMemory(192); // max mem

  Serial.begin(115200);

  Wire.begin();
  Wire.setClock(400000);
  aic.setVerbose(2); // for hardware validation, not required for production
  int8_t testCodec = AIC_ALL_CODECS;
  if (CODECS == 1)
    testCodec = 0;

  muxesFound = aic.begin();
  Serial.printf("Muxes found %i\n", muxesFound);
  aic.listMuxes();  

  // when issued before enable() all inputs are affected
  aic.inputMode(AIC_DIFF); // or AIC_DIFF
  //aic.setHPF(AIC_HPF_0045); // default value

  if(!aic.enable(testCodec)) // After enable() DAC and ADC are muted
    Serial.println("Failed to initialise codec");

  // After enable() DAC and ADC are muted. Codecs to change need to be explicitly specified.
  aic.volume(1, -1, testCodec);  // muted on startup
  aic.inputLevel(0, -1, testCodec); // level in dB: 0 = line level, ~-50 = mic level

  for (int i=0;i<8;i++)
  {
    wavs[i]->begin(0.5f,220.0f + 110.0f*i,WAVEFORM_TRIANGLE);
    wavs[i]->phase(15.0f*i);
  }

  for (int i=0;i<2;i++)
    for (int j=0;j<4;j++)
      mixers[i]->gain(j,0.25f);

  Serial.printf("Audio block size %d samples; sample rate %.2f; %d channels\n",AUDIO_BLOCK_SAMPLES,AUDIO_SAMPLE_RATE_EXACT,AUDIO_CHANNELS);
  Serial.println("Running");
}

uint32_t lastBlocks;
void loop() 
{
  if (millis() - lastBlocks > 500)
  {
    lastBlocks = millis();
    Serial.printf("Blocks %d; max %d; slot size %d\n",AudioMemoryUsage(),AudioMemoryUsageMax(),AUDIO_SUBSLOT_SIZE);
  }
}

USB loopback

The following is a USB loopback for latency tests. The AUDIO_SUBSLOT_SIZE needs to be set for compilation (custom Teensy menu setting) to 2, 3 or 4 bytes. We made our tests for 3 or 4. While the USB stream is cabable of >16 bits, the audio infrastructure is not, in this case.

#include <Audio.h>

#define CODECS 4
#define RST_PIN 22 

#define AUDIO_kHz ((int) AUDIO_SAMPLE_RATE / 1000)
#define AUDIO_CHANNELS USB_AUDIO_NO_CHANNELS_480

// Changing string in descriptor keeps Windows 10 happier
extern "C"
{
    struct usb_string_descriptor_struct
    {
        uint8_t bLength;
        uint8_t bDescriptorType;
        uint16_t wString[6+1+1+2+1];
    };

  usb_string_descriptor_struct usb_string_serial_number={
    2+(6+1+1+2+1)*2,3,
    {'A','u','d','i','o','-','0'+AUDIO_CHANNELS,'/','0'+(AUDIO_kHz / 10),'0' + (AUDIO_kHz % 10),'B'}
  };
}

AudioInputUSBOct         usb_oct_in;
AudioOutputUSBOct        usb_oct_out;

AudioConnection          patchCord10(usb_oct_in, 0, usb_oct_out, 0);
AudioConnection          patchCord11(usb_oct_in, 1, usb_oct_out, 1);
AudioConnection          patchCord12(usb_oct_in, 2, usb_oct_out, 2);
AudioConnection          patchCord13(usb_oct_in, 3, usb_oct_out, 3);
AudioConnection          patchCord14(usb_oct_in, 4, usb_oct_out, 4);
AudioConnection          patchCord15(usb_oct_in, 5, usb_oct_out, 5);
AudioConnection          patchCord16(usb_oct_in, 6, usb_oct_out, 6);
AudioConnection          patchCord17(usb_oct_in, 7, usb_oct_out, 7);

void setup() 
{
  AudioMemory(192); // max mem

  Serial.begin(115200);

  Serial.printf("Audio block size %d samples; sample rate %.2f; %d channels\n",AUDIO_BLOCK_SAMPLES,AUDIO_SAMPLE_RATE_EXACT,AUDIO_CHANNELS);
  Serial.println("Running");
}

uint32_t lastBlocks;
void loop() 
{
  if (millis() - lastBlocks > 500)
  {
    lastBlocks = millis();
    Serial.printf("Blocks %d; max %d; slot size %d\n",AudioMemoryUsage(),AudioMemoryUsageMax(),AUDIO_SUBSLOT_SIZE);
    AudioMemoryUsageMaxReset();
  }
}

At 48kHz sample rate, block size 64 and sub slot size 3 or 4 bytes, the USB round trip latency is ca. 170–190 frames (3.6–4.0 ms), including DAW buffer size.

Heavy/Puredata

We have developed the hvcc_teensy generator which allows to convert Puredata patches to Arduino-style library code for the Teensy Audio (or OpenAudio) infrastructure.