Skip to content

Commit 2c87613

Browse files
committed
adds simpler getperiod function for skew correction
1 parent ad7137e commit 2c87613

File tree

6 files changed

+160
-62
lines changed

6 files changed

+160
-62
lines changed

Project.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ Compat = "34da2185-b29b-5c13-b0c7-acf172513d20"
66
DSP = "717857b8-e6f2-59f4-9121-6e50c889abd2"
77
FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341"
88
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
9-
LsqFit = "2fda8390-95c7-5789-9bda-21331edee243"
10-
Roots = "f2b01f46-fcfa-551c-844a-d8ac1e96c665"
9+
Optim = "429524aa-4258-5aef-a3af-852621145aeb"
10+
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
1111
SampledSignals = "bd7594eb-a658-542f-9e75-4c4d8908c167"
1212
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
1313
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"

src/MeasureIR.jl

+6-4
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,19 @@ module MeasureIR
33
using Compat: @warn
44
using Unitful: s, Hz, Time, Frequency, uconvert, NoUnits
55
using SampledSignals: SampleBuf, samplerate
6-
using Roots: find_zero
76
using DSP: DSP, gaussian, hanning, hilbert
8-
using DSP: FIRFilter, FIRWindow, filt, db2amp, xcorr, nextfastfft
7+
using DSP: FIRFilter, FIRWindow, filt, db2amp, pow2db, xcorr, nextfastfft
98
using DSP: digitalfilter, Bandpass, Lowpass
9+
using DSP: resample
1010
using FFTW: rfft, irfft
1111
using LinearAlgebra: dot
1212
using Statistics: mean, quantile
13-
using LsqFit: curve_fit, stderror
13+
using Optim: NewtonTrustRegion, optimize, converged, minimizer
14+
using Printf
1415

1516
export stimulus, analyze, noisefloor, prepadding, snr
1617
export expsweep, golay, mls, rpms, impulse
17-
export pilot, findpilot, pilotsync
18+
export pilot, findpilot, pilotsync, getperiod
1819

1920
"""
2021
Abstract supertype for impulse response measurements. Subtypes should define a
@@ -84,6 +85,7 @@ include("expsweep.jl")
8485
include("mls.jl")
8586
include("rpms.jl")
8687
include("pilot.jl")
88+
include("period.jl")
8789

8890
# workaround needed because DSP.jl doesn't handle SampleBufs
8991
# and Float32s well. We dispatch to an internal _analyze to avoid method

src/period.jl

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""
2+
Estimate the period of `x`, from an initial guess `L`. The result will be the
3+
peak of the continuous cross-correlation between `0.5L` and `1.5L`, i.e. the
4+
period is not constrained to be an integer.
5+
6+
The algorithm is to first compute a discrete cross-correlation and oversample by
7+
4x to account for inter-sample peaks. We use the peak of the oversampled
8+
cross-correlation as a starting point for a numerical optimization of the
9+
average-squared-distance function in the frequency domain.
10+
"""
11+
function getperiod(x::AbstractVector, L)
12+
maxD = L÷2
13+
14+
# create two signals that are offset by L (our expected period length), and
15+
# zero-pad by to do a linear (rather than circular) convolution. We set up
16+
# the padding to pre-shift so the cross-correlation starts start at their
17+
# left-most lag (`-maxD`), and the `maxD`th sample will actually be L lag)
18+
# we also add extra padding to account for time aliasing from the fractional
19+
# delay. Otherwise it can wrap around. 512 samples is enough for a sinc
20+
# function to decay more than 60dB
21+
22+
# note this could be problematic if the signal is temporally compact, e.g.
23+
# an impulse train. Innacuracy in `L` could chop off an impulse from one or
24+
# the other of these.
25+
x1 = [zeros(maxD); x[L+1:end]; zeros(512)]
26+
x2 = [x[1:(end-L)]; zeros(maxD); zeros(512)]
27+
28+
m = length(x1)
29+
X1 = rfft(x1)
30+
X2 = rfft(x2)
31+
M = length(X1)
32+
33+
# do a discrete cross-correlation and upsample. The maximum here should be
34+
# a pretty good estimate of the true peak. Should be valid from 1:8L, which
35+
# represents lags from -L:L (upsampled by 4)
36+
xc = resample(irfft(X1 .* conj.(X2), m)[1:2maxD+1], 4)
37+
D_est1 = (findmax(xc)[2]-1)/4-maxD
38+
39+
# note if `m` were guaranteed to be even the upper bound here would be π
40+
ω = range(0, (M-1)*2π/m, length=M)
41+
42+
# error function for frequency-domain delay estimation. This can be
43+
# interpreted as a frequency-domain ASDF (average-squared distance function)
44+
# as a function of the continuous delay
45+
# err(D) = sum(abs2(X1[i] - X2[i] * exp(-im*ω[i]*(D+maxD)))
46+
# for i in 1:M)*(m-L)/(m-L-abs(maxD-D))
47+
err(D) = sum(abs2(X1[i] - X2[i] * exp(-im*ω[i]*(D+maxD)))
48+
for i in 1:M)
49+
50+
# opt = optimize(D->err(first(D)), [D_est1], NewtonTrustRegion(), autodiff=:forward)
51+
opt = optimize(err, D_est1-0.5, D_est1+0.5)
52+
if !converged(opt)
53+
throw(ErrorException("Delay estimation failed to converge"))
54+
end
55+
56+
# return the delay
57+
L+minimizer(opt)[1]
58+
end

src/pilot.jl

+15-35
Original file line numberDiff line numberDiff line change
@@ -14,48 +14,28 @@ function pilotfilt(sig, ω)
1414
end
1515

1616
"""
17-
Compute reasonable onset and offset thresholds to find a pulse of length L
18-
within `sig`.
19-
"""
20-
function pulsethreshs(sig, L)
21-
if length(sig) < 4L
22-
@warn "`pulsethreshs` assumes `length(sig) > 4L` to estimate noise statistics"
23-
end
24-
lfrac = L/length(sig)
25-
qs = quantile(sig, [0.75, 1-lfrac/2])
26-
qs[1] .+ (0.55, 0.45) .* (qs[2]-qs[1])
27-
end
28-
29-
"""
30-
Find a pilot tone in the given signal. Note that the total signal length should
31-
be at least 4 times longer than the expected length of the tone, so we can
32-
estimate noise statistics.
17+
Find a pilot tone in the given signal.
3318
"""
3419
function findpilot(sig, freq, len; samplerate=1)
20+
# TODO: this isn't robust to a wideband energy increase. It looks for an
21+
# energy pulse in the target band, but that could also get triggered if
22+
# everything just gets louder
3523
ω = freqnorm(freq, samplerate)
3624
L = durnorm(len, samplerate)
3725
filtered = pilotfilt(sig, ω)
26+
3827
env = abs2.(hilbert(filtered))
39-
onthresh, offthresh = pulsethreshs(env, L)
40-
# lpf = gaussian(round(Int, 0.25*L), 0.15)
41-
lpf = digitalfilter(Lowpass(1/4L),
42-
FIRWindow(;transitionwidth=30/L))
43-
smoothed = filt(lpf, env)
44-
onset = findfirst((x->x>onthresh), smoothed)
45-
onset === nothing && return nothing, nothing
46-
offset = onset+findfirst(x->x<offthresh, smoothed[onset+1:end])
47-
filtdelay = length(lpf)÷2
48-
onset -= filtdelay
49-
offset === nothing && return onset, nothing
50-
offset -= filtdelay
51-
if !isapprox(offset-onset, L; rtol=0.5)
52-
@warn "Found pilot with length $(offset-onset) instead of $L"
53-
end
28+
_, onset = findmax(xcorr(env, ones(L); padmode=:none))
29+
onset -= L
5430

