florisvanvugt
Member
Hello Teensy and audio enthusiasts,
I am loving the Teensy and the audio library. I have an issue with the timing accuracy though. Let me first describe the issue and then give you details like source code.
I want to play a sound at a regular interval, say 600 ms. It's a very short sound. The issue is that the intervals are not exactly 600 ms. A curious phenomenon appears where the interval is sometimes slightly too long and then slightly too short. Specifically, the interval is sometimes 600.6 ms, and other times 597.7 ms. The average interval is pretty much spot on at 600.01 ms. Here you see a histogram (yes, actually a histogram!) of the actually produced intervals:
So there appears to be a magic 2.90 ms "quantisation" of the intervals.
So my question is: am I doing something wrong? Is this behaviour somehow expected from the audio library? Is there something that can be done to fix this? What I want is to be able to play at pretty much exactly 600 ms intervals.
Any help greatly appreciated! And thanks for all the developers of Teensy & the Audio library, it's really fantastic.
Implementation details
See below for the Teensy source code. I connected the audio jack of the Teensy Audio board to my PC sound card and recorded the Teensy output as a wave file (about 25 minutes of recording of the Teensy generating the sounds). Then I took the original WAV file that Teensy is supposed to play (included in the source code here below), and I used a Python script to determine the exact onsets of that wave file in the Teensy audio output (using a cross correlation technique). I can give details of the latter procedure as well, but I am very confident nothing is wrong there because a friend had exactly the same analysis using a completely different recording, hardware setup, and onset detection technique.
Also, the above issue does not just hold for 600 ms intervals. When trying to produce other interval durations, I get the same effect.
The actually produced intervals:
overall intervals:
mean 600.010253 SD 1.157552 ms
The two bars in the plot above correspond to:
mean 597.684822 SD 0.006784 ms
mean 600.586433 SD 0.007828 ms
As you can see, each bar is actually reproduced remarkably precise (with only several microseconds jitter)! But why are there these two modes? And is there a way to Teensy output a single mode, centered at exactly 600 ms?
In case this is useful, here is the actual recording from the Teensy: http://132.206.106.46:4404/simplified_metronome_25min_07April2018.wav
This is the source code for the Teensy (with audio board):
/*
* Play a metronome sound
*
*/
#include <Audio.h>
// Converted from metronome.wav, using 44100 Hz, u-law encoding
const unsigned int AudioSampleMetronome[353] = {
0x0100052C,0x04010101,0x06830004,0x3C322719,0x5F554D44,0x57656765,0xF4F0E4C0,0xE7EEF2F4,
0xDDE0E0E2,0x42AFCDD7,0x77726759,0x6D737779,0xF0E48A61,0x6150D6ED,0xCA505D61,0xF8F7F4E9,
0xF1F4F6F7,0x6750D8EB,0x7875716E,0x76757678,0x6C737576,0xF6EED259,0xF5F5F7F8,0xF5F5F6F6,
0xDDECF3F5,0x60615706,0x746D625D,0x75757777,0x73757676,0x08596770,0xF4F0E8D9,0xE3EDF3F5,
0xA99FC3D6,0xEEE9E1D0,0xD8E5EBEE,0x756E5E80,0x6E747777,0xC2145364,0xE7D5C3C3,0xF7F7F6F2,
0xF4F4F5F6,0x54CEE9F2,0x72727068,0x74757271,0xC2526570,0x30D1E0DE,0xC13C4D4D,0xEBEAE7DD,
0xDBE6EAEB,0x66604CBD,0x964A5E66,0x685A34B5,0x676F7170,0xC0B33E5B,0xDFD7CEC4,0xDCE1E2E2,
0xCCCCCFD4,0x534594C5,0x565B5B58,0x4441424D,0x50423E42,0xA1515A58,0xE7E8E4D7,0xD0D5DDE3,
0xA7BAC5CB,0xC3AC8D8F,0x40C3D2D0,0x5D616159,0x494B5056,0x4C4D4C4A,0x2B343E46,0xD8D0C094,
0xB4D1D9DB,0x1839443A,0x3C311C8B,0xB8344443,0xDADBD9D1,0xE6E2DCD9,0xD3E1E6E8,0x62605290,
0x66626161,0x6A6A6A69,0x5A646869,0x18072D48,0xBC12312C,0xDEDBD7D0,0xDADDE0E0,0xD1D2D6D8,
0xEBE6DED4,0xD9E3E9EC,0x625D49C0,0x414D5760,0x48312031,0x65646157,0x51596063,0xCBC5A540,
0x31B3C3C9,0x1C4E544D,0xDBD9D6CB,0xE1E2E1DF,0xCBD6DCE0,0x393F36AA,0x4A454037,0x64615951,
0x434F5A62,0x49494642,0x4D474546,0x47515452,0xB78D2237,0xE6E2DACD,0xE7EAEAE9,0xDFDBDEE3,
0xE1E2E3E2,0xC0D3DCE0,0x5B564B2E,0x4E4E545A,0x51535351,0x544F4D4F,0x5E61605B,0x41485057,
0xD4CCB629,0xE0E1DFDA,0xD4D5D7DB,0xAAB7C5D0,0xA70A83A0,0x33BAC5BF,0x5D5D5A51,0x434C5459,
0x44464442,0x4B494442,0x2F3D4449,0xB4B1A803,0xCEC9C2B9,0xC5C9CDD0,0x8891AEBD,0xB9B6B2A4,
0xD0CFC8C0,0x2592BCCA,0xA9AD9B1E,0x42403305,0x463D393F,0x983F4B4C,0xC7CCCBC2,0xCDC8C2C2,
0xCACBCCCE,0x3024B0C4,0x1EAEAC12,0x2D414640,0x41351E06,0x51525048,0x504C494C,0x41454A50,
0xB2A32338,0xC2C1BCB7,0x3620ADBE,0xB419363C,0xD1D0CDC5,0xC9D0D2D2,0x311CA8BF,0x17263336,
0x514B3F28,0x40484F52,0xB9A11D33,0xD5D5D1C7,0xC0C5CDD2,0xC4C4C1BE,0x238AB0BE,0x3F39342F,
0x9E313F41,0xABB9BEB8,0x4C463A1A,0x4C4A4B4D,0x494D4E4D,0x4A464445,0x2A414A4D,0x2702A29A,
0x1B2A2F2F,0xBBB7B19B,0xD2CEC7C1,0xD7D8D7D5,0xCCD0D3D5,0x17A8BBC5,0x31373831,0x453A302B,
0x4A4E4F4C,0x832D3B44,0xC4C2BAAE,0x0CA8BAC2,0xA9901A21,0xC2C0BCB5,0xB9C1C2C2,0x343422A6,
0xA7A20128,0x3D2F07A2,0x41444543,0x8A2C373E,0xB5BBBBB2,0xBBB7B1B0,0xB1B7BBBC,0x413A2B95,
0x45454442,0x2A353C42,0xB4B5AC02,0xABA8A7AE,0xA9B0B0AF,0x0109859C,0xBCB6AB99,0xC3C3C2C0,
0xB9BABDC1,0xA2B1B7B9,0x211E1B04,0x2C2D2E28,0x31373530,0xB6AA951E,0xC1C3C2BF,0xA9B6BCC0,
0x43403417,0x3C414445,0xAC9B1F32,0xBEB6B1B0,0xC4C2C2C1,0xBBBFC2C4,0x9BB0B7BA,0x3F39311D,
0x49464240,0x504E4C4A,0x4B4E5051,0x30384146,0xBDB3A01C,0xCFCDC8C3,0xC7CACDCF,0xB4BBC1C4,
0x8394A1AB,0x840B110B,0x1F009291,0x3A39352E,0x3B3B3B3B,0x4042413E,0x93162C37,0xACA8A5A1,
0xA9AEB0B0,0x0192A0A5,0xA2A39B86,0xA7A29C9D,0xB9B7B3AD,0xBEBDBAB9,0xB9BBBCBD,0x2290AAB5,
0x44423C32,0x42434444,0x39404243,0x1A242B32,0x0F0C0B10,0xA49A8709,0xAFAEACA9,0xADA9AAAD,
0xBBB6B3B1,0xBCC0C1C0,0x87A1B0B7,0x24221910,0x2B262323,0x2E30302F,0x06162128,0x929A988F,
0x27241905,0x97011A25,0xA5A3A2A0,0xA5A7A9A7,0x989B9FA2,0x9E9F9D99,0x9997989B,0xA1A3A39F,
0x0983929B,0x8C87020A,0x0A05838A,0x0C080809,0x04050A0C,0x10110E07,0x09060309,0x91850208,
0x9E9E9D99,0xB0AFA9A3,0xAAADAFB0,0x128198A4,0x22211D18,0x1F212223,0x010C141A,0x96908C86,
0xA5A4A29E,0x9DA3A6A5,0x8F949597,0x0F060185,0x070B1212,0x93880407,0x00829095,0x82868783,
0x8A888482,0x9693908B,0x90909396,0x0D06868E,0x1E171110,0x23222221,0x21232423,0x0A151A1E,
0x95959389,0x97979795,0x95979796,0x878C9092,0x82838283,0x83000100,0x928E8885,0x98989896,
0x92959898,0x8A8C8B8C,0x04030085,0x09040202,0x1110100D,0x02060D10,0x05050402,0x81020405,
0x8F8C8884,0x91908E8F,0x8F909191,0x87898B8D,0x03018184,0x090A0907,0x06070708,0x02040405,
0x01020203,0x00000001,0x00000080,0x80000000,0x00000000,0x00000000,0x00000000,0x00000000,
0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,
0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,
0x00000000,
};
/*
Setting up infrastructure for capturing taps (from a connected FSR)
*/
boolean prev_active = false; // Whether we were active on the previous loop iteration
int metronome_interval = 600; // Time between metronome clicks
unsigned long next_metronome_t = 0; // the time at which we should play the next metronome beat
unsigned long current_t = 0; // the current time (in ms)
unsigned long prev_t = 0; // the time stamp at the previous iteration (used to ensure correct loop time)
unsigned long missed_frames = 0;
/*
Setting up the audio
*/
float sound_volume = .5; // the volume
// Create the Audio components.
// We create two sound memories so that we can play two sounds simultaneously
AudioPlayMemory sound0;
AudioPlayMemory sound1;
AudioMixer4 mix1; // one four-channel mixer (we'll only use two channels)
AudioOutputI2S headphones;
// Create Audio connections between the components
AudioConnection c1(sound0, 0, mix1, 0);
AudioConnection c2(sound1, 0, mix1, 1);
AudioConnection c3(mix1, 0, headphones, 0);
AudioConnection c4(mix1, 0, headphones, 1); // We connect mix1 to headphones twice so that the sound goes to both ears
// Create an object to control the audio shield.
AudioControlSGTL5000 audioShield;
void setup(void) {
/* This function will be executed once when we power up the Teensy */
Serial.begin(9600); // Initiate serial communication
Serial.print("TeensyTap starting...\n");
// Audio connections require memory to work. For more
// detailed information, see the MemoryAndCpuUsage example
AudioMemory(10);
// turn on the output
audioShield.enable();
audioShield.volume(sound_volume);
// reduce the gain on mixer channels, so more than 1
// sound can play simultaneously without clipping
mix1.gain(0, 0.5);
mix1.gain(1, 0.5);
Serial.print("TeensyTap ready.\n");
}
void do_activity() {
/* This is the usual activity loop */
/* If this is our first loop ever, initialise the time points at which we should start taking action */
if (prev_t == 0) { prev_t = current_t; } // To prevent seeming "lost frames"
if (next_metronome_t == 0) { next_metronome_t = current_t+metronome_interval; }
if (current_t > prev_t) {
// Main loop tick (one ms has passed)
if ((prev_active) && (current_t-prev_t > 1)) {
// We missed a frame (or more)
missed_frames += (current_t-prev_t);
}
/*
* Deal with the metronome
*/
// Is this a time to play a metronome click?
if (current_t >= next_metronome_t) {
// Play metronome click
sound1.play(AudioSampleMetronome);
// And schedule the next upcoming metronome click
next_metronome_t += metronome_interval;
}
// Update the "previous" state of variables
prev_t = current_t;
}
}
void loop(void) {
/* This is the main loop function which will be executed ad infinitum */
current_t = millis(); // get current time (in ms)
do_activity();
}
I am loving the Teensy and the audio library. I have an issue with the timing accuracy though. Let me first describe the issue and then give you details like source code.
I want to play a sound at a regular interval, say 600 ms. It's a very short sound. The issue is that the intervals are not exactly 600 ms. A curious phenomenon appears where the interval is sometimes slightly too long and then slightly too short. Specifically, the interval is sometimes 600.6 ms, and other times 597.7 ms. The average interval is pretty much spot on at 600.01 ms. Here you see a histogram (yes, actually a histogram!) of the actually produced intervals:
So there appears to be a magic 2.90 ms "quantisation" of the intervals.
So my question is: am I doing something wrong? Is this behaviour somehow expected from the audio library? Is there something that can be done to fix this? What I want is to be able to play at pretty much exactly 600 ms intervals.
Any help greatly appreciated! And thanks for all the developers of Teensy & the Audio library, it's really fantastic.
Implementation details
See below for the Teensy source code. I connected the audio jack of the Teensy Audio board to my PC sound card and recorded the Teensy output as a wave file (about 25 minutes of recording of the Teensy generating the sounds). Then I took the original WAV file that Teensy is supposed to play (included in the source code here below), and I used a Python script to determine the exact onsets of that wave file in the Teensy audio output (using a cross correlation technique). I can give details of the latter procedure as well, but I am very confident nothing is wrong there because a friend had exactly the same analysis using a completely different recording, hardware setup, and onset detection technique.
Also, the above issue does not just hold for 600 ms intervals. When trying to produce other interval durations, I get the same effect.
The actually produced intervals:
overall intervals:
mean 600.010253 SD 1.157552 ms
The two bars in the plot above correspond to:
mean 597.684822 SD 0.006784 ms
mean 600.586433 SD 0.007828 ms
As you can see, each bar is actually reproduced remarkably precise (with only several microseconds jitter)! But why are there these two modes? And is there a way to Teensy output a single mode, centered at exactly 600 ms?
In case this is useful, here is the actual recording from the Teensy: http://132.206.106.46:4404/simplified_metronome_25min_07April2018.wav
This is the source code for the Teensy (with audio board):
/*
* Play a metronome sound
*
*/
#include <Audio.h>
// Converted from metronome.wav, using 44100 Hz, u-law encoding
const unsigned int AudioSampleMetronome[353] = {
0x0100052C,0x04010101,0x06830004,0x3C322719,0x5F554D44,0x57656765,0xF4F0E4C0,0xE7EEF2F4,
0xDDE0E0E2,0x42AFCDD7,0x77726759,0x6D737779,0xF0E48A61,0x6150D6ED,0xCA505D61,0xF8F7F4E9,
0xF1F4F6F7,0x6750D8EB,0x7875716E,0x76757678,0x6C737576,0xF6EED259,0xF5F5F7F8,0xF5F5F6F6,
0xDDECF3F5,0x60615706,0x746D625D,0x75757777,0x73757676,0x08596770,0xF4F0E8D9,0xE3EDF3F5,
0xA99FC3D6,0xEEE9E1D0,0xD8E5EBEE,0x756E5E80,0x6E747777,0xC2145364,0xE7D5C3C3,0xF7F7F6F2,
0xF4F4F5F6,0x54CEE9F2,0x72727068,0x74757271,0xC2526570,0x30D1E0DE,0xC13C4D4D,0xEBEAE7DD,
0xDBE6EAEB,0x66604CBD,0x964A5E66,0x685A34B5,0x676F7170,0xC0B33E5B,0xDFD7CEC4,0xDCE1E2E2,
0xCCCCCFD4,0x534594C5,0x565B5B58,0x4441424D,0x50423E42,0xA1515A58,0xE7E8E4D7,0xD0D5DDE3,
0xA7BAC5CB,0xC3AC8D8F,0x40C3D2D0,0x5D616159,0x494B5056,0x4C4D4C4A,0x2B343E46,0xD8D0C094,
0xB4D1D9DB,0x1839443A,0x3C311C8B,0xB8344443,0xDADBD9D1,0xE6E2DCD9,0xD3E1E6E8,0x62605290,
0x66626161,0x6A6A6A69,0x5A646869,0x18072D48,0xBC12312C,0xDEDBD7D0,0xDADDE0E0,0xD1D2D6D8,
0xEBE6DED4,0xD9E3E9EC,0x625D49C0,0x414D5760,0x48312031,0x65646157,0x51596063,0xCBC5A540,
0x31B3C3C9,0x1C4E544D,0xDBD9D6CB,0xE1E2E1DF,0xCBD6DCE0,0x393F36AA,0x4A454037,0x64615951,
0x434F5A62,0x49494642,0x4D474546,0x47515452,0xB78D2237,0xE6E2DACD,0xE7EAEAE9,0xDFDBDEE3,
0xE1E2E3E2,0xC0D3DCE0,0x5B564B2E,0x4E4E545A,0x51535351,0x544F4D4F,0x5E61605B,0x41485057,
0xD4CCB629,0xE0E1DFDA,0xD4D5D7DB,0xAAB7C5D0,0xA70A83A0,0x33BAC5BF,0x5D5D5A51,0x434C5459,
0x44464442,0x4B494442,0x2F3D4449,0xB4B1A803,0xCEC9C2B9,0xC5C9CDD0,0x8891AEBD,0xB9B6B2A4,
0xD0CFC8C0,0x2592BCCA,0xA9AD9B1E,0x42403305,0x463D393F,0x983F4B4C,0xC7CCCBC2,0xCDC8C2C2,
0xCACBCCCE,0x3024B0C4,0x1EAEAC12,0x2D414640,0x41351E06,0x51525048,0x504C494C,0x41454A50,
0xB2A32338,0xC2C1BCB7,0x3620ADBE,0xB419363C,0xD1D0CDC5,0xC9D0D2D2,0x311CA8BF,0x17263336,
0x514B3F28,0x40484F52,0xB9A11D33,0xD5D5D1C7,0xC0C5CDD2,0xC4C4C1BE,0x238AB0BE,0x3F39342F,
0x9E313F41,0xABB9BEB8,0x4C463A1A,0x4C4A4B4D,0x494D4E4D,0x4A464445,0x2A414A4D,0x2702A29A,
0x1B2A2F2F,0xBBB7B19B,0xD2CEC7C1,0xD7D8D7D5,0xCCD0D3D5,0x17A8BBC5,0x31373831,0x453A302B,
0x4A4E4F4C,0x832D3B44,0xC4C2BAAE,0x0CA8BAC2,0xA9901A21,0xC2C0BCB5,0xB9C1C2C2,0x343422A6,
0xA7A20128,0x3D2F07A2,0x41444543,0x8A2C373E,0xB5BBBBB2,0xBBB7B1B0,0xB1B7BBBC,0x413A2B95,
0x45454442,0x2A353C42,0xB4B5AC02,0xABA8A7AE,0xA9B0B0AF,0x0109859C,0xBCB6AB99,0xC3C3C2C0,
0xB9BABDC1,0xA2B1B7B9,0x211E1B04,0x2C2D2E28,0x31373530,0xB6AA951E,0xC1C3C2BF,0xA9B6BCC0,
0x43403417,0x3C414445,0xAC9B1F32,0xBEB6B1B0,0xC4C2C2C1,0xBBBFC2C4,0x9BB0B7BA,0x3F39311D,
0x49464240,0x504E4C4A,0x4B4E5051,0x30384146,0xBDB3A01C,0xCFCDC8C3,0xC7CACDCF,0xB4BBC1C4,
0x8394A1AB,0x840B110B,0x1F009291,0x3A39352E,0x3B3B3B3B,0x4042413E,0x93162C37,0xACA8A5A1,
0xA9AEB0B0,0x0192A0A5,0xA2A39B86,0xA7A29C9D,0xB9B7B3AD,0xBEBDBAB9,0xB9BBBCBD,0x2290AAB5,
0x44423C32,0x42434444,0x39404243,0x1A242B32,0x0F0C0B10,0xA49A8709,0xAFAEACA9,0xADA9AAAD,
0xBBB6B3B1,0xBCC0C1C0,0x87A1B0B7,0x24221910,0x2B262323,0x2E30302F,0x06162128,0x929A988F,
0x27241905,0x97011A25,0xA5A3A2A0,0xA5A7A9A7,0x989B9FA2,0x9E9F9D99,0x9997989B,0xA1A3A39F,
0x0983929B,0x8C87020A,0x0A05838A,0x0C080809,0x04050A0C,0x10110E07,0x09060309,0x91850208,
0x9E9E9D99,0xB0AFA9A3,0xAAADAFB0,0x128198A4,0x22211D18,0x1F212223,0x010C141A,0x96908C86,
0xA5A4A29E,0x9DA3A6A5,0x8F949597,0x0F060185,0x070B1212,0x93880407,0x00829095,0x82868783,
0x8A888482,0x9693908B,0x90909396,0x0D06868E,0x1E171110,0x23222221,0x21232423,0x0A151A1E,
0x95959389,0x97979795,0x95979796,0x878C9092,0x82838283,0x83000100,0x928E8885,0x98989896,
0x92959898,0x8A8C8B8C,0x04030085,0x09040202,0x1110100D,0x02060D10,0x05050402,0x81020405,
0x8F8C8884,0x91908E8F,0x8F909191,0x87898B8D,0x03018184,0x090A0907,0x06070708,0x02040405,
0x01020203,0x00000001,0x00000080,0x80000000,0x00000000,0x00000000,0x00000000,0x00000000,
0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,
0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,0x00000000,
0x00000000,
};
/*
Setting up infrastructure for capturing taps (from a connected FSR)
*/
boolean prev_active = false; // Whether we were active on the previous loop iteration
int metronome_interval = 600; // Time between metronome clicks
unsigned long next_metronome_t = 0; // the time at which we should play the next metronome beat
unsigned long current_t = 0; // the current time (in ms)
unsigned long prev_t = 0; // the time stamp at the previous iteration (used to ensure correct loop time)
unsigned long missed_frames = 0;
/*
Setting up the audio
*/
float sound_volume = .5; // the volume
// Create the Audio components.
// We create two sound memories so that we can play two sounds simultaneously
AudioPlayMemory sound0;
AudioPlayMemory sound1;
AudioMixer4 mix1; // one four-channel mixer (we'll only use two channels)
AudioOutputI2S headphones;
// Create Audio connections between the components
AudioConnection c1(sound0, 0, mix1, 0);
AudioConnection c2(sound1, 0, mix1, 1);
AudioConnection c3(mix1, 0, headphones, 0);
AudioConnection c4(mix1, 0, headphones, 1); // We connect mix1 to headphones twice so that the sound goes to both ears
// Create an object to control the audio shield.
AudioControlSGTL5000 audioShield;
void setup(void) {
/* This function will be executed once when we power up the Teensy */
Serial.begin(9600); // Initiate serial communication
Serial.print("TeensyTap starting...\n");
// Audio connections require memory to work. For more
// detailed information, see the MemoryAndCpuUsage example
AudioMemory(10);
// turn on the output
audioShield.enable();
audioShield.volume(sound_volume);
// reduce the gain on mixer channels, so more than 1
// sound can play simultaneously without clipping
mix1.gain(0, 0.5);
mix1.gain(1, 0.5);
Serial.print("TeensyTap ready.\n");
}
void do_activity() {
/* This is the usual activity loop */
/* If this is our first loop ever, initialise the time points at which we should start taking action */
if (prev_t == 0) { prev_t = current_t; } // To prevent seeming "lost frames"
if (next_metronome_t == 0) { next_metronome_t = current_t+metronome_interval; }
if (current_t > prev_t) {
// Main loop tick (one ms has passed)
if ((prev_active) && (current_t-prev_t > 1)) {
// We missed a frame (or more)
missed_frames += (current_t-prev_t);
}
/*
* Deal with the metronome
*/
// Is this a time to play a metronome click?
if (current_t >= next_metronome_t) {
// Play metronome click
sound1.play(AudioSampleMetronome);
// And schedule the next upcoming metronome click
next_metronome_t += metronome_interval;
}
// Update the "previous" state of variables
prev_t = current_t;
}
}
void loop(void) {
/* This is the main loop function which will be executed ad infinitum */
current_t = millis(); // get current time (in ms)
do_activity();
}