AudioBottle

The .audiobottle Archive Format

An open, documented, verifiable archive format for audio session preservation.

Edition2026-07-16
DocumentsContainer format split-v1 · manifest format 1.2 · manifest structural schema v3
Canonical sourceThis document is derived from the AudioBottle architecture record (AD-3) and the shipped JSON Schemas. On any discrepancy, the JSON Schemas win.
Machine-readable companionshttps://audiobottle.app/schemas/audbot-manifest-split-v1.json · https://audiobottle.app/schemas/inventory/v1.json
Canonical plain-text editionformat.md — the versioned, diffable specification text this page renders.

Format revision notice (2026-07-16). This edition documents structural schema v3, the WavPack-native revision: AIFF, CAF and W64 containers are fed directly to WavPack (whole-file, wrapper-preserving) and reconstructed natively on restore. The pre-release v1/v2 schemas — which routed those containers through a macOS system converter and recorded conversion bookkeeping fields — never shipped in any production archive and are refused by all shipped readers (Section 2). split-v1 archives begin at schema v3.

1. Purpose and philosophy

.audiobottle is the archive format written by Christophe Darlot for AudioBottle, a macOS tool that archives DAW sessions with lossless audio compression. A typical Pro Tools session archives at 50–75% below its original size, with every audio sample preserved bit-exactly.

The format is open and publicly documented. This is deliberate:

This document is the canonical, versioned specification of the format. It is maintained in lockstep with the shipped schemas: a change to the format changes both, in the same release.

2. Format version and forward compatibility

The container format is identified by content, not by filename. The identity check is the audbot_format field of the manifest (Section 4):

"audbot_format": "split-v1"

split-v1 is the only container format defined today. It is locked as a JSON Schema const: a split-v1 manifest can never carry another value, and a future incompatible container revision will use a new format string (e.g. a hypothetical split-v2) with its own schema and its own section in a revised edition of this document.

Three version fields, three jobs:

FieldTypeCurrent valueMeaning
audbot_formatstring const"split-v1"Container-layout identity. The load-bearing forward-compatibility lock. A new string means a new, incompatible container.
audbot_schema_versioninteger ≥ 13Structural version of the manifest schema. Bumped on every field addition or interpretation change. v1/v2 were pre-release development schemas that never shipped in production; v3 (WavPack-native, current) is the baseline every production archive carries. A manifest missing this field is a pre-release artifact.
audbot_versionsemver string"1.2"Human-readable manifest format revision. Minor revisions are forward-compatible. Informative; the machine checks are the two fields above.

Required reader behavior (this is what AudioBottle itself does, and what a conforming third-party reader must do):

  1. If audbot_format is missing → the file is not a valid archive of this family. Refuse with an integrity error.
  2. If audbot_format is present but not a value the reader knows → the archive was produced by a newer writer. Refuse extraction and tell the user to update their reader. AudioBottle's own message: "…created by a newer version of AudioBottle… Please update AudioBottle to extract." Never attempt best-effort extraction of an unknown format.
  3. If audbot_schema_version is greater than the highest version the reader knows → same refusal: the manifest may carry fields the reader cannot interpret safely. Refuse with a "please update" message. This check runs before JSON Schema validation, so users get a friendly upgrade message instead of an opaque additionalProperties rejection.
  4. If audbot_schema_version is missing or below 3 → the archive is a pre-release development artifact whose compressed entries a v3 reader would mis-restore (they assumed a fixed intermediate format rather than native container reconstruction). Refuse with a clear "made by an older AudioBottle" message — never attempt extraction, never fail with an opaque schema error.
  5. Only then validate the manifest against the published JSON Schema (Section 10).

A reader that knows split-v1 / schema v3 can read every production archive: v3 is the first schema any shipped writer emits, so the accepted window is exactly v3 today. Future bumps are additive from this baseline; an incompatible container change would use a new audbot_format string.

3. Container layout

An .audiobottle file is a plain, uncompressed POSIX tar archive (pax interchange format, POSIX.1-2001) containing exactly three members, in this fixed order:

