Skip to content

feat(export): songs export to WAV, AIFF, FLAC, Apple Lossless, MP3, AAC, Opus and Ogg Vorbis, and song.mp3 no longer needs ffmpeg - #46

Merged
devlato merged 3 commits into
mainfrom
feat/export
Oct 20, 2026
Merged

devlato merged 3 commits into
mainfrom
feat/export

Conversation

@devlato

@devlato devlato commented Oct 20, 2026

Copy link
Copy Markdown
Owner

Songs can be exported in every popular format, including the ones the web plays, and every song folder gets its song.mp3 without ffmpeg.

claudius export <song folder or audio file> -f mp3,flac:16,opus writes the master as:

  • WAV: 24-bit, 16-bit at 44.1 kHz (CD), or 32-bit float;
  • AIFF, FLAC, Apple Lossless;
  • MP3 at 320 to 128 kbps, or VBR V0 to V9;
  • AAC in .m4a, Opus in .opus, and Ogg Vorbis.

--list shows every format, and any this machine cannot write. The studio menu, MCP tool and HTTP download come in a follow-up pull request.

How it's built

A new crate, claudius-encode, the counterpart of claudius-decode:

  • In Rust, in process:
    • WAV (hound) and AIFF (written here);
    • FLAC (flacenc, the pack installer's code moved here);
    • MP3 (rusty_mp3) and Opus (opus-pure).
  • Built in: Ogg Vorbis, through vorbis_rs, which compiles libvorbis with aoTuV via cc.
  • The system's encoder: AAC and Apple Lossless come from Core Audio's afconvert on macOS, and from ffmpeg elsewhere when it is installed.

Licences: Apache-2.0 and BSD-3 only. LAME (LGPL) was ruled out by deny.toml. A pure-Rust AAC encoder exists (oxideav-aac), but it took 36 s to encode 30 s of audio.

What else is in it:

  • Resampling to 44.1 kHz: a polyphase Kaiser-windowed sinc with about 95 dB of alias rejection, flat to 20 kHz.
  • 16-bit files: TPDF dither from a fixed seed, so the same song always makes the same file.
  • Tags: ID3v2.4 in MP3, Vorbis comments in FLAC, Opus and Ogg, and ffmpeg's tags for AAC and ALAC.
  • FLAC: every file is decoded again before it is kept. Two ways of choosing predictors are tried and the smaller valid one wins. A 24-bit master went from 97% of its raw size to 82%.
  • Supporting changes:
    • claudius-decode gains decode_bytes;
    • the engine gains an export module;
    • song.mp3 and the server's playback MP3 are encoded in process;
    • the doctor's ffmpeg line now says what ffmpeg is still for;
    • the telemetry Worker learns the export command. It needs a redeploy after the merge.

How I checked

Quality, on 30 s of a real render, against ffmpeg's encoders at the same bitrate (mean difference in third-octave band energy, decoded by ffmpeg):

claudius reference
MP3 320 0.26 dB LAME 0.39 dB
MP3 V0 0.47 dB LAME 0.48 dB
MP3 192 1.28 dB LAME 1.28 dB
Opus 160 0.67 dB libopus 0.71 dB
AAC 256 Core Audio, the same encoder Apple 0.19 dB

Every format of a 3.5-minute song, all ten exported in 4.4 s, decodes:

  • Core Audio decodes all ten, with exact lengths, so AAC and Opus playback is gapless.
  • symphonia decodes all but Opus, which it does not support.
  • Chromium 156 decodes MP3, AAC, Opus, Ogg Vorbis, FLAC and WAV.
  • WebKit 27.2 decodes all ten. The ALAC file, which Apple's own encoder wrote, stops at 158 s in WebKit. AIFF and ALAC are documented as formats for Apple's apps, not the web.

Tests:

  • new unit tests: specs, lossless WAV and AIFF round trips, dither, FLAC tags and the bound, the MP3 Info frame, gapless Opus, deterministic Vorbis, Core Audio AAC and ALAC, and the resampler's alias rejection;
  • the CLI test exports a rendered song to three formats and decodes them, and checks the errors;
  • cargo xtask ci, cargo deny check, cargo xtask site with lychee (0 broken links), and the Worker's tests pass.

…AC, Opus and Ogg Vorbis, and song.mp3 no longer needs ffmpeg
@devlato
devlato enabled auto-merge (squash) October 20, 2026 02:22
@devlato
devlato merged commit ad2723e into main Oct 20, 2026
26 checks passed
@devlato
devlato deleted the feat/export branch October 20, 2026 02:51
devlato added a commit that referenced this pull request Oct 20, 2026
The formats `claudius export` writes
([#46](#46)) are now everywhere
else too.

- **Studio:** an **export** menu in the toolbar, after the stems menu.
It lists the presets in two groups, lossless and compressed. Each is a
download of the latest render, named after the song. A format this
machine cannot write is greyed out and says why on hover: AAC and Apple
Lossless without Core Audio or ffmpeg. The menu says when the render
predates the latest change. It works in the native and the web studio;
the demo, which has no server, shows no menu.
- **MCP:** an `export` tool, with `formats` and an optional `path`. It
writes into the song's folder (or `path`), rendering first when the song
changed since its last render.
- **API:**
- `Command::Export` writes from the latest render when it is of the song
as it is now, and otherwise says to render first.
- `GET /api/export/{version}/{name}.{ext}?format=SPEC` serves that
render as a download (`Content-Disposition: attachment`), with the
format's media type. It is encoded on request, and the latest export is
kept in memory for a second click.

CodeQL flagged the first version of the download (9
`rust/path-injection` alerts). The version came from `Path<(u64,
String)>`, and the format named a cache file. Neither could leave the
cache, but the route now parses the version from a string, as
`/api/audio` does, and keeps nothing on disk. No path is built from the
request except the render's number.
- `State.exports` lists every preset, with its label and why it cannot
be written here, when it cannot.

Docs: the Exporting page gains the studio, the tool and the API. Also
updated: the MCP tool table, the HTTP routes and commands, the studio
page, and the skill's tool list and saving section.

How I checked:
- **A new server test:** a rendered song exports by command (two formats
into a folder; a bad spec is refused) and as a download. The download is
checked for type, filename and a decodable 44.1 kHz FLAC, plus a 400 for
a mismatched extension and a 404 for a render that isn't there.
- **MCP:** the test checks that `save` and `export` are listed.
- **The native studio**, driven by `CLAUDIUS_UI_PLAN`: it rendered a
song, and the export menu was opened and captured with all thirteen
presets.
- `cargo xtask ci` passes, and `cargo xtask site` builds the web studio,
with lychee reporting 0 broken links.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant