Skip to content

Working with Chapters

TagLib-Wasm reads and writes chapter markers — the named, timestamped sections used by audiobooks, podcasts, and long-form recordings — through the Full API's AudioFile.getChapters() and AudioFile.setChapters().

Quick Start

typescript
import { TagLib } from "taglib-wasm";

const taglib = await TagLib.initialize();
using file = await taglib.open("audiobook.m4b");

// Read chapters (always ordered by start time)
for (const ch of file.getChapters()) {
  console.log(
    `${ch.startTimeMs}–${ch.endTimeMs ?? "?"} ${ch.title} (${ch.source})`,
  );
}

// Replace all chapters
file.setChapters([
  { startTimeMs: 0, title: "Intro" },
  { startTimeMs: 95_000, title: "Chapter 1" },
  { startTimeMs: 612_000, title: "Chapter 2" },
]);
file.save();

The Chapter Type

typescript
interface Chapter {
  /** Chapter start, milliseconds from the start of the file. */
  startTimeMs: number;
  /**
   * Chapter end, milliseconds. Explicit for ID3v2 CHAP frames; inferred for
   * MP4 chapters (the next chapter's start, or the track duration for the
   * last one). `undefined` only when the duration can't be determined.
   */
  endTimeMs?: number;
  /** Chapter title. */
  title?: string;
  /** ID3v2 CHAP element ID. `undefined` for MP4 chapters. */
  id?: string;
  /** Which container structure this chapter was read from. */
  source?: "id3" | "nero" | "quicktime";
}

How chapters are stored

FormatRead fromWritten by setChapters()
MP3ID3v2 CHAP framesID3v2 CHAP frames
MP4 (.m4a/.m4b/.mp4)QuickTime chapter track (preferred) or Nero chpl atomQuickTime track, Nero atom, or both — see below
Otherthrows UnsupportedFormatError

When an MP4 file has both a QuickTime chapter track and a Nero chpl atom, getChapters() returns the QuickTime chapters (they have no length limit and are what Apple devices read). Each returned chapter reports which structure it came from via source.

Choosing an MP4 chapter style

For MP4 files, setChapters() accepts a SetChaptersOptions with mp4ChapterStyle:

typescript
file.setChapters(chapters, { mp4ChapterStyle: "both" });
ValueWritesRead byNotes
"quicktime" (default)a QuickTime chapter trackApple Books / Podcasts / iOS and most playersno chapter-count limit
"nero"a Nero chpl atomffmpeg, foobar2000, MP4Boxignored by Apple devices; max 255 chapters
"both"both of the aboveeverythingwith >255 chapters the QuickTime track gets all, the Nero atom gets the first 255

The structure(s) you don't select are removed, so the file ends up with exactly what you asked for. mp4ChapterStyle is ignored for MP3 (which always writes ID3v2 CHAP frames).

Chapter end times

  • MP3: endTimeMs is part of the CHAP frame. If you omit it when writing, it's filled from the next chapter's start time — and from the track duration for the last chapter.
  • MP4: chapters are start-time-only on disk, so endTimeMs is always inferred on read (the next chapter's start, or audioProperties().durationMs for the last one) and ignored on write.

Clearing chapters

typescript
file.setChapters([]); // removes all chapter structures
file.save();

Examples

List chapters as a table of contents

typescript
const fmt = (ms: number) => {
  const s = Math.round(ms / 1000);
  return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
};

for (const ch of file.getChapters()) {
  console.log(`${fmt(ch.startTimeMs)}  ${ch.title ?? "(untitled)"}`);
}

Build chapters from a cue list

typescript
const cues = [
  { at: 0, name: "Opening" },
  { at: 132_500, name: "Topic 1" },
  { at: 945_000, name: "Topic 2" },
];

file.setChapters(cues.map((c) => ({ startTimeMs: c.at, title: c.name })));
file.save();

Maximum compatibility for a podcast MP4

typescript
using file = await taglib.open("episode.m4a");
file.setChapters(chapters, { mp4ChapterStyle: "both" });
file.save();

See also

Released under the MIT License.