I'm trying to do some DSP that I have never done before and a nudge into the right direction would come in handy. The context is replication of this project.

The system architecture is the following:

system block diagram

An excitation signal is generated by a DAC. I use a 10 sample sine table to generate a 200 kHz sine wave which is subsequently filtered and the resulting SINAD is about 60 dB. The signal is then connected to the eight port passive system (see project description) via two analogue multiplexers, one carrying the excitation signal and the other the reference GND. All eight ports are connected to a 12 bit (at least 10 ENOB) ADC. The DC component is centred within the ADCs FSR by the input filter. The DAC and the ADCs are integrated into a microcomputer (Cortex-M4F).

The information I'm interested in is in the incoming signal's amplitude.

My current solution is the following:

  • sample the signal with 1.25 Msps and collect 256 samples (about 40 periods of the signal with about 6 samples per period)
  • estimate the signals mean and MS (mean square, like RMS but without applying the sqrt) using the 256 samples and the common equations
  • calculate the MS of the AC component with $$ U_{RMS} = \sqrt{U_{DC}^2 + U_{AC,RMS}^2} \Leftrightarrow ()^2\\ U_{MS} = U_{DC}^2 + U_{AC,MS} \Leftrightarrow\\ U_{AC,MS} = U_{MS} - U_{DC}^2 $$

I'm happy with the MS of the AC component because it incorporates the information, just like the amplitude of the signal because of their direct relation.

I've read a few articles, especially this one Precision measurement of sine wave amplitude with ADC which sounded like it was exactly my problem. I couldn't wrap my head around the accepted solution though and thus asking this question.

I'm not very experienced in DSP and don't know efficient algorithms to make the most of my simple setup. What would you do to measure the signals amplitude, especially in the context of maximising the result's ENOB under the condition of 256 samples?

I captured some ADC data. Have a look at it here to get an impression of the signal's properties.

The steps in the signal amplitude hail from switching the system excitation and measurement ports to a different state. I included the switching action because it gives an impression of the signal conditions in all relevant cases, amplitude-wise. The sections with no visible signal aren't relevant, that's the case when the specific ADC channel is connected to the reference ground.


I started with the implementation of Olli's and Cedron's idea today. Let me show you the code I wrote.

First I generated the sine and cosine tables for the DFT. First try, I didn't choose a period of 45 samples, as Olli suggested, but the theoretical length of

$$ \frac{samples}{period} = \frac{f_{sample}}{f_{signal}} = \frac{1.25~MHz}{200~kHz} = \frac{25}{4}\\ N_{samples} = \frac{25}{4} \cdot N_{periods} \mid N_{samples}, N_{periods} \in \mathbb{N}^+\\ \Rightarrow N_{periods} = 4 \cdot k \mid k \in \mathbb{N}^+ $$

which means that my tables need to be 25 entries long. I generated them with Matlab.

s1_15 = quantizer('fixed', 'round', 'saturate', [16, 15]);

x = linspace(0, 4 * 2 * pi, 25 + 1);
x(end) = [];

sin_table = num2int(s1_15, sin(x));
cos_table = num2int(s1_15, cos(x));

My previous sample buffer was 256 deep, the new sample buffer holds 250, the next smallest length that meets the requirement above.

// ch1 and ch2 alternating
u16 adc_data[500];

// sine and cosine table for single frequency DFT calculation
// s1.15 number format
const s16 dft_sin_table[25] = {0, 27667, 29649, 4107, -25248, -31164, -8149, 22431, 32188, 12063, -19261, -32703, -15786, 15786, 32703, 19261, -12063, -32188, -22431, 8149, 31164, 25248, -4107, -29649, -27667};

const s16 dft_cos_table[25] = {32767, 17558, -13952, -32510, -20887, 10126, 31739, 23887, -6140, -30467, -26510, 2058, 28715, 28715, 2058, -26510, -30467, -6140, 23887, 31739, 10126, -20887, -32510, -13952, 17558};

