Curious variable beep durations using OutputUSB and PlayQueue

lutzray

Member
Bonjour à tous,

I'm using a T4 as an audio USB device plugged into a Mac. Planning to send audio BFSK data to a host (eventually, cellphones), I explored the awesome™ Teensy Audio library and played with AudioPlayQueue.

Curiously, the length of the produced beeps recorded on the mac doesn't match the length coded into the T4 sketch... Am I missing something? I know some handshake is done between host and devices, but I thought isochronous transfers where stable once negotiated (although without any error correction).

Here's the test sketch, it should produce 5011 samples of a 440 Hz tone, repeatedly:
C++:
#include <Audio.h>

AudioPlayQueue queue1;
AudioOutputUSB usb1;
AudioConnection patchCord1(queue1, 0, usb1, 0);
AudioConnection patchCord2(queue1, 0, usb1, 1);

void setup() {
  AudioMemory(10);
  queue1.setBehaviour(AudioPlayQueue::ORIGINAL);  // blocking when full
  queue1.setMaxBuffers(4);
}

void queueBeep() {
  float phas = 0.f;
  for (int i = 0; i < 5011; i++) { // should be exactly 50 whole cycles at 440 Hz
    int16_t sample = (int16_t)(sin(phas) * 8000.);
    phas += 440. / AUDIO_SAMPLE_RATE_EXACT * TWO_PI;
    if (phas > TWO_PI)
      phas -= TWO_PI;
    queue1.play(sample);
  }
}

void loop() {
  queueBeep();
  delay(200);
}

And for a 100 seconds interval I recorded the T4 USB audio stream on macOS 12.7.6 and measured all the 329 beeps duration and no beep had the desired length of 5011:

Code:
[4992 4992 5120 4992 4991 4991 4992 4992 4991 5120 4992 4992 4992 4992
 4992 5120 4991 4992 4992 4992 4992 4992 5119 4992 4992 4992 4992 4992
 4992 5120 4992 4992 4992 4991 4992 4991 5120 4992 4992 4992 4991 4992
 5120 4992 4992 4992 4992 4992 4991 5120 4992 4991 4992 4992 4992 4992
 5120 4992 4990 4992 4992 4992 4992 5120 4992 4992 4992 4992 4991 5119
 4992 4991 4992 4992 4992 4992 5120 4992 4992 4992 4991 4991 4992 5120
 4991 4992 4992 4991 4992 4992 5120 4992 4992 4992 4990 4992 5120 4992
 4992 4992 4992 4992 4992 5120 4992 4991 4992 4992 4992 4992 5120 4992
 4992 4992 4992 4992 4991 5120 4992 4992 4992 4991 4992 5120 4992 4992
 4992 4991 4992 4992 5120 4992 4992 4992 4990 4992 4992 5119 4992 4992
 4992 4991 4992 5120 4990 4992 4992 4991 4992 4992 5120 4992 4991 4992
 4992 4992 4992 5119 4992 4992 4991 4992 4992 4991 5120 4992 4992 4991
 4992 4992 5118 4992 4992 4992 4992 4992 4992 5120 4992 4992 4991 4991
 4992 4991 5120 4992 4992 4992 4991 4992 4991 5120 4992 4991 4992 4992
 4992 5120 4992 4991 4992 4992 4992 4992 5120 4992 4990 4992 4992 4992
 4992 5120 4992 4991 4992 4992 4991 4991 5120 4992 4992 4992 4992 4992
 5120 4992 4992 4992 4991 4991 4992 5120 4992 4992 4992 4991 4992 4992
 5120 4992 4992 4992 4992 4992 4992 5120 4992 4992 4992 4992 4992 5120
 4992 4991 4992 4992 4991 4992 5120 4992 4992 4992 4992 4992 4991 5120
 4992 4992 4992 4991 4992 5120 4991 4992 4992 4991 4992 4992 5120 4991
 4992 4992 4990 4992 4992 5120 4992 4992 4992 4991 4992 4992 5119 4992
 4992 4992 4992 4992 5120 4992 4991 4992 4991 4992 4992 5119 4992 4992
 4992 4992 4992 4991 5120 4992 4992 4991 4992 4992 4992 5120 4992 4992
 4992 4992 4992 5120 4992 4992 4991], mean duration: 5010.8 samples

