The Cylinder Knows Where It Left Off
The music box cylinder rotates. When it completes a revolution, it doesn’t stop — it simply begins again. The pins that plucked the first notes now pluck them once more, the melody looping without intervention.
This is, computationally speaking, a circular buffer. A fixed region of memory where the write pointer chases the read pointer around and around, wrapping from end to beginning without reallocating, without copying, without fuss. Operating systems in the 1970s discovered this structure was perfect for handling streams of data that never stopped: keyboard input, network packets, audio samples. The cylinder predates all of them by a century, encoding melodies into brass using the same topological trick.
Here’s a minimal implementation in Swift — modern, typed, slightly fussy about boundaries:
struct RingBuffer<T> {
private var storage: [T?]
private var head = 0
init(capacity: Int) { storage = Array(repeating: nil, count: capacity) }
mutating func push(_ item: T) {
storage[head] = item
head = (head + 1) % storage.count
}
func peek(offset: Int) -> T? {
storage[(head + offset) % storage.count]
}
}
And in Perl, which doesn’t care about your types and just wants to get the job done:
sub ring_push {
my ($buf, $cap, $idx_ref, $val) = @_;
$buf->[$$idx_ref] = $val;
$$idx_ref = ($$idx_ref + 1) % $cap;
}
my @buffer = (undef) x 8;
my $i = 0;
ring_push(\@buffer, 8, \$i, "C4");
ring_push(\@buffer, 8, \$i, "E4");
ring_push(\@buffer, 8, \$i, "G4");
print join(", ", grep { defined } @buffer), "\n"; # C4, E4, G4
The modulo operator does all the work: (position + 1) % capacity. That single % is the mathematical equivalent of a cylinder completing its revolution. Punch a pin at position 47 on a 48-note cylinder, and the next pin wraps to position 0. Write a byte at index 1023 in a 1024-byte buffer, and the next write lands at index 0.
The trade-off is fixed capacity. A ring buffer can’t grow without rebuilding itself. Neither can a brass cylinder — the diameter is cast, the circumference immutable. You choose your loop length at the foundry, not at runtime. This constraint forces economy: every pin must earn its place in the rotation.
I’ve been staring at the cylinder I bent yesterday, trying to understand its pin spacing. The previous owner transcribed something — a hymn, I think, in 3/4 time — but the timing is slightly off, the tempo drifting across the rotation. It’s a buffer with corrupted data, still readable, still playing, just wrong in ways that become clearer with each revolution.
Tomorrow I’ll try mapping the existing pins before I punch new ones. Reverse-engineering someone else’s circular buffer, one brass spike at a time.