diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..20813f125360f3941d0cb1657851163514c6e504 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +GSPR TDOA Hyper resolution +================== + +`gsrp_tdoa_hyperres.py` is a python script made to compute the time difference of arrival (TDOA) +using the geometric steered response power (GSRP) method. +This script also produces TDOA with a resolution higher than the initial sampling rate by interpolating +the GSPR loss function with a second-order polynomial. + +Prerequisite +------------ +This program uses the following libraries (other versions might work): + +Usage +----- + +usage: gsrp_tdoa_hyperres.py [-h] [-c CHANNELS] [-i INVERSE] [-f FRAME_SIZE] + [-s HOP_SIZE] [-m MAX_TDOA] [-l LOW] [-u UP] + [-d DECIMATE] [-t] [-e] [--start SECONDS] + [--end SECONDS] + infile outfile + + +Arguments +----- +``` +positional arguments: + infile The sound file to process. + outfile The text or npy file to write results to. Each row + gives the position (in samples), cross-correlation + product, the independent TDOAs (in samples), and TDOAs + derived from the independent ones. For plots, the + panes give the cross-correlation product, independent + TDOAS and derived TDOAs from top to bottom. + +optional arguments: + -h, --help show this help message and exit + -c CHANNELS, --channels CHANNELS + The channels to cross-correlate. Accepts two or more, + but beware of high memory use. To be given as a comma- + separated list of numbers, with 0 referring to the + first channel (default: all channels). + -i INVERSE, --inverse INVERSE + Inverse the channel. To be given as a comma-separated + list of numbers,with 0 referring to the first channel + once channels have been chosen by --channels. + -f FRAME_SIZE, --frame-size FRAME_SIZE + The size of the cross-correlation frames in seconds + (default: 0.2) + -s HOP_SIZE, --hop-size HOP_SIZE + The step between the beginnings of sequential frames + in seconds (default: 0.01), or the postion in second + if csv file path is given. + -m MAX_TDOA, --max-tdoa MAX_TDOA + The maximum TDOA in seconds (default: 0.0011). + -l LOW, --low LOW Bottom cutoff frequency. Disabled by default. + -u UP, --up UP Top cutoff frequency. Disabled by default. + -d DECIMATE, --decimate DECIMATE + Downsample the signal by the given factor. Disabled by + default + -t, --temporal If given, any decimation will be applied in the time + domain instead of the spectral domain. + -e, --erase Erase existing outfile. If outfile existe and --erase + is not provide, the script will exit. + --start SECONDS If given, only analyze from the given position. + --end SECONDS If given, only analyze up to the given position. +``` diff --git a/compute_tdoa_hyperres.py b/compute_tdoa_hyperres.py deleted file mode 100755 index 174a4b0f2140a5f098354587ad5bdb4e67105999..0000000000000000000000000000000000000000 --- a/compute_tdoa_hyperres.py +++ /dev/null @@ -1,334 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Computes TDOA estimates from a multi-channel recording. - -For usage information, call with --help. - -Authors: Maxence Ferrari, Marion Poupard, Jan Schlüter -""" -from __future__ import division - -import os -import sys -import itertools -from argparse import ArgumentParser -from sklearn.pipeline import Pipeline -from sklearn.preprocessing import PolynomialFeatures -from sklearn.linear_model import LinearRegression -import numpy as np -from numpy.fft import rfft, irfft -import scipy.signal -import soundfile as sf -from c_corr import c_corr -from scipy.signal.windows import tukey -try: - from tqdm import trange -except ImportError: - trange = range - -# colors used for plotting -CLR_ENERGY = '#C46362' -CLR_TDOA = '#1F77B4' -CLR_DERIVED_TDOA = '#94B9D3' - - -def opts_parser(): - usage =\ -"""Computes TDOA estimates from a multi-channel recording. -""" - parser = ArgumentParser(description=usage) - parser.add_argument('infile', - type=str, - help='The sound file to process.') - parser.add_argument('outfile', - type=str, - help='The text or npy file to write results to, or "display" or ' - 'a pdf or png file to plot results instead. For text and npy ' - 'files, each row gives the position (in samples), ' - 'cross-correlation product, the independent TDOAs (in ' - 'samples), and TDOAs derived from the independent ones. For ' - 'plots, the panes give the cross-correlation product, ' - 'independent TDOAS and derived TDOAs from top to bottom.') - parser.add_argument('-c', '--channels', - type=intlist, default=None, - help='The channels to cross-correlate. Accepts two or more, ' - 'but beware of high memory use. To be given as a ' - 'comma-separated list of numbers, with 0 referring to the ' - 'first channel (default: all channels).') - parser.add_argument('--start', metavar='SECONDS', - type=float, default=None, - help='If given, only analyze from the given position.') - parser.add_argument('--end', metavar='SECONDS', - type=float, default=None, - help='If given, only analyze up to the given position.') - parser.add_argument('-f', '--frame-size', - type=float, default=0.2, - help='The size of the cross-correlation frames in seconds ' - '(default: %(default)s)') - parser.add_argument('-s', '--hop-size', - type=str, default=0.01, - help='The step between the beginnings of sequential frames ' - 'in seconds (default: %(default)s), or the postion in second' - 'if csv file path is given.') - parser.add_argument('-m', '--max-tdoa', - type=float, default=0.0011, - help='The maximum TDOA in seconds (default: %(default)s). ' - 'Compute as maximum distance between two microphones ' - '(or hydrophones) divided by the minimum speed of ' - 'sound (330 m/s in air, 1450 m/s in water).') - parser.add_argument('-l', '--lower-freq', - type=float, default=0, - help='Cutoff frequency of a high-pass filter to apply on the ' - 'samples before correlating (default: %(default)s). Set ' - 'to 0 to disable.') - parser.add_argument('-u', '--upper-freq', - type=float, default=0, - help='Cutoff frequency of a low-pass filter to apply on the ' - 'samples before correlating (default: %(default)s). Set ' - 'to 0 to disable.)') - parser.add_argument('-d', '--decimate', - type=int, default=1, - help='Reduce temporal resolution by the given factor when ' - 'computing TDOAs. Make sure to pass a low enough ' - '--upper-freq to avoid artifacts (default: %(default)s).') - parser.add_argument('--decimate-in-time', - action='store_true', - help='If given, any decimation will be applied in the time ' - 'domain instead of the spectral domain.') - parser.add_argument('-t', '--result-threshold', - type=str, default=None, - help='Remove all points below a threshold in cross-correlation ' - 'product. "abs:N" (e.g., abs:12.5) applies an absolute threshold, ' - '"rel" applies a relative threshold, "rel:M,N" allows to tune its ' - 'parameters (e.g., rel:90,2.5). The relative threshold is set to ' - 'the mean + N * stddev of all cross-correlation product values ' - 'below the M-percentile. Defaults are M=95, N=1.') - parser.add_argument('-i', '--inverse', - type=intlist, default=None, - help='Inverse the channel. To be given as a comma-separated list of numbers,' - 'with 0 referring to the first channel once channels have been chosen by --channels' - ) - parser.add_argument('-e', action='store_false', help='Erase exiting out file') - return parser - - -def intlist(s): - return list(map(int, s.split(','))) - - -def slicer(down, up, ndim, n): - index = np.mgrid[ndim * [slice(0, n)]] - bounds = np.linspace(down, up, n + 1).astype(np.int) - slices = np.asarray([slice(a, b) - for a, b in zip(bounds[:-1], bounds[1:])]) - return slices[index].reshape(ndim, -1).T - - -def corr(data, sr, pos, w_size, max_tdoa, decimate=1): - num_channels = data.shape[1] - num_channel_pairs = num_channels * (num_channels - 1) // 2 - - # apply zero-padding - data = np.pad(data, (((w_size - 1) // 2, (w_size - 1) // 2), (0, 0)), - 'constant') - - # prepare window - win = np.expand_dims(tukey(w_size, 0.2), -1) - - # prepare index matrices - v1 = np.empty(num_channel_pairs, np.int8) - v2 = np.empty(num_channel_pairs, np.int8) - mat = np.zeros((num_channel_pairs, num_channels - 1), np.int8) - for k, (i, j) in enumerate(itertools.combinations(range(num_channels), 2)): - if i > 0: - mat[k, i-1] = -1 - mat[k, j-1] = 1 - v1[k] = i - v2[k] = j - cc_size = min(w_size, int(2 * max_tdoa // decimate)) - slices = slicer(-(cc_size // 2), cc_size // 2, (num_channels - 1), 16) - tausf = [] - for j in range(len(slices)): - taus = np.mgrid[slices[j]].reshape(num_channels - 1, -1).astype(np.int16) - taus2 = np.matmul(mat, taus) - tausf += [taus2[:, np.abs(taus2).max(0) <= cc_size // 2]] - tausf = np.hstack(tausf) - dw_size = w_size // decimate - tausf %= dw_size - - # run the TDOA search - tdoas = np.zeros((len(pos), num_channels), np.float32) - tdoas2 = np.zeros((len(pos), num_channels -1), np.float32) - cc = np.empty((num_channel_pairs, dw_size), np.float32) - poly = PolynomialFeatures(2) - lin = LinearRegression() - pipe = Pipeline([('poly',poly),('lin',lin)]) - ind = np.triu_indices(num_channels -1) - for i in trange(len(pos)): - fft = rfft(win * data[pos[i]:w_size + pos[i]], axis=0) - if decimate > 1: - fft = fft[:(len(fft) - 1) // decimate + 1] - fft = np.asarray(fft, dtype=np.complex64) - cc[:] = irfft(fft[:, v2] * np.conj(fft[:, v1]), axis=0).T - cc -= cc.min(-1, keepdims=True) - val, index = c_corr(cc, tausf) - tdoas[i, 0] = val - tdoas[i, 1:] = ((tausf[:(num_channels - 1), index] + dw_size//2) % dw_size) - dw_size//2 - #hyper resolution - taus = tdoas[i,1:num_channels] + np.stack(np.meshgrid(*(num_channels-1) * (np.arange(-2,3),)),0).reshape(num_channels-1, -1).T - taus = np.matmul(mat, taus.T) - taus = taus[:, np.abs(taus).max(0) <= cc_size // 2] - mean = taus.mean(-1)[:3] - coef = pipe.fit((taus).T[:,:3] - mean, - cc[np.expand_dims(np.arange(num_channel_pairs), 1), taus.astype(np.int)].prod(0) - ).named_steps['lin'].coef_ - der = np.zeros((num_channels -1, num_channels - 1)) - der[ind] = coef[4:] - tdoas2[i] = np.linalg.lstsq(der + der.T, -coef[1:4], rcond=None)[0] + mean - return np.hstack((np.expand_dims(pos, -1), tdoas)), np.hstack((np.expand_dims(pos, -1), tdoas2)) - - -def to_float(data): - """ - Converts integer samples to float samples, dividing by the datatype's - maximum on the way. If the data is in floating point already, just - converts to float32 if needed. - """ - data = np.asanyarray(data) - if np.issubdtype(data.dtype, np.floating): - return np.asarray(data, dtype=np.float32) - else: - return np.divide(data, np.iinfo(data.dtype).max, dtype=np.float32) - - -def main(): - # parse command line - parser = opts_parser() - options = parser.parse_args() - if options.e and os.path.isfile(options.outfile): - return None - if options.result_threshold is None: - pass - elif options.result_threshold.startswith('abs:'): - options.result_threshold = ('abs', float(options.result_threshold[4:])) - elif options.result_threshold == 'rel': - options.result_threshold = ('rel', (95, 1)) - elif options.result_threshold.startswith('rel:'): - options.result_threshold = ('rel', tuple( - float(v) for v in options.result_threshold[4:].split(',', 1))) - else: - parser.error('invalid --result-threshold: %s' % options.result_threshold) - - # load audio file - print("Loading %s..." % options.infile) - samples, sr = sf.read(options.infile) - if options.start is not None or options.end is not None: - samples = samples[ - options.start and int(options.start * sr) or None: - options.end and int(options.end * sr) or None] - - # keep only required channels and convert to float - if options.channels is not None: - samples_ = np.empty((len(samples), len(options.channels)), dtype=np.float32) - for s, c in zip(samples_.T, options.channels): - s[:] = to_float(samples[:, c]) - samples = samples_ - else: - samples = to_float(samples) - options.channels = range(samples.shape[1]) - - if options.inverse is not None: - for c in options.inverse: - samples[:, c] *= -1 - - # apply temporal lowpass and/or highpass, if needed - if options.lower_freq or options.upper_freq: - print("Filtering...") - nyquist = sr / 2 - if options.lower_freq and not options.upper_freq: - sos = scipy.signal.butter(8, options.lower_freq / nyquist, - output='sos', btype='highpass') - elif options.lower_freq and options.upper_freq: - sos = scipy.signal.butter(8, (options.lower_freq / nyquist, - options.upper_freq / nyquist), - output='sos', btype='bandpass') - elif not options.lower_freq and options.upper_freq: - sos = scipy.signal.butter(8, options.upper_freq / nyquist, - output='sos', btype='lowpass') - samples = scipy.signal.sosfilt(sos, samples, axis=0) - - # decimate in time domain, if requested - if options.decimate and options.decimate_in_time: - samples = samples[::options.decimate] - sr /= options.decimate - - # prepare list of positions to compute TDOAs for - if os.path.isfile(options.hop_size): - pos = (sr * np.loadtxt(options.hop_size, delimiter=',')).astype(int).ravel() - else: - try: - pos = np.arange(0, len(samples), int(sr * float(options.hop_size))) - except ValueError: - raise ValueError(f'Error: hop size {options.hop_size} is nethier an existing file nor a float') - - # handle it to the algorithm - print("Computing TDOAs...") - result1, result2 = corr(samples, sr, pos, int(sr * options.frame_size), - max_tdoa=int(np.ceil(sr * options.max_tdoa)), - decimate=(options.decimate - if not options.decimate_in_time else 1)) - - # compute additional non-independent TDOAs - additional1 = [(b - a) for a, b in itertools.combinations(result1.T[2:], 2)] - result1 = np.hstack((result1,) + tuple(a[:, np.newaxis] for a in additional1)) - additional2 = [(b - a) for a, b in itertools.combinations(result2.T[1:], 2)] - result2 = np.hstack((result2,) + tuple(a[:, np.newaxis] for a in additional2)) - - # filter the background noise - if options.result_threshold is not None: - method, args = options.result_threshold - values = result1[:, 1] - if method == 'abs': - result1 = result1[values > args] - elif method == 'rel': - percentile, stddev = args - below_percentile = values[values < np.percentile(values, percentile)] - result1 = result1[values > np.mean(below_percentile) + - stddev * np.std(below_percentile)] - - # save or plot results - if options.outfile.endswith('.npy'): - np.save(options.outfile, result1) - np.save(options.outfile[:-4]+'2.npy', result2) - elif (options.outfile == 'display' or - options.outfile[-3:] in ('pdf', 'png')): - import matplotlib as mpl - import matplotlib.pyplot as plt - fig, axes = plt.subplots(result1.shape[1] - 1, 1, sharex=True) - num_channels = len(options.channels) - num_tdoas = num_channels - 1 - num_derived_tdoas = num_tdoas * (num_tdoas - 1) // 2 - colors = ([CLR_ENERGY] + - [CLR_TDOA] * num_tdoas + - [CLR_DERIVED_TDOA] * num_derived_tdoas) - times = result1[:, 0] / sr - if options.start is not None: - times += options.start - for ax, r, c in zip(axes, result1.T[1:], colors): - ax.plot(times, r, '.', markersize=3, color=c) - axes[-1].xaxis.set_major_formatter(mpl.ticker.FuncFormatter( - lambda v, _: ('%d:%02.1f' % (v // 60, v % 60)))) - fig.suptitle(os.path.basename(options.infile)) - if options.outfile == 'display': - plt.show() - else: - plt.savefig(options.outfile) - else: - np.savetxt(options.outfile, result1, delimiter=',') - np.savetxt(options.outfile+'2', result2, delimiter=',') - print("Done.") - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/gsrp_tdoa_hyperres.py b/gsrp_tdoa_hyperres.py new file mode 100755 index 0000000000000000000000000000000000000000..0365e7d16abc53604eeef75eeacd1ea5c345317e --- /dev/null +++ b/gsrp_tdoa_hyperres.py @@ -0,0 +1,221 @@ +import os +import sys +import itertools +import argparse +from sklearn.pipeline import Pipeline +from sklearn.preprocessing import PolynomialFeatures +from sklearn.linear_model import LinearRegression +import numpy as np +from numpy.fft import rfft, irfft +import scipy.signal as sg +import soundfile as sf +from c_corr import c_corr +from scipy.signal.windows import tukey +try: + from tqdm import trange +except ImportError: + trange = range + + +class BColors: + HEADER = '\033[95m' + OKBLUE = '\033[94m' + OKCYAN = '\033[96m' + OKGREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + + +def intlist(s): + return list(map(int, s.split(','))) + + +def slicer(down, up, ndim, n): + index = np.mgrid[ndim * [slice(0, n)]] + bounds = np.linspace(down, up, n + 1).astype(np.int) + slices = np.asarray([slice(a, b) + for a, b in zip(bounds[:-1], bounds[1:])]) + return slices[index].reshape(ndim, -1).T + + +def corr(data, sr, pos, w_size, max_tdoa, decimate=1): + num_channels = data.shape[1] + num_channel_pairs = num_channels * (num_channels - 1) // 2 + + # apply zero-padding + data = np.pad(data, (((w_size - 1) // 2, (w_size - 1) // 2), (0, 0)), + 'constant') + + # prepare window + win = np.expand_dims(tukey(w_size, 0.2), -1) + + # prepare index matrices + v1 = np.empty(num_channel_pairs, np.int8) + v2 = np.empty(num_channel_pairs, np.int8) + mat = np.zeros((num_channel_pairs, num_channels - 1), np.int8) + for k, (i, j) in enumerate(itertools.combinations(range(num_channels), 2)): + if i > 0: + mat[k, i-1] = -1 + mat[k, j-1] = 1 + v1[k] = i + v2[k] = j + cc_size = min(w_size, int(2 * max_tdoa // decimate)) + slices = slicer(-(cc_size // 2), cc_size // 2, (num_channels - 1), 16) + tausf = [] + for j in range(len(slices)): + taus = np.mgrid[slices[j]].reshape(num_channels - 1, -1).astype(np.int16) + taus2 = np.matmul(mat, taus) + tausf += [taus2[:, np.abs(taus2).max(0) <= cc_size // 2]] + tausf = np.hstack(tausf) + dw_size = w_size // decimate + tausf %= dw_size + + # run the TDOA search + tdoas = np.zeros((len(pos), num_channels), np.float32) + tdoas2 = np.zeros((len(pos), num_channels -1), np.float32) + cc = np.empty((num_channel_pairs, dw_size), np.float32) + poly = PolynomialFeatures(2) + lin = LinearRegression() + pipe = Pipeline([('poly',poly),('lin',lin)]) + ind = np.triu_indices(num_channels -1) + for i in trange(len(pos)): + fft = rfft(win * data[pos[i]:w_size + pos[i]], axis=0) + if decimate > 1: + fft = fft[:(len(fft) - 1) // decimate + 1] + fft = np.asarray(fft, dtype=np.complex64) + cc[:] = irfft(fft[:, v2] * np.conj(fft[:, v1]), axis=0).T + cc -= cc.min(-1, keepdims=True) + val, index = c_corr(cc, tausf) + tdoas[i, 0] = val + tdoas[i, 1:] = ((tausf[:(num_channels - 1), index] + dw_size//2) % dw_size) - dw_size//2 + + # hyper resolution + taus = tdoas[i,1:num_channels] + np.stack(np.meshgrid(*(num_channels-1) * (np.arange(-2,3),)),0).reshape(num_channels-1, -1).T + taus = np.matmul(mat, taus.T) + taus = taus[:, np.abs(taus).max(0) <= cc_size // 2] + mean = taus.mean(-1)[:3] + coef = pipe.fit((taus).T[:,:3] - mean, + cc[np.expand_dims(np.arange(num_channel_pairs), 1), taus.astype(np.int)].prod(0) + ).named_steps['lin'].coef_ + der = np.zeros((num_channels -1, num_channels - 1)) + der[ind] = coef[4:] + tdoas2[i] = np.linalg.lstsq(der + der.T, -coef[1:4], rcond=None)[0] + mean + return np.hstack((np.expand_dims(pos, -1), tdoas)), np.hstack((np.expand_dims(pos, -1), tdoas2)) + + +def main(args): + if args.erase and os.path.isfile(args.outfile): + print(f'{BColors.WARNING}{args.outfile} already exist and erase is not set {BColors.ENDC}') + return 1 + + # load audio file + print(f'Loading {args.infile}...') + sr = sf.info(args.infile).samplerate + sound, sr = sf.read(args.infile, start=int(sr*args.start), stop=int(sr*args.end) if args.end is not None else None, + dtype=np.float32, always_2d=True) + if args.channels is not None: + sound = sound[:, args.channels] + else: + args.channels = list(range(sound.shape[1])) + if sound.shape[1] < 2: + raise ValueError(f'{BColors.FAIL}{args.infile} with channels {args.channel} has not enough channels' + f'{BColors.ENDC}') + + if args.inverse is not None: + for c in args.inverse: + sound[:, c] *= -1 + + if not (args.low is None and args.up is None): + print("Filtering...") + if args.low is not None: + if args.highpass is None: + sos = sg.butter(3, 2*args.low/sr, 'highpass', output='sos') + else: + sos = sg.butter(3, [2*args.low/sr, 2*args.up/sr], 'bandpass', output='sos') + else: + sos = sg.butter(3, 2*args.up/sr, 'lowpass', output='sos') + sound = sg.sosfiltfilt(sos, sound, axis=0) + + if args.decimate and args.temporal: + sound = sound[::args.decimate] + sr /= args.decimate + + # Position where the TDOAs are computed + if os.path.isfile(args.hop_size): + pos = (sr * np.loadtxt(args.hop_size, delimiter=',')).astype(int).ravel() + else: + try: + pos = np.arange(0, len(sound), int(sr * float(args.hop_size))) + except ValueError: + raise ValueError(f'Error: hop size {args.hop_size} is neither an existing file nor a float') + + + print("Computing TDOAs...") + result1, result2 = corr(sound, sr, pos, int(sr * args.frame_size), + max_tdoa=int(np.ceil(sr * args.max_tdoa)), + decimate=(args.decimate + if not args.temporal else 1)) + + # compute additional non-independent TDOAs + additional1 = [(b - a) for a, b in itertools.combinations(result1.T[2:], 2)] + result1 = np.hstack((result1,) + tuple(a[:, np.newaxis] for a in additional1)) + additional2 = [(b - a) for a, b in itertools.combinations(result2.T[1:], 2)] + result2 = np.hstack((result2,) + tuple(a[:, np.newaxis] for a in additional2)) + + if args.outfile.endswith('.npy'): + np.save(args.outfile, result1) + np.save(args.outfile[:-4] + '_2.npy', result2) + else: + np.savetxt(args.outfile, result1, delimiter=',') + np.savetxt((lambda x1, x2, x3: x1 + '_2' + x2 + x3)(*args.outfile.rpartition('.', 1)), result2, delimiter=',') + print("Done.") + return 0 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Computes TDOA estimates from a multi-channel recording.') + parser.add_argument('infile', type=str, help='The sound file to process.') + parser.add_argument('outfile', type=str, help='The text or npy file to write results to. Each row gives the ' + 'position (in samples), cross-correlation product, the independent ' + 'TDOAs (in samples), and TDOAs derived from the independent ones. ' + 'For plots, the panes give the cross-correlation product, ' + 'independent TDOAS and derived TDOAs from top to bottom.') + + parser.add_argument('-c', '--channels', type=intlist, default=None, + help='The channels to cross-correlate. Accepts two or more, but beware of high memory use. To ' + 'be given as a comma-separated list of numbers, with 0 referring to the first channel ' + '(default: all channels).') + parser.add_argument('-i', '--inverse', type=intlist, default=None, + help='Inverse the channel. To be given as a comma-separated list of numbers,' + 'with 0 referring to the first channel once channels have been chosen by --channels.') + + parser.add_argument('-f', '--frame-size', type=float, default=0.2, + help='The size of the cross-correlation frames in seconds (default: %(default)s)') + parser.add_argument('-s', '--hop-size', type=str, default=0.01, + help='The step between the beginnings of sequential frames in seconds (default: %(default)s), ' + 'or the postion in second if csv file path is given.') + parser.add_argument('-m', '--max-tdoa', type=float, default=0.0011, + help='The maximum TDOA in seconds (default: %(default)s).') + + parser.add_argument('-l', '--low', type=float, default=None, help='Bottom cutoff frequency. Disabled by default.') + parser.add_argument('-u', '--up', type=float, default=None, help='Top cutoff frequency. Disabled by default.') + + parser.add_argument('-d', '--decimate', type=int, default=1, help='Downsample the signal by the given factor. ' + 'Disabled by default') + parser.add_argument('-t', '--temporal', action='store_true', help='If given, any decimation will be applied in the ' + 'time domain instead of the spectral domain.') + + parser.add_argument('-e', '--erase', action='store_false', help='Erase existing outfile. If outfile existe and ' + '--erase is not provide, the script will exit.') + + parser.add_argument('--start', metavar='SECONDS', type=float, default=0, + help='If given, only analyze from the given position.') + parser.add_argument('--end', metavar='SECONDS', type=float, default=None, + help='If given, only analyze up to the given position.') + + args = parser.parse_args() + + sys.exit(main(args))