HexoDSP icon indicating copy to clipboard operation
HexoDSP copied to clipboard

Goertzel DSP implementation

Open theloni-monk opened this issue 2 years ago • 7 comments

I think this mostly does what I want it to. Could probably use more testing.

theloni-monk avatar Jul 12 '22 22:07 theloni-monk

I have some concerns regarding the performance of this filter. Currently it recalculates the output over the complete 100 or 200 samples in it's buffer for every input sample. This should be checked in HexoSynth and the DSP CPU should be observed for this filter (eg. by testing as plugin in a DAW or via qjackctl). And maybe a note about the performance should be made in the help text for this node.

This is part of my DSP node checklist for adding this to HexoDSP:

  • Implement boilerplate (done)
  • Document boilerplate (done)
  • DSP implementation (done, except necessary improvements)
  • Parameter fine tuning (test in HexoSynth, if the parameters feel right)
  • DSP tests for all params
  • Ensure Documentation is properly formatted for the GUI
  • Add CHANGELOG.md entry to HexoSynth
  • Add table entry in README.md in HexoSynth

WeirdConstructor avatar Jul 13 '22 06:07 WeirdConstructor

Regarding the performance, I could see a different kind of node implementation as valuable for using it in a sound synthesis patch:

  • Have a Gz3Filt, which provides 3 outputs for 3 diffrerent frequencies
  • With 3 input parameters to configure the frequencies that should be detected.
  • Have an input setting parameter that defines the static resolution (the implied output delay) of the filter. Have the user decide if they want the result after X=1, 2, 4, 10, ... milliseconds. And mention in the documentation the accuracy implications this has (the longer they can wait, the more accurate the detection).
  • The outputs should output the most recently detected value and change each X milliseconds
  • Apply a user configurable slew limiter to each output as convenience

This exploits the efficiency of the Goertzel algorithm better in context of a modular synthesizer in my opinion. Because you get a precise (depending on the output delay settings) very fine multi band bandpass filter.

WeirdConstructor avatar Jul 13 '22 07:07 WeirdConstructor

Have an input setting parameter that defines the static resolution (the implied output delay) of the filter. Have the user decide if they want the result after X=1, 2, 4, 10, ... milliseconds. And mention in the documentation the accuracy implications this has (the longer they can wait, the more accurate the detection). The outputs should output the most recently detected value and change each X milliseconds

I'm interested in this implementation, so I have some questions. Feel free to answer as many as would not be annoying. For this would it be too expensive to have that latency but window it such that the value still changes every frame, i.e. take overlapping windows and compute. Also for the input frequencies, would these be parameters or inputs? could they be both? I agree the GzFilt3 would be more interesting. Are CV signals restricted to (-1,1) and we would have to map that to a frequency range? or can they be whatever?

theloni-monk avatar Jul 13 '22 11:07 theloni-monk

I'm interested in this implementation, so I have some questions. Feel free to answer as many as would not be annoying. For this would it be too expensive to have that latency but window it such that the value still changes every frame, i.e. take overlapping windows and compute.

How would the window work? The performance hit comes from calculating Q0, Q1 and Q2 for the whole buffer again and again per input sample. You can of course amortize this by recalculating only each 5, 10 or 50 samples. But that is not as cheap as just accepting the longer delays and run goertzel Q0/Q1/Q2 calculations only once per new input sample.

I believe, correct me if I am wrong, that is also the whole point for Goertzel vs. FFT, as detecting only a few known frequencies does not require a full FFT anymore, but only running Q0/Q1/Q2 calculation once per input sample.

Also for the input frequencies, would these be parameters or inputs?

In HexoDSP there are two kinds of inputs for a node: Input parameters inp and atoms at. Input parameters get the iconic octagon knobs and accept sample accurate inputs from other nodes. Atoms are settings that are not automateable, they are settings that are manually changed by a user typically.

could they be both? I agree the GzFilt3 would be more interesting. Are CV signals restricted to (-1,1) and we would have to map that to a frequency range? or can they be whatever?

All signals in the DSP graph are (kind of) restricted to [-1,1]. "Kind of" because that is more a convention than a technical restriction. Even though most nodes clamp their inputs to that range. The mapping between the DSP graph signal and more meaningful values that are used by the actual mathematic calculations is done by "denormalizing" the values. This is specified in the big parameter/input matrix in mod.rs and hidden in statements like these:

   let freq = inp::BOsc::freq(inputs);

WeirdConstructor avatar Jul 13 '22 12:07 WeirdConstructor

But that is not as cheap as just accepting the longer delays and run goertzel Q0/Q1/Q2 calculations only once per new input sample. I believe, correct me if I am wrong, that is also the whole point for Goertzel vs. FFT, as detecting only a few known frequencies does not require a full FFT anymore, but only running Q0/Q1/Q2 calculation once per input sample.

Yes and no... so the more samples you take the thinner the bins of the equivalent fft become, but the more you are averaging over the sample. So computing a new Q0/Q1/Q2 for each new sample instead of each new buffer is eqivalent to finding the amplitude that a given frequency has had averaged over the entire life of the node. This is why you want to compute it for a window over the input stream, instead of the whole input stream. Currently, I am making a new window every sample, this is needlessly expensive however. So as you say, it would make sense to have a param for how often to calculate with respect to a new window.

theloni-monk avatar Jul 13 '22 12:07 theloni-monk

Yes and no... so the more samples you take the thinner the bins of the equivalent fft become, but the more you are averaging over the sample. So computing a new Q0/Q1/Q2 for each new sample instead of each new buffer is eqivalent to finding the amplitude that a given frequency has had averaged over the entire life of the node. This is why you want to compute it for a window over the input stream, instead of the whole input stream. Currently, I am making a new window every sample, this is needlessly expensive however. So as you say, it would make sense to have a param for how often to calculate with respect to a new window.

Yes, I would love to give the user that control of how big the window is, which means how accurate the frequency they pass in via parameter is detected. And I would see Gz3Filt not using overlapping windows (which requires saving input samples in a buffer, which is often used for visualizing a spectrum of frequencies), but by resetting Q0/Q1/Q2 to 0 (like described in the website you linked) and restarting for the next window. That would also get rid of any internal buffering.

The output would then be the most recently computed amplitude of the frequency.

WeirdConstructor avatar Jul 13 '22 12:07 WeirdConstructor

Sounds good, I'll work on these over the next week or so when I have free time

theloni-monk avatar Jul 13 '22 12:07 theloni-monk