55-
# only pull the middle 80% of the tone to account for any other onset/offset
56-
# irregularities
57-
slack = 0.05(offset-onset)
58-
round.(Int, (onset+slack, offset-slack))
31+
prerollpower = sum(x->x^2, filtered[1:onset-1]) / (onset-1)
32+
pilotpower = sum(x->x^2, filtered[(0:L-1).+onset]) / L
33+
34+
snr = pow2db(pilotpower/prerollpower)
35+
if snr < 6
36+
@warn @sprintf "Narrowband Pilot SNR only %.2f dB" snr
37+
end
38+
onset
5939
end
6040

6141
"""

test/Project.toml

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
[deps]
22
DSP = "717857b8-e6f2-59f4-9121-6e50c889abd2"
33
FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341"
4-
LsqFit = "2fda8390-95c7-5789-9bda-21331edee243"
5-
MeasureIR = "1d92ad0d-f520-5482-bb8f-e5ddddeec5c1"
4+
MeasureIR = "4e68e94c-cd2d-5c3b-bae2-1718e3447baa"
65
Optim = "429524aa-4258-5aef-a3af-852621145aeb"
76
SampledSignals = "bd7594eb-a658-542f-9e75-4c4d8908c167"
7+
Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb"
8+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
89
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"

test/runtests.jl

+76-19
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,55 @@
11
using MeasureIR
22
using Unitful: s, kHz
33
using SampledSignals: SampleBuf, samplerate
4-
# using Suppressor
4+
using Suppressor
55
using DSP
66
using Test
77

8+
"""
9+
Simulate running the given stimulus through a real system, including a linear
10+
IR, nonlinear distortion, noise, and clock skew.
11+
12+
If `dist` is given, it should be a multiplier that scales the stimulus before
13+
passing into `tanh`. The distorted signal is scaled to have the same energy as
14+
the original. A multiplier of 1 gives pretty mild distortion, and it goes up
15+
from there.
16+
17+
Returns the simulated response, as well as the IR used.
18+
"""
19+
function simIR(stim; ir=nothing, dist=nothing, noisepow=-Inf, skew=1.0)
20+
if dist !== nothing
21+
stimenergy = sum(x->x^2, stim)
22+
stim .= tanh.(stim)
23+
distenergy = sum(x->x^2, stim)
24+
25+
stim .*= sqrt(sigenergy/distenergy)
26+
end
27+
if ir === nothing
28+
ir = [zeros(100); 1; randn.() .* exp.(range(-1.5, -8, length=7500))]
29+
end
30+
resp = conv(stim, ir)
31+
if noisepow > -Inf
32+
resp .+= db2amp(noisepow) .* randn.()
33+
end
34+
if skew != 1.0
35+
resp = resample(resp, skew)
36+
end
37+
38+
resp, ir
39+
end
40+
41+
"""
42+
Generates a broadband random signal of length `N` that's periodic with period
43+
`P`. `P` does not need to be an integer.
44+
"""
45+
function randperiodic(N, P)
46+
f = 1/P # fundamental freq
47+
# we add eps() here so we don't get hit by floating-point error when f
48+
# divides evenly into 1
49+
H = Int(fld(0.5+eps(), f)) # number of harmonics we can fit below nyquist
50+
sum(cos.(2π.*(h.*f.*(0:N-1).+rand()))./H for h in 1:H)
51+
end
52+
853
function testmeasure(measfunc)
954
meas = measfunc(8192)
1055
stim = stimulus(meas)
@@ -103,10 +148,10 @@ end
103148
meas = golay(L)
104149
ir = randn(L÷2) .* exp.(linspace(0,-8, L÷2))
105150
stim = stimulus(meas)
106-
out = @color_output false @capture_err analyze(meas, conv(stim, ir))
151+
out = @capture_err analyze(meas, conv(stim, ir))
107152
@test out == ""
108153
ir = randn(3L÷2) .* exp.(linspace(0,-8, 3L÷2))
109-
out = @color_output false @capture_err analyze(meas, conv(stim, ir))
154+
out = @capture_err analyze(meas, conv(stim, ir))
110155
@test out == string("Warning: nonsilent samples past end of analysis ",
111156
"window. Check your test signal is long enough for ",
112157
"the response\n")
@@ -117,17 +162,17 @@ end
117162
# L = 4096
118163
# meas = expsweep(L)
119164
# stim = stimulus(meas)
120-
# out = @color_output false @capture_err analyze(meas, stim)
165+
# out = @capture_err analyze(meas, stim)
121166
# @test out == ""
122-
# out = @color_output false @capture_err analyze(meas, tanh.(stim*2))
167+
# out = @capture_err analyze(meas, tanh.(stim*2))
123168
# @test out == "Warning: Energy in noncausal IR is above noise floor. Check for nonlinearity\n"
124169
#
125170
# # now try with some noise and a more realistic IR
126171
# testir = randn(512) .* exp.(linspace(0,-8, 512)) / 2
127172
# resp = conv(stim, testir)[1:length(stim)]
128-
# out = @color_output false @capture_err analyze(meas, stim.+randn.().*0.1)
173+
# out = @capture_err analyze(meas, stim.+randn.().*0.1)
129174
# @test out == ""
130-
# out = @color_output false @capture_err analyze(meas, tanh.(stim*2).+randn.().*0.1)
175+
# out = @capture_err analyze(meas, tanh.(stim*2).+randn.().*0.1)
131176
# @test out == "Warning: Energy in noncausal IR is above noise floor. Check for nonlinearity\n"
132177
# end
133178

@@ -189,34 +234,46 @@ end
189234
# plt = plot(welch_pgram(sig[(1:10000) .+ 50000], 256).power)
190235
@testset "findpilot" begin
191236
sr = 48kHz
237+
dur = 1s
192238
@testset "f=$f" for f in 0.99kHz:0.005kHz:1.01kHz
193239
sig = [zeros(50000); pilot(dur, f, samplerate=sr); zeros(48000*10)]
194240
sig .+= 8 .* randn.() # SNR of -21.1dB - not bad!
195-
onset, offset = findpilot(sig, 1kHz, 1s; samplerate=sr)
196-
# make sure we don't over-shoot
197-
@test offset-onset <= 48000
198-
# undershooting by a bit is OK
199-
@test offset-onset > 48000*0.7
200-
@test onset >= 50000
201-
@test onset <= 50000+48000*0.3
241+
onset = findpilot(sig, 1kHz, 1s; samplerate=sr)
242+
@test isapprox(onset, 50001, rtol=0.1)
202243
end
203244
end
204245

205-
# TODO: for some reason this is failing when skew == 1
206246
@testset "pilotsync" begin
207-
L = 5s
247+
dur = 5s
208248
sr = 48kHz
209249
f = 1kHz
250+
L = 5*48000
210251
@testset "skew=$skew" for skew in 0.99:0.005:1.01
211-
p = pilot(L, f; samplerate=sr)
252+
p = pilot(dur, f; samplerate=sr)
212253
sig = [zeros(50000);
213254
isapprox(skew, 1) ? p : resample(p, skew);
214255
zeros(48000*15)]
215256
sig .+= randn.() ./ sqrt(2) # SNR of 0dB
216-
onset, offset = findpilot(sig, f, L; samplerate=sr)
217-
@test isapprox(pilotsync(sig[onset:offset], f; samplerate=sr),
257+
onset = findpilot(sig, f, dur; samplerate=sr)
258+
@test isapprox(pilotsync(sig[(0:L-1) .+ onset], f; samplerate=sr),
218259
1/skew, rtol=5e-7)
219260
end
220261
end
221262

263+
@testset "getperiod" begin
264+
N = 65535
265+
meas = mls(N, 20)
266+
267+
# test at a variety of skews in ppm
268+
skews = 1 .+ (-400:80:400) ./ 1e6
269+
270+
@testset "skew=$skew" for skew in skews
271+
resp, ir = simIR(stimulus(meas), skew=skew, noisepow=-8.2+20) # -20dB SNR
272+
period = getperiod(resp, N)
273+
err = (period-skew*N)/period
274+
@show skew, err
275+
@test isapprox(period, skew*N, rtol=1e-7)
276+
end
277+
end
278+
222279
end # @testset MeasureIR

0 commit comments

Comments
 (0)