void evaluation(void) {
    // Accumulators for dot product
    // s21.15 number format
    s64 dot_prod_sin[2] = {0};
    s64 dot_prod_cos[2] = {0};

    // Index for sine and cosine lookup
    uint table_index = 0;

    // for each sample in sample buffer
    for (uint it_sample = 0; it_sample <= 500 - 1; it_sample += 2) {
        // First channel

        // sample in u12.0 number format
        // multiplication result in s13.15 number format
        dot_prod_sin[0] += adc_data[it_sample] * dft_sin_table[table_index];
        dot_prod_cos[0] += adc_data[it_sample] * dft_cos_table[table_index];

        // Second channel

        // sample in u12.0 number format
        // multiplication result in s13.15 number format
        dot_prod_sin[1] += adc_data[it_sample + 1] * dft_sin_table[table_index];
        dot_prod_cos[1] += adc_data[it_sample + 1] * dft_cos_table[table_index];

        // Increment and wrap table index
        table_index += 1;
        if (table_index > 24) {
            table_index = 0;

The first results looked pretty good and where lots more expressive than my previous solution. The computational complexity was totally acceptable and worked for me.

  • 1
    $\begingroup$ @Fat32 I got your point, they are users on this forum xD $\endgroup$
    – Simon
    Commented Jan 15, 2019 at 22:47
  • 2
    $\begingroup$ @OlliNiemitalo I finally managed to capture some data xD. I added a link to the data and a plot, so you can check whether you imported the data correctly. $\endgroup$
    – Simon
    Commented Jan 16, 2019 at 17:18
  • 2
    $\begingroup$ @Fat32, Hmmmm, don't confuse us with the law firm of "Marcus, Olli, Cedron and Kootsookos", aka MOCK. ;-) $\endgroup$ Commented Jan 17, 2019 at 5:11
  • 1
    $\begingroup$ @OlliNiemitalo They are indeed. I added a block diagram to make the system structure more clear. $\endgroup$
    – Simon
    Commented Jan 17, 2019 at 11:05
  • 2
    $\begingroup$ You do realize that there is a standard IEEE algorithm for that? IEEE-STD-1057 pdfs.semanticscholar.org/d47e/… $\endgroup$
    – Ben
    Commented Jan 24, 2019 at 21:25

3 Answers 3


In a way this does not directly answer your question, because I will be suggesting that you change the section length from 256 to 225 (or 270 if you can afford it) to do coherent sampling.

Data analysis

Within each 256-sample section, your data seems to be periodic with period 45. Because the oscillator and the digital-to-analog converter (DAC) clocks originate from the same master clock via frequency dividers and/or multipliers, the discrete-time signal is bound to be periodic. That is, if we do not take into account possible time-varying qualities of the non-digital part of the system. That the frequency of interest is a known constant is a great advantage, because then only the amplitude of the oscillation at that frequency needs to be detected, not the frequency itself.

enter image description here
Figure 1. First 256 samples of channel 1. The horizontal axis grid is distributed every 45 samples. Lines connect the samples for easier counting of oscillation cycles of which there appear to be $7$ per period. The continuous-time frequency of interest is 194.444... kHz, $7/45$ times the sampling frequency, a bit off from the specified 200 kHz.

Fourier method

If the apparent discrete-time period $P = 45$ is true or very nearly true, it will be advantageous to do the analysis over $N = PL$ samples, where $L$ is integer. This analysis length sets up discrete Fourier transform (DFT) bins at 1) the negative frequency of interest, 2) the positive frequency of interest, and 3) zero frequency. A sinusoid consists of both a negative-frequency and a positive-frequency complex exponential: $\cos(x)$ $=$ $\tfrac{1}{2}e^{-ix}$ $+$ $\tfrac{1}{2}e^{ix}.$ The complex exponential representation is convenient, because the magnitude of either the positive or the negative frequency DFT bin directly gives half the mid-to-top amplitude of the sinusoid. The $N$ complex exponential basis functions of DFT are orthogonal, that is to say uncorrelated. So any one of the three listed input signal components present can be detected with no disturbance from the presence of the other two.

Setting up the positive frequency of interest as $\omega = 2\pi\times7/45$ and the analysis window length as $N = 45\times5 = 225,$ the value of the DFT bin of interest is calculated from data $x$ as:

$$X_\omega = \frac{1}{N}\sum_{k=0}^{N-1}e^{-i\omega k}x[k] = \frac{1}{N}\sum_{k=0}^{N-1}\big(\cos(\omega k) - i\sin(\omega k)\big)x[k]$$

If $x$ was just a sinusoid (cosine) of mid-to-peak amplitude $A$ and initial phase $\theta$ we would obtain:

$$X_\omega = \frac{1}{N}\sum_{k=0}^{N-1}e^{-i\omega k}A\cos(\omega k + \theta) = \frac{Ae^{i\theta}}{2} + \frac{Ae^{-i(2\theta - 2\omega + \pi)/2} - Ae^{-i(4N\omega + 2\theta - 2\omega + \pi)/2}}{4N\sin(\omega)}$$

If $A\cos(\omega k)$ is $2N\text{-}$periodic, then the last term above vanishes. In coherent sampling $A\cos(\omega k)$ is $N\text{-}$periodic and therefore also $2N\text{-}$periodic, in which case we have:

$$|X_\omega| = \frac{A}{2}\quad\Rightarrow\quad A = 2|X_\omega|$$

The estimated mean square power of the sinusoid at the same frequency is:


This is related to the estimated mid-to-peak amplitude of the sinusoid by:

$$A = 2\times\sqrt{|X_\omega|^2} = \tfrac{2}{\sqrt{2}}\times\sqrt{2|X_\omega|^2} = \sqrt{2}\times\sqrt{2|X_\omega|^2}$$

where $\sqrt{2}$ is the crest factor of sine wave. The squared magnitude of a complex number $C$ can be calculated by $|C|^2 = \operatorname{Re}(C)^2 + \operatorname{Im}(C)^2.$

Depending on how you use the obtained results, often you can do what you want with an intermediate result. In that case it may not be needed to calculate the normalization by $\frac{1}{N}$ or $\frac{2}{N},$ to multiply by $\sqrt{2},$ or to calculate the square root to get from squared magnitude to magnitude.

You can do the estimation directly using memory-stored complex exponential or cosine and sine coefficients (like @CedronDawg), using the Goertzel algorithm, or by calculating a full DFT (not recommended).

With coherent sampling, the sinusoid mean square power estimate $2|X_\omega|^2$ is equivalent to IEEE 1057 3-parameter estimate (known frequency, unknown sinusoid phase, sinusoid amplitude and noise level), because of the orthogonality of the DFT basis vectors. $2|X_\omega|^2$ is a biased estimate of the mean square sinusoid power, as we will see in a later section. This is noted in Alegria, 2009.


