9.15. Filtering and spectrograms¶
The FFT page gave the application a way to inspect a buffer’s frequency content. The rest of the signal-processing surface covers the adjacent jobs: smoothing or band-pass-filtering a stream of samples, computing magnitude spectra in a streaming loop without allocating, and re-interpreting raw peripheral buffers as float arrays:
ulab.scipy.signal.sosfilt()– IIR filter via cascaded second-order sections.ulab.utils.spectrogram()– magnitudeabs(fft(...))with no intermediate allocations.ulab.utils.from_int16_buffer()and the otherulab.utilsfrom_*_bufferhelpers – pull a float array out of a buffer whose dtype the built-infrombuffer()does not cover.
9.15.1. IIR filtering with sosfilt¶
sosfilt() applies cascaded
second-order sections (SOS) – the numerically robust
way to apply an IIR filter. sos is a sequence of
length-6 sections; x is the 1-D input:
from ulab import numpy as np
from ulab import scipy as sp
x = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
sos = [[1, 2, 3, 1, 5, 6],
[1, 2, 3, 1, 5, 6]]
y = sp.signal.sosfilt(sos, x)
Each row of sos holds six coefficients
[b0, b1, b2, a0, a1, a2] for one biquad section.
The sos array is usually pre-computed on a PC with
scipy.signal.iirfilter(..., output='sos') and copied
into the camera script as a Python literal.
The optional zi= keyword carries filter state across
buffers. Pass an initial state of shape
(n_sections, 2) and the function returns
(y, zf) – the filtered output and the final state
– so the final state of one buffer feeds the initial
state of the next:
y0, zf0 = sp.signal.sosfilt(sos, buffer0, zi=zi)
y1, zf1 = sp.signal.sosfilt(sos, buffer1, zi=zf0)
# ...
This is the standard shape for a streaming filter on buffered data – microphone input read 1024 samples at a time, ADC samples accumulated in DMA-driven chunks, IMU readings collected over a window.
9.15.2. Spectrograms¶
ulab.utils.spectrogram() computes the magnitude
of the Fourier transform. It is conceptually equivalent
to
np.abs(np.fft.fft(signal))on a complex-output FFT build, ornp.sqrt(real * real + imag * imag)on a split-output build,
but does it in one call – without holding the
intermediate real * real, imag * imag, the sum,
or the output of abs() in RAM at any
point. That makes it the right tool in any loop where
spectra are computed repeatedly:
from ulab import numpy as np
from ulab import utils
x = np.linspace(0, 10, num=1024)
spectrum = utils.spectrogram(x)
The argument shape mirrors fft():
one 1-D real-or-complex array on complex-output builds,
one or two real arrays (real, imag) on split
builds.
Three keyword arguments help with allocation:
scratchpad=None– a 1-D dense float array of length2 * len(signal)spectrogram()uses for working space.out=None– a 1-D float array to write the result into.log=False– whenTrue, takelog()of the magnitude before returning, folded into the same call.
The streaming pattern is to allocate everything once and never allocate again:
from ulab import numpy as np
from ulab import utils
N = 1024
scratch = np.zeros(2 * N)
out = np.zeros(N)
while True:
signal = read_samples(N)
utils.spectrogram(signal, out=out, scratchpad=scratch,
log=True)
# ``out`` now holds log-magnitudes; feed forward ...
Compare to the obvious-but-wasteful version:
while True:
signal = read_samples(N)
spectrum = np.log(utils.spectrogram(signal)) # two allocations
Both produce the same numbers, but the first version allocates nothing inside the loop – the cam keeps the same memory in use every iteration, and the loop runs faster.
9.15.3. Wider-than-16-bit peripheral buffers¶
frombuffer() only handles the dtypes
numpy itself defines
(uint8 / int8, uint16 / int16,
float). When a peripheral produces 32-bit integer
samples – a 24- or 32-bit ADC, a high-resolution
microphone – ulab.utils exposes explicit
conversion helpers:
from_int16_buffer(),from_uint16_buffer()from_int32_buffer(),from_uint32_buffer()
Each takes a bytes-like buffer and returns a float
ndarray:
from ulab import utils
buf = bytearray([1, 1, 0, 0, 0, 0, 0, 255])
utils.from_uint32_buffer(buf)
# array([257.0, 4278190080.0])
The functions accept the same allocation-saving knobs as
spectrogram():
count=andoffset=to skip a header or limit the read.out=to write into a pre-allocated float array.byteswap=Truewhen the peripheral disagrees with the MCU on byte order.
The combined shape – one from_int32_buffer call
straight into one spectrogram call, both with
out= buffers from outside the loop – is the right
template for a streaming spectrum analyser running on a
high-resolution microphone.
For the complete reference, see
scipy.signal — Signal processing and the
ulab.utils documentation.