my-session.audiobottle             (outer tar, pax format, NOT compressed)
├── 1. manifest.json          — UTF-8 JSON, the archive's oracle (Section 4)
├── 2. incompressible.tar     — plain pax tar: all audio + already-compressed media
└── 3. compressible.tar.zst   — pax tar compressed with zstd level 3: session
                                 metadata, caches, presets, text/XML

Rules a writer must honor and a reader may rely on:

3.1 incompressible.tar

A plain (uncompressed) pax tar holding every file whose bytes do not benefit from general-purpose compression:

Member paths inside the bundle are relative, POSIX-style, mirroring the source session's layout — with one substitution: WavPack-compressed audio appears under its audbot_path (the original path with .wv appended to the full filename: Kick.aifKick.aif.wv). Keeping the source extension in the stem makes same-stem sources (Kick.wav + Kick.aif + a W64 Kick) collision-free by construction. The manifest's audio_files array (Section 4.3) maps every audio file's original relative_path to its in-archive audbot_path. Paths are stored verbatim as the source filesystem reported them; Unicode normalization of member paths is not specified. The writer refuses to build an archive whose paths would collide on a case-insensitive or normalization-insensitive filesystem, so a reader never has to resolve such a collision.

3.2 compressible.tar.zst

A pax tar of everything else — DAW project files (.ptx), waveform caches, ARA data, plugin settings, backups, text/XML — compressed as a standard zstd stream at level 3. The level is a schema const: split-v1 archives always use level 3 (measured as the best size/CPU trade-off for this content class). Any conforming zstd decoder (the reference zstd CLI, python-zstandard, libzstd bindings) decodes it; decoders do not need to know the level.

3.3 Why the split?

4. The manifest

manifest.json is a UTF-8 JSON object validated by the published schema audbot-manifest-split-v1.json (JSON Schema draft 2020-12). The schema sets additionalProperties: false at every level: a split-v1 manifest contains only the fields below. New fields arrive only with an audbot_schema_version bump (Section 2).

4.1 Top-level fields (all twelve are required)

FieldTypeSemantics
audbot_formatstring, const "split-v1"Container identity and forward-compat lock (Section 2).
audbot_versionstring, semver (^[0-9]+\.[0-9]+(\.[0-9]+)?$)Manifest format revision, currently "1.2". Minor revisions forward-compatible.
audbot_schema_versioninteger ≥ 1Structural schema version, currently 3 (Section 2).
source_session_namestring, non-emptyName of the archived source folder. Extractors use it as the default restored-folder name.
created_atstringArchive creation time, UTC ISO 8601 with trailing Z, seconds precision (YYYY-MM-DDTHH:MM:SSZ).
audbot_creatorstring, non-emptyFree-form writer identification, stored verbatim and displayed by restore tools. AudioBottle writes AudioBottle <version> (<OS> <release> <machine>). Informative only — never parse it for behavior.
wavpack_compression_powerenum: fast | normal | high | very_high | extra_highWavPack effort level used at write time (Section 6.1). Informative for readers: .wv files self-describe; any WavPack decoder handles any level.
incompressible_tarobjectIntegrity + statistics for member 2 (Section 4.2).
compressible_tar_zstobjectIntegrity + statistics for member 3 (Section 4.2).
audio_filesarrayPer-audio-file record: what was done to each audio file and how to restore it (Section 4.3).
audio_summaryobjectAggregates: total_audio_files (integer), by_treatment (map treatment → count), by_format (map source format → count).
data_summaryobjectAggregates: incompressible_file_count, compressible_file_count, incompressible_uncompressed_total_bytes, compressible_uncompressed_total_bytes (all integers ≥ 0).

4.2 Bundle descriptors

incompressible_tar (all fields required):

FieldTypeSemantics
filenameconst "incompressible.tar"Member name in the outer tar.
sha256string, ^[0-9a-f]{64}$SHA-256 of the member's stored bytes — the plain tar exactly as it sits in the outer container (Section 7).
file_countinteger ≥ 0Files inside the bundle.
uncompressed_total_bytesinteger ≥ 0Sum of the original source sizes of the bundled files — not the stored .wv sizes. This is the number an extractor uses for its restored-size / free-space estimate.
by_categoryobjectFile-count breakdown across nine fixed categories: audio_compressed_wavpack, audio_passthrough_already_compressed, audio_passthrough_non_pcm, video, image, archive, document, adobe_disk, other_incompressible. All nine keys are always present (zero when empty).

