Skip to content

savgol

Savitzky-Golay smoothing and differentiation filter.

For each input sample, savgol fits a polynomial to the last winsize samples by least squares and outputs the value of that polynomial at the centre of the window. It can also output a derivative of the polynomial instead of its value, which gives a smoothed slope or curvature of the signal.

Because the fit depends linearly on the samples, the whole operation is a fixed FIR filter. Its coefficients are computed once at initialization. Compared with an ordinary lowpass of similar bandwidth, a Savitzky-Golay filter keeps the height and width of peaks better. This makes it useful for envelopes, control signals and any material where the shape of a transient matters more than strong attenuation of high frequencies.

Syntax

ares = savgol(asig, iwinsize, iorder [, ideriv] [, idelta])
kres = savgol(ksig, iwinsize, iorder [, ideriv] [, idelta])
ares savgol asig, iwinsize, iorder [, ideriv] [, idelta]
kres savgol ksig, iwinsize, iorder [, ideriv] [, idelta]

Initialization

iwinsize -- the number of samples in the window. It must be odd and at least 3. A longer window smooths more.

iorder -- the degree of the fitted polynomial, from 0 to iwinsize - 1. For the same window, a higher order follows the signal more closely and smooths less. Order 0 is a plain moving average. When smoothing, an odd order gives the same result as the even order below it: order 1 is also a moving average, and order 3 smooths exactly like order 2.

ideriv (optional, default 0) -- which derivative of the fitted polynomial to output, from 0 to iorder. 0 smooths the signal, 1 outputs its slope, 2 its curvature, and so on.

idelta (optional, default 1) -- the spacing between two input samples, which scales the derivatives. With the default of 1, derivatives are given per sample. To get them per second, use 1/sr for an audio input or 1/kr for a control input. It must be greater than 0 and has no effect when ideriv is 0.

Invalid values cause an initialization error.

Performance

asig, ksig -- the input signal.

ares, kres -- the filtered signal, or the requested derivative.

The output belongs to the centre of the window, so it lags the input by (iwinsize - 1) / 2 samples at a-rate, or by the same number of control periods at k-rate. The window starts filled with zeros: the first iwinsize - 1 outputs are the response to a signal that was silent before the note started.

Each output sample costs iwinsize multiplications and additions.

The fit reproduces any polynomial up to degree iorder exactly. The other side of this is a weak stopband: content near the Nyquist frequency is reduced, not removed. For example, with iwinsize = 11 and iorder = 2, a component at the Nyquist frequency keeps about 14% of its amplitude. If the stopband matters more than the shape of the signal, use a conventional lowpass filter.

To get the coefficients themselves, use savgolmat.

Examples

Smoothing and slope of a control signal

The input is the parabola t² plus a small jitter that changes sign at every control period. With an order 2 fit, the slope comes out exact once the window is full, while the jitter in the smoothed value shrinks from 0.05 to about 0.007. Both outputs are five control periods late, so the expected values in parentheses are taken five control periods back. It uses the file savgol.csd.

Smoothing a control signal and measuring its slope.
<CsoundSynthesizer>
<CsOptions>
-n -d -m0
</CsOptions>
<CsInstruments>
sr = 48000
ksmps = 480
nchnls = 1
0dbfs = 1

; With ksmps = 480 there are 100 control periods per second.
; The input is the parabola t^2 plus a jitter of +-0.05 that flips sign at
; every control period. savgol fits a parabola to each window of 11 values,
; so it removes the jitter and returns the parabola itself, together with
; its slope. Both come out (11 - 1) / 2 = 5 control periods late.

instr 1
  t:k = timeinsts()
  flip:k init 1
  flip = -flip
  x:k = t * t + 0.05 * flip

  smooth:k = savgol(x, 11, 2)
  ; derivative 1, with delta = 1/kr so that the slope is per second
  slope:k = savgol(x, 11, 2, 1, 1/kr)

  ; what the parabola and its slope were 5 control periods ago
  lag:k = t - 5/kr
  if metro(4) == 1 then
    printks("t=%4.2f  in=%6.3f  smooth=%6.3f (%6.3f)  slope=%6.3f (%6.3f)\n", 0, \
            t, x, smooth, lag * lag, slope, 2 * lag)
  endif
endin
</CsInstruments>
<CsScore>
i 1 0 2
e
</CsScore>
</CsoundSynthesizer>

Its output is:

t=0.01  in=-0.050  smooth= 0.004 ( 0.002)  slope=-0.227 (-0.080)
t=0.26  in= 0.118  smooth= 0.037 ( 0.044)  slope= 0.420 ( 0.420)
t=0.51  in= 0.210  smooth= 0.219 ( 0.212)  slope= 0.920 ( 0.920)
t=0.76  in= 0.628  smooth= 0.497 ( 0.504)  slope= 1.420 ( 1.420)
t=1.01  in= 0.970  smooth= 0.929 ( 0.922)  slope= 1.920 ( 1.920)
t=1.26  in= 1.638  smooth= 1.457 ( 1.464)  slope= 2.420 ( 2.420)
t=1.51  in= 2.230  smooth= 2.139 ( 2.132)  slope= 2.920 ( 2.920)
t=1.76  in= 3.148  smooth= 2.917 ( 2.924)  slope= 3.420 ( 3.420)

The first line is printed before the window is full, which is why it is off.

Audio lowpass

A sawtooth is played through several combinations of window size and order. A longer window makes the sound darker, and a higher order with the same window makes it brighter. It uses the file savgol-audio.csd.

savgol on an audio signal with different windows and orders.
<CsoundSynthesizer>
<CsOptions>
; Select audio/midi flags here according to platform
-odac     ;;;realtime audio out
;-iadc    ;;;uncomment -iadc if realtime audio input is needed too
; For Non-realtime ouput leave only the line below:
; -o savgol-audio.wav -W ;;; for file output any platform
</CsOptions>
<CsInstruments>
sr = 48000
ksmps = 32
nchnls = 2
0dbfs = 1

; savgol as an audio lowpass. A sawtooth is played through windows of
; different sizes and polynomial orders. A longer window removes more of the
; high end; for the same window, a higher order keeps more of it.
; Order 0 is a plain moving average.

instr 1
  winsize:i = p4
  order:i = p5
  prints("winsize %d, order %d\n", winsize, order)

  saw:a = vco2(0.3, 110)
  sig:a = savgol(saw, winsize, order)

  env:k = linseg(0, 0.02, 1, p3 - 0.1, 1, 0.08, 0)
  sig *= env
  out(sig, sig)
endin
</CsInstruments>
<CsScore>
; p4 = winsize, p5 = order
i 1 0    1.5  3  2  ; window as short as the order allows: almost unfiltered
i 1 2    1.5 31  0  ; moving average over 31 samples: dark
i 1 4    1.5 31  2  ; same window, order 2: brighter
i 1 6    1.5 31  6  ; same window, order 6: brighter still
i 1 8    1.5 81  6  ; longer window, same order: darker again
e
</CsScore>
</CsoundSynthesizer>

See also

Specialized Filters

savgolmat

Credits

Author: Pasquale Mainolfi
Italy
New in Csound version 7.0
October 2026