If there is no coherent sampling relationship between the analysis window length and the frequency of interest, then the analysis scheme could be modified in one of two ways (not recommended), with unfavorable consequences:

  • Use the DFT bin nearest to the frequency of interest: In this case the zero-frequency bias does not disturb detection (still orthogonal to the complex exponential corresponding to the selected bin), but detection is not perfect. The detected amplitude fluctuates with the phase of the frequency of interest.
  • Allow the frequency of interest $\omega$ to be of form other than $2\pi n/N,$ where $n$ is integer: The complex exponential of frequency $\omega$ is not orthogonal to the zero-frequency or to frequency $-\omega$ complex exponentials, both of which will pollute detection. The detected amplitude which fluctuates with the phase of frequency $-\omega$. (One exception is that if the scheme is modified by allowing $N = PK / 2$ where $K$ is integer, then the positive and negative frequency of interest complex exponentials will be orthogonal, but the zero frequency is not orthogonal to those and will still pollute detection).

We can confirm the validity of the above statements for your problem by testing detection on a zero-noise synthetic version of your signal by calculating the normalized detected amplitude as:

$$\frac{2}{N}\left|\sum_{k=0}^{N-1}\Big(b + \cos\left(2\pi \tfrac{7}{45} k + \alpha\right)\Big)e^{-i2\pi f k}\right|$$

where $f$ is the detector frequency which is a DFT bin frequency if it is in format $M/N,$ where $M$ is an integer, $\alpha$ is the phase of the unit-amplitude sine wave to be detected, and $b$ is the zero frequency bias. The results look like this:

enter image description here
Figure 2. Fourier method detected amplitude as function of phase $\alpha$ for:
black: $N = 225, f = 7/45, b = 0 \text{ or } b = 1,$
red: $N = 256, f = 7/45, b = 0,$
blue: $N = 256, f = 7/45, b=1,$ and
purple: $N = 256, f = 40/256, b = 0 \text{ or } b = 1.$
For the purple curve, $f$ chooses the nearest bin, noting that $256\times7/45 = 39.8222\ldots$

As can be seen in Fig. 2, perfect amplitude detection is achieved when $N = 225$ (black line), not when $N = 256$ (other colored lines).

SNR estimation

The residual noise $r$ is formed by subtracting from the noisy signal $x$ the fitted sinusoid $y$ and the mean of $x$:

$$X_0 = \frac{1}{N}\sum_{k=0}^{N-1}x[k],\quad X_\omega = \frac{1}{N}\sum_{k=0}^{N-1}x[k]e^{-i\omega k}$$ $$y[k] = 2\operatorname{Re}(X_\omega)\cos(\omega k) - 2\operatorname{Im}(X_\omega)\sin(\omega k)\quad\text{for all $k$}$$ $$r[k] = x[k] - X_0 - y[k]\quad\text{for all $k$}$$

enter image description here
Figure 3. Residual noise $r[k]$ in coherent sampling for the first 225 samples of channel 1.

The signal-to-noise ratio (SNR) can be estimated by:

$$\text{SNR} = \frac{\frac{1}{N}\sum_{k=0}^{N-1}y[k]^2}{\frac{1}{N}\sum_{k=0}^{N-1}r[k]^2} = \frac{\sum_{k=0}^{N-1}y[k]^2}{\sum_{k=0}^{N-1}r[k]^2}$$

For the first 256 samples of the channel 1 (Figs. 1 and 3), the estimated signal-to-noise ratio is 4746 (36.8 dB). With coherent sampling, the signal model components and the residual are orthogonal, and we can write the same SNR estimate simply as:

$$\mathrm{SNR} = \frac{2|X_\omega|^2}{\frac{1}{N}\sum_{k=0}^{N-1}x[k]^2 - X_0^2 - 2|X_\omega|^2}$$

Bias correction

With white additive noise, the above coherent sampling SNR estimate will be optimistically biased, because some of the noise power is subtracted from itself by hiding in the three DFT bins of the detected signal components. This implies that the detected amplitude is also biased; the more there is true noise, the larger the amplitude detected. The following (rather intuitively constructed) formula gives the spectral subtraction estimate of the mean square of the signal, if the noise is white:

$$2|X_\omega|^2 - \frac{2}{N}\left(\frac{N}{N-3}\left({\frac{1}{N}\sum_{k=0}^{N-1}x[k]^2 - X_0^2 - 2|X_\omega|^2}\right)\right)$$

The guiding principle of this is that the square of the absolute value of each of the $N$ DFT bins has on average an equal portion of the total noise power added to it. The mean square of noise is first estimated (the part of the formula in the outer parentheses) based on the $N-3$ bins that do not include frequencies $-\omega,$ $0,$ and $\omega,$ and the estimated contribution of the noise to the biased estimate of mean square of the signal, $2|X_\omega|^2,$ is subtracted from it. This subtraction may give a negative estimate, which would result in a larger estimation error than if the estimate was clamped to zero from below. This is done in the clamped spectral subtraction estimate of the mean square of the signal:

$$\max\left(2|X_\omega|^2 - \frac{2}{N-3}\left({\frac{1}{N}\sum_{k=0}^{N-1}x[k]^2 - X_0^2 - 2|X_\omega|^2}\right),\, 0\right)$$

enter image description here
Figure 4. Simulated coherent sampling ($N=225,$ $\omega = 2\pi\times7/45$) mean square signal estimation in presence of additive Gaussian white noise. From top to bottom: Mean of @Simon's method estimate (green), the biased estimate or equivalently the IEEE 1057 3-parameter estimate (red), the clamped spectral subtraction estimate (purple), the spectral subtraction estimate (yellow), and true (blue) signal power, through 1000 Monte Carlo estimations, as function of true signal power.

enter image description here
Figure 5. Mean square of estimation error as a fraction of mean square signal power in presence of additive Gaussian white noise. The same simulation and color code as in Fig. 4. (Less is better.)

enter image description here
Figure 6. The same as Fig. 5 but relative to the mean fractional estimation error for the biased estimate of mean square signal. (Less is better.)

Fig. 4 demonstrates the seemingly unbiased quality (all the way to the Monte Carlo noise floor) of the spectral subtraction estimator. The clamped spectral subtraction estimate is biased. Mean square of the white Gaussian noise process was kept at 1 (0 dB), so the horizontal axis is also the true SNR. As expected, the plots were found not to be affected by the amount of zero frequency offset. @Simon's method is described in a section further down. @Simon's method estimate of mean square signal is:

$$\frac{1}{N}\sum_{k=0}^{N-1}x[k]^2 - X_0^2$$

More important than the mean of multiple estimates (Fig. 4), which shows whether there is bias, is the mean square error of the estimates (Fig. 5). Compared (Fig. 6) to the biased estimator, the spectral subtraction estimator and the clamped spectral subtraction estimator become useful (somewhat more accurate) for SNR below about -10 dB, and @Simon's method becomes useful (does not give too much more error) for SNR above about 30 dB. At very low SNR, compared to the biased estimator, @Simon's method is about 38 dB worse, and the spectral subtraction estimator about 3 dB better, clamping improving this to 4 dB better, even though the clamped estimator is (more) biased. At high SNR, there appears to be no accuracy penalty from choosing a more advanced method of the four, only extra computational cost.

The implied clamped spectral subtraction SNR estimator derived from the clamped spectral subtraction estimator of mean square signal is:

$$\mathrm{SNR} = \frac{\max\left(2|X_\omega|^2 - \frac{2}{N-3}\left({\frac{1}{N}\sum_{k=0}^{N-1}x[k]^2 - X_0^2 - 2|X_\omega|^2}\right),\,0\right)}{\frac{N}{N-3}\left({\frac{1}{N}\sum_{k=0}^{N-1}x[k]^2 - X_0^2 - 2|X_\omega|^2}\right)} = \max\left(\frac{2|X_\omega|^2\left(1-\frac{3}{N}\right)}{{\frac{1}{N}\sum_{k=0}^{N-1}x[k]^2 - X_0^2 - 2|X_\omega|^2}}-\frac{2}{N},\,0\right)$$

When plotted, the SNR estimators looks nearly identical to the curves of the corresponding mean square signal estimators of Fig. 4.

Further ideas

If there is not much noise and quantization error is significant, and you want to get the best performance, then you could ensure that the shortest period in the discrete signal matches your analysis window length. This avoids repeating the same quantization error (See Fig. 1 for the repeats of the waveform) for equally repeated values of the complex exponential in the DFT bin calculation. These coherent repeats cause an increase in accumulation of quantization error. Just changing your sine table length from 10 to 11 should increase the shortest period from 45 samples to 99 samples $(7/45\times{10/11}$ $=$ $14/99).$ This changes the oscillator frequency, too, which may be a problem. A higher DAC sampling frequency and sine generator table size would give more flexibility in fine tuning.

If you know the phase of the sinusoid, you can improve the detection further. Instead of a cosine and a sine table, which you "compare" the signal against in DFT, you could have a single table already in the correct phase.

It may be that you have both phase noise and additive noise in your data.

@Simon's method

The algorithm you are currently using can also be analyzed in zero-noise conditions, in which it calculates the detected amplitude from the noise-free synthetic signal as:

$$\sqrt{2}\times\sqrt{\frac{1}{N}\sum_{k=0}^{N-1}\Big(b + \cos\left(2\pi \tfrac{7}{45} k + \alpha\right)\Big)^2 - \left(\frac{1}{N}\sum_{k=0}^{N-1}\Big(b + \cos\left(2\pi \tfrac{7}{45} k + \alpha\right)\Big)\right)^2},$$

Under ideal conditions, the method works perfectly with $N = 225$ and has better performance at $N = 256$ than any of the Fourier approaches for the same $N:$

enter image description here
Figure 7. @Simon's zero-frequency power subtraction method detected amplitude as function of phase $\alpha$ for:
black: $N = 225, b = 0 \text{ or } b = 1$ and
red: $N = 256, b = 0 \text{ or } b = 1.$

Your method subtracts from the estimated total power the estimated zero-frequency power. It does not have the wide-band frequency discrimination quality of the suggested Fourier approach (See: Fourier transform of the rectangular window) and will have inferior performance if there is significant wide-band noise in the signal (see Figs. 4 and 5). One wide-band noise source is analog-to-digital converter (ADC) quantization noise. On average, its power stays roughly constant over different amplitudes of the signal of interest.

Octave script for Figs. 4 and 5

Set M = 1000 (very slow) in the script to reproduce Figs. 4-6. After the main calculation, you can run the plotting parts of the script separately to get individual plots.

pkg load statistics;

N = 225;  # Input data length
omega = 2*pi*7/45;  # Signal and detection frequency, must be DFT bin freq.
M = 100;  # Monte Carlo sample size (plot was made with 1000, which is slow)
A_signal_min = sqrt(2)*10^(-60/20);  # Minimum sinusoid amplitude, must be positive
A_signal_max = sqrt(2)*10^(60/20);  # Maximum sinusoid amplitude, must be positive
S = 200;  # Number of graph points is S+1
DC = 1;  # DC offset
MS_noise = 1;  # Noise process mean square level (plot labels correct if 1)
plot_IEEE1057 = 0;  #1: plot IEEE1057 3-parameter estimate equivalent to the algorithm in Four-Parameter Sinefit version by Marko Neitola, 0: do not plot
plot_SNR = 0;  #1: plot SNR estimates, 0: do not plot
A_signal = A_signal_min*(A_signal_max/A_signal_min).^((0:S)/S);
MS_signal = A_signal.^2/2;
k = 0:N-1;
phasor = exp(-i*omega*k);
if plot_IEEE1057
  D0=[cos(omega*k)' sin(omega*k)' ones(N,1)];
  [Q,R] = qr(D0,0);

