1
$\begingroup$

As a followup to my True Peak detection question, I'm trying to implement a detection method by following this documentation using the Catmull-Rom interpolation method.

What I've done so far can be seen in my Desmos worksheet (includes sample of y -data which creates the issue in question).

enter image description here

I've (probably) got stationary points solving to work correctly but, calculation used with the cases not having critical points and are solved through finding inflection point using equation $x = \frac{-b}{3a}$ are not giving proper results. I suppose the result could be at least improved by using suitable conditional clauses. As seen in the Desmos worksheet, resulting x values are used as an input for the interpolation function to get the peak level solved (yi = interpolation(y, x), where y is up-sampled data, interpolated using half-polyphase FIR low-pass filter).

Any suggestions to get this issue solved?

Edit: Here are some C++ code at Compiler Explorer.

catmullrom4() is the function name related to this issue:

float catmullrom4(float* y){   
   // Solve tp through stationary and inflection points
   float a, b, c, d, x, yi;
   
   a = -0.5f*y[0] + 1.5f*y[1] - 1.5f*y[2] + 0.5f*y[3];
   b = y[0] - 2.5f*y[1] + 2.f*y[2] - 0.5f*y[3];
   c = -0.5f*y[0] + 0.5f*y[2];
   d = y[1];
  
  float s1 = (-b - std::sqrt((b*b) - (3.0f*a*c))) / (3.0f*a);
  float s2 = (-b + std::sqrt((b*b) - (3.0f*a*c))) / (3.0f*a);
  float s3 = -b/(3.0f*a);
  
  float abs1 = std::abs(s1);
  float abs2 = std::abs(s2);
  float abs3 = std::abs(s3);

  if(abs2 > abs1 ){
    x = s1;
  }
  else if (abs1 > abs2){
      x = s2;
  }
  else{
     x = s3;
    if (abs3 > 3.0f){ 
          x = 1.0f;
          }
    else if (abs3 < 2.0f){
        x = 0.0f;
        }
    }
  return (x * (x * (a * x + b) + c) + d);
}

EDIT: Updated the Desmos and Compiler Explorer links

$\endgroup$
16
  • 2
    $\begingroup$ That's an awful amount of work and complexity for a relatively simple problem. What's wrong with just up- sampling and poly-phase lowpass filtering? It's fast and easy to implement. How "precise" does your estimation need to be? $\endgroup$
    – Hilmar
    Commented Sep 17, 2022 at 13:53
  • $\begingroup$ I'm actually using half-polyphase LPF (FIR) with the up-sampled data and now want to interpolate it with some cubic spline method ... this implementation is partly done because of 'dsp -guys' on another forum say that solving TP just using max(abs(Y)) is not the best way to do it. ;) Someone there suggested (maybe) this but mentioned taking roots etc. so ... maybe this is not the most complicate solution I'm after. I don't even know how precise it could be... . $\endgroup$
    – Juha P
    Commented Sep 17, 2022 at 14:44
  • 3
    $\begingroup$ Look, with all these type of problems there is a very shallow curve of "accuracy" vs "effort". You can spend 10 times the effort so squeeze another 0.02 dB of accuracy out of the thing. So the question is not what it CAN be, the question is what does it NEED to be for your specific application. A good peak limiter should have 0.5dB - 1dB of margin anyway, so if your True Peak estimator comes in within 0.5dB for 99.9% of all source material, you are good. Optimizing for a completely outlandish test case seems like a huge waste of time to me. $\endgroup$
    – Hilmar
    Commented Sep 17, 2022 at 15:19
  • 1
    $\begingroup$ @PeterK., OK, added some C++ code to the post. I have Octave code as well if needed. $\endgroup$
    – Juha P
    Commented Sep 17, 2022 at 20:22
  • 1
    $\begingroup$ why are you using the 4-point cubic? there is a little ambiguity about which 4 points. with the discrete peak and its two adjacent samples, you can model a nice quadratic with a clear location in time and peak height. i s'pose with the cubic, you can take the derivative (which is quadratic) and set it to zero. Then the discrete peak and its neighbor that is largest would be the two middle points of the 4 points. either way, you should upsample with a windowed sinc first. and if you do that, the quadratic peak location is as good as you need. $\endgroup$ Commented Sep 18, 2022 at 2:03

3 Answers 3

3
$\begingroup$

I think you are suffering needlessly here. This is a well known problem with an equally well known solution. Below is the code for up sampling by 4 with a 32 tap FIR filter. The error for the worst case sine wave is less than 0.1dB and for the outlandish double impulse it's 0.3dB.

This can be implemented efficiently as a polyphase filter with 8 taps each. The 0th phase doesn't need to be calculated since it's a pure delay. So you need a total of 24 multiplies per sample (no division or transcendental functions). You can further optimize using symmetry properties: the 1rst and 3rd phases are time reversals of each other and the 2nd phase is the time reversal of itself. So there are only 12 unique coefficients.

If that's not good enough, please explain why.

%% find true peak to upsampling

%% test with a worst case sine wave: true peak error is 1.414
nx = 256;

x1 = sin(2*pi*(0:nx-1)'/4+pi/4);
% x = 0*x;
% x(100:101) = 1;
y1=truePeak(x1,4,32);
fprintf('Sine Wave true peak error = %6.2fdB\n',20*log10(max(y1)));

%% test with a double impulse
x2 = zeros(nx,1);
gain = 1/(2*sinc(0.5));
x2(nx/2) = gain;
x2(nx/2+1) = gain;
y2=truePeak(x2,4,32);
fprintf('Double impulse true peak error = %6.2fdB\n',20*log10(max(y2)));

%% true peak finder (not optimized)
function ymax = truePeak(x,nUp,nFir)
% desugn the lowpass filter as a windowed sinc
t = (-nFir/2:nFir/2)'/nUp;
h = sinc(t).*hamming(nFir+1);
nx = length(x);
y = zeros(nx,nUp);
for i=1:nUp
  y(:,i) = conv(x,h(i:nUp:end),'same');
end
ymax = max(abs(y),[],2);
end
$\endgroup$
8
  • 1
    $\begingroup$ Besides suffering needlessly, if this is operating on raw audio and tracking peaks, I think there needs to be some "forgetting factor", where we only consider peaks in a reasonably narrow window of time. It may be they need a sliding maximum filter. But, if the audio has been decently upsampled by a factor of 2 or 4, simple quadratic peak interpolation is good enough. $\endgroup$ Commented Sep 18, 2022 at 0:01
  • $\begingroup$ Your method is good enough of course (there's a typo in sin() input). I have to check if I manage to bring that sinc interpolation into C++. $\endgroup$
    – Juha P
    Commented Sep 18, 2022 at 10:55
  • $\begingroup$ Good catch, thanks. Fixed. $\endgroup$
    – Hilmar
    Commented Sep 18, 2022 at 11:41
  • $\begingroup$ @robertbristow-johnson: As written the algorithm produces one output sample per input sample (not the peak over the whole segment). You could use this directly as the sidechain input to a limiter. $\endgroup$
    – Hilmar
    Commented Sep 18, 2022 at 11:45
  • $\begingroup$ @JuhaP: I'm not sure what platform you are working on, but if you have access to any DSP library at all, I'm sure it includes an FIR filter, which is really the only thing you need here. $\endgroup$
    – Hilmar
    Commented Sep 18, 2022 at 11:46
2
$\begingroup$

If the goal is to identify inter-sample overs such that you can normalize a signal and still not have voltages out of a D/A converter exceeding some maximum, you are essentially modelling the D/A conversion process, and without that knowledge, I think that there can be no absolute limit.

The D/A could be filtered by a close approximation to a sinc - or it could not.

$\endgroup$
2
1
$\begingroup$

OK, followed @robertbristow-johnson's suggestion in comments and implemented TP detection based on quadratic functions.

Here's the C++ code at Compiler Explorer site with 4x up-sampled data from test case in Hilmar̈́s answer. Given C++ implementation needs some modifications to work with different up-sampling ratios.

Here's also Desmos sheet demonstrating the main functionality of the implementation.

$\endgroup$
1
  • $\begingroup$ Hi, I looked at the code but did not go through it thoroughly. Some stuff I do not recognize. Anyway, as long as your discrete peak (which I think you call y[1]) is a larger value than either of its adjacent neighbors (y[0] and y[2]), then you are guaranteed that the "true" peak location will be between y[0] and y[2] and closer to y[1] than to either of y[0]or y[2]. It's mathematically guaranteed, you need not test for that being out of range. What you need to test for is that the peak height, y[1], is larger than both y[0] and y[2]. That's the only test you need. $\endgroup$ Commented Oct 1, 2022 at 19:46

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.