# atome
[](https://crates.io/crates/atome) [](https://docs.rs/atome/)
**A**udio **T**ranslucent **O**ptimized **M**acGyver **E**ngine
A real-time audio engine over [`cpal`](https://github.com/RustAudio/cpal). `atome`
handles the parts every audio application has to build anyway — device and host
enumeration, output streams with sample-accurate scheduling, capture streams,
multi-device routing, plugin chains, and audio file decoding — and leaves the
audio callback allocation-free while doing it.
It is generic over the sample type. `OutputClass<i16>` is `i16` from
`add_samples` all the way to the device; nothing is converted behind your back.
`atome` is synchronous and thread-based. There is no `async` API and no runtime
requirement — the mixer runs on its own thread and hands audio to the callback
over a lock-free ring buffer.
## Status
Early. The version is `0.8.0`, APIs are not stable, and breaking changes should
be expected until `1.0`.
**Working today:**
- Output streams (`OutputClass<S>`) with a mixer thread, indexed sample-accurate
scheduling, and a stop that clears both the mixer and the committed ring buffer
- Capture streams (`InputClass<S>`)
- Format alignment: planar↔interleaved weaving, channel up/down mapping, linear
resampling
- Device and host enumeration and lookup
- `AudioEngine<S>` over many devices, with routing resolved at construction and
plugin chains at three levels
- Plugin hosting: internal (Rust functions), VST3, and AU v2/v3, all through one
`Plugin::apply`
- Fourteen built-in effects (`plugins::atome`) with a shared parameter vocabulary
- Audio file import: container/encoding identification from magic bytes, then
decoding through Symphonia, whole-file or block at a time
**Not there yet** — the full list is in [`planning/TODO.md`](planning/TODO.md):
- **The engine does not carry captured audio from an input to its outputs.**
Routing is resolved and the streams are built, but the lock-free hand-off
between them is unwritten. `examples/live_thru.rs` does that forwarding by
hand, and is the shape it will take
- No sample/voice layer, no signal graph, no buses. `AudioEngine` mixes by
scheduling into an output, not by walking a graph
- Export is FLAC only (`export` feature); nothing lossy is written yet
- No duplex operation, no drift correction between unsynced device clocks
- VST 2.4 is a module of documentation and nothing else: there is no maintained
Rust crate to host through, so `Plugin::load` refuses the format
- No CI. `.github/workflows/publish.yml` covers releases only
- No benchmarks
## Installation
```toml
[dependencies]
atome = "0.8.0"
```
Every feature is off by default. Decoding audio files needs `import`, and
hosting plugins needs the feature for that format:
```toml
[dependencies]
atome = { version = "0.8.0", features = ["import", "vst3", "au3"] }
```
## Quick start
Decode a file and play it. This is `examples/play_file.rs`, shortened.
```rust
use atome::device::AtomeDevice;
use atome::output::{OutputType, SampleRate};
use atome::{import, output, AudioEngine};
use cpal::traits::StreamTrait;
use std::path::Path;
fn main() -> Result<(), cpal::Error> {
// Identification reads the file's bytes, never its name.
let decoded = import::decode(Path::new("assets/kick.wav"))?;
let devices = output::list_devices(None)?;
let device = devices.first().expect("no output devices");
let rate = SampleRate::from_hz(decoded.sample_rate).unwrap_or(SampleRate::Hz48k);
let mut engine = AudioEngine::<f32>::new(
vec![], // inputs
vec![AtomeDevice::output(device.clone(), OutputType::CoreAudio)],
rate,
vec![decoded.channels], // one count per output
None, // device picks the buffer size
vec![], // engine-wide plugins
)?;
let output = engine.outputs_mut()[0].output_mut();
// The file's sample type becomes the output's here; `align_samples` squares
// up rate and channel count. Both are no-ops when they already agree.
let samples = output.align_samples(
&decoded.samples.to_vec::<f32>(),
SampleRate::from_hz(decoded.sample_rate).unwrap_or(rate),
decoded.channels,
true, // already interleaved
)?;
let stream = output.build_stream()?;
stream.play()?;
output.add_samples(&samples, 0)?;
std::thread::sleep(std::time::Duration::from_secs_f64(decoded.duration() + 0.5));
Ok(())
}
```
`make example-play` runs the full version.
## Core concepts
### Sample type
`SampleType` is implemented for every type cpal can hand a stream callback —
`i8, i16, I24, i32, i64, u8, u16, U24, u32, u64, f32, f64`. It is a type
parameter rather than a runtime tag, so the callback is picked once when the
stream is built, and an `i16` pipeline never round-trips through `f32`.
### OutputClass
Owns one playback device's stream. `add_samples(&samples, index)` schedules
interleaved samples at an absolute sample index, summing with anything already
scheduled there — so two sources become one mix without either knowing about the
other. The call only hands a command to the mixer thread; no audio touches the
ring buffer on the calling thread, and the audio callback only pops from it.
`stop()` clears the mixer's queue *and* the samples already committed to the ring
buffer, so the tail is at most one buffer long rather than one buffer of audio
you have already lost control of.
### InputClass
The mirror, and deliberately much smaller: an input only has to hand over what
the device just gave it, so there is no queue and no mixer. You supply a
callback, and it runs **on the audio thread** — push the samples somewhere
lock-free and do the work elsewhere.
### AtomeDevice and AudioEngine
An `AtomeDevice` is a cpal device plus what `atome` needs on top of it: the host
it came through, its own plugin chain, and — for an input — which outputs it
feeds. Direction is a property of the device, so handing an output to the input
list fails at construction rather than when the wrong kind of stream is built.
```rust
use atome::device::AtomeDevice;
use atome::output::OutputType;
// A talkback mic that reaches the monitors and not the main mix.
let mic = AtomeDevice::default_input(OutputType::CoreAudio)
.expect("no input device")
.route_to(["Monitors"]);
```
An input that names no outputs feeds all of them. Names are resolved to indices
once, when the engine is built, so a name matching nothing is an error there
rather than silence at runtime.
`output_channels` is a list — `[2, 2, 5]` — because a stereo pair and a 5.1 rig
on one engine need different counts. A list of the wrong length is rejected, not
silently truncated.
### Plugins
The same `Plugin` attaches at three levels, and where it attaches is what decides
how much audio it hears:
| An input's `AtomeDevice` | that input alone, before routing |
| `AudioEngine::new` directly | everything |
| An output's `AtomeDevice` | what that device plays |
| Internal (Rust functions) | — | always |
| VST 3 | `vst3` | all platforms |
| AU v2 | `au` | macOS / iOS only |
| AU v3 | `au3` | macOS / iOS only |
| VST 2.4 | `vst` | no backend — `load` refuses it |
The shortest plugin is a Rust function, needing no feature and nothing installed:
```rust
use atome::Plugin;
against its own fields, a VST3 or AU against the names the plugin reports, and
`set_params`, `set_param`, `set_params_str`, `params`, and `param_schema` read
the same either way. A set is all-or-nothing: every name and value is checked
before any is written.
Each constructor takes a sample rate, because almost everything here derives a
coefficient from one and `process` only gets a buffer and a channel count. Build
the effect for the rate of the stream it will sit in.
### Importing audio (`import` feature)
Identification is two steps and neither looks at the file name: recognise the
container by its magic bytes, then read that container's header for the encoding
inside. `.ogg` says nothing about Vorbis versus Opus, and `.m4a` covers both AAC
and ALAC.
```rust
use atome::import;
use std::path::Path;
let path = Path::new("assets/pad.flac");
let encoding = import::find_type(path)?; // Encoding::Flac
let decoded = import::decode(path)?; // whole file, in the file's own type
let mut blocks = import::stream::<f32>(path)?; // or a block at a time, as f32
```
`decode` returns `Decoded`, whose `samples` is a `Samples` enum carrying the
file's *own* sample type — a 16-bit WAV is `I16`, a 24-bit FLAC is `I24`. Match
on it to avoid a conversion, or call `to_vec::<S>()` when you need one type.
**Decodes today:** PCM (WAV, AIFF, CAF), MP3, Vorbis, FLAC, ALAC, and Opus with
`import-opus` — plus AAC (LC, HE-AAC v1 and v2) through the **operating
system's** decoder only: AudioToolbox, Media Foundation, or MediaCodec. AAC is
patent-pooled, so no AAC decoder is bundled; on Linux, which has no OS decoder
for it, AAC is refused by name. The MP4, Matroska,
and Ogg demuxers needed to reach them come with `import`.
### Writing FLAC (`export` feature)
```rust
use atome::export;
// Anything import reads — a video's soundtrack included — straight to FLAC,
// a block at a time: 16-bit sources stay 16-bit, lossy ones land at 24.
let summary = export::to_flac("clip.mp4", "clip.flac")?;
```
`FlacWriter` writes one block at a time and `write_flac` writes a `Decoded`.
Pure Rust, through `flacenc`.
Each has a twin taking a writer instead of a path — `to_flac_into`,
`write_flac_into`, `FlacWriter::new` — for output that is not a file of its own:
anything `Write + Seek` (a `Cursor`, a file already open, an entry of a
[`pfac`](https://crates.io/crates/pfac) bundle). Seek is required because the
header's sample count and MD5 are only known at the end and are patched in
afterwards.
```rust
use std::io::Cursor;
let mut flac = Cursor::new(Vec::new());
let summary = export::to_flac_into("clip.mp4", &mut flac)?;
assert_eq!(summary.bytes, flac.get_ref().len() as u64);
```
**Identified but not decodable:** AC-3, E-AC-3, DTS, DTS-HD MA, Dolby TrueHD,
WMA, AMR-NB, AMR-WB. None has a published, maintained Rust decoder to point a
feature at, so these fail with a clear "no decoder" error rather than a misread
file.
## Feature flags
| `asio` | ASIO output via cpal's ASIO backend. Windows in practice; needs the ASIO SDK and LLVM/Clang on `PATH` |
| `vst` | VST 2.4 module. Documentation only — no backend |
| `vst3` | VST 3 hosting via `truce-rack-vst3` |
| `au` | Audio Unit v2 hosting. macOS / iOS only |
| `au3` | Audio Unit v3 (App Extension) hosting. macOS / iOS only |
| `plugins` | Every plugin format at once |
| `import` | Audio file decoding — PCM, MP3, Vorbis, FLAC, ALAC in pure Rust; AAC through the OS |
| `import-he-aac` | An alias of `import`, kept so existing builds resolve: HE-AAC is the OS's to decode |
| `import-opus` | Opus via libopus, built from source. Needs a C compiler |
| `import-all` | Every format with a working decoder |
| `export` | FLAC writing, frame by frame. Pure Rust. With `import`, `export::to_flac` |
## Examples
`make help` lists them all. `example1`–`example4` build real streams and print
how they are wired without making sound; the `example-*` targets are audible.
| `make example1` | The smallest engine: one input, one output, no routing |
| `make example2` | An input routed to one specific output |
| `make example3` | Several outputs with different channel counts, partial routing |
| `make example4` | Plugins at all three levels, and what each one hears |
| `make example-play` | Decode a file and play it. **Makes sound** |
| `make example-effects` | File → plugin chain → device, block by block. **Makes sound** |
| `make example-thru` | Microphone → all three plugin levels → device. **Headphones** |
| `make example-mix` | A file and a microphone summed into one output. **Headphones** |
`example-thru` and `example-mix` open a microphone and play it back — through a
speaker that is a feedback loop.
## Platform support
Developed and run on macOS (CoreAudio) and Windows (WASAPI, ASIO). The
`OutputType` enum names only those hosts — CoreAudio, WASAPI, ASIO, DirectSound,
WDMKS, MME — so while cpal itself reaches ALSA and JACK, there is no variant for
them and Linux is untested.
### Testing on every operating system
```sh
make docker-test # every system this host can run
make docker-test SYSTEMS="debian alpine" # just these
make fixtures # the decode fixtures, for a local `make test`
```
Runs from Linux, Windows (`docker\run.ps1`, or `run.sh` from Git Bash), or an
Apple-silicon Mac. Debian, Ubuntu, Fedora, and Alpine run the whole suite in
containers carrying ffmpeg, which makes every decode fixture first so none of
those tests skips, and a null ALSA device so the device tests run. Windows is
cross-built and tested under Wine; Android is cross-built with the NDK. The
native systems are skipped wherever they cannot run rather than failed: macOS
(and the iOS cross-build) on a Mac, a real Windows container only on a Windows
host.
## Contributing
Issues and PRs are welcome. Since this crate touches real-time audio code, please
include a brief note on how a change was tested (a sample project, a recording,
or a description of the signal chain) when contributing to the engine, mixer, or
plugin internals.
## License
Licensed under the [MIT License](LICENSE).