mean_estimate_MS_biased = zeros(1, S+1);
mean_estimate_MS_spectral_subtraction = zeros(1, S+1);
mean_estimate_MS_clamped_spectral_subtraction = zeros(1, S+1);
mean_estimate_MS_Simon = zeros(1, S+1);
mean_estimate_MS_IEEE1057 = zeros(1, S+1);
mean_estimate_SNR = zeros(1, S+1);
mean_estimate_SNR_spectral_subtraction = zeros(1, S+1);
mean_estimate_SNR_clamped_spectral_subtraction = zeros(1, S+1);
MS_frac_err_estimate_MS_biased = zeros(1, S+1);
MS_frac_err_estimate_MS_spectral_subtraction = zeros(1, S+1);
MS_frac_err_estimate_MS_clamped_spectral_subtraction = zeros(1, S+1);
MS_frac_err_estimate_MS_Simon = zeros(1, S+1);
MS_frac_err_estimate_MS_IEEE1057 = zeros(1, S+1);
MS_frac_err_estimate_SNR = zeros(1, S+1);
MS_frac_err_estimate_SNR_spectral_subtraction = zeros(1, S+1);
MS_frac_err_estimate_SNR_clamped_spectral_subtraction = zeros(1, S+1);
for m = 1:M
  for s = 1:S+1
    x = random("normal", 0, MS_noise, [1, N]) + A_signal(s)*cos(omega*k + 2*pi*rand()) + DC;
    MS_x = mean(x.*x);
    X_0 = mean(x);
    X_omega = mean(x.*phasor);
    estimate_MS_biased = 2*abs(X_omega)^2;
    mean_estimate_MS_biased(s) += estimate_MS_biased;
    MS_frac_err_estimate_MS_biased(s) += ((estimate_MS_biased-MS_signal(s))/MS_signal(s))^2;
    estimate_MS_spectral_subtraction = 2*abs(X_omega)^2 - 2/(N-3)*(MS_x - X_0^2 - 2*abs(X_omega)^2);
    mean_estimate_MS_spectral_subtraction(s) += estimate_MS_spectral_subtraction;
    MS_frac_err_estimate_MS_spectral_subtraction(s) += ((estimate_MS_spectral_subtraction-MS_signal(s))/MS_signal(s))^2;
    estimate_MS_clamped_spectral_subtraction = estimate_MS_spectral_subtraction;
    if estimate_MS_clamped_spectral_subtraction < 0 estimate_MS_clamped_spectral_subtraction = 0; end
    mean_estimate_MS_clamped_spectral_subtraction(s) += estimate_MS_clamped_spectral_subtraction;
    MS_frac_err_estimate_MS_clamped_spectral_subtraction(s) += ((estimate_MS_clamped_spectral_subtraction-MS_signal(s))/MS_signal(s))^2;
    estimate_MS_Simon = MS_x - X_0^2;
    mean_estimate_MS_Simon(s) += estimate_MS_Simon;
    MS_frac_err_estimate_MS_Simon(s) += ((estimate_MS_Simon-MS_signal(s))/MS_signal(s))^2;
    if plot_IEEE1057 
      x0 = R\(Q.'*x');
      estimate_MS_IEEE1057 = (x0(1)*x0(1) + x0(2)*x0(2))/2;
      mean_estimate_MS_IEEE1057(s) += estimate_MS_IEEE1057;
      MS_frac_err_estimate_MS_IEEE1057(s) += ((estimate_MS_IEEE1057-MS_signal(s))/MS_signal(s))^2;
    if plot_SNR
      estimate_SNR = 2*abs(X_omega)^2/(MS_x - X_0^2 - 2*abs(X_omega)^2);
      mean_estimate_SNR(s) += estimate_SNR;
      estimate_SNR_spectral_subtraction = 2*abs(X_omega)^2*(1-3/N)/(MS_x - X_0^2 - 2*abs(X_omega)^2) - 2/N;
      mean_estimate_SNR_spectral_subtraction(s) += estimate_SNR_spectral_subtraction;
      estimate_SNR_clamped_spectral_subtraction = max(2*abs(X_omega)^2*(1-3/N)/(MS_x - X_0^2 - 2*abs(X_omega)^2) - 2/N, 0);
      mean_estimate_SNR_clamped_spectral_subtraction(s) += estimate_SNR_clamped_spectral_subtraction;
mean_estimate_MS_biased /= M;
mean_estimate_MS_spectral_subtraction /= M;
mean_estimate_MS_clamped_spectral_subtraction /= M;
mean_estimate_MS_Simon /= M;
if plot_IEEE1057 mean_estimate_MS_IEEE1057 /= M; end
if plot_SNR
  mean_estimate_SNR /= M;
  mean_estimate_SNR_spectral_subtraction /= M;
  mean_estimate_SNR_clamped_spectral_subtraction /= M;