compressible_tar_zst (all fields required):

FieldTypeSemantics
filenameconst "compressible.tar.zst"Member name in the outer tar.
sha256string, ^[0-9a-f]{64}$SHA-256 of the member's stored bytes — the zstd-compressed data as it sits in the outer container. Verify before decompressing.
file_countinteger ≥ 0Files inside the bundle.
stored_size_bytesinteger ≥ 0Size of compressible.tar.zst after zstd.
uncompressed_total_bytesinteger ≥ 0Sum of source bytes before zstd. Doubles as a decompression budget for readers (Section 8.3).
zstd_levelinteger const 3Compression level used. Informative — decoders don't need it.
by_top_level_dirobjectMap of top-level source directory → {file_count, uncompressed_bytes}. Files at the source root are bucketed under ".".

4.3 audio_files entries

One entry per audio file in the archive. Required fields:

FieldTypeSemantics
relative_pathstring, non-emptyThe file's original path, relative to the source root (POSIX separators). This is where the restored file belongs — extension included (a WavPack-compressed AIFF restores as that exact AIFF path, never as a renamed intermediate).
treatmentenumWhat the writer did — one of the three treatments below.
audbot_pathstring, non-emptyThe file's path inside incompressible.tar: relative_path with .wv appended for WavPack-compressed audio, or the original path unchanged for passthrough.

The three treatments, and what a reader must do to restore each:

treatmentMeaningRestore action
compressed_wavpackPCM audio container (WAV/BWF including RF64/BW64, AIFF/AIFF-C, CAF, W64) compressed losslessly to .wv in whole-file, wrapper-preserving mode.wvunpack the .wv to relative_path — WavPack reconstructs the original container natively (Section 6.2; W64 padding note in Section 6.3).
passthrough_already_compressedAlready-compressed audio (FLAC, ALAC, MP3, AAC, Opus, Ogg; non-PCM AIFF-C or CAF payloads) stored as-is.Copy to relative_path unchanged.
passthrough_non_pcmAudio stored as-is rather than risk a lossy or failed transform: WAV with a non-PCM codec (ADPCM, µ-law, A-law, …), an unparseable header, Sound Designer II, or a PCM variant WavPack does not ingest. passthrough_reason states why, per file (Section 6.4).Copy to relative_path unchanged.

Optional per-entry fields (all nullable; populated where the treatment makes them meaningful): original_format, audio_codec, sample_rate_hz, channels, duration_seconds, original_size_bytes, wav_chunks_preserved (boolean), passthrough_reason, aiff_compression_type, caf_format_id, stored_size_bytes.

4.4 Illustrative example

Abridged and illustrative — the schema is the normative shape:

