If you edit podcasts or long recordings in Audacity, you probably use label tracks: little text marks on the timeline ("Intro", "Interview", "Outro"). Audacity can split a project into one file per label (Export Multiple → Split files based on Labels). I recently added the same thing to a browser-based .aup3/.aup4 tool, plus a batch mode for several projects at once. Reproducing the rule exactly took more care than I expected, so here is how it works and what tripped me up.
Where labels live
A label track is stored in the project document as <labeltrack> containing <label t="…" t1="…" title="…"/> elements. t is the start in seconds and t1 the end. I checked the writer (LabelTrack::WriteXML) in Audacity 3.7.9 and 4.0.0 and the same names show up in .aup3, .aup4 and the old XML-based 1.x/2.x .aup, so one reader covers all three. Audacity 4.0 only adds an isSelected attribute.
Two label kinds share this structure:
- a region label has
t1 > t; - a point label has
t1 == t(or a missing or backwardst1, which I treat as a point).
That distinction drives the whole split rule.
The split rule
I followed what Audacity's export dialog code does (ExportAudioDialog::UpdateLabelExportSettings, read at the 4.0.0 tag):
- Every label starts a file.
- A region label ends its file at the region's own end.
- A point label ends its file at the next label's start.
- The last point label runs to the end of the audio.
- Audio before the first label becomes a file only if you ask for it ("Include audio before first label", off by default), and it is named after the project.
- An empty title becomes
untitled. - Files are numbered
01-Title,02-Title, … so they sort.
The core is small:
const sorted = [...labels].sort((a, b) => a.start - b.start || a.end - b.end);
sorted.forEach((label, index) => {
const isPoint = label.end <= label.start;
const end = !isPoint
? label.end
: index < sorted.length - 1
? sorted[index + 1].start
: duration;
raw.push({
number: index + 1,
title: label.title.trim() || "untitled",
start: label.start,
end,
});
});
Notice that a region label's end is not clipped to the next label. Overlapping regions produce overlapping files, which is what Audacity's rule does too.
Pitfalls
Segments need clamping. Label times come from a document a user may have edited, shortened, or hand-crafted. I clamp both ends into [0, duration] and drop any segment shorter than one sample (1/48000 s). Otherwise you export empty files.
File names are a mess. Titles contain /, :, ?, quotes and control characters, and two labels can have the same title. I replace the characters file systems reject with _ (similar to what Audacity's Internat::SanitiseFilename does), collapse whitespace, cap at 120 characters, and de-duplicate case-insensitively, since macOS and Windows treat Intro and intro as the same file:
let fileName = base;
for (let n = 2; used.has(fileName.toLowerCase()); n++) {
fileName = `${base} (${n})`;
}
used.add(fileName.toLowerCase());
Audacity only looks at the first label track. A project can have several. Audacity's split always uses the first. I let the user pick which one, since a project with one label track for chapters and another for edit notes is common.
Each piece must be exactly its span of the mix. I render each file as a window of the whole mixed-down audio, rather than re-mixing each piece separately. A test checks that a split file is exactly its span of the mix.
Giving the pieces names players can read
Split MP3s carry a title and track number, written as an ID3v2.3 tag in front of the encoded stream. I chose v2.3 over v2.4 for its wider player support. The consequence is that v2.3 has no UTF-8, so text frames are UTF-16 with a byte-order mark. Frame sizes are plain big-endian in 2.3, and only the tag header's size is "syncsafe" (7 bits per byte) — an easy detail to get backwards.
Batch: many projects, one worker
If you drop two or more .aup3/.aup4 files (each with its own -wal if it has one) or a folder of them, the page lists them instead of opening one. The design decisions:
- One project in memory at a time. The batch opens and exports projects sequentially in the same Web Worker. Opening them in parallel would multiply memory use, and projects can be large.
- One failure never stops the batch. A project whose document doesn't decode is skipped and named in the report rather than aborting the run.
-
Everything goes into one ZIP, built in the browser, with a
batch report.txt. Per project it lists the files produced, any unreadable blocks, effects that weren't rendered, and what was skipped or failed and why. - Optionally split each project at its labels, with a folder per project and each project's label file.
Deciding what a drop is lives in a pure planning function that looks at the dropped file names and returns a kind (single project, batch, and so on), so it can be unit-tested without a browser. Recovery and downgrade pages keep one project at a time.
What is free and what isn't
For transparency: reading labels and exporting Audacity's own label text file (start, end, title, tab-separated, six decimals — the format File → Import → Labels reads back) or "m:ss Title" timestamps is free. Splitting at labels, the chaptered MP3 and batch export are paid features in the tool.
Testing
The unit tests pin the document reading, the split rule, both text formats, the ID3 bytes and the free/paid split. The fixtures are two existing test projects with label tracks added; the audio is untouched. An end-to-end script runs the feature with real pointer events on desktop and phone viewports. Its first run caught label names overlapping in the phone-width timeline lane, so a point label's text now gets only the room up to the next label.
Try it
If you want to see this without installing Audacity, the splitting feature is described at Waveward's split-by-labels page. Disclosure: I built it. Everything runs in your browser, and projects are not uploaded.
Takeaways
- Copy the reference implementation's rule, including its odd corners, and note which version you read.
- Treat region and point labels differently; the next label's start is the only end a point label has.
- Clamp, drop empty segments, sanitize and de-duplicate names.
- Batch work: sequential, isolated failures, and a report that says what was skipped.
Top comments (0)