Counting Crossings: The Conductor's Secret Frequency Detector
Picture this: you’re conducting Brahms’ Fourth, and your smart baton needs to know if the violins are drifting sharp on that exposed A. The orchestra generates complex waveforms, but buried in all that acoustic complexity is something beautifully simple—every time the sound pressure crosses from positive to negative (or vice versa), you’ve witnessed half a cycle.
Zero-crossing detection exploits this elegant relationship. Count the crossings, divide by time, and you’ve got frequency. It’s the digital equivalent of watching a pendulum—pure, mathematical simplicity that early signal processors could actually compute in real-time.
def zero_crossings(signal, sample_rate):
crossings = sum(1 for i in range(1, len(signal))
if signal[i-1] * signal[i] < 0)
return crossings * sample_rate / (2 * len(signal))
# Simulated violin A (440 Hz) with some drift
import math
time = [i/8000 for i in range(1000)] # 1000 samples at 8kHz
violin_a = [math.sin(2 * math.pi * 445 * t) for t in time]
print(f"Detected: {zero_crossings(violin_a, 8000):.1f} Hz")
Zig’s approach prioritises the real-time constraints of an actual conductor baton, where every microsecond matters:
fn zeroCrossings(signal: []const f32, sample_rate: f32) f32 {
var crossings: u32 = 0;
for (signal[1..], 1..) |sample, i| {
if (signal[i - 1] * sample < 0) crossings += 1;
}
return @as(f32, @floatFromInt(crossings)) * sample_rate / (2.0 * @as(f32, @floatFromInt(signal.len)));
}
The trade-off is delicious: zero-crossing detection is blindingly fast and needs minimal memory, making it perfect for microcontrollers. But it struggles with harmonically rich instruments—that violin’s overtones can fool the algorithm into hearing phantom frequencies. Modern pitch trackers use sophisticated autocorrelation or cepstral analysis, but when your conductor baton has 32KB of RAM and needs answers in milliseconds, sometimes counting crossings is exactly the right amount of clever.