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

Frequency domain sweep synthesis #199

Merged
merged 49 commits into from Apr 25, 2024
Merged

Conversation

f-brinkmann
Copy link
Member

@f-brinkmann f-brinkmann commented Jun 2, 2021

Changes proposed in this pull request:

  • Add and test frequency domain sweep synthesis

  • make group delay an optional return parameter

  • apply 'none' fft normalisation

@f-brinkmann f-brinkmann added dsp feature For requesting new features labels Jun 2, 2021
Copy link
Member

@sikersten sikersten left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest a different structure by splitting up the functionality:

  • add domain option ('time' or 'frequency') to linear_sweep and exponential_sweep, explanation with ripples in respective other domain
  • add perfect_sweep function, this would avoid to have a lot of unused parameters
  • add function to generate sweep with given magnitude spectrum

I think this structure is a little more user-friendly. Of course the functions could share private methods needed for the sweep generation in the background.

@f-brinkmann
Copy link
Member Author

Yes, this would have the benefit of making the functions more explicit. The disadvantage is that time and frequency domain synthesis require different parameters, thus making the functions less explicit:

  • n_fade_out (time)
  • start_margin (freq)
  • stop_margin (freq)
  • double (freq)

That's maybe something we can discuss during the next weekly. I can image both...

Copy link
Member

@mberz mberz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only looked at naming of functions and properties so far.

return signal, group_delay