{
  "audbot_format": "split-v1",
  "audbot_version": "1.2",
  "audbot_schema_version": 3,
  "source_session_name": "MixFinal_AlbumX",
  "created_at": "2026-07-16T14:30:22Z",
  "audbot_creator": "AudioBottle 1.0.0 (Darwin 24.6.0 arm64)",
  "wavpack_compression_power": "high",
  "incompressible_tar": {
    "filename": "incompressible.tar",
    "sha256": "9f2c…64 hex chars…a1d0",
    "file_count": 214,
    "uncompressed_total_bytes": 96843125760,
    "by_category": {
      "audio_compressed_wavpack": 211,
      "audio_passthrough_already_compressed": 2,
      "audio_passthrough_non_pcm": 0,
      "video": 1, "image": 0, "archive": 0,
      "document": 0, "adobe_disk": 0, "other_incompressible": 0
    }
  },
  "compressible_tar_zst": {
    "filename": "compressible.tar.zst",
    "sha256": "77b3…64 hex chars…09ce",
    "file_count": 1582,
    "stored_size_bytes": 48211433,
    "uncompressed_total_bytes": 512004096,
    "zstd_level": 3,
    "by_top_level_dir": {
      ".": {"file_count": 3, "uncompressed_bytes": 18874368},
      "Session File Backups": {"file_count": 40, "uncompressed_bytes": 402653184}
    }
  },
  "audio_files": [
    {
      "relative_path": "Audio Files/Kick_01.wav",
      "treatment": "compressed_wavpack",
      "audbot_path": "Audio Files/Kick_01.wav.wv",
      "original_format": "WAV", "audio_codec": "pcm_s24le",
      "sample_rate_hz": 48000, "channels": 1,
      "original_size_bytes": 34567890, "stored_size_bytes": 17283945,
      "wav_chunks_preserved": true
    },
    {
      "relative_path": "Audio Files/Room_L.aif",
      "treatment": "compressed_wavpack",
      "audbot_path": "Audio Files/Room_L.aif.wv",
      "original_format": "aif",
      "original_size_bytes": 28901234, "stored_size_bytes": 12704481
    }
  ],
  "audio_summary": {
    "total_audio_files": 213,
    "by_treatment": {"compressed_wavpack": 211,
                     "passthrough_already_compressed": 2},
    "by_format": {"wav": 208, "aif": 3, "mp3": 2}
  },
  "data_summary": {
    "incompressible_file_count": 214,
    "compressible_file_count": 1582,
    "incompressible_uncompressed_total_bytes": 96843125760,
    "compressible_uncompressed_total_bytes": 512004096
  }
}

5. The inventory

The inventory is the build-time interim record of the archiving pipeline, described by the published schema inventory/v1.json. During archiving, the writer walks the source folder, classifies every file (bundle assignment + audio treatment), performs the compression, and records everything — including tool stderr — in an inventory.json inside its working directory. The packaging step reads the inventory back to assemble the manifest.

The inventory is not a member of the archive. A reader of an .audiobottle file never encounters it; everything a reader needs was distilled into manifest.json. The inventory schema is published for pipeline transparency — it documents precisely how the manifest's audio_files, audio_summary, and data_summary were derived, and it lets auditors and tool authors reproduce or verify the classification logic. Its schema_version is const 1 for the lifetime of split-v1.

Inventory entries carry the same audio fields as manifest audio_files entries, plus build-only diagnostics that deliberately do not ship in the manifest (wavpack_stderr, source_root and work_dir absolute paths).

6. Audio handling

6.1 WavPack, and why not FLAC

PCM audio is compressed with WavPack (BSD-licensed, stable since the early 2000s, independently implemented). WavPack natively handles all bit depths used in session work — 16-bit, 24-bit, and 32-bit float — without conversion. FLAC, the more famous alternative, does not encode 32-bit float natively; adopting it would have forced an integer↔float conversion step on floating-point stems, i.e. a transform where a bug means silent damage. WavPack removes that entire risk class. Compression is lossless by definition: decode always reproduces the exact original samples.

