Audio Library; Timing of Commands

Status
Not open for further replies.
I would like to make brief pulses of white noise. I can do this by directly writing random numbers to the DAC using a timer. This approach works fine but it is limited in the range of volumes it can generate so I tried using the audio shield. The audio shield gives the required range of volumes that I need but introduced a new problem with the timing.

The code listing should put out a 10 ms pulse of white noise. I checked this on the scope and the duration of the pulse is quite variable from trial to trial, by relatively large amounts +/- 2ms. I am probably doing this wrong but not sure how to fix problem. Since the calls to the audio objects are made from within the timer's callback function it is not a problem of priority, as far as I can tell.


Code:
#include <Audio.h>
#include "IntervalTimer.h"

// objects
IntervalTimer           timer1;
AudioSynthNoiseWhite    whitenoise;         
AudioOutputI2S          i2s1;          
AudioConnection         patchCord1(whitenoise, 0, i2s1, 1);
AudioConnection         patchCord2(whitenoise, 0, i2s1, 0);
AudioControlSGTL5000    sgtl5000;

// constants
const int period1 = 50; // us    20kHz
const int baudrate = 115200;

// trial parameters
const unsigned long pulse2Start = 200000;  // 200 ms, start of second pulse is fixed
float Background_Vol = 0.0001;   // background volume, Scale 0.0001 - 1.0
float Amp_Vol = 0.8;    // 0.8 corresponds to  the maximum undistorted output for full scale signal, higher values noticeably increase background noise
float Volume1 = 0.5;        // volume of first pulse
unsigned long Duration1 = 10000;  // duration of first pulse
unsigned long start = 0;  // time at start of trial
bool Background_on = true;

void setup() {
  // digital pin
  pinMode(ledPin, OUTPUT);

  // Audio board setup
  AudioMemory(10);
  whitenoise.amplitude(Background_Vol);
  sgtl5000.enable();
  sgtl5000.volume(Amp_Vol);
  
  timer1.begin(timer1_callback, period1);
  Serial.begin(baudrate);
}

void loop() {
  while ( Serial.available() > 0 ) {
    String inString = Serial.readStringUntil( '\n' );
        
    // parse the string for a command, use 'startsWith' to ignore newline character
    if ( inString.startsWith( "ASR" ) ) {
      run_Trial();
    } else {
      // ignore unknown commands
    }
  }
} // main loop

void run_Trial (void) {
  start = micros();  // determines the start time of the trace, needed to time white noise output
  Serial.flush();
} // run_Trial

void timer1_callback(void) {
  unsigned long timeNow = micros() - start; //time at start of function
  
  // timing of pulse
  if ( (timeNow >= pulse2Start)  &&  (timeNow < (pulse2Start + Duration1)) ){
    if (Background_on == true) {
      whitenoise.amplitude(Volume1);
      Background_on = false;
    }
  }
  else if (Background_on == false) {
    whitenoise.amplitude(Background_Vol);
    Background_on = true;
  }
}  // timer1_callback
 
The audio library updates on a 128 sample (approx 2.9 ms) schedule. From the Arduino sketch, you can generally only get 128 sample timing precision.

To get sample-level control, you'll need to customize the library. It's not very difficult. Here's a page about the basics:

http://www.pjrc.com/teensy/td_libs_AudioNewObjects.html

Here's the white noise synthesis code.

https://github.com/PaulStoffregen/Audio/blob/master/synth_whitenoise.cpp

Each iteration of the loop generates 4 samples. You could pretty easily delete half, so it generates 2 samples per loop. Since it packs the samples into 32 bit numbers, editing for 1 sample would be harder than just deleting lines. Hopefully 2 sample precision is good enough?

Then you'll need to add code to create less than 128 samples under some circumstances. You'll want to pad the unused part of the block with zeros.
 
Hopefully 2 sample precision is good enough?

Yes I think two sample precision would be fine for this purpose. I don't understand how to get there. Specifically where do you change to 2 rather than 128 samples? Also, to simplify things, I think the C rand() function would be good enough to generate the white noise.
 
Oh, I should have mentioned, you can also get precise 10ms pulse width by using the envelope objects. But you can't control when the envelope begins, so you might still need to dig into the library code if you need the timing between pulses to also be sample level accurate.


I don't understand how to get there. Specifically where do you change to 2 rather than 128 samples?

First, you have to understand the code wasn't designed for finer than 128 sample timing, so you're going to be redesigning stuff. This isn't a quick & simple hack.

Hopefully you can see how that loop put more samples into the buffer on each iteration, until it's full. Then it stores the seed into the object's state, transmits the 128 samples to the rest of the audio system and releases the buffer.

The only part that's a little tricky is this:

Code:
	val1 = pack_16b_16b(n2, n1);

The variables n1 and n2 are a pair of random 16 bit numbers. It packs them into a 32 bit number, and then writes both at once. It actually generates 4 numbers and writes both 32 bit numbers together. That's a minor speed optimization, because the chip has a 32 bit bus and because it pipelines successive bus operations.

So, the part you need to change is to not fill all 128 samples in some circumstances. You'll fill part of the buffer with random numbers, and part with zeros.

How you do that is up to you! But you'll almost certainly have to add some extra state variables to the object. Then in the update() function, you'd look at the values in those variables and decide if you want to transmit a full 128 samples, or partial buffer. Of course you'll modify your state as you go.

Maybe if you want a 10 ms burst every 1 second, perhaps on every update you'd increment a sample counter. When it gets to 44100 (or better yet, the SAMPLE_RATE constant), you'd start putting random numbers into the buffer. Actually, when it to within 128 of that threshold, perhaps you start putting zeros into the buffer, and put randoms in after it crosses the threshold. This is all pretty straightforward programming stuff.... maintain state, check it, do whatever is appropriate based on the state, and update the state so the next time you continue doing the task for the next 128 samples.

The main thing you need to understand is this code isn't going to write itself. The AudioSynthNoiseWhite object is designed to transmit all 128 samples, or nothing. If you want finer timing control than turning on/off 128 sample chunks, you have to modify the code to transmit the samples when you want them. Most of that work is going to be adding more state to the object, so you know when you want a partial block, and how much of it needs to be zeros, and whether you want the beginning or later part of the block to be silent.

Also, to simplify things, I think the C rand() function would be good enough to generate the white noise.

Sure, you could use the C library. If you do, I hope you'll do a quick test to query the CPU usage of your C lib approach and the optimized code that's in the library now. The audio library automatically collects the CPU usage info, so your new object will have it too. I'd be curious to hear how using rand() compares with the optimized code?
 
Last edited:
So thanks to your help I was able to solve this problem, albeit not particularly elegantly. I set AUDIO_BLOCK_SAMPLES = 2 in AudioStream.h and modified AudioSynthNoiseWhite to pack a single 32 bit number. This seems to work OK. Since I am only using the library for this function these changes don't seem to disrupt anything else that I am doing. Because I am sending the data to two different channels I think this is now single sample resolution, although I am a bit vague on this.

I tried to test the timing resolution with a constant output (rather than random noise). This could only be done down to about 100us because there is some kind of filter on the headphone output but it seems to work reliably down to 100us, which is perfectly adequate this purpose.

It would be good to find a slightly more elegant solution. I guess I could define my own versions of AudioStream and AudioSynthNoiseWhite and use them as the basis for the upstream code.
 
Status
Not open for further replies.
Back
Top