def magnitude_weighted_sweep(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The more I think about this, I'd prefer magnitude_spectrum_weighted_sweep:

Suggested change
def magnitude_weighted_sweep(
def magnitude_spectrum_weighted_sweep(

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - changed :)

pyfar/signals/deterministic.py Outdated Show resolved Hide resolved
Comment on lines 359 to 377
magnitude : Signal, string
Specify the magnitude response of the sweep.

signal
The magnitude response as :py:class:`~pyfar.classes.audio.Signal`
object. If ``signal.n_samples`` is smaller than `n_samples`, zeros
are padded to the end of `signal`. Note that `frequency_range` is
not required in this case.
``'linear'``
Design a sweep with linearly increasing frequency and a constant
magnitude spectrum.
``'exponential'``
Design a sweep with exponentially increasing frequency. The
magnitude decreases by 3 dB per frequency doubling and has constant
energy in fiters of relative constant bandwidth (e.g. octaves).
``'perfect'``
Perfect sweep according to [#]_. Note that the parameters
`start_margin`, `stop_margin`, `frequency_range` and `double` are
not required in this case.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling this magnitude is a bit misleading. The effect on the magnitude is only a secondary effect.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed it to sweep_type. But it should not be critical any more since it was moved to a private function

@f-brinkmann f-brinkmann requested a review from al-de January 7, 2022 12:42
@mberz mberz mentioned this pull request Sep 19, 2022
18 tasks
Base automatically changed from develop to main October 13, 2022 11:39
@f-brinkmann f-brinkmann changed the base branch from main to develop October 18, 2023 12:36
Copy link
Member

@ahms5 ahms5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you, small comments

pyfar/signals/deterministic.py Outdated Show resolved Hide resolved
Copy link
Member

@sikersten sikersten left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for implementing, great feature! I only had a look at the documentation yet :)

The linear sweep can also be generated in the frequency domain (see
:py:func:`~linear_sweep_freq`). Time domain synthesis exhibits a constant
temporal envelope in trade of slight ripples in the magnitude response.
Frequency domain synthesis exhibits smooth magnitude spectra and in trade
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Frequency domain synthesis exhibits smooth magnitude spectra and in trade
Frequency domain synthesis exhibits smooth magnitude spectra in trade

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

fade_in : int
Duration of a squared sine fade-in in samples. The fade starts at the
first sample of the sweep that is closer than 60 dB to the absolute
maximum of the sweep time signal. The default ``0`` does not apply
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default is None?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, corrected

fade_out : int
Duration of a squared cosine fade-out in samples. The fade ends at the
last sample of the sweep that is closer than 60 dB to the absolute
maximum of the sweep time signal. The default ``0`` does not apply
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default is None?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

Comment on lines 264 to 266
The order of the Butterworth filters that are applied to limit the
frequency range by a high-pass if ``frequency_range[0] > 0`` and/or by
a low-pass if ``frequency_range[1] < sampling_rate / 2``. The default
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest to move this information to frequency_range, where the Butterworth filter is already mentioned.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea. Done as suggested

pyfar/signals/deterministic.py Show resolved Hide resolved
Comment on lines 547 to 548
group_delay_sweep : FrequencyData
The group delay of the sweep in seconds as a single sided spectrum.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional?

Comment on lines +562 to +564
>>> magnitude = pf.dsp.filter.low_shelve(
... pf.signals.impulse(2**16), 500, 20, 2)
>>> magnitude = pf.dsp.filter.butterworth(magnitude, 8, 50, 'highpass')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about just using a simple bandpass instead? Then no one would need to think about how the low shelve filter looks like (as I did :D)

-------
sweep : Signal
The sweep signal. The Signal is in the time domain, has a maximum
absolute amplitude of 1 and the ``rms`` FFT normalization
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think rms normalization is not correct here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will be corrected

-------
sweep : Signal
The sweep signal. The Signal is in the time domain, has a maximum
absolute amplitude of 1 and the ``rms`` FFT normalization
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why rms normalization?

Comment on lines +640 to +646
>>> auto_correlation = np.empty(2**8)
>>> for idx, shift in enumerate(range(-2**7, 2**7)):
>>> auto_correlation[idx] = np.dot(
... sweep.time.flatten(),
... np.roll(sweep.time.flatten(), shift))
>>>
>>> auto_correlation /= pf.dsp.energy(sweep)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remember correctly, alternative methods to calculate the autocorrelation are the convolution with the time-flipped (real-valued) signal, or via the inverse fourier transform of the squared magnitude spectrum. Maybe these approaches lead to shorter code using pyfar.dsp functionality?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, this can be done in a more compact way and especially without a loop. For example using https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.correlate.html

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried this, but its not cyclic (regardless of mode and method) and causes ringing in the auto-correlation:

import pyfar as pf
import numpy as np
import scipy.signal as sgn
import matplotlib.pyplot as plt
%matplotlib inline

sweep = pf.signals.linear_perfect_sweep(2**8)
# compute auto-correlation
auto_correlation = sgn.correlate(
    sweep.time.flatten(), sweep.time.flatten())
auto_correlation /= pf.dsp.energy(sweep)
# plot auto-correlation
with pf.plot.context():
    plt.plot(np.arange(-2**7, 2**7), auto_correlation)
    plt.gca().set_xlim(-2**7, 2**7)
    plt.gca().set_xlabel('time lag in samples')
    plt.gca().set_ylabel('auto correlation')

I'm this leaving this as it was...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mh, this is a peculiar issue with the integral approximation in SciPy I'd assume...

pingelit
pingelit previously approved these changes Apr 5, 2024
Copy link
Contributor

@pingelit pingelit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sikersten has some good points that should be addressed. But I'll already aprove.

pingelit
pingelit previously approved these changes Apr 5, 2024
ahms5
ahms5 previously approved these changes Apr 10, 2024
Comment on lines 215 to 216
n_samples, frequency_range, start_margin, stop_margin, fade_in=None,
fade_out=None, butterworth_order=8, sampling_rate=44100):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the default for the fade in and out None, when the docstring states that the default is 0 samples?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might have reviewed an outdated version. The docstring mentions that the default is None after someone found this in an earlier round.

Comment on lines 261 to 265
butterworth_order : int, None
The order of the Butterworth filters that are applied to limit the
frequency range by a high-pass if ``frequency_range[0] > 0`` and/or by
a low-pass if ``frequency_range[1] < sampling_rate / 2``. The default
is ``8``.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's a bit more intuitive to call this argument bandpass order or similar?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, renamed to bandpass_orderbut left the description as it was.

Comment on lines 707 to 710
S. Müller, P. Massarani. 'Transfer Function Measurement with Sweeps.
Directors Cut Including Previously Unreleased Material and some
Corrections. J. Audio Eng. Soc. 2001, 49 (6), 443-471 (all equations
referenced in the code refer to this).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatted

Comment on lines +640 to +646
>>> auto_correlation = np.empty(2**8)
>>> for idx, shift in enumerate(range(-2**7, 2**7)):
>>> auto_correlation[idx] = np.dot(
... sweep.time.flatten(),
... np.roll(sweep.time.flatten(), shift))
>>>
>>> auto_correlation /= pf.dsp.energy(sweep)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, this can be done in a more compact way and especially without a loop. For example using https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.correlate.html


The exponential sweep can also be generated in the time domain
(:py:func:`~exponential_sweep_time`). Frequency domain synthesis
exhibits smooth magnitude spectra in trade of a slightly irregular
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
exhibits smooth magnitude spectra in trade of a slightly irregular
exhibits smooth magnitude spectra at the cost of a slightly irregular

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed the wording everywhere

The length of the sweep in samples.
sampling_rate : int, optional
The sampling rate in Hz. The default is ``44100``.
group_delay : boolean, optional
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
group_delay : boolean, optional
return_group_delay : boolean, optional

fade_in=None,
fade_out=None,
sampling_rate=sampling_rate,
group_delay=group_delay)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
group_delay=group_delay)
return_group_delay=reuturn_group_delay)



def _frequency_domain_sweep(
n_samples, sweep_type, frequency_range, butterworth_order,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bandpass

def _frequency_domain_sweep(
n_samples, sweep_type, frequency_range, butterworth_order,
start_margin, stop_margin, fade_in, fade_out, sampling_rate,
group_delay):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
group_delay):
return_group_delay):


# normalize to time domain amplitude of almost one
# (to avoid clipping if written to fixed point wav file)
sweep = pyfar.dsp.normalize(sweep) * (1 - 2**-15)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
sweep = pyfar.dsp.normalize(sweep) * (1 - 2**-15)
sweep = pyfar.dsp.normalize(sweep)

That is not something this function should take care of. That's the problem of a wav writer!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed

@f-brinkmann f-brinkmann dismissed stale reviews from ahms5 and pingelit via 13f12b7 April 24, 2024 14:42
Copy link
Member Author

@f-brinkmann f-brinkmann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied everything as suggested apart from using scipy.signal.correlate (see comment above)

Comment on lines 215 to 216
n_samples, frequency_range, start_margin, stop_margin, fade_in=None,
fade_out=None, butterworth_order=8, sampling_rate=44100):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might have reviewed an outdated version. The docstring mentions that the default is None after someone found this in an earlier round.

Comment on lines 261 to 265
butterworth_order : int, None
The order of the Butterworth filters that are applied to limit the
frequency range by a high-pass if ``frequency_range[0] > 0`` and/or by
a low-pass if ``frequency_range[1] < sampling_rate / 2``. The default
is ``8``.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, renamed to bandpass_orderbut left the description as it was.

Comment on lines 707 to 710
S. Müller, P. Massarani. 'Transfer Function Measurement with Sweeps.
Directors Cut Including Previously Unreleased Material and some
Corrections. J. Audio Eng. Soc. 2001, 49 (6), 443-471 (all equations
referenced in the code refer to this).
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formatted

pyfar/signals/deterministic.py Show resolved Hide resolved
def exponential_sweep_freq(
n_samples, frequency_range, start_margin, stop_margin, fade_in=None,
fade_out=None, butterworth_order=8, sampling_rate=44100,
group_delay=False):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, changed everywhere

Comment on lines 453 to 456
group_delay : boolean, optional
Return the analytical group delay of the sweep. This can be used to
compute the times at which distortion products appear. The default is
``False``.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

Comment on lines +572 to +578
>>> import pyfar as pf
>>> magnitude = pf.dsp.filter.low_shelve(
... pf.signals.impulse(2**16), 500, 20, 2)
>>> magnitude = pf.dsp.filter.butterworth(magnitude, 8, 50, 'highpass')
>>> sweep = pf.signals.magnitude_spectrum_weighted_sweep(
... 2**16, magnitude, 5000, 1000)
>>> pf.plot.time_freq(sweep)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Id like to keep it simple, here - especially while our interpolation function still overshoots :(

Comment on lines 599 to 600
system identification. It is orthogonal to delayed versions of itself and
can be played back in a loop.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed it - although infinitely long signals are hard to play in a loop

Comment on lines +640 to +646
>>> auto_correlation = np.empty(2**8)
>>> for idx, shift in enumerate(range(-2**7, 2**7)):
>>> auto_correlation[idx] = np.dot(
... sweep.time.flatten(),
... np.roll(sweep.time.flatten(), shift))
>>>
>>> auto_correlation /= pf.dsp.energy(sweep)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried this, but its not cyclic (regardless of mode and method) and causes ringing in the auto-correlation:

import pyfar as pf
import numpy as np
import scipy.signal as sgn
import matplotlib.pyplot as plt
%matplotlib inline

sweep = pf.signals.linear_perfect_sweep(2**8)
# compute auto-correlation
auto_correlation = sgn.correlate(
    sweep.time.flatten(), sweep.time.flatten())
auto_correlation /= pf.dsp.energy(sweep)
# plot auto-correlation
with pf.plot.context():
    plt.plot(np.arange(-2**7, 2**7), auto_correlation)
    plt.gca().set_xlim(-2**7, 2**7)
    plt.gca().set_xlabel('time lag in samples')
    plt.gca().set_ylabel('auto correlation')

I'm this leaving this as it was...


# normalize to time domain amplitude of almost one
# (to avoid clipping if written to fixed point wav file)
sweep = pyfar.dsp.normalize(sweep) * (1 - 2**-15)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed

@f-brinkmann f-brinkmann requested a review from mberz April 24, 2024 14:44
Copy link
Member

@mberz mberz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fade in and fade out parameters are inconsistent with the time domain implementation of the sweep signals

Comment on lines 216 to 217
n_samples, frequency_range, start_margin, stop_margin, fade_in=None,
fade_out=None, bandpass_order=8, sampling_rate=44100,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
n_samples, frequency_range, start_margin, stop_margin, fade_in=None,
fade_out=None, bandpass_order=8, sampling_rate=44100,
n_samples, frequency_range, start_margin, stop_margin, fade_in=0,
fade_out=0, bandpass_order=8, sampling_rate=44100,

Is this type conversion required here? The docstring states that the type is 'int'
Also, this parameter is inconsistent with the time domain sweep generation function, which uses n_fade_in.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats true - renamed to n_fade_* and used 0 as the default

Comment on lines 397 to 398
n_samples, frequency_range, start_margin, stop_margin, fade_in=None,
fade_out=None, bandpass_order=8, sampling_rate=44100,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
n_samples, frequency_range, start_margin, stop_margin, fade_in=None,
fade_out=None, bandpass_order=8, sampling_rate=44100,
n_samples, frequency_range, start_margin, stop_margin, fade_in=0,
fade_out=0, bandpass_order=8, sampling_rate=44100,

see above

Comment on lines 501 to 502
n_samples, magnitude_spectrum, start_margin, stop_margin,
fade_in=None, fade_out=None, sampling_rate=44100,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see above

Comment on lines +640 to +646
>>> auto_correlation = np.empty(2**8)
>>> for idx, shift in enumerate(range(-2**7, 2**7)):
>>> auto_correlation[idx] = np.dot(
... sweep.time.flatten(),
... np.roll(sweep.time.flatten(), shift))
>>>
>>> auto_correlation /= pf.dsp.energy(sweep)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mh, this is a peculiar issue with the integral approximation in SciPy I'd assume...

Comment on lines +728 to +731
.. [#] S. Müller, P. Massarani. 'Transfer Function Measurement with Sweeps.
Directors Cut Including Previously Unreleased Material and some
Corrections.' J. Audio Eng. Soc. 2001, 49 (6), 443-471. (all
equations referenced in the code refer to this).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This citation is only suited for non-perfect sweeps, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes and no I'd say. It does not mention perfect sweeps if I remember correctly but the method generally also holds for the perfect linear sweep at least.

@f-brinkmann f-brinkmann requested a review from mberz April 25, 2024 08:27
@f-brinkmann f-brinkmann merged commit 40622ad into develop Apr 25, 2024
9 checks passed
@f-brinkmann f-brinkmann deleted the feature/spectral_sweep_synthesis branch April 25, 2024 08:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dsp feature For requesting new features v0.7.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants