FermataFMT-001
2026 –
Digital audio workstation
Put it backAudio & music crate
Back to Audio & music crateFMT-001 · 2026 – · Audio & music

Fermata

Digital audio workstation

A Logic Pro–style DAW built from scratch. The Rust audio engine never waits on a lock, independent tracks run on a pool of real-time threads, and it hosts VST3 plug-ins.

Stack
Rust, Tauri 2, React, TypeScript
Links
Project site ↗ · GitHub ↗
Fermata screenshot
Fermata's project site (app screenshots to come)

Overview

Fermata is a digital audio workstation modelled on Logic Pro's workflow and layout. The audio engine and the VST3 host are written in Rust, and the interface is Tauri 2 with React 19 and TypeScript. Logic sets the scope: the repo keeps a list of every Logic Pro feature by stage and marks which ones Fermata has.

It covers the path from recording to export: audio and MIDI recording with count-in, cycle recording into take folders and swipe comping, Logic's editing tools, a piano roll, a drum step sequencer, channel strips with an eight-band EQ, eight effect slots and eight sends, five automation modes, six built-in synths, a sampler, 16 effects and VST3 hosting.

It's alpha software. I develop and test it on Linux. The Windows and macOS builds pass the engine tests and headless checks on a silent stand-in device, but nobody has run them on a real machine yet. I built it with Claude Code, developing independent features in parallel in separate git worktrees.

Architecture

The code is a Cargo workspace: vst3-host for plug-in hosting, daw-engine for the project model, real-time graph, device I/O and rendering (no UI code), and daw for the Tauri app. The React frontend draws the tracks area, editors and meters on canvas.

The engine is split into two halves that never share mutable data. Session runs off the audio thread and owns the project, loaded audio, plug-in controllers, undo history and recorder. Every edit goes through it. Engine is the real-time graph, and it only hears about edits through Cmd messages. Each user edit is one Tauri command and one undo step.

Keeping the audio thread honest

Code on the audio thread may not allocate, free, lock, block or do I/O. Buffers are sized up front, and edits arrive through a pattern the docs call Cmd in, Garbage out. The session builds whatever the engine needs (a track's DSP state, a new region list) on its own thread and sends it over a wait-free rtrb ring. The engine swaps it in at the start of the next callback and sends the old object back on a second ring, so the memory gets freed on the main thread.

Those rings used to be crossbeam_channels, and I learned the difference the hard way. A crossbeam receiver can spin while a preempted sender finishes its write. At real-time priority, the audio thread starved the sender until the kernel's RLIMIT_RTTIME limit killed the app with SIGXCPU. rtrb never waits.

Failures on the audio thread are contained rather than fatal. Denormals are flushed to zero for the whole callback. If callbacks fall behind for more than 60 ms, the engine sheds load for 250 ms so the thread can sleep before the kernel kills it. Panics are caught, because unwinding into the driver's C callback would abort the process. After every plug-in, NaNs are silenced and runaway levels clamped, and a toast names the plug-in. A headless check, rt_alloc_check, plays a project that uses every feature through a counting allocator and fails if any block allocates once warmed up.

Multi-core without changing the sound

Independent tracks can play at the same time on a small pool of real-time threads. The requirement I set was that the output stay bit for bit the same as the serial loop's, so nothing else in the program has to care which path ran.

The hard part is the mix. Float addition isn't associative, so if threads sum into a bus in whatever order they finish, the result changes from run to run. On the pool, no strip writes into another. Each strip keeps its output and sends in its own buffers, and a bus adds up its feeders in the serial loop's exact order. That makes switching between serial and parallel at any block boundary inaudible, and lets the tests compare renders exactly.

The pool has no lock or queue. Each task is a bit in a few atomic words, and whoever clears a ready bit owns the task, so a preempted worker can only hold up the one task it took. If a worker gets stuck, the block is given up after 0.1 s and treated as an overload. The engine also checks that the task graph covers every route before using it, and plays serially until it does. A light project never engages the pool, so it pays nothing.

Hosting plug-ins safely

A plug-in that crashes during scanning shouldn't take the DAW with it. Fermata runs its own binary as fermata --scan-vst3 <bundle> for each bundle, four at a time, and kills any child that takes more than 20 seconds.

Results

Measured on an i7-13700KF, mean block time:

  • 24 instances of CHOWTapeModel at a 512-frame buffer: 4822 µs on one thread, 754 µs on eight.
  • 16 Dragonfly Hall reverbs at 128 frames: 430 µs on one thread, 111 µs on eight.
  • Keeping workers on the performance cores took 24 CHOWTapeModels from 340 µs to 270 µs at 128 frames.

Eight threads helped wide projects only 10 to 25% more than six while tying up another performance core, so the automatic setting stops at six. A soak_check makes a few hundred random edits with the pool running and compares parallel renders with serial ones bit for bit, and the pool's tests ran clean under ThreadSanitizer.

What's next

The current release is 0.1.0-alpha.5. The biggest open item is running the Windows and macOS builds on real hardware. Multi-core processing is Linux-only for now, VST3 editor windows don't open on Windows yet, and the synth models haven't been tuned by ear or compared with the instruments they're modelled on.