Turn a folder of MP3 chapters into a single .m4b for Apple Books

Jason Cohen published a new book, Hidden Multipliers. If you buy it directly from him you get the DRM free files: EPUB, PDF, and the audiobook. That is a great way to sell a book, kudos to him for doing this.

The audiobook arrives as a zip file. Inside there is one MP3 per chapter and a single cover image. That plays fine anywhere, but I wanted it in Apple Books, and Books only takes an audiobook as one .m4b file with chapter markers inside it. So the folder has to become a single file.

ffmpeg does all of it. The script below reads the chapter durations, builds the chapter table, concatenates the audio, attaches the cover, and writes the .m4b.

What the filenames have to look like

The script takes the metadata straight out of the filenames, so they have to follow one pattern:

NNN Artist - Album - Chapter Title.mp3

For example:

003 Jason Cohen - Hidden Multipliers - Chapter 1.mp3

The leading number sets the play order. The artist and the album come from the first file. The rest of the name becomes the chapter title you see in the Books player. The cover image has to sit in the same folder, named cover.jpg, cover.jpeg, or cover.png.

The script

You need ffmpeg and ffprobe. On a Mac, brew install ffmpeg gives you both.

#!/usr/bin/env bash
# Convert a folder of numbered MP3 chapters into a single .m4b audiobook
# for Apple Books. Requires ffmpeg + ffprobe (brew install ffmpeg).
#
# Expected filename format (chapters and metadata are parsed from it):
#   NNN Artist - Album - Chapter Title.mp3
#   e.g. 003 Jason Cohen - Hidden Multipliers - Chapter 1.mp3
# Cover art: cover.jpg / cover.jpeg / cover.png in the same folder.
#
# Usage: make-m4b.sh [audiobook-dir]   (defaults to current directory)
# Env:   BITRATE=96k to override AAC bitrate (default 64k, mono).

set -euo pipefail

dir="${1:-.}"
cd "$dir"

files=$(ls -1 [0-9]*.mp3 2>/dev/null | sort) || {
  echo "No numbered .mp3 files in $dir" >&2; exit 1
}

# ---- metadata from the first filename: "NNN Artist - Album - Chapter" ----
first=$(printf '%s\n' "$files" | head -1)
rest=${first#* }            # drop "NNN "
artist=${rest%% - *}        # up to first " - "
album=${rest#* - }; album=${album%% - *}   # between first and second " - "
album=${album:-$(basename "$dir")}

# ---- cover art ----
cover=""
for c in cover.jpg cover.jpeg cover.png Cover.jpg; do
  [ -f "$c" ] && { cover=$c; break; }
done

# ---- chapters: one line of "duration<TAB>title" per file ----
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT

printf '%s\n' "$files" | while IFS= read -r f; do
  d=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$f")
  t=${f#* - }; t=${t#* - }; t=${t#* - }; t=${t%.mp3}   # strip NNN, artist, album
  printf '%s\t%s\n' "$d" "$t" >> "$tmp/parts.tsv"
  printf "file '%s'\n" "$(pwd)/$f" >> "$tmp/list.txt"
done

awk -F'\t' 'BEGIN { s=0; print ";FFMETADATA1" }
  { e = s + $1*1000;
    printf "\n[CHAPTER]\nTIMEBASE=1/1000\nSTART=%d\nEND=%d\ntitle=%s\n", int(s+0.5), int(e+0.5), $2;
    s = e }' "$tmp/parts.tsv" > "$tmp/chapters.ffmeta"

# ---- encode ----
out="$album.m4b"
echo "Encoding $out from $(printf '%s\n' "$files" | wc -l | tr -d ' ') chapters..."
ffmpeg -y -f concat -safe 0 -i "$tmp/list.txt" -i "$tmp/chapters.ffmeta" \
  ${cover:+-i "$cover"} \
  -map 0:a -map_metadata 1 ${cover:+-map 2 -c:v copy -disposition:v:0 attached_pic} \
  -c:a aac -b:a "${BITRATE:-64k}" -ac 1 \
  -metadata title="$album" -metadata artist="$artist" -metadata album="$album" \
  -metadata genre="Audiobook" \
  -movflags +faststart -f mp4 "$out"
echo "Done: $dir/$out"

Save it as make-m4b.sh, make it executable, and point it at the unzipped folder:

$ chmod +x make-m4b.sh
$ ./make-m4b.sh ~/Downloads/audiobook-dir

It writes {title}.m4b next to the MP3 files.

What the script does, step by step

ffprobe reports the length of each MP3. The awk block adds those lengths up and writes an FFMETADATA file with one [CHAPTER] block per file, where each chapter starts where the last one ended. This is what gives you the chapter list and the skip-to-next-chapter button in the player.

The concat demuxer joins the audio in the order of the file list. The cover image comes in as a third input and is marked attached_pic, which is how a still image becomes cover art in an MP4 container instead of a video track.

Speech does not need much bitrate or a second channel, so the default is 64 kbps mono AAC. That keeps a long book down to a size worth putting on a phone. If the recording has music or you want more headroom, set BITRATE=96k before the command.

-movflags +faststart moves the index to the front of the file, so a player can start without reading the whole thing first.

Getting it into Books

Mac

Just drag the .m4b into the Books app, or File → Import. Done.

iPhone

Getting the audiobook onto an iPhone is the annoying part. iCloud sync does not apply to audiobooks you import into Books on a Mac, only to ebooks in EPUB or PDF format. Audiobooks bought from Apple’s store sync fine. So you have to connect the iPhone with a cable and sync the file to Books through Finder. AirDrop transfers the file, but iOS often will not offer Books as a destination for it.

  1. Add to Books: Drag the .m4b into Books on Mac. Verify it shows under Library → Audiobooks (not in Music).
  2. Open Finder: Select your iPhone under “Locations” in the sidebar. Unlock phone, tap Trust if asked.
  3. Audiobooks tab: Click it in the row of tabs at the top of the device window.
  4. Enable sync: Tick “Sync audiobooks onto [iPhone]”. Choose All or Selected audiobooks and pick your book.
  5. Confirm warning: If prompted about replacing an existing library, click Remove & Sync.
  6. Apply: Click Apply/Sync, wait for the progress bar to finish, then Eject.
  7. Check phone: Books → Library → Audiobooks.

Tips

  • Enable “Show this iPhone when on Wi-Fi” in the General tab to skip the cable next time.
  • Repeat this after every new book, there is no automatic sync for sideloaded audiobooks.