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:
- Official audio library, 16 bit only, with restrictions of sample rate (default 44.1 kHz) and block size (default 128)
- OpenAudio_ArduinoLibrary – Floating point extensions to audio library
- hexefx_audiolib_F32 – More extensions of floating point audio
- Notes on enabling 48kHz USB audio
- Multi-channel USB audio
- TDM Library with DMA
Networking:
- AES67
- PTP
- Basic IEEE 1588 tests (examples lwip_1588_input, lwip_1588_timer)
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.