public
Last active

Distance between two microphones from delay of collinear source

  • Download Gist
delay.py
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
"""
Measure the distance between two microphones based on the delay of a
clap from a point that is collinear with the two microphones.
 
Make sure your signal isn't clipping.
 
First test:
2 electrets 26 cm apart, hand clap
Distance: -25.72 cm
Sub-sample distance: -26.02 cm
 
Second test:
Laptop's stereo mics, 6.9 cm apart, snapping fingers to one side
Distance: 6.79 cm
Sub-sample distance: 6.85 cm
"""
 
from __future__ import division
from scikits.audiolab import flacread
from scipy.signal import butter, lfilter, fftconvolve
from numpy import argmax
 
# download from https://gist.github.com/endolith/255291#file-parabolic-py
from parabolic import parabolic
 
signal, fs, enc = flacread('stereo recording.flac')
 
# Quick high-pass filter
cutoff = 500 # Hz
B, A = butter(1, cutoff / (fs/2), btype='high')
signal = lfilter(B, A, signal, axis=0)
 
# Cross-correlation of the two channels (same as convolution with one reversed)
corr = fftconvolve(signal[:, 0], signal[::-1, 1], mode='same')
 
# Find the offset of the peak. Zero delay would produce a peak at the midpoint
delay = int(len(corr)/2) - argmax(corr)
distance = delay / fs * 343 # m/s = speed of sound
print("Distance: %.2f cm" % (distance * 100))
 
# More accurate sub-sample estimation with parabolic interpolation:
delay = int(len(corr)/2) - parabolic(corr, argmax(corr))[0]
distance = delay / fs * 343 # m/s = speed of sound
print("Sub-sample distance: %.2f cm" % (distance * 100))
readme.md
Markdown

Example of using cross-correlation to measure the delay between two sources, and then using it to calculate distance between 2 microphones.

realtime screenshot.png
realtime_delay.py
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
"""
Listens for impulsive sounds. When one is heard, it displays the recording of
the left and right channels on the top, and their cross-correlation on the
bottom, finds the peak using parabolic/quadratic interpolation, plots
the peak as a red dot and prints the measured distance between the microphones.
 
If you disable the crest factor check, and play white noise with a phone, you
can see the red dot move back and forth as you vary the angle to the
microphones.
"""
 
from __future__ import division, print_function
import numpy as np
from matplotlib.mlab import rms_flat
from matplotlib import pyplot as plt
import pyaudio
from scipy.signal import fftconvolve, butter, lfilter
from PyQt4.QtGui import QApplication
from parabolic import parabolic
 
 
fs = 96000 # sampling rate
format = pyaudio.paFloat32 # max = 1.0
channels = 2
chunk = int(fs/4)
 
 
def crest_factor(signal):
"""
Crest factor of a 1D signal
"""
peak = np.amax(np.absolute(signal))
rms = rms_flat(signal)
return peak / rms
 
 
def callback(in_data, frame_count, time_info, status):
"""
Called on each incoming frame to process data
"""
global result
global result_waiting
 
if in_data:
print('.', end='')
result = np.fromstring(in_data, dtype=np.float32)
result = np.reshape(result, (chunk, 2)) # stereo
result_waiting = True
else:
print('no input')
 
return None, pyaudio.paContinue
 
 
# Initialize blank plots
plt.figure(1)
plt.subplot(2, 1, 1)
t = np.arange(chunk)/fs
plt_L = plt.plot(t, np.ones(chunk), 'blue')[0]
plt_R = plt.plot(t, -np.ones(chunk), 'red' )[0] # Red = Right
plt.margins(0, 0.1)
plt.grid(True, color = '0.7', linestyle='-', which='major', axis='both')
plt.grid(True, color = '0.9', linestyle='-', which='minor', axis='both')
plt.title('Recording')
plt.xlabel('Time [seconds]')
plt.ylabel('Amplitude')
 
plt.subplot(2, 1, 2)
lags = np.arange(chunk)-chunk/2
plt_corr = plt.plot(lags, np.zeros(chunk))[0]
plt_peak = plt.plot(0, 0, 'ro')[0]
plt.margins(0, 0.1)
plt.grid(True, color = '0.7', linestyle='-', which='major', axis='both')
plt.grid(True, color = '0.9', linestyle='-', which='minor', axis='both')
plt.title('Cross-correlation %.2f' % 0)
plt.xlabel('Lag [samples]')
plt.ylabel('Amplitude')
plt.margins(0.1, 0.1)
plt.xlim(-100, 100)
plt.ylim(-10, 10)
 
# Generate quick high-pass filter for motor hums, etc
cutoff = 500 # Hz
B, A = butter(1, cutoff / (fs/2), btype='high')
 
# Initialize global variables used by callback function
result = np.zeros((chunk, channels))
result_waiting = False
 
p = pyaudio.PyAudio()
 
if not p.is_format_supported(fs, 0, channels, format):
p.terminate()
raise RuntimeError('Format not supported')
 
# open stream using callback (3)
stream = p.open(format=format,
channels=channels,
rate=fs,
output=False,
input=True,
frames_per_buffer=chunk,
stream_callback=callback)
 
print('Press Ctrl+C or close plot window to stop')
 
try:
stream.start_stream()
 
try:
while plt.fignum_exists(1): # user has not closed plot
if result_waiting:
result_waiting = False
 
# High-pass filter
result = lfilter(B, A, result, axis=0)
sig_L = result[:, 0]
sig_R = result[:, 1]
 
# Only update plots on impulsive sound
# (Disable this for continuous tracking of continuous sources,
# like a phone playing white noise)
cf = crest_factor(sig_L)
if cf > 18:
plt_L.set_data(t, sig_L)
plt_R.set_data(t, sig_R)
 
corr = fftconvolve(sig_R, sig_L[::-1], mode='same')
 
# Update plots
plt.subplot(2, 1, 1)
plt.title('Recording (crest factor: {:.2f})'.format(cf))
 
plt.subplot(2, 1, 2)
plt_corr.set_data(lags, corr)
argpeak, amppeak = parabolic(corr, np.argmax(corr))
plt_peak.set_data(argpeak-chunk/2, amppeak)
plt.ylim(np.amin(corr)*1.1, np.amax(corr)*1.1)
 
distance = (argpeak-chunk/2) / fs * 343 # m
plt.title('Cross-correlation (dist: {:.2f} cm)'.format(distance * 100))
plt.draw()
 
# doesn't work in Spyder without this
# https://code.google.com/p/spyderlib/issues/detail?id=459
QApplication.processEvents()
 
except KeyboardInterrupt:
print('\nCtrl+C: Quitting')
else:
print('\nFigure closed: Quitting')
 
finally:
plt.close('all')
 
stream.stop_stream()
stream.close()
 
p.terminate()

what are your imports?

Here are the imports that I used:

from scikits.audiolab import flacread
from scipy.signal import butter, lfilter, fftconvolve
from numpy import argmax

from parabolic import *

Please note that you need to download endolith's parabolic.py file from https://gist.github.com/endolith/255291#file-parabolic-py

I tested delay.py on my .wav file and it seemed to work... somewhat

Sorry for the crappy documentation. I'll improve it. Too bad Github doesn't notify you of comments on gists. >:(

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.