Skip to content

bensquire/AudiobookForge

Repository files navigation

AudiobookForge

A modern macOS app that combines MP3 files into a single chaptered .m4b audiobook — with real metadata lookup, embedded cover art, and a background encode queue.

Build Release Latest Downloads Platform Swift License

Download latest → · Releases · Releasing docs


AudiobookForge — three-pane layout: chapters, metadata, queue

Think Audiobook Builder, but with one-shot drag-folder UX, real metadata lookup from Audnexus / iTunes, and a native SwiftUI interface.

Requires Apple Silicon (arm64). Intel Macs are not supported.

Quick start

./scripts/bootstrap.sh        # installs deps, builds bundled ffmpeg, generates xcodeproj
./scripts/build.sh debug      # builds a debug .app at build/Build/Products/Debug/
open build/Build/Products/Debug/AudiobookForge.app

First run takes ~7-10 min because it compiles ffmpeg + fdk_aac from source (stripped to ~10 MB, only the codecs/muxers we use). Subsequent runs short-circuit.

Or open AudiobookForge.xcodeproj in Xcode after running xcodegen generate.

Tests

./scripts/test.sh                                    # whole suite
./scripts/test.sh -only OutputPathResolverTests      # single class

The unit suite covers the pure logic — path resolution, codec parsing, chapter-file building, encode-job helpers, formatting, model invariants, queue manager. SwiftUI views and subprocess-driven services (ffmpeg, metadata APIs) aren't unit-tested; those want integration tests.

Lint & format

./scripts/format.sh    # auto-fix what SwiftFormat / SwiftLint can
./scripts/lint.sh      # check-only; CI runs exactly this and fails on diff

Config lives in .swiftformat and .swiftlint.yml. SwiftFormat handles whitespace, line wrapping, redundant self, trailing-comma policy, etc. SwiftLint enforces a curated subset (we disable the rules that fight idiomatic patterns — short loop vars, deliberate trailing commas, modern one-liner braces — and opt into the high-signal ones like first_where, redundant_nil_coalescing, prefer_self_in_static_references).

Project layout

project.yml                    # XcodeGen config (the source of truth)
AudiobookForge.xcodeproj/      # generated, gitignored
AudiobookForge/
├── App.swift                  # @main + Scene
├── ContentView.swift          # top-level HSplitView
├── Models/                    # Plain Swift structs / @Observable models
├── Services/                  # FFmpegRunner, AudioProbe, MetadataSearch, EncodeJob
├── Views/                     # SwiftUI views
├── Resources/bin/             # Bundled ffmpeg (gitignored, built from source)
├── Info.plist                 # Generated by XcodeGen from project.yml
└── AudiobookForge.entitlements
scripts/
├── bootstrap.sh               # one-shot dev setup
├── build-ffmpeg.sh            # build the bundled ffmpeg + libfdk_aac
└── build.sh                   # xcodebuild wrapper (debug | release | archive)
.github/workflows/build.yml    # CI: unsigned macOS build on every push

How the encode pipeline works

Each queued book takes one of two paths through EncodeJob.runInner, depending on whether the sources can be remuxed losslessly:

Remux path — when every source file is already AAC with a uniform sample rate + channel layout and the user picked "Match source" bitrate. A single ffmpeg invocation reads the chapters via the concat demuxer, picks up an FFMETADATA1 chapter file and an optional cover image as extra inputs, and writes the final .m4b with -c:a copy (no re-encoding). Seconds for a 25-hour book.

Re-encode path — everything else. Two phases:

  1. Phase 1 (parallel) — each chapter source is encoded independently into an intermediate .m4a via libfdk_aac, with codec parameters pinned from chapter 0 (sample rate, channels, profile) so the intermediates concatenate losslessly. Up to min(chapters, activeProcessorCount, 12) ffmpeg children run in parallel via withThrowingTaskGroup + a ConcurrencyLimiter actor.
  2. Phase 2 (concat + cover) — one final ffmpeg with the concat demuxer over the intermediates, the FFMETADATA1 chapter file, and the optional cover image, all muxed with -c:a copy. Near-instant.

Both paths write to out.m4b.partial and atomic-rename on success, so a cancel or crash never leaves a stub .m4b. Output filename comes from the template {author}/{title}/{title}.m4b by default; collisions auto-bump via OutputPathResolver to Title (2).m4b, (3), etc.

Up-front each source is probed via AudioProbe.swift: AVFoundation for duration / tags / codec / sample-rate / channels, plus a 1-second ffmpeg -t 1 -f null call to read the codec context's bitrate from stderr (more accurate than AVF's container-divided estimate for MP3 with embedded cover art).

Progress comes from each ffmpeg's time= lines on stderr, throttled to per-percent updates and aggregated across parallel chunks by ProgressAggregator.

Metadata lookup

MetadataSearch.swift queries two APIs in parallel:

  • Audnexus (https://api.audnex.us) — community Audible aggregator used by Audiobookshelf and Plex. Free, no key, returns ASIN, narrators, series, description, cover URL.
  • iTunes Search API — free fallback for non-Audible titles.

Hits are deduplicated by (title, author) and shown as clickable rows that apply to the current project. The selected Audnexus hit is then re-fetched (enrich) for full description and high-res cover.

Releasing

See RELEASING.md for the full playbook. TL;DR:

# one-time: add 6 GitHub Secrets (cert, password, team ID, 3x notary creds)
git tag v0.1.0 && git push origin v0.1.0
# → GitHub Actions builds, signs (Developer ID), notarizes, packages a DMG,
#   and attaches it to a new GitHub Release.

Dry-run locally without burning a tag:

brew install xcodegen create-dmg
scripts/release.sh 0.1.0

Improvement Ideas

  • Persistable projects (.audiobookforge document type) — queue survives app quit
  • Reorderable chapter list with merge/split
  • Drag chapter boundaries when source files don't map 1:1 to chapters
  • Preset library (saved bitrate + filename-template combos)
  • Audible region selection on metadata search
  • Sparkle auto-updates from GitHub Releases

License

The AudiobookForge source code is released under the MIT License — use it, fork it, ship a closed-source product based on it, do whatever; just keep the copyright notice and don't sue me if it eats your library.

Third-party components

Release DMGs ship a bundled ffmpeg binary built from source (scripts/build-ffmpeg.sh) with libfdk_aac statically linked for AAC encoding. ffmpeg itself is LGPL in the configuration we build; libfdk_aac is distributed under the Fraunhofer FDK AAC Codec Library license, which is free for distribution in commercial products. Both attributions appear in each release's notes alongside the upstream source links.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors