Fleet
2026-06-18
BackFive small standing stones sit in a room like furniture. Dark, silent, the kind of object you stop seeing after a week. Then at 6pm, with nobody touching anything, they wake at once and softly sing: a piece of sacred polyphony, one voice per stone, a faint light rising inside each one while it holds its part and fading when it falls quiet. When the last note dies they go back to being rock. That is the whole thing. A stone circle that performs its evening song by itself, and asks almost nothing of you in return.
Fleet is that choir. Five battery-powered ESP32-S3 statues that stand inert most of the day and sing together at 6pm and on demand, with no central brain and no wires between them. One promise governs every hard decision in it, and I kept coming back to it whenever I was tempted to add something:
a small distributed choir you rarely have to charge.
A choir you have to nurse every day would not be the thing. The whole point is the rite happening unattended, indefinitely, so the survival state had to be the design, not an afterthought. The code is public at github.com/ma-r-s/Fleet. Here is what it sounds like.
No samples are playing back there. Every note is being computed, live, on five separate microcontrollers that found their downbeat over the air a second before they came in.
Each statue is one retail Waveshare ESP32-S3 "AI Smart Speaker" kit, chosen because it ships whole: an ESP32-S3 with WiFi and BLE, an ES8311 codec driving a real speaker, a seven-pixel WS2812B ring for the inner light, a LiPo cell with charging, a real-time clock, and buttons. Not a bare dev board with a breadboard taped to it. That decision bought me a working instrument on day one and let me spend my time on the parts that are actually hard: the voice, and getting five of them to agree.
The bodies are the part that makes them a choir of stones rather than five speakers in a row, and they are not shapes I invented. Each one is derived from a real 3D scan of a Rouergat statue-menhir, a prehistoric stone votive figure, the kind of weathered standing rock with a face half-eroded out of it that sits in a regional museum. The scans came in slab-like and flat, too thin to hold a board, so a custom browser tool samples each footprint in forty vertical bands, finds the thin axis, and pushes the surface outward along it, "fattening" the figure toward round without ever forcing it into an honest cylinder. Height and local variation survive; the erosion and the asymmetry survive. Then a flat base, then one blind cylindrical cavity bored up through it, 59mm across and 41mm deep, sized so the assembled board slides in upright with the LEDs sitting low in the body to glow through the stone. Five figures, Rodez, Montels, Jasse I, Jasse II, Albespy, a tall narrow cantor next to a short broad mask next to a cowled elder, so the group reads as a family of individuals.
They are printed in marble PLA. One warm terracotta-stone color across all five, not a palette. There is a part of the design that wanted to color-code the voices, white for the bass up to pink for the soprano, and it is a nice idea on paper that I did not build. What you can rotate below is what comes off the printer: five stones of the same marble, told apart by body, posture, and face, with a faint light that lives inside the rock rather than on it. The minimum wall thickness around each cavity varies with the scan, so how evenly that light reads through the surface is still something I have to measure on a real print rather than claim.
The first real decision was that the voice is procedural. No sample banks, no recordings. Any stone can be any pitch and any vowel instantly, and there is nothing to manage in flash. The second decision was how to make that voice, and the honest first version was the textbook one: a band-limited sawtooth pushed through parallel formant filters, source and filter, the way most formant synths work. It sang. It also fell apart exactly where a choir needs it most.
Source-filter synthesis degrades at the top of the range. When the pitch is high the harmonics get too sparse to trace the shape of the formants, the resonances the filter is supposed to color, and the result thins out and buzzes. That is the soprano problem, and a choir without a soprano is not a choir. So the voice moved to FOF synthesis, the Fonction d'Onde Formantique technique from IRCAM's CHANT program in the early 1980s, built specifically for the sung voice. Instead of filtering a buzzy waveform every sample, FOF fires a short grain, a sinusoid at each formant frequency under a soft attack and an exponential decay, once per glottal period. It has forty years of proven choral results behind it, it is cheaper at runtime so there is CPU headroom for several voices and reverb on the ESP32-S3, and it simply does not have the soprano failure mode. It runs at 32 kHz, five formants, with vibrato and reverb. The old source-filter voice is still in the tree as the baseline the host tests check against.
None of that makes it a choir yet. Five boards running identical DSP play five notes, which is an organ, not an ensemble. The life of a real choir is in tiny human imperfection: each voice detuned by a few cents, its own vibrato rate and depth and onset, a little timing jitter coming in. Each board generates those differences from its own MAC address, so every stone is permanently slightly out of tune with the others in its own fixed way, unique but always itself. And the spatial spread is real and free here, which is the one thing a chorus plugin can never fake: five genuinely independent sources standing in the room, not one voice copied five times.
Mostly the choir sings on vowels: the sound is five sustained formant voices blending in a room, not diction. But it can sing actual words, and that path is real. English text becomes a string of phonemes in ARPABET, the ASCII phoneme alphabet from CMUdict, each phoneme then driving the formant synthesis: vowels as held formant targets, consonants as short noise bursts and rapid formant glides spliced onto the front of the note. The widget below does the whole transform in your browser. Type a sentence, watch it turn into the ARPABET string the stones would actually sing, and hear it synthesized by a port of the same FOF voice the firmware runs, the same DSP, moved into JavaScript.
Each phoneme drives the same FOF formant synthesis the statues run, ported from the firmware C to JavaScript. Vowels are held formant targets; consonants are short noise bursts and formant glides. The timbre is a close approximation of the hardware, not bit-identical.
If it sounds intelligible-but-robotic, that is the honest ceiling, not a bug I will fix later. Sung consonants wedged between long pitched vowels are inherently mushier than spoken ones; you cannot hold a clean t the way you hold an ah. The best this could ever be is a robotic choir singing recognizable words, never crisp human diction. Which is why words are the secondary capability and the held five-voice polyphony is the point. The instrument loves slow sacred Renaissance counterpoint, Palestrina, plainchant, long notes melting into reverb. It does not love fast runs, and I stopped trying to make it.
Getting five boards to come in on the same downbeat, over the air, with no shared wall clock and no node in charge, took the longest of anything in the project. My first attempt looks reasonable written down: elect a conductor by rank, let it stream the notes to everyone else. It produced a smear. A board that had just rebooted and woken had heard no peers yet, computed itself as rank zero, and self-elected, so there were several conductors at once, each with its own idea of when the beat was, each streaming. The choir came in like a stumble. The fix was to delete the conductor entirely.
What replaced it is a leaderless mesh. No node is in charge, not a designated conductor and explicitly not the hub. Any node can start a concert, a button on a statue or the hub, and starting it is just a broadcast announcement, not a command. From there every awake board gossips a countdown, not a timestamp but a duration, "fire in N milliseconds", which needs no shared clock to be meaningful. They converge by always taking the earliest credible downbeat they hear. The rule is bounded, so one late packet cannot drag the whole fleet early. On that downbeat each statue plays its own seat's part from its own copy of the score. Nothing is streamed, so nothing depends on any one board staying alive. Whatever subset happens to be powered on sings together; the rest of the room is silent and the piece still runs to its end. In the final second before the beat the gossip goes dense, every 25ms instead of every 200, and that burst is what pulls the entrance from a smear down to twenty or thirty milliseconds.
All of this rides ESP-NOW, direct radio between boards with no router in the path. Both firmwares are compiled with the same hardcoded channel, so the two ends can never disagree about where to listen. WiFi exists only to flash new firmware. The day-to-day life of the choir never touches it.
There are two modes and only two, and the difference is just whether WiFi is on. Dev is WiFi up, for over-the-air flashing, and it drains a statue's battery in an hour or two, so you flash and you leave it. Inert is the stone: WiFi off, the board deep-sleeping through most of every ten-second cycle and waking for about a second and a half to listen for a command on the fixed channel, marked by a glow so dim you would miss it. A statue that has heard nothing at all for about a day assumes it is stranded and wakes itself up to be reachable. A statue whose cell drops too low parks itself in an hour-long radio-off sleep above the brownout floor rather than dying on the bench, and rejoins on its own once it is charged. Charge the fleet and walk away, and every stone finds the hub again without you ever opening the tools. That is the promise, kept in the power code.
Underneath it is a black box, a flight recorder that writes every state change and every battery reading to raw flash so the record survives reboots and even a fully dead cell. You can read back what happened to a board before it died. Which is how I found the limitation I keep coming back to. On an inert deep-sleep wake the battery sense divider is unpowered. There is no voltage to read at the pin, no matter how long you let it settle: a read there returns zero, always. Deep-sleep voltage is simply not measurable on this board, full stop.
And the flight recorder, the thing built to tell me what was happening, had been faithfully filling its own finite, non-wrapping log with those unreadable zeros until it ran out of room, recording in detail that it could see nothing. The fix was to drop the failed reads, stash the last good awake voltage, and feed that into the heartbeat so the hub still shows a real percentage. But the deeper lesson stuck: knowing exactly what the hardware cannot measure turned out to matter as much as anything it can. A battery readout I could have measured on the bench all day, and would have trusted, and would have been wrong.
Fleet is not a product and I am not pretending it could be. It is a personal object made because it should exist, and the bar was never repeatability or a roadmap. The bar was a feeling: that a room could hold five quiet stones you mostly forget about, and that once a day they would lift their voices together without being asked, and that the technology would disappear into something that feels old and still. The voice is robotic when it reaches for words and luminous when it just holds a chord. The light is faint. The whole thing rests far more than it sings. That is exactly right. I built it for the minute at 6pm when the stones come alive, and then go back to being rock.
CC BY-NC 4.0 © ma-r-s
?