But (funnily ?) the mean is exactly 5011 samples... I confirmed my automated length measurements by checking some beep lengths manually in Audacity. The discrepancy between the 5011 desired duration and those observed is not that serious (20 samples too short and 109 too long) but it produces superfluous discontinuities in the signal... I would like to start and finish beeps on a null value. Any suggestion?

The recording and length values computation is done with this python script:
Python:
import sounddevice as sd, numpy as np
from skimage.morphology import closing
from skimage.measure import regionprops_table, label
# using image analysis tools for 1-D audio? Yes.
fs = 44100
duration = 100 # seconds
# defaut audio input is Teensy USB Audio
audioData = sd.rec(int(duration * fs), samplerate=fs, channels=1, dtype='int16')
sd.wait()  # block until recording is finished
oneBitAmplitude = np.abs(audioData)>1 # from sinus to 1 bit square wave
oneBitAmplitudeClosed = closing(oneBitAmplitude.T[0], np.ones(5)) # removing gaps
synthImage = np.array([oneBitAmplitudeClosed,oneBitAmplitudeClosed]) # going 2-D for using regionprops_table ()
props = regionprops_table(label(synthImage), properties=('centroid', 'bbox'))
# use found bounding boxes coords to compute widths (ie lengths). Remove ends, frequently truncated
lengths = (props['bbox-3'] - props['bbox-1'])[1:-1]
print('%s, mean duration: %.1f samples'%(lengths, lengths.mean()))
 
Last edited:
5011 samples doesn’t fill an exact number of audio blocks - it’s a bit over 39. So queueBeep() will leave a part-filled block awaiting completion, transmitting only the completed ones. The next call will do likewise, so you’ll transmit either 39 or 40 blocks each time.

Your Python is slightly mis-counting, but it’s very close, and in any case the USB transmission isn’t entirely perfect, I believe, so there may genuinely be the odd missing or extra sample.

You should pad the last block with silence until you have sent exactly 40 blocks each time. Note also that you will not observe a consistent 200ms gap between beeps, because the audio update interval is every 128 samples, or about 2.9ms, and not synchronised to the millisecond timing.
 
Thanks for your reflection about the audio_block_struct.data[ ] size.
Your Python is slightly mis-counting, but it’s very close,
No, my script is spot on (give or take one sample), I verified, zooming in to the single sample:
spoton.gif


And what mystifies me is the presence of those extra samples in the longer 5120 anomalous beeps:
extra.png

Who computed them? Where do they come from? :) The selected zone is exactly the 5011 samples I want.
 
No, my script is spot on (give or take one sample)
That’s what I meant by “slightly” :) I think the issue is:
oneBitAmplitude = np.abs(audioData)>1
as the count will miss samples with the values 0 and 1. In your post there’s an instance where it’s out by 2 samples, in fact… as it happens, that’s not very relevant to diagnosing the main issue, but it’s good practice to understand all anomalies as sometimes it’s those subtle ones that contain the vital clue.

And what mystifies me is the presence of those extra samples in the longer 5120 anomalous beeps:
extra.png

Who computed them? Where do they come from? :) The selected zone is exactly the 5011 samples I want.
Your code computed them … nothing else could be doing it for you! But the audio system didn’t emit them when you expected it to.

The first call to queueBeep() generates 5011 samples, filling and transmitting 39 audio blocks (4992 samples) and leaving one block with 19 samples in. It’s not full, so it’s not transmitted. The next call fills and transmits that block, and 38 more, leaving a pending block with 38 samples in it. After 6 calls the pending block has 6*19=114 samples, so the seventh call transmits that, another 39 blocks, for a total of 40*128=5120 samples, and leaves a pending block with 5 samples in.

To get closer to what you want, you should always generate 5120 samples, made up of 5011 of your beep waveform and 109 of silence to get to exactly 40 audio blocks. But as I noted previously, you will then find they aren’t emitted at exactly regular intervals - that’s a different thing, though.

I slightly lied about the audio system not making up data: if you starve it of real audio data (as you do, for 200ms at a time), it sort of fills the gaps with silence. This can get a bit messy internally, and has been known to give unexpected results, so is something to watch out for.
 
Back
Top