The writer records its effort level in wavpack_compression_power (fast = -f, normal, high = -h [AudioBottle's default], very_high = -hh, extra_high = -h -x3). Levels trade write-time CPU for 1–3% ratio; decoding is identical for all levels — any WavPack decoder reads any of them. Writers also pass -m, embedding an MD5 of the raw audio data inside each .wv for per-file verification (wvunpack -v).

6.2 WAV / BWF / AIFF / CAF: byte-for-byte whole files

WavPack compresses the entire input file, not just the samples: every non-audio byte of the source container is stored as a wrapper inside the .wv, and wvunpack reconstructs the original container natively — no intermediate format ever exists. This whole-file mode covers all the PCM containers the writer compresses:

6.3 W64: audio bit-perfect, container padding tolerance

Sony Wave64 (W64, the 64-bit broadcast container that exceeds the 4 GB RIFF limit) is compressed natively like the containers above, with one disclosed difference: WavPack regenerates the container's trailing 8-byte-alignment padding on restore, so the restored file may be up to 7 bytes longer than the source, with an identical common prefix. The guarantee for W64 is therefore audio bit-perfect — the decoded samples are identical (wvunpack -v verifies the embedded MD5) — while the container is not guaranteed byte-exact. DAWs read W64 chunks by their declared 64-bit sizes, so the regenerated padding is inert.

6.4 Everything else: passthrough

Already-compressed audio (FLAC, ALAC, MP3, AAC, Opus, Ogg — and AIFF-C or CAF files carrying such codecs), non-PCM WAV payloads, Sound Designer II files (a legacy container WavPack does not read), and the rare PCM variants WavPack does not ingest all pass through untouched — recompressing lossy or already-losslessly-compressed data is pointless, and transcoding it would be destructive. passthrough_reason documents why, per file. The passthrough principle is a safety valve: when in doubt, the writer stores original bytes rather than transform them.

Passthrough metadata disclosure: a passthrough file's data fork is preserved byte-for-byte. macOS resource forks and extended attributes (including Finder tags and color labels) are not stored in the archive — the bundles carry data-fork bytes only. This applies to every passthrough file and is a property of the format's tar-based bundles, disclosed here so it is never a silent surprise.

7. Integrity verification

Corruption detection is layered so each failure mode is caught at the cheapest possible point, and damage is localized:

LayerDetectsCost
Outer tar structureTruncation, non-tar dataFails at open — milliseconds
manifest.json parse + schemaManifest corruption, wrong format familyKilobytes read
audbot_format / audbot_schema_versionFuture-format archivesTwo field checks
Two SHA-256 digestsAny content corruption, per bundleOne streaming read of each member
WavPack MD5 (wvunpack -v)Per-audio-file damage localizationOptional, per file

The two digests cover each inner member's stored bytes exactly as they sit in the outer tar: incompressible_tar.sha256 covers the plain tar bytes; compressible_tar_zst.sha256 covers the zstd-compressed bytes (hash before decompressing — this also protects the decoder from crafted streams). A reader MUST verify a bundle's digest before trusting or delivering any content extracted from it. Because each bundle has its own digest, a mismatch identifies which bundle is damaged; when multiple backup copies of an archive exist, a user can be told precisely which copy has an intact audio bundle.

Verification without extraction is cheap by design: read member 1 (kilobytes), then stream-hash members 2 and 3 without writing anything. This is AudioBottle's "validate" operation, and takes one sequential read of the file.

8. Writing a minimal extractor

This section is the recipe for an independent reader. AudioBottle's free cross-platform extractor follows exactly this algorithm; nothing below relies on AudioBottle code.

8.1 Toolchain

8.2 Algorithm

  1. Open the file as a tar archive. Failure ⇒ not an archive / structurally corrupt.
  2. Read the first member. Its name must be manifest.json; parse as UTF-8 JSON.
  3. Check audbot_format. Missing ⇒ integrity error. Not a known value ⇒ refuse: "created by a newer version — please update" (Section 2).
  4. Check audbot_schema_version. Missing or below 3 ⇒ refuse: pre-release artifact (Section 2). Greater than the highest you support ⇒ refuse with "please update".
  5. Validate the manifest against the published schema for the format (Section 10).
  6. Preflight (recommended): incompressible_tar.uncompressed_total_bytes + compressible_tar_zst.uncompressed_total_bytes approximates the restored size — check destination free space before writing.
  7. Extract and verify the two bundles. Stream each remaining member out of the outer tar, computing SHA-256 over its stored bytes as you go; compare against the manifest digest. Mismatch ⇒ stop; report which bundle is damaged. Do not deliver partial content.
  8. Unpack incompressible.tar (plain tar) and compressible.tar.zst (zstd-decode, then untar) into a scratch directory.
  9. Restore audio per audio_files[].treatment (Section 4.3): wvunpack every compressed_wavpack entry's .wv with the output path set to the entry's exact relative_path — wvunpack reconstructs the native container regardless of the output filename, so the path you pass is the filename the user gets; passthrough entries are already in place. Validate relative_path first (Section 8.3).
  10. Deliver atomically. Assemble everything in scratch; promote to the user-visible destination in one rename only after every step succeeded. Never leave a half-restored session where a user can mistake it for a complete one.

8.3 Security posture for implementers

Treat the archive as untrusted input, like any container format:

8.4 Cross-platform notes

An extractor in Python runs from a single source tree on macOS and Windows: tarfile, hashlib, and json are standard library; zstandard is a portable package. Exactly one external tool is platform-specific:

8.5 Sketch (Python, illustrative)

import hashlib, json, tarfile, zstandard

KNOWN_FORMATS = {"split-v1"}
MIN_SCHEMA_VERSION = 3
MAX_SCHEMA_VERSION = 3

with tarfile.open("session.audiobottle", "r") as outer:
    first = outer.next()
    assert first.name == "manifest.json"
    manifest = json.load(outer.extractfile(first))

    fmt = manifest.get("audbot_format")
    if fmt not in KNOWN_FORMATS:
        raise SystemExit(f"Unknown format {fmt!r} — please update your extractor.")
    schema_version = manifest.get("audbot_schema_version", 1)
    if schema_version < MIN_SCHEMA_VERSION:
        raise SystemExit("Pre-release manifest schema — re-create the archive.")
    if schema_version > MAX_SCHEMA_VERSION:
        raise SystemExit("Newer manifest schema — please update your extractor.")

    for key in ("incompressible_tar", "compressible_tar_zst"):
        member = outer.next()
        assert member.name == manifest[key]["filename"]
        h, fh = hashlib.sha256(), outer.extractfile(member)
        with open(member.name, "wb") as out:
            for chunk in iter(lambda: fh.read(1 << 20), b""):
                h.update(chunk); out.write(chunk)
        if h.hexdigest() != manifest[key]["sha256"]:
            raise SystemExit(f"{member.name} is corrupt (SHA-256 mismatch).")

with tarfile.open("incompressible.tar") as t:
    t.extractall("restored", filter="data")
with open("compressible.tar.zst", "rb") as fh:
    with zstandard.ZstdDecompressor().stream_reader(fh) as reader:
        with tarfile.open(fileobj=reader, mode="r|") as t:
            t.extractall("restored", filter="data")
# For each manifest["audio_files"] entry with treatment "compressed_wavpack":
#   validate relative_path (Section 8.3), then
#   wvunpack -y "restored/<audbot_path>" -o "restored/<relative_path>"
# Passthrough entries are already in place.

9. Versioning policy

9.1 File extension note (naming precision)

The public file extension is .audiobottle — chosen because it is brand-unique and self-identifying: an engineer who receives one and searches the extension finds this specification. Two precision points:

AudioBottle names archives <source-folder-name>.audiobottle, appending a -YYYYMMDD-HHMMSSZ UTC timestamp suffix when needed to avoid replacing an existing artifact — an informative convention only, never parsed on read.

9.2 Manifest format history

audbot_versionaudbot_schema_versionChange
1.01 (implicit — the field did not exist yet)Initial split-v1 manifest (pre-release; never shipped in production).
1.12Added the required top-level field audbot_schema_version (the explicit forward-compat check in Section 2); added the optional audio-file fields aiff_compression_type and caf_format_id (pre-release; never shipped in production).
1.23WavPack-native ingestion: AIFF/CAF/W64 fed directly to WavPack and reconstructed natively on restore; audbot_path appends .wv to the full filename; compressed entries restore to their exact original relative_path (extension included); removed the intermediate-conversion treatment and its bookkeeping fields; Sound Designer II routes to passthrough. First production schema — readers refuse anything below 3 (Section 2).

10. The JSON Schemas

Two machine-readable schemas are the normative companions to this document, served at their $id URLs (JSON Schema draft 2020-12):

Schema$idRole
Manifest (split-v1)https://audiobottle.app/schemas/audbot-manifest-split-v1.jsonNormative shape of manifest.json — the file a reader validates against (Section 4).
Inventory (v1)https://audiobottle.app/schemas/inventory/v1.jsonBuild-time interim record (Section 5) — pipeline transparency; not present inside archives.

The served copies are exact copies of the schemas shipped inside AudioBottle itself (enforced by the project's release checks). Validate manifests with any draft 2020-12 validator. On any discrepancy between this document's prose and the schemas, the schemas win — and the discrepancy is a bug in this document; please report it.

11. Licensing

The openness of this format is asymmetric by design: reading is free for everyone, forever; writing is the licensed function of the AudioBottle product.


AudioBottle — Bottle your audio. Uncork it anytime.
This specification is published so that no archive ever depends on us.