MS_frac_err_estimate_MS_biased /= M;
MS_frac_err_estimate_MS_spectral_subtraction /= M;
MS_frac_err_estimate_MS_clamped_spectral_subtraction /= M;
MS_frac_err_estimate_MS_Simon /= M;
if plot_IEEE1057 MS_frac_err_estimate_MS_IEEE1057 /= M; end

# First plot
hold off
plot(10*log10(MS_signal/MS_noise), 10*log10(MS_signal));
hold on
plot(10*log10(MS_signal/MS_noise), 10*log10(mean_estimate_MS_biased));
plot(10*log10(MS_signal/MS_noise), 10*log10(mean_estimate_MS_spectral_subtraction));
plot(10*log10(MS_signal/MS_noise), 10*log10(mean_estimate_MS_clamped_spectral_subtraction));
plot(10*log10(MS_signal/MS_noise), 10*log10(mean_estimate_MS_Simon));
if plot_IEEE1057 plot(10*log10(MS_signal/MS_noise), 10*log10(mean_estimate_MS_IEEE1057), "+"); end
hold off
xlabel("true signal power and SNR (dB)");
ylabel("mean estimated signal power (dB)");
xlim([10*log10(A_signal_min^2/2), 10*log10(A_signal_max^2/2)])
ylim([10*log10(A_signal_min^2/2), 10*log10(A_signal_max^2/2)])

# Second plot
hold off
plot([10*log10(A_signal_min^2/2), 10*log10(A_signal_min^2/2)], [10*log10(A_signal_min^2/2), 10*log10(A_signal_min^2/2)]);  # Dummy, to get colors right
hold on
plot(10*log10(MS_signal/MS_noise), 10*log10(MS_frac_err_estimate_MS_biased));
plot(10*log10(MS_signal/MS_noise), 10*log10(MS_frac_err_estimate_MS_spectral_subtraction));
plot(10*log10(MS_signal/MS_noise), 10*log10(MS_frac_err_estimate_MS_clamped_spectral_subtraction));
plot(10*log10(MS_signal/MS_noise), 10*log10(MS_frac_err_estimate_MS_Simon));
if plot_IEEE1057 plot(10*log10(MS_signal/MS_noise), 10*log10(MS_frac_err_estimate_MS_IEEE1057), "+"); end
hold off
xlabel("true signal power and SNR (dB)");
ylabel("mean square fractional estimation error (dB)");
xlim([10*log10(A_signal_min^2/2), 10*log10(A_signal_max^2/2)])
grid on

# Third plot
hold off
plot([10*log10(A_signal_min^2/2), 10*log10(A_signal_min^2/2)], [10*log10(A_signal_min^2/2), 10*log10(A_signal_min^2/2)]);  # Dummy, to get colors right
hold on
plot(10*log10(MS_signal/MS_noise), 10*log10(MS_frac_err_estimate_MS_biased./MS_frac_err_estimate_MS_biased));
plot(10*log10(MS_signal/MS_noise), 10*log10(MS_frac_err_estimate_MS_spectral_subtraction./MS_frac_err_estimate_MS_biased));
plot(10*log10(MS_signal/MS_noise), 10*log10(MS_frac_err_estimate_MS_clamped_spectral_subtraction./MS_frac_err_estimate_MS_biased));
plot(10*log10(MS_signal/MS_noise), 10*log10(MS_frac_err_estimate_MS_Simon./MS_frac_err_estimate_MS_biased));
if plot_IEEE1057 plot(10*log10(MS_signal/MS_noise), 10*log10(MS_frac_err_estimate_MS_IEEE1057./MS_frac_err_estimate_MS_biased), "+"); end
hold off
xlabel("true signal power and SNR (dB)");
ylabel("mean square fractional estimation error improvement (dB)");
xlim([10*log10(A_signal_min^2/2), 10*log10(A_signal_max^2/2)])
ylim([-10, 40]);
grid on

if plot_SNR
  # Fourth plot
  hold off
  plot(10*log10(MS_signal/MS_noise), 10*log10(MS_signal));
  hold on
  plot(10*log10(MS_signal/MS_noise), 10*log10(mean_estimate_SNR));
  plot(10*log10(MS_signal/MS_noise), 10*log10(mean_estimate_SNR_spectral_subtraction));
  plot(10*log10(MS_signal/MS_noise), 10*log10(mean_estimate_SNR_clamped_spectral_subtraction));
  hold off
  xlabel("true signal power and SNR (dB)");
  ylabel("mean estimated SNR (dB)");
  xlim([10*log10(A_signal_min^2/2), 10*log10(A_signal_max^2/2)])
  ylim([10*log10(A_signal_min^2/2), 10*log10(A_signal_max^2/2)])

  # Fifth plot
  hold off
  plot(10*log10(MS_signal/MS_noise), 10*log10(MS_signal));
  hold on
  plot(10*log10(MS_signal/MS_noise), 10*log10(mean_estimate_SNR));
  plot(10*log10(MS_signal/MS_noise), 10*log10(mean_estimate_SNR_spectral_subtraction));
  plot(10*log10(MS_signal/MS_noise), 10*log10(mean_estimate_SNR_clamped_spectral_subtraction));
  hold off
  xlabel("true signal power and SNR (dB)");
  ylabel("mean estimated SNR (dB)");
  xlim([10*log10(A_signal_min^2/2), 10*log10(A_signal_max^2/2)])
  ylim([10*log10(A_signal_min^2/2), 10*log10(A_signal_max^2/2)])  


