Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

aliasing issue with floating-point resampling ratio #620

Open
ericphanson opened this issue Jan 17, 2025 · 9 comments
Open

aliasing issue with floating-point resampling ratio #620

ericphanson opened this issue Jan 17, 2025 · 9 comments

Comments

@ericphanson
Copy link
Contributor

ericphanson commented Jan 17, 2025

When calling resample with a floating-point value, it takes a different codepath than with a rational value. The floating-point version seems more susceptible to aliasing issues.

Here is an example, minimized from a real-world issue, that demonstrates the problem:

using DSP, CairoMakie

function hpf(x, freq; fs)
    return filtfilt(digitalfilter(Highpass(freq; fs), Butterworth(4)), x)
end

function lpf(x, freq; fs)
    return filtfilt(digitalfilter(Lowpass(freq; fs), Butterworth(4)), x)
end

function middle_third(x)
    third = div(length(x), 3)
    return x[third:(2 * third + 1)]
end

function plt_filters(x; fs)
    x = lpf(x, 35.0; fs)
    x = hpf(x, 0.5; fs)
    return x
end

sine_wave(freq_hz) = sin.(2π .* freq_hz .* ts)

n = 45 # seconds
fs = 250
ts = range(0, n; length=fs * n)
data = 0.05 * sine_wave(0.75) + 0.01 * sine_wave(5.0) + 0.025 * sine_wave(10.0) +
       # lots of high frequency noise
       sum(100 * sine_wave(f) for f in 90:125)

resampling_ratio = 1 / 1.00592
resampled = resample(data, resampling_ratio)
ts_resampled = resample(ts, resampling_ratio)

rational_resampled = resample(data, rationalize(resampling_ratio))
rational_ts_resampled = resample(ts, rationalize(resampling_ratio))

# individual axes
fig = let
    fig = Figure()
    colors = Makie.wong_colors()
    ax_kwargs = (; ylabel="Data", limits=(nothing, nothing, -1, 1))

    ax1 = Axis(fig[1, 1]; ax_kwargs...)
    l1 = lines!(ax1, middle_third(ts), middle_third(plt_filters(data; fs)); color=colors[1])

    ax2 = Axis(fig[2, 1]; ax_kwargs...)
    l2 = lines!(ax2, middle_third(ts_resampled), middle_third(plt_filters(resampled; fs));
                color=colors[2])

    ax3 = Axis(fig[3, 1]; xlabel="Time (s)", ax_kwargs...)
    l3 = lines!(ax3, middle_third(rational_ts_resampled),
                middle_third(plt_filters(rational_resampled; fs)); color=colors[3])

    Legend(fig[4, 1], [l1, l2, l3], ["original", "resampled", "rational resampled"];
           orientation=:horizontal)
    fig
end

save("mwe.png", fig)

# combined
fig = let
    fig = Figure()
    colors = Makie.wong_colors()
    ax_kwargs = (; ylabel="Data", limits=(nothing, nothing, -1, 1))
    ax = Axis(fig[1, 1]; xlabel="Time (s)", ax_kwargs...)

    lines!(ax, middle_third(ts_resampled), middle_third(plt_filters(resampled; fs));
           color=colors[2], label="resampled")
    lines!(ax, middle_third(rational_ts_resampled),
           middle_third(plt_filters(rational_resampled; fs)); color=colors[3],
           label="rational resampled")
    lines!(ax, middle_third(ts), middle_third(plt_filters(data; fs)); color=colors[1],
           label="original")

    axislegend(ax)
    fig
end
save("mwe-combined.png", fig)
fig

Image
Image

@wheeheee
Copy link
Member

Could you test this with 0.7.9 to see if this might be a regression with the changes in 0.8.0?

@ericphanson
Copy link
Contributor Author

This actually is on 0.7.9, I will try to run it on 0.8.0 but I need to update my code for changes (no fs argument to Lowpass)

@wheeheee
Copy link
Member

Sorry, didn't quite notice. Fingers crossed it magically disappears in 0.8...

@ericphanson
Copy link
Contributor Author

Unfortunately not, same results on 0.8

DSP v0.8 code
using DSP, CairoMakie

function hpf(x, freq; fs)
    return filtfilt(digitalfilter(Highpass(freq), Butterworth(4); fs), x)
end

function lpf(x, freq; fs)
    return filtfilt(digitalfilter(Lowpass(freq), Butterworth(4); fs), x)
end

function middle_third(x)
    third = div(length(x), 3)
    return x[third:(2 * third + 1)]
end

function plt_filters(x; fs)
    x = lpf(x, 35.0; fs)
    x = hpf(x, 0.5; fs)
    return x
end

