Capturing the Echo Before You Heard It
When you’re listening for meteor reflections, the ionization flash lasts 0.1 to 2 seconds. By the time your detector crosses threshold and triggers recording, half the event is already gone. You need the audio from before the detection—but you can’t record everything forever.
Circular buffers solve this by overwriting old samples continuously. You’re always recording the last N seconds into a fixed-size ring. When the detector fires, you freeze the buffer and save it. The meteor’s leading edge is still there, written five seconds ago, waiting at position 38,400 in a 48,000-sample loop.
Scheme’s approach treats the buffer as a stateful closure:
(define (make-ring-buffer size)
(let ((buf (make-vector size 0))
(pos 0))
(lambda (op . args)
(case op
((write) (vector-set! buf pos (car args))
(set! pos (modulo (+ pos 1) size)))
((read) (vector-ref buf (modulo (+ pos (car args)) size)))
((freeze) (vector->list buf))))))
Java uses an array with modulo arithmetic, the textbook version from signal processing courses:
class RingBuffer {
private double[] buf;
private int pos = 0;
RingBuffer(int size) { buf = new double[size]; }
void write(double sample) {
buf[pos] = sample;
pos = (pos + 1) % buf.length;
}
double[] freeze() {
double[] copy = new double[buf.length];
System.arraycopy(buf, pos, copy, 0, buf.length - pos);
System.arraycopy(buf, 0, copy, buf.length - pos, pos);
return copy;
}
}
The freeze operation in Java rotates the buffer so sample zero is at the start—the oldest sample becomes the first element. In Scheme, you’d map over indices and read in order. Either way, you’re reconstructing time from a circular space.
Every pre-roll audio recorder, oscilloscope trigger, and meteor detection station uses this pattern. The past isn’t gone; it’s been rotating in place, waiting for something worth saving.