In summary, if possible, choose a window length that fits an integer number of periods of your frequency of interest, and directly calculate the magnitude of the DFT bin at the frequency of interest. This gives no phase-dependent fluctuation and is unaffected by zero frequency bias, see Fig. 2. Otherwise this approach to detection will give error, even in zero-noise zero-drift conditions, even when using the correct frequency for detection instead of a DFT bin frequency. If your SNR may be at times poor, test if the clamped spectral subtraction estimator improves your results.


Francisco Alegria, "Bias of amplitude estimation using three-parameter sine fitting in the presence of additive noise", June 2009, Measurement 42(5):748-756 (doi, pdf)

  • $\begingroup$ That was quite a lot of information. I definitely need to brush up on DFT to fully understand the proposed method. I'm not quite sure though, whether this method will provide me with an advantage over my current method, which I know is crude but works quite well and is very fast. Using 256 samples isn't a hard requirement. I can use more or less if it has an advantage. The fairly large deviation from 200 kHz is interesting though. I use a crystal with max 50 ppm. I might have to check my clock settings. $\endgroup$
    – Simon
    Commented Jan 17, 2019 at 11:47
  • $\begingroup$ @Simon I analyzed your method, too, under the zero-noise condition. It will also benefit from switching to $N = 225$ or $N = 270.$ $\endgroup$ Commented Jan 17, 2019 at 15:11
  • $\begingroup$ @Simon, There seems to be an implicit assumption in Olli's answer that the sampling frames and analysis frames are one and the same. They don't need to be. You can have analysis frames spanning over acquisition frames (requires buffering) or simply choose your analysis frame as a subsection of your acquisition frame. $\endgroup$ Commented Jan 17, 2019 at 15:33
  • $\begingroup$ @CedronDawg this answer assumes $N$ consecutive samples are to be analyzed, per question. $\endgroup$ Commented Jan 17, 2019 at 16:53
  • 1
    $\begingroup$ @OlliNiemitalo You did some really crazy stuff there, in your recent edit. This tells me, that the DFT method without noise compensation is pretty good for my current requirements. $\endgroup$
    – Simon
    Commented Jan 24, 2019 at 11:54

A quick and dirty might be to simply find all the local maxima in a zone, sort them, and average the top few. Do the same for the minima in reverse. Subtract the latter from the former, divide by two.

That is not guaranteed to be precise though and you have to ensure that you don't have a whole number of sample points per cycle or they will all line up the same each cycle.

The proper way is to have two basis vectors and find your signal as a linear combination of them. Yes, you calculate a single DFT bin at the frequency of interest. This will give you amplitude, and phase if you want it. The good news is you can get good results from a single cycle, but take two or more for noise mitigation. In the sample program I chose five.

Here is the program. You will need to tweak it a bit to make it recognize your zones, file length, etc. A frame that spans an amplitude change boundary will obviously get a bad read.

You can make the process a lot more efficient by knowing where the zones are and taking fewer frame samples. Lot of ways to take this.

The idea is to give you some working code you can play with so you can learn the math.

import numpy as np

def main():

#---- Read in the Data File

        f = open( "data.csv", "r" )

        theRows = []

        for row in f:
            theRows.append( row )


        theRowCount = len( theRows ) 

#---- Build Signal Array

        theChannelValues  = np.zeros( theRowCount )
        theChannelIndex   = 0

        for r in range( theRowCount ) :
            theValues = theRows[r].rstrip().split( "," )
            theChannelValues[r] = theValues[theChannelIndex]

#---- Estimate Frequency

        thePeakCount = 0
        theFirstSpot = -1
        theLastSpot = -1

        for s in range( 10, 210 ) :
            if theChannelValues[s] >= theChannelValues[s-1] and \
               theChannelValues[s] >= theChannelValues[s+1] :
                print "Max at " + str( s )
                if theFirstSpot == -1 :
                   theFirstSpot = s
                   theLastSpot = s
                   thePeakCount += 1

        print theFirstSpot, theLastSpot                   

        theCyclesPerSample = float( thePeakCount ) \
                           / float(theLastSpot - theFirstSpot )

        print "Samples per cycle: " + str( 1.0 / theCyclesPerSample )

#---- Build Basis Vectors

        theCyclesPerFrame = 5

        theSamplesPerFrame = theCyclesPerFrame / theCyclesPerSample

        theRadiansPerSample = theCyclesPerSample * 2.0 * np.pi

        N = int( theSamplesPerFrame + 0.5 )

        theC = np.zeros( N )
        theS = np.zeros( N )

        for n in range( N ) :
            theAngle = theRadiansPerSample * n
            theC[n]  = np.cos( theAngle )
            theS[n]  = np.sin( theAngle )

#---- Measure Signal Frame by Frame

        theFactor = 2.0 / float( N)

        theStart = 0

        for f in range( 100 ) :
            theLimit = theStart + N

            theDotC = theChannelValues[theStart:theLimit].dot( theC )
            theDotS = theChannelValues[theStart:theLimit].dot( theS )

            theMag = theFactor * np.sqrt( theDotC*theDotC + theDotS*theDotS ) 

            print f, theMag

            theStart = theLimit


Here is an output excerpt for those who don't care to or can't run it.

Samples per cycle: 6.43333333333
0 542.814867988
1 548.585610118
2 547.425033167
3 551.011654022
4 545.654363432
5 550.587903304
6 553.360391478
7 537.68546946
8 391.858803202
9 395.333637678
10 392.753814378
11 391.284856065
12 389.907641197
13 385.425898115
14 382.921216041
15 359.681851586
16 22.9742734918
17 22.4568332805
18 22.4377296186
19 21.8988868527
20 22.1896466722
21 21.955041401
22 22.1747325702
23 21.7120535962
24 244.361827764
25 243.919290421

