ffmpeg M4B Chapters: Commands That Work, and Where m4b-tool or a Mac App Fits
You merged your MP3s with ffmpeg and got a working file — but no chapters. That is normal. MP3 folders carry no chapter list, so there was nothing to copy across. Chapters have to be written in, by you.
There are two ways from here. AudioBo builds the book with chapters, tags and cover in one pass — no terminal at all. Or you write them yourself: the commands, the traps and the checks are the rest of this page.
Chapters are not part of the sound. They are a separate list that travels alongside the audio — a title and a start time for each chapter. A folder of MP3 files has no such list in it, so when you join those files end to end there is nothing to carry across, and the finished book comes out with none.
Which means the command did not fail. It joined the audio, which is what you asked it to do. Writing the chapter list is a second job, and on the command line nobody does it for you: every title, every start time, in order, handed in as a second piece of input.
A file that already has chapters behaves differently — pass one through unchanged and its chapters come with it. It is joining loose files that loses them.
Would rather not write that list by hand? AudioBo builds the book in one pass — chapters, tags and cover — and takes over the part that is genuinely hard to script: it fetches the chapter list for the book, checks it against the audio you actually have, and names the mismatch when there is one, so you find out before the file is finished rather than after. You can hear a boundary before you commit to it.
It drives the same ffmpeg underneath, and it is a Mac App Store app — on a Linux box or in a cron job it is no help at all, and it does not unlock a protected store download.
Everything below was run on an M1 Pro under macOS 26.6, against ffmpeg 8.0 and m4b-tool 0.5.2, on test books I built for the purpose: three MP3 chapters of 60, 120, and 90 seconds, mono, 64 kbps, 22,050 Hz, with titles in the tags and one deliberately containing a comma — plus a second book of thirty 87-second parts when file count mattered.
What -map_chapters actually does
You end up handing ffmpeg two things: the audio, and the chapter list you wrote. Either of them can be carrying chapters already, so something has to say which one wins. That is the whole job of this flag — and the trouble is not what it does when you use it, but what it quietly does when you leave it out.
It picks which input the output’s chapter table is copied from. That is all it does, and the confusion comes entirely from its default.
Three behaviors, all measured on the test file:
-map_chapters 1takes the chapter table from your second input.-map_chapters -1writes no chapter table at all.- Leave it off, and ffmpeg 8.0 takes chapters from the first input that has any.
That last one is the trap. Reproduce it in one command:
ffmpeg -i book.m4b -i newchapters.txt -map 0:a -c copy out.m4b
A chaptered .m4b as input 0, a replacement chapter file as input 1, the flag omitted — and you get the old chapters back with no warning. I ran exactly that: the output kept Introduction / Chapter One / Chapter Two, Revisited from the source. Adding -map_chapters 1 gave the new two-chapter list. The command that looked like it did nothing was doing precisely what it was told.
Metadata is a separate switch. In the same test, the book title stayed at the source’s until I added -map_metadata 1 — chapters defaulting to input 1 does not drag the title and artist along with them.
And a plain -c copy remux of an already-chaptered M4B keeps its chapters; I checked, and all three came through. If yours are disappearing, the concat demuxer is where it happened, and diagnosing a chapterless M4B is the page that owns that argument.
The build, as one script
In plain terms, the script does by hand what a program with a window would do for you. It goes through the MP3 files one at a time, asks each one how long it runs, and keeps a running total — that total is where the next chapter starts. It writes those titles and times out to a small text file, then hands ffmpeg both things at once: the audio to join, and the list to attach to it. The cover goes on in the same pass.
This is the version I ran, unedited. Run it on your own folder: put it next to your MP3 files with a cover.jpg and run bash build-m4b.sh "Book Title". One thing to check first: the loop takes files in strict character order, where Chapter 10.mp3 sorts ahead of Chapter 2.mp3 — if your names carry unpadded numbers, pad them before building, or the book assembles out of order without a word of warning.
#!/bin/bash
set -e
book="${1:-Audiobook}"
: > files.txt
printf ';FFMETADATA1\ntitle=%s\n' "$book" > chapters.txt
start=0
for f in *.mp3; do
esc=${f//\'/\'\\\'\'}
printf "file '%s/%s'\n" "$PWD" "$esc" >> files.txt
dur=$(ffprobe -v error -show_entries format=duration -of 'default=nw=1:nk=1' "$f")
ms=$(printf '%.0f' "$(echo "$dur * 1000" | bc -l)")
end=$((start + ms))
title=$(ffprobe -v error -show_entries format_tags=title -of 'default=nw=1:nk=1' "$f")
[ -z "$title" ] && title=${f%.*}
printf '[CHAPTER]\nTIMEBASE=1/1000\nSTART=%s\nEND=%s\ntitle=%s\n' \
"$start" "$end" "$title" >> chapters.txt
start=$end
done
ffmpeg -nostdin -loglevel error \
-f concat -safe 0 -i files.txt -i chapters.txt -i cover.jpg \
-map 0:a -map 2:v -map_metadata 1 -map_chapters 1 \
-c:a aac -b:a 64k -ac 1 -c:v copy -disposition:v:0 attached_pic \
-metadata media_type=2 -brand "M4B " \
-f mp4 "$book.m4b"
No cover to hand? Drop -i cover.jpg, -map 2:v, -c:v copy and the -disposition flag, and the rest still runs.
The chapter file it writes is plain text in ffmpeg’s own metadata format, and you can hand-edit it before the final command if the boundaries need nudging:
;FFMETADATA1
title=The Good Soldier Svejk
[CHAPTER]
TIMEBASE=1/1000
START=0
END=60000
title=Introduction
Four details in there are worth stopping on.
-of 'default=nw=1:nk=1', not csv=p=0. The CSV writer quotes any field containing a comma. Asked for my third chapter’s title, it returned "Chapter Two, Revisited" with the quotes included, and those literal quotes end up baked into the chapter title inside the finished book. Ask for the plain writer and the title comes back clean.
The apostrophe escape. The concat demuxer wraps paths in single quotes, so a filename like Dawn's Early Light.mp3 truncates the path at the apostrophe — ffmpeg’s error message points at a file called Dawns and stops. ${f//\'/\'\\\'\'} closes the quote, escapes the apostrophe, and reopens. Ugly, and correct. It is a common enough trap that AudioBo needed its own fix for it in 1.3.1.
-metadata media_type=2. This writes the stik atom that marks the file as an audiobook rather than generic audio. ffmpeg will not add it on its own — a build without the flag had no stik atom anywhere in it, and with the flag AtomicParsley reports Atom "stik" contains: Audiobook. It costs you nothing and it is the flag people spend evenings not finding.
-brand "M4B ". Pinning -f mp4 as the script does gives you major_brand=isom when you leave the brand off. Let ffmpeg pick the muxer from the .m4b extension instead and you get M4A , which is the file telling every app that opens it to treat it as music. Either way -brand "M4B " fixes it, and what those four characters actually buy you is the argument on what an M4B file declares about itself.
Two things about the output that surprise people
Two things in the finished file look like mistakes and are not. Both turn up the first time anyone inspects the output, so they are worth naming before you go chasing them.
ffmpeg writes both chapter formats. MPEG-4 stores chapters two different ways, and I expected the muxer to pick one. It writes both: mp4chaps --list --chapter-any reports a QuickTime chapter track, --chapter-nero reports the same three chapters from the chpl atom, and both are present in the raw bytes. So that is not where a hand-rolled file falls down. What to do about a file carrying only one of them belongs to diagnosing a chapterless M4B.
You cannot control stream order, and the reason is the cover. Map the cover first with -map 2:v -map 0:a and it still lands after the audio — but only when it is marked attached_pic. Drop that disposition and the muxer honors your order exactly, video first. So it is the MP4 muxer sinking attached pictures below the real tracks, not -map being ignored. It is also the order you want, so leave it alone.
The chapter track shows up in ffprobe output as a second stream of type data, between the audio and the cover. That is the QuickTime text track doing its job, not a corruption.
Where your chapter marks actually land
Jump to the start of a chapter in the finished book and, for a sliver of a second, you are still hearing the end of the one before it. It is a few hundredths of a second, it is the same size whether your book is three files or thirty, and nothing you typed caused it.
The script computes chapter starts by adding up source durations, and the finished book does not quite agree with them. Worth knowing by how much, before you go hunting for a bug.
My marks said 60.000 and 180.000. Seek to 60.000 in the finished file and you still hear the first chapter’s tone; it changes at about 60.045. The gap is one AAC frame of encoder priming — 1024 samples, which on this 22,050 Hz build is 46.4 milliseconds.
Two things make that number less alarming than it looks.
It does not accumulate. Built from 3 files the output ran 1024 samples long; built from 30, also 1024 samples. A constant offset of one frame, not a growing one.
It scales with sample rate, not with your book. One frame is always 1024 samples, so it is 46 milliseconds at 22,050 Hz and 23 at 44,100 — I built the same three chapters at 44.1 kHz and measured the tone change 23.2 milliseconds after the mark.
It is also specific to the concat path. Transcoding a single MP3 straight to AAC came out sample-exact: 1,323,000 samples for 60 seconds at 22,050 Hz, no overshoot at all. The frame appears when the concat demuxer is the thing feeding the encoder.
Twenty-three milliseconds is one AAC frame, and chapter boundaries in a narrated book tend to land in silence rather than mid-syllable. That is the margin you would be arguing about. Do not fix it.
m4b-tool
If writing that script is the part you were hoping to skip, somebody has already written it. There is a free tool that takes a folder of files and gives you back a finished book in one line — and it makes a few decisions on your behalf that are worth knowing about before you point it at a book you care about.
sandreas/m4b-tool is the other answer to this question and it is a good one. Installing it took two commands — brew tap sandreas/tap, then brew install sandreas/tap/m4b-tool — and pulled in PHP, mp4v2, and a standalone fdk-aac encoder. No compiling, no Docker.
What you get for that is one command instead of a script. Point it at a folder and that is the whole of it:
m4b-tool merge ./MyBook --output-file="MyBook.m4b" --no-chapter-reindexing --jobs=8
It reads the folder, builds chapters from the file boundaries, and writes stik as Audiobook by default — the flag plain ffmpeg leaves out. It writes both chapter formats too. Source titles come across as the chapter names, though the book’s own title arrives as whatever the first file was called, which you will want to set by hand. And it parallelizes: on the 30-file book, 43 and a half minutes of audio, --jobs=8 finished in about 6 seconds against about 15 at the default --jobs=1, with my ffmpeg script between the two. Run the whole thing again and every one of those numbers moves by half a second. Read them at scale before you get excited — the gap is seconds here and tens of seconds on a full-length book, not hours.
Three measured caveats, all fixable, none a reason to avoid it.
Chapter titles get renumbered by default. My first run produced chapters named 1, 2, 3 instead of the titles in the source tags. That is the documented --chapter-algo=legacy default, which detects index-only chapter names and reindexes them; --no-chapter-reindexing restored Introduction / Chapter One / Chapter Two, Revisited exactly.
It picks its own sample rate. Running it verbosely shows the ffmpeg command it builds for each source file, and it hard-codes -ab 64k -ar 22050. Fed 44.1 kHz sources, it wrote a 22,050 Hz book without comment. That is a defensible default for speech and a bad surprise for music-heavy material. --audio-samplerate and --audio-bitrate override it.
Chapter marks drift with file count. Because it encodes each source file separately and then joins the results, every boundary picks up another priming frame in the chapter table. On the 30-file book the last chapter was marked at 2524.334 seconds against the 2523.000 the durations add up to — 1.334 seconds late. Its chapter table counts in whole milliseconds, so that is 29 boundaries times a rounded-down 46 rather than the 46.4 the frame actually takes. The audio itself still changes at about 2523.05, so jumping to that mark drops you more than a second into the new chapter rather than at its start. Total duration came out identical to ffmpeg’s, so this is the marks moving, not the audio. On a book of thirty parts you will not care. On a book of a hundred and fifty CD tracks, do the arithmetic first.
Where the command line is genuinely the right tool
Not as a concession. As the answer, in these cases:
- Batches. Forty books through the same pipeline is a
forloop and a coffee. No GUI competes with that, and none should pretend to. - Servers and headless machines. Audiobookshelf on a NAS, a cron job watching a drop folder, a Docker container. There is no Mac in that picture and there does not need to be.
- Reproducibility. A script is a written record of exactly what produced a file. Nothing with a checkbox gives you that.
- Anything unusual. Odd sample rates, a source format nobody supports, a chapter scheme you invented. ffmpeg has a flag for it, and if not, a filter.
- Cost. Free, and still free in ten years.
What AudioBo does that the commands cannot — check the chapters against the audio
The commands above are correct and they will build you a perfectly good audiobook. What they cannot do is tell you whether the result is right, because every check on this page has been a number rather than a sound.
You cannot hear a chapter boundary in ffprobe output. When the timestamps came from a YouTube description or an Audible listing, they can belong to a different edition, or a copy split differently, or one with a sponsor read at the front — and the file is finished before you find out. You cannot see whether a chapter title is CHAPTER XVII for the forty-second time. Fixing either means editing a text file blind and running the whole encode again.
This is where AudioBo is worth a look, and what it takes over is exactly that gap:
- Chapters checked against the audio. It fetches the book’s chapter data from Audible or Apple Books and compares it with your actual file — and when the two disagree, it names the mismatch specifically: same length but split differently, a different edition, extra audio at the start.
- Boundaries you can hear. You preview each break by ear before applying it, rather than after the encode is done.
- Fixes without a rebuild. Corrected chapter names and metadata go straight back into the existing file with ⌘U, no re-encode, since 1.2.2.
- Names at batch scale. The forty-second
CHAPTER XVIIis one pass in Batch Rename with a live before-and-after preview, not an evening in a text editor.
If you do want the mechanical version: it drives ffmpeg and does not pretend otherwise. Since 1.3.2 there is a Log button in the toolbar showing live ffmpeg output, encoding speed, and every tag as it is written. The same release added a Speed tab that sets how many files are converted at once, with a built-in speed test to pick the number — the same idea as --jobs, and its own release notes say it makes little difference on a single book, which is the accurate version.
It is a Mac App Store app. If your workflow is a cron job on a Linux box, none of that helps you, and m4b-tool is your tool.
Two boundaries worth naming. Cutting a finished M4B back into per-chapter files is the opposite operation, with its own commands, on splitting an M4B into chapters. Repairing tags, covers, and mangled chapter titles in a book you already have is on editing M4B metadata on a Mac. And if the source is a protected store download, none of this applies — ffmpeg needs an account key the store does not hand out, and AudioBo does not remove DRM either; the M4B to MP3 guide carries that discussion in full.
Common questions
Why does my M4B have no chapters after an ffmpeg concat?
Because the concat demuxer joins audio streams, and a chapter table is not a stream. There is nothing for it to carry across, and the per-file boundaries are not recorded anywhere. You have to write the chapter list yourself, as a second input in ffmpeg's metadata format, and point -map_chapters at it. A plain -c copy remux of an already-chaptered file keeps its chapters fine.
What does -map_chapters actually do?
It selects which input the output's chapter table comes from. -map_chapters 1 takes it from your second input, -map_chapters -1 writes none at all. Leave it off and ffmpeg 8.0 takes chapters from the first input that has any — which silently keeps the old chapter list when you are trying to replace it. Metadata is a separate switch and needs -map_metadata.
Does ffmpeg write chapters that Apple's apps can read?
Yes, and it writes both of the formats MPEG-4 files use, so a hand-rolled file is not second-class here. What ffmpeg leaves out unless you ask is the audiobook media-type flag. Add -metadata media_type=2 and the file reports itself as an audiobook rather than generic audio; leave it off and there is no stik atom in the output at all.
Is m4b-tool better than plain ffmpeg?
It is one command instead of a script, it sets the audiobook media-type flag by default, and its --jobs option encodes source files in parallel. On a 30-file test book it finished in about 6 seconds at --jobs=8 against about 15 at the default --jobs=1. Read its defaults first: it renumbers chapter titles and resamples to 22,050 Hz unless you say otherwise.
Why are my chapter marks a few tens of milliseconds early?
Because the AAC encoder adds one frame of priming when the concat demuxer feeds it, and marks computed from source durations do not know about it. One frame is 1024 samples: 46 milliseconds at 22,050 Hz, 23 at 44,100. On the concat path it is a constant offset, not a growing one — the same 1024 samples on a 3-file and a 30-file book. m4b-tool, which encodes each file separately, does accumulate it. Leave the concat one alone.