Isokratis is a practice app for Byzantine chanters. A chanter needs a stable drone — the isokratima — to sing against, and the app synthesizes that drone in real time while the singer chooses the scale and the starting note. The interesting engineering lives in two places: the music model and the audio graph.
Music theory, encoded
Byzantine chant does not use equal temperament. The octave is divided into 72 equal units called moria, and each scale is a sequence of interval steps in that space. So instead of hardcoding frequencies, the model works like the theory does:
- 72 moria per octave — a frequency factor of 2^(m/72) per morium
- Seven notes — Νη, Πα, Βου, Γα, Δι, Κε, Ζω — on a fixed tonic (Κε = 220 Hz)
- A scale is an interval pattern; the note grid is generated by rotating that pattern across the octave
Selecting a different scale changes only the interval list. The pitch is always derived from the tonic through the moria arithmetic, which keeps the whole engine consistent — and makes microtonal shifts natural, because a morium is the smallest unit.
The audio graph
The synthesis chain is deliberately small and typed:
AudioSource → ActiveVoice → DroneEngine → destination
Each ActiveVoice is either an oscillator (continuous tone) or a buffer sample (recorded drone), routed through a low-pass filter, an EQ stage and a gain node with an ADSR envelope. The DroneEngine owns the master gain and exposes events (start, stop, voicesChanged) so the UI stays in sync without polling.
The parts that take real care:
- Pitch shifting in moria, not cents: coarse steps of ±6 and fine steps of ±1, up to ±30 moria total. For oscillators this is just
frequency = tonic * 2^(m/72); for samples the engine applies the same ratio at playback rate. - Envelopes that don’t click. Attack and release ramps sound trivial until you hear the pop of an unramped gain change.
- Polyphony in the premium tier — multiple voices at once, each with its own envelope and filter settings.
Persistence and sync
Settings and presets live in IndexedDB per user (user:123:settings), which makes the app feel instant. On the server, a Cloudflare D1 database stores accounts, sessions and premium state, and cross-device sync uses a stale-while-revalidate helper: IndexedDB answers immediately, the API refreshes in the background.
What the engine taught me
The Web Audio API is surprisingly complete for this use case — the only thing you cannot do is ignore the audio thread’s state machine. Every gain ramp, every voice start, has to be scheduled or it clicks. That discipline carries over: the same care shows up in the API layer, where session validation is cached in Cloudflare KV with a TTL, so a chanter’s first note of the day is not slowed down by a database round-trip.
If you sing Byzantine chant, give it a try. If you write audio code, the moria arithmetic is the part I would steal.