This code for basis building will be slightly more efficient:

        theAngle = 0

        for n in range( N ) :
            theC[n]   = np.cos( theAngle )
            theS[n]   = np.sin( theAngle )
            theAngle += theRadiansPerSample

Also, it occurred to me that you might not know how to do a dot product.

        theDotC = theChannelValues[theStart:theLimit].dot( theC )
        theDotS = theChannelValues[theStart:theLimit].dot( theS )

These two lines can be replaced by this loop:

        theDotC = 0.0
        theDotS = 0.0

        for n in range( N ) :
            v = theChannelValues[theStart+n]
            theDotC += v * theC[n]
            theDotS += v * theS[n]

Followup to Simon's comment:

Interesting, Olli's answer is sort of an elaboration of mine. Noticed he keyed in on two points. One, the commensurability (ratios of small integers) leads to repeat patterns. Two, you want your "DFT frame" to be as close to a whole number of cycles as you can make it. This concentrates your fundamental information of interest into one bin. The DC and harmonics are orthogonal to the basis vectors so they get zeroed out in your results automatically.

If you didn't know the frequency exactly, you could set your length to a whole number plus a half cycles, then calculate the bin on either side. You can find formulas in my blog for an exact frequency calculation (not an approximation!) and exact magnitude and phase calculation independent of frame alignment. I don't think that is necessary unless somehow you need super precision. As long as you are within one sample of a whole number of cycles, your answer is going to be darn accurate. Lengthening the frame reduces any small error introduced by this. So does having more samples in each cycle.

Yes, I know you knew your zone widths, but I was trying to provide a more general solution that readily adapts to your situation. Instead of doing frame after frame, my recommendation is you center your frame in the middle of a zone, pick a number of cycles that fills most of it, but leaves a little room on the edges, and apply the approach in this answer, then onto the next zone. I don't think it can be done any better to get the best accuracy with the least effort. The phase variance between frames will be irrelevant because you only care about the one result.

Numerically, the Goertzel is an equivalent bin calculation. It builds the basis vectors on the fly with a clever trick. However, it is more efficient to precalculate and store the basis vectors if you need them repeatedly. Compare the dot product loop with the Goertzel calculations. The advantage of Goertzel is that it doesn't require the memory storage for the basis vector arrays, and it calculates the sin and cos values using the principle that when you multiply two complex values you are adding their angles.

It's a nice diagram. I'm afraid I am not really a hardware guy so I wouldn't know the particulars, like delays and stuff. So just because your know the zone widths and when the boundaries for excitation are, doesn't necessarily mean you know where the boundaries are from your return stream. I would suspect it is a constant small delay though, perhaps even negligible.

I do like Olli's diagram. It does look like a technical drawing of some sort of drive shaft though. What is common in our answers is an emphasis on selectin a frame with a whole number of cycles. He did a nice job of explaining the penalty if you don't. You should also realize since you are calculating only one bin value there is no advantage whatsoever of picking an N that is a power of two or any other particular value.

I do think that a one bin problem solution like this gives an excellent foundation for understanding the DFT as a parallel set of these. I.e. if you had two tones it would be advantageous to set your frame length so both tones have a whole number of cycles in them. That way they each get their own bin without interference from the other. Unfortunately, in this case my simple peak counting frequency estimation won't work. If you did not know the frequencies ahead of time, or they were expected to be inconsistent, you could find them by doing a DFT of an arbitrary length and then applying frequency formulas at the humps.

One final point, the DFT does not care about the units or any specifics on your situation, especially the sampling rate. It cares about how many samples there are. It works with frequencies in terms of cycles per frame. It is when you converting to real world frequencies that knowing the sampling rate is required.

  • $\begingroup$ I can work with that. Maybe you should have a second look at the edited question. I tried to make clear that I actually know where the zone boundaries are, because I initiate the switching. How does your proposed method compare of the Goertzel algorithm? The underlying should be the same, isn't it? Calculate a single DFT bin at 200 kHz. $\endgroup$
    – Simon
    Commented Jan 17, 2019 at 11:32
  • $\begingroup$ @Simon, I've added a followup. $\endgroup$ Commented Jan 17, 2019 at 14:23
  • $\begingroup$ @Simon, Come to think of it, two adjacent zones, rather than one might be better for you. Taking two dot products that are half as long is the same amount of work so it won't cost you more. However, you will get two answers, one for each half zone. You can then compare the two values to ensure they give the same result as a validity check. Then take their average for your result. $\endgroup$ Commented Jan 17, 2019 at 14:44
  • 1
    $\begingroup$ That is actually a very good idea. I will try it. $\endgroup$
    – Simon
    Commented Jan 22, 2019 at 9:46

Not an answer, just storing a representative piece (first 2048 samples from the first channel) of the question's data for easy access and so that it won't be lost. Comma separated values.


enter image description here

  • $\begingroup$ That was a very good idea! May I steal that idea and incorporate it into the question, for completeness? $\endgroup$
    – Simon
    Commented Jan 17, 2019 at 9:15
  • $\begingroup$ Sure! Or we can keep it here so that if you or anyone edits the question they won't be in trouble with all those numbers. You can link to this answer. I made it a community wiki answer. $\endgroup$ Commented Jan 17, 2019 at 10:25
  • 1
    $\begingroup$ So true. I'll link to it. $\endgroup$
    – Simon
    Commented Jan 17, 2019 at 10:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.