sine_wave(freq_hz) = sin.(2π .* freq_hz .* ts)

n = 45 # seconds
fs = 250
ts = range(0, n; length=fs * n)
data = 0.05 * sine_wave(0.75) + 0.01 * sine_wave(5.0) + 0.025 * sine_wave(10.0) +
       # lots of high frequency noise
       sum(100 * sine_wave(f) for f in 90:125)

resampling_ratio = 1 / 1.00592
resampled = resample(data, resampling_ratio)
ts_resampled = resample(ts, resampling_ratio)

rational_resampled = resample(data, rationalize(resampling_ratio))
rational_ts_resampled = resample(ts, rationalize(resampling_ratio))

# individual axes
fig = let
    fig = Figure()
    colors = Makie.wong_colors()
    ax_kwargs = (; ylabel="Data", limits=(nothing, nothing, -1, 1))

    ax1 = Axis(fig[1, 1]; ax_kwargs...)
    l1 = lines!(ax1, middle_third(ts), middle_third(plt_filters(data; fs)); color=colors[1])

    ax2 = Axis(fig[2, 1]; ax_kwargs...)
    l2 = lines!(ax2, middle_third(ts_resampled), middle_third(plt_filters(resampled; fs));
                color=colors[2])

    ax3 = Axis(fig[3, 1]; xlabel="Time (s)", ax_kwargs...)
    l3 = lines!(ax3, middle_third(rational_ts_resampled),
                middle_third(plt_filters(rational_resampled; fs)); color=colors[3])

    Legend(fig[4, 1], [l1, l2, l3], ["original", "resampled", "rational resampled"];
           orientation=:horizontal)
    fig
end

save("mwe.png", fig)

# combined
fig = let
    fig = Figure()
    colors = Makie.wong_colors()
    ax_kwargs = (; ylabel="Data", limits=(nothing, nothing, -1, 1))
    ax = Axis(fig[1, 1]; xlabel="Time (s)", ax_kwargs...)

    lines!(ax, middle_third(ts_resampled), middle_third(plt_filters(resampled; fs));
           color=colors[2], label="resampled")
    lines!(ax, middle_third(rational_ts_resampled),
           middle_third(plt_filters(rational_resampled; fs)); color=colors[3],
           label="rational resampled")
    lines!(ax, middle_third(ts), middle_third(plt_filters(data; fs)); color=colors[1],
           label="original")

    axislegend(ax)
    fig
end
save("mwe-combined.png", fig)
fig

Image
Image

@wheeheee
Copy link
Member

wheeheee commented Jan 18, 2025

I tried to resample using a FIRFilter and different , and found that it gets smoother with Nϕ=64 (there are still artifacts, presumably when ϕAccumulator jumps back). But then the resampled signal also becomes shifted / scaled, and after a bit of digging I found that these lines here don't seem to pass on correctly...

function FIRFilter(rate::AbstractFloat, Nϕ::Integer=32)
h = resample_filter(rate, Nϕ)
FIRFilter(h, rate)
end

On L200, FIRFilter looks like it should have an additional argument of , but upon fixing it, the smoothing effect previously observed disappears, so that's really not the problem here.

@wheeheee
Copy link
Member

wheeheee commented Jan 18, 2025

Ok, saw wrongly. Indeed, choosing a higher value of does reduce artifacts. Currently, that parameter is not exposed in resample, and also as raised in another issue resample_filter isn't really documented either, probably should do that too.

There are still going to be artifacts, but less, if you use something like

resample_phases(s, rate, Nϕ=32) = filt(FIRFilter(resample_filter(rate, Nϕ), rate, Nϕ), s)
resampled = resample_phases(data, resampling_ratio, 128)
ts_resampled = resample_phases(ts, resampling_ratio, 128)

@wheeheee
Copy link
Member

You could also play around with rel_bw in resample_filter to see if it can give you what you want. I find setting Nϕ=64 and rel_bw=0.7 looks good for this example at least.

@martinholters
Copy link
Member

When working on #596, I din't take a closer look at the algorithm, but from what I recollect, it does upsampling by an integer (?) followed by linear interpolation to obtain the proper points "in-between". Neither of those operations should cause those peaks significantly exceeding the input signal's range. So I do think this exposes some kind of bug, which may be circumvented by choosing suitable parameters, but preferably should be fixed, of course.

@wheeheee
Copy link
Member

wheeheee commented Jan 20, 2025

Just to add on, the used to create pfb for the rational resampling example here is quite large (6250 vs default 32 for float). So this might not just be a problem with FIRArbitrary resampling...
edit: the graphs shown here are the result of filtering the resampled signals with a bandpass filter...without that, the resampled signals don't seem to exceed the input signal's range.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants