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
Conversation
There was a problem hiding this 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'
) tolinear_sweep
andexponential_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.
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:
That's maybe something we can discuss during the next weekly. I can image both... |
There was a problem hiding this 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.
pyfar/signals/deterministic.py
Outdated
return signal, group_delay | ||
|
||
|
||
def magnitude_weighted_sweep( |
There was a problem hiding this comment.
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:
def magnitude_weighted_sweep( | |
def magnitude_spectrum_weighted_sweep( |
There was a problem hiding this comment.
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
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
Co-authored-by: Marco Berzborn <mberz@users.noreply.github.com>
…/pyfar/pyfar into feature/spectral_sweep_synthesis
There was a problem hiding this 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
There was a problem hiding this 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 :)
pyfar/signals/deterministic.py
Outdated
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Frequency domain synthesis exhibits smooth magnitude spectra and in trade | |
Frequency domain synthesis exhibits smooth magnitude spectra in trade |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default is None
?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default is None
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed
pyfar/signals/deterministic.py
Outdated
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
Outdated
group_delay_sweep : FrequencyData | ||
The group delay of the sweep in seconds as a single sided spectrum. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional?
>>> magnitude = pf.dsp.filter.low_shelve( | ||
... pf.signals.impulse(2**16), 500, 20, 2) | ||
>>> magnitude = pf.dsp.filter.butterworth(magnitude, 8, 50, 'highpass') |
There was a problem hiding this comment.
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)
pyfar/signals/deterministic.py
Outdated
------- | ||
sweep : Signal | ||
The sweep signal. The Signal is in the time domain, has a maximum | ||
absolute amplitude of 1 and the ``rms`` FFT normalization |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will be corrected
pyfar/signals/deterministic.py
Outdated
------- | ||
sweep : Signal | ||
The sweep signal. The Signal is in the time domain, has a maximum | ||
absolute amplitude of 1 and the ``rms`` FFT normalization |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why rms
normalization?
>>> 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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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...
There was a problem hiding this 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.
pyfar/signals/deterministic.py
Outdated
n_samples, frequency_range, start_margin, stop_margin, fade_in=None, | ||
fade_out=None, butterworth_order=8, sampling_rate=44100): |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
pyfar/signals/deterministic.py
Outdated
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``. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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_order
but left the description as it was.
pyfar/signals/deterministic.py
Outdated
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). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Formatting
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
formatted
>>> 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) |
There was a problem hiding this comment.
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
pyfar/signals/deterministic.py
Outdated
|
||
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
exhibits smooth magnitude spectra in trade of a slightly irregular | |
exhibits smooth magnitude spectra at the cost of a slightly irregular |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed the wording everywhere
pyfar/signals/deterministic.py
Outdated
The length of the sweep in samples. | ||
sampling_rate : int, optional | ||
The sampling rate in Hz. The default is ``44100``. | ||
group_delay : boolean, optional |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
group_delay : boolean, optional | |
return_group_delay : boolean, optional |
pyfar/signals/deterministic.py
Outdated
fade_in=None, | ||
fade_out=None, | ||
sampling_rate=sampling_rate, | ||
group_delay=group_delay) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
group_delay=group_delay) | |
return_group_delay=reuturn_group_delay) |
pyfar/signals/deterministic.py
Outdated
|
||
|
||
def _frequency_domain_sweep( | ||
n_samples, sweep_type, frequency_range, butterworth_order, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bandpass
pyfar/signals/deterministic.py
Outdated
def _frequency_domain_sweep( | ||
n_samples, sweep_type, frequency_range, butterworth_order, | ||
start_margin, stop_margin, fade_in, fade_out, sampling_rate, | ||
group_delay): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
group_delay): | |
return_group_delay): |
pyfar/signals/deterministic.py
Outdated
|
||
# 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed
There was a problem hiding this 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)
pyfar/signals/deterministic.py
Outdated
n_samples, frequency_range, start_margin, stop_margin, fade_in=None, | ||
fade_out=None, butterworth_order=8, sampling_rate=44100): |
There was a problem hiding this comment.
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.
pyfar/signals/deterministic.py
Outdated
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``. |
There was a problem hiding this comment.
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_order
but left the description as it was.
pyfar/signals/deterministic.py
Outdated
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). |
There was a problem hiding this comment.
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
Outdated
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): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good point, changed everywhere
pyfar/signals/deterministic.py
Outdated
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``. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed
>>> 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) |
There was a problem hiding this comment.
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 :(
pyfar/signals/deterministic.py
Outdated
system identification. It is orthogonal to delayed versions of itself and | ||
can be played back in a loop. |
There was a problem hiding this comment.
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
>>> 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) |
There was a problem hiding this comment.
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...
pyfar/signals/deterministic.py
Outdated
|
||
# 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed
There was a problem hiding this 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
pyfar/signals/deterministic.py
Outdated
n_samples, frequency_range, start_margin, stop_margin, fade_in=None, | ||
fade_out=None, bandpass_order=8, sampling_rate=44100, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
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
pyfar/signals/deterministic.py
Outdated
n_samples, frequency_range, start_margin, stop_margin, fade_in=None, | ||
fade_out=None, bandpass_order=8, sampling_rate=44100, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
pyfar/signals/deterministic.py
Outdated
n_samples, magnitude_spectrum, start_margin, stop_margin, | ||
fade_in=None, fade_out=None, sampling_rate=44100, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see above
>>> 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) |
There was a problem hiding this comment.
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...
.. [#] 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). |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
Changes proposed in this pull request:
Add and test frequency domain sweep synthesis
make group delay an optional return parameter
apply 'none' fft normalisation