Ranging using Teensy 4 + mic and speaker, timing variance issue

Status
Not open for further replies.

ahinduja

Member
Hi all,

The project I am trying out is to do acoustic ranging using a Teensy 4 + speaker + I2S microphone. The issue I need help with is the large variance in the time elapsed between the sound being sent and heard.

Code:
/*
  SD -> pin 8 (in1)/pin 5 (in2)
  L/R -> GND/VCC (left/right channel)
  WS -> pin 20 (LRCLK1)/pin 3 (LRCLK2)
  SCK -> pin21 (BCLK1)/pin 4 (BCLK2)
*/

/*
   Sample rate is 44.1khz
   Therefore max frequency is 22.05khz
   For FFT1024 we have each bin = 22.05/512 = 43.06 hz
   For FFT256 we have each bin = 22.05/128 = 172.26 hz
   for 8khz bin 45-49 for FFT256
   For 8khz 184-189 for FFT1024
*/

#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>
#include <TimeLib.h>
#include "TeensyThreads.h"

// GUItool: begin automatically generated code
AudioSynthWaveformSine   sine1;
AudioAmplifier           amp1;
AudioOutputI2S2          i2s2;
AudioInputI2S            i2s1;           //xy=500,322
AudioAnalyzeFFT1024      fft1024_1;      //xy=701,316
AudioAnalyzeFFT256       fft256_1;
//AudioConnection          patchCord1(i2s1, 0, fft1024_1, 0);
AudioConnection          patchCord1(i2s1, 0, fft256_1, 0);
AudioConnection          patchCord2(sine1, 0, amp1, 0);
AudioConnection          patchCord3 (amp1, 0, i2s2, 0);

IntervalTimer pulse;

// GUItool: end automatically generated code

const float freq = 8000.0;
const float amp = 0.8;
const float g = 0.5;
elapsedMillis sincebeep;
elapsedMillis sincesilence;
int t1;
int t2;
bool first_ping = false;
elapsedMicros sincePing;
const double air_speed = 0.0343;



void setup() {
  //Serial.begin(9600);
  AudioMemory(70);
  while (!Serial) ; // wait until Arduino Serial Monitor opens
  setSyncProvider(getTeensy3Time);
  if (timeStatus() != timeSet)
    Serial.println("Unable to sync with the RTC");
  else
    Serial.println("RTC has set the system time");

  //while (!Serial && millis () < 3000);

  //  delay (6000);
  //AudioNoInterrupts();
  //AudioMemory (2);
  amp1.gain (0.8);
  sine1.amplitude(0.0);
  sine1.frequency(freq);
  //AudioInterrupts();
  Serial.println("setup done");
  //  Timer1.initialize(10000000);
  //  Timer3.initialize(11000000);
  //  Timer1.attachInterrupt(beepStart);
  //  Timer3.attachInterrupt(beepEnd);
  threads.addThread(beep);
  threads.addThread(FFT); 

  //  pulse.begin(beep, 500000); //call beep every 5 secs

}



void printDigits(int digits) {
  // utility function for digital clock display: prints preceding colon and leading 0
  Serial.print(":");
  if (digits < 10)
    Serial.print('0');
  Serial.print(digits);
}

void digitalClockDisplay() {
  // digital clock display of the time
  Serial.print(hour());
  printDigits(minute());
  printDigits(second());
  Serial.print(" ");
  Serial.print(day());
  Serial.print(" ");
  Serial.print(month());
  Serial.print(" ");
  Serial.print(year());
  Serial.println();
}


time_t getTeensy3Time()
{
  return Teensy3Clock.get();
}


//void beepStart()
//{
//  sine1.amplitude(0.8);
//  sincePing = 0;
//}
//
//void beepEnd()
//{
//  sine1.amplitude(0.0);
//}
void beep()
{

  while (1) {

    if (sincebeep >= 50)
    {
      sine1.amplitude(0.0);
      sincebeep = sincebeep - 50;

    }
    if (sincesilence >= 10000)
    {
      sine1.amplitude(0.8);
      sincePing = 0;
      sincesilence = sincesilence - 10000;
      first_ping = false;

    }
    threads.yield();
  }
}

void FFT()
{ while (1) {
    t1 = micros();

    if (fft256_1.available()) {
      //  Serial.println(fft256_1.read(45,49)*100);

      if (fft256_1.read(45, 49) * 100 > 0.4 && first_ping == false)
      {
            digitalClockDisplay();
            Serial.print("heard ping, delay of :");
        //    long long int t3 = t2-t1;
        noInterrupts();
        Serial.println(sincePing);
        interrupts();
        t2 = micros();
        //     Serial.println(t2-t1);
        double time_in_usec = sincePing;
        double dist = air_speed * (time_in_usec);
        //    Serial.print("distance :");
        //    Serial.println(dist);
        first_ping = true;
      }
    }
  }
}

//loop time check
void loop() {

}


I am using 2 threads, one to play the 8khz sine tone periodically and another running FFT to detect the pulse. When the amplitude of the speaker is raised, I set an "elapsedMicros sincePing" timer variable to 0, and when the other microphone hears it in the other thread I display that time. In theory this time can be used to calculate the distance of the speaker to the microphone by using the speed of sound in air. These are the results I am getting for now:-
(When a few cm away from the mic)
Code:
13:41:13 1 5 2020

heard ping, delay of :22005

13:41:23 1 5 2020

heard ping, delay of :22025

13:41:33 1 5 2020

heard ping, delay of :22005

13:41:43 1 5 2020

heard ping, delay of :10603

13:41:53 1 5 2020

heard ping, delay of :31624

13:42:03 1 5 2020

heard ping, delay of :28204

13:42:13 1 5 2020

heard ping, delay of :26005

13:42:23 1 5 2020

heard ping, delay of :23805

13:42:33 1 5 2020

heard ping, delay of :22005

13:42:43 1 5 2020

heard ping, delay of :22005

13:42:53 1 5 2020

heard ping, delay of :22005

(when about 30cm away from the mic)

Code:
13:49:46 1 5 2020

heard ping, delay of :44005

13:50:16 1 5 2020

heard ping, delay of :31257

13:50:26 1 5 2020

heard ping, delay of :27837

13:50:36 1 5 2020

heard ping, delay of :25637

13:50:45 1 5 2020

heard ping, delay of :22218

13:50:56 1 5 2020

heard ping, delay of :22005

13:51:06 1 5 2020

heard ping, delay of :22006

13:51:16 1 5 2020

heard ping, delay of :22005

13:51:26 1 5 2020

heard ping, delay of :22005

13:51:36 1 5 2020

heard ping, delay of :32001

13:51:46 1 5 2020

heard ping, delay of :29801


These numbers don't really make sense since they are inconsistent, and become more so once the speaker is moved further away.

I think I have a few things I can check to make sure there isn't something in the background which is a problem, like for example the frequency the FFT is being computed at being constant? Or maybe there is a better way of getting the elapsed time between the signals? Any inputs would be appreciated. Thanks!

-Akshay
 
I don't see how this can possibly work. The time between consecutive FFT256 will be 256/44100 = 5.8 milliseconds. Therefore, you can't resolve the time between sending a ping and "hearing" it any better than this, which corresponds to a distance of a bit less than two metres.
If you can get around that problem, you still have to contend with the amount of time it takes to detect an 8kHz tone. I presume that whichever technique you use, it will need at least one cycle of 8kHz to be able to determine that the tone is present. In one cycle of 8kHz, sound can travel about 47cm. You'd be unable to resolve any distance with an accuracy better than that. This is why ultrasound is used for motion sensors. The higher frequency permits better resolution.

Pete
 
Hi Pete, thanks for the information! I guess I am limited by hardware in this case. Either way the maximum frequency I could get is 20Khz if I switch to perhaps a PDM microphone instead of the I2S one I have now. That might help reduce cycle time a bit.
I don't have an electronics background, but based on some rudimentary knowledge I have, if I were to use band filters instead of the FFT to pick on a smaller frequency range (one which a speaker could produce), I might at least be able to overcome the time delay of consecutive FFTs. Do you think that is a more feasible solution?
The reason I am not looking into the readily available ultrasonic modules is that I want to deploy this system in water, and waterproofing the microphone breakout board with silicone has been possible, but won't be so for the ultrasonic range finders since its a far more complex shape.
-Akshay
 
There is a project of some users in this forum. They wrote and built a bat detector (->ultrasonic). I don't know much about it - as far as I know they use almost normal microphones and a higher samplerate. Maybe a good idea to ask them?
 
If this is going to be used in water, that changes the numbers. The speed of sound in distilled water is about 1481m/s and in seawater it's about 1531 m/s. That would improve the resolution quite a bit. The FFT256 would have a resolution of about 50cm and the tone detection of 8kHz would have a resolution of around 12cm. Increasing the tone to 20kHz would improve the tone detection resolution by a factor of 2.5.

I can't help with band filters, I'm not a hardware guy :)

Pete
 
I was intending to imply that the filters be done in software via the biquad class in the audio library. Definitely not hardware. It requires real skill and/or money to get a good, tight bandpass filter in hardware. In software, you can keep cascading filters to sharpen your response until you run out of cpu. Especially if you are not worrying about quantization build-up from fixed-point IIR filters.
 
Last edited:
How long is your tone pulse? Presuming it is more than one cycle long, that's going to set your resolution (besides the fft length) not your operating frequency. To get your operating frequency to set your resolution, you'll need to do 1-cycle pulses or you'll need to do cross-correlation.

Assuming that you are using tone pulses that are many cycles long, does it fit within one fft block?

If it is close to one fft block in duration, don't forget that your pulse might arrive at a time such that it ends up straddling two fft blocks. That'll degrade your sensitivity. It's not a horrible reduction, but you should be aware.

To mitigate, one can do overlapping fft blocks. Or, one can switch to time-domain filtering via biquads or FIR, as previously mentioned. Really, though, this is a modest problem compared to the other challenges of just getting it working at all.
 
I just looked at your code. It looks like your tone pulse is 50 milliseconds long?

If so, that's 2205 samples assuming that you're running at the standard Audio Library rate of ~44100 Hz. That's a lot longer than your ffts. So, if the rest of your system is running correctly, you'll detect your signal in several ffts in a row. You'll have to account for that in your logic.
 
Increasing your frequency to 20 kHz will only improve your resolution if you were to move beyond your FFT approach. Unless you're lucky enough to have crazy-high signal to noise ratio, your resolution is going to be limited by your pulse length (50 ms) or your FFT length (256 pts? If so, that'd be 5.8 msec). Either one is way longer than the limit due to your operating frequency (1/(8kHz) = 0.125 msec). So, you have the freedom to choose your operating frequency based on other concerns.

A really good way to pick your operating frequency is to choose the frequency where your speaker system is loudest. Or, choose the frequency where any background noise is quietest. Either one of these choices will give you a higher signal to noise ratio, which will make your life much easier.

For example, depending on how nice your equipment is, you might find that your speaker is way louder around 1 kHz compared to 8 kHz. Like, maybe 10 dB louder. If so, feel free to shift down there...your ranging resolution will still be limited by your pulse length or fft length but you will have gained an extra 10 dB of SNR for free.
 
Pete, Frank, Chipaudette, thanks for the valuable information and feedback!
My current setup is to develop a proof of concept in air, I only have a couple waterproof microphones but will have to wait till the university labs open back up again due to Covid-19 to do more tests in water for ranging, so I guess for the time being I'd have to be content with poor resolution with the current system (unless I could improve upon it somehow)
Right now I am using one of the cheaper 8ohm speakers for in air tests. Once we move to in water, we are planning on using a much more potent speaker like the one here http://www.lubell.com/UW30.html (although by this time I'd hope to have them on different teensy boards which are somehow time synced in a cheap and lightweight way). Most of these speakers have a response only till about 10Khz. Using an underwater transducer instead, which could do ultrasonic, would increase costs significantly and the project is mainly aiming to remain low cost.

Chipaudette, yes as you noticed my pulse is long enough to be detected by multiple successive FFTs and I just note the 1st one heard as my sample for timing it.
I will look into the biquad filters you mentioned! Although are they typically faster in processing than the FFT?

Frank, I did look into the bat detector, but I guess I'd need to go through it more as it was fairly complicated and I set it aside since moving to ultrasonic is something I'd like to avoid due to hardware complexity and costs.
 
If this is going to be used in water, that changes the numbers. The speed of sound in distilled water is about 1481m/s and in seawater it's about 1531 m/s. That would improve the resolution quite a bit. The FFT256 would have a resolution of about 50cm and the tone detection of 8kHz would have a resolution of around 12cm. Increasing the tone to 20kHz would improve the tone detection resolution by a factor of 2.5.

I can't help with band filters, I'm not a hardware guy :)

Pete

Hey Pete, just a quick question, how did you come up with the FFT256 resolution of 50cm in water? I thought the time interval is only dependent on the sampling rate and FFT bin count? So with the same 5.8ms and sound would travel approx 7.3m in water right?
 
If the FFT isn't going to work for you, then you can cascade a few IIR (biquad) filters.

Cascade filters.png

You'll need to configure each biquad filter by issuing commands in your setup() function. You'll want to use "biquadXX.setBandpass(stage, frequency, Q);" If you're really doing separate biquads as I've shown in the figure, set stage to zero, set frequency to your tone's frequency (8000 Hz?) and set Q at least to 1.0 and you could try up to 3.0. The lower value will give a filter that is less focused on 8 kHz, but will avoid any numerical stability problems that might pop up when cascading 3 high-Q filters in series. I'd start with 1.0 and then, once you get the whole rest of the system working, you can try higher Q to get tighter filters.

After filtering, you see that your filtered waveforms will end up in those queue objects. You'll need to write code that you put in your loop() function that will: (1) look to see if data is available in each queue, (2) scan through the processed audio to find see if the echo has arrived. To see how to work with the queue objects, look at the example code in the Audio Library. They'll show you how to see if data is available and they'll show you how to access the data when it is available.

For step (2), where you want to detect the presence of your echo, the simplest (though not most sensitive) method is simply to loop through the audio block looking for the sample index with the max(abs(value)). You'll need to pick some threshold value where, if you detect a sample that is larger than the threshold, you know that an echo is present. That's your detector! Be sure to note what sample was the peak. That's your best time estimate of when the echo arrived.

The time accuracy of your detector will depend upon how long your pulse is. As already discussed, you're using a fairly long pulse. Your "max(abs(value))" could end up being found nearly anywhere in that long pulse. So, your long pulse will result in less-than-optimal resolution. That's why shorter pulses can be better.

Alternatively, if you have a high SNR, you can set a lower detection threshold and just look for the start of the echo. If you've got the SNR, this will give you much higher resolution.

If you don't have high SNR, using max(abs(value)) as your detector might not be sensitive enough. In that case, you'll want to use a smarter way of detecting your pulse when scanning through the queue data. Usually "smarter" means averaging, which is easy to do and effective, but there are decisions about how long an average to use. That's refinement that can be done later.
 
This was very informative. Thank you! I'll have to read up more on this. From the snapshot of the audio library GUI, I see that two channels are being used from the i2s1 and then being fed into i2s2. I am currently using only a single mic so I guess I need to use just one channel? Also why do we need to feed this as input into i2s2? Won't processing the queue be the endpoint, since i2s2 will be used for the speaker? (Unless this was just an example for representation purposes :) )
 
Regarding the I2S output, I find that the system never operates quite right if I don't have any audio output specified. So, even if I'm not listening to the audio output, I still connect it.

You are using the audio output for your tone pulse, so you'll already have it connected, so that'll probably make it all work out just fine.
 
As for the two channels, this is going back to my recommendation that you measure the time of the transmitted pulse as well as the time of the received pulse. You had expressed some confusion over why your transmitted pulse seemed to have variable timing. If you actually measure the time of each transmitted pulse, you don't have to worry about any variability in its timing. You'll simply be taking the difference of the *measured* time of the transmission and the *measured* time of the echo. That time difference should be the best representation of the travel time, which should give you the best estimate of the distance that you're looking to measure.

To look for both the outgoing and return pulses, you can do that with one microphone. You'll just need to place your one microphone in a good location to catch both the outgoing and the incoming. That's probably pretty easy to do. Or, you could choose to do it with two microphones, looking for the outgoing pulse in one mic and the incoming echo in the other mic. If you use two mics, you need two identical signal processing chains. Having two identical signal processing chains is what I showed.
 
Status
Not open for further replies.
Back
Top