# The `.audiobottle` Archive Format

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

| | |
|---|---|
| Edition | 2026-07-16 |
| Documents | Container format `split-v1` · manifest format `1.2` · manifest structural schema `v3` |
| Canonical source | This document is derived from the AudioBottle architecture record (AD-3) and the shipped JSON Schemas. On any discrepancy, the JSON Schemas win. |
| Machine-readable companions | [`https://audiobottle.app/schemas/audbot-manifest-split-v1.json`](https://audiobottle.app/schemas/audbot-manifest-split-v1.json) · [`https://audiobottle.app/schemas/inventory/v1.json`](https://audiobottle.app/schemas/inventory/v1.json) |
| Presentation edition | This file (`format.md`) is the canonical plain-text specification. A presentation edition is published at [`https://audiobottle.app/format.html`](https://audiobottle.app/format.html). |

> **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](https://audiobottle.app), 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:

- **Your archives are not hostages.** A studio archiving a decade of client work must be able to open those archives in ten years, on whatever machine exists then, whether or not AudioBottle does. Everything in an `.audiobottle` file is built from open, stable, independently implemented standards: POSIX tar, zstd, JSON, SHA-256, WavPack. No proprietary container, no encryption, no obfuscation.
- **Manual recovery is always possible.** With stock command-line tools (`tar`, `zstd`, `wvunpack`) and this document, a competent engineer can recover every file from an archive by hand. Section 8 gives the exact procedure.
- **Third-party readers are welcome.** This document plus the two published JSON Schemas are a complete, sufficient specification for writing an independent extractor. AudioBottle's own free cross-platform extractor is built against this same specification.
- **Verifiability is built in.** The archive carries two SHA-256 digests and per-file WavPack MD5 hashes. A reader can prove an archive intact — or identify precisely which half is damaged — before trusting a single byte of content.

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):

```json
"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:

| Field | Type | Current value | Meaning |
|---|---|---|---|
| `audbot_format` | string const | `"split-v1"` | Container-layout identity. **The load-bearing forward-compatibility lock.** A new string means a new, incompatible container. |
| `audbot_schema_version` | integer ≥ 1 | `3` | Structural 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_version` | semver 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:

- **Manifest first.** `manifest.json` is the first logical member of the outer tar. This lets a reader parse and verify the manifest — and answer "what is this archive, is it intact enough to trust, how much disk will it need?" — by reading a few kilobytes, without streaming through multi-gigabyte bundles. A reader must reject an archive whose first member is not `manifest.json`.
- **Exactly three members**, named exactly `manifest.json`, `incompressible.tar`, `compressible.tar.zst`. These names are `const`-locked in the manifest schema. All three members are always present, even when a bundle contains zero files.
- **The outer tar is never compressed.** Its two large members are respectively incompressible (audio) and already compressed (zstd); compressing the wrapper would burn CPU for nothing.
- **Pax format note for low-level implementers:** the writers use the pax interchange format, so a pax extended-header record may physically precede a member's ustar header. Any conforming tar implementation (GNU tar, bsdtar, Python `tarfile`, Go `archive/tar`, …) resolves this transparently — "first member" means first *logical* member. Do not hand-parse 512-byte blocks; use a tar library.

### 3.1 `incompressible.tar`

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

- **Audio, WavPack-compressed:** PCM audio stored as `.wv` files (Section 6) — WAV/BWF (including RF64/BW64), AIFF/AIFF-C, CAF and W64 containers, all fed to WavPack whole-file. Already entropy-dense — zstd would waste minutes for ~0% gain.
- **Audio, passthrough:** already-compressed codecs (FLAC, ALAC/M4A, MP3, AAC, Opus, Ogg), non-PCM WAV payloads (ADPCM, µ-law, A-law), Sound Designer II, and the rare PCM variants WavPack does not ingest — stored as-is, bytes unchanged.
- **Already-compressed media:** video (`.mp4 .mov .m4v .qt .avi .mkv .webm`), images (`.jpg .jpeg .png .gif .webp .heic`), archives (`.zip .gz .bz2 .xz .7z .rar`), documents (`.pdf .epub`), Adobe/disk images (`.psd .ai .dmg .iso`) — as-is.

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.aif` → `Kick.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?

- **CPU honesty.** Compressing audio/video with zstd wastes 15–45 minutes on a 200 GB session for ~0% gain. The split skips it entirely.
- **Localized damage reports.** One digest per bundle (Section 7) tells a user *which half* of a damaged archive is affected — "your audio is intact, the session metadata bundle is corrupt" is actionable when choosing among backup copies.
- **Manifest as single oracle.** All integrity data lives in one JSON file at a fixed position. There is no sidecar checksum file to lose.

## 4. The manifest

`manifest.json` is a UTF-8 JSON object validated by the published schema [`audbot-manifest-split-v1.json`](https://audiobottle.app/schemas/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)

| Field | Type | Semantics |
|---|---|---|
| `audbot_format` | string, const `"split-v1"` | Container identity and forward-compat lock (Section 2). |
| `audbot_version` | string, semver (`^[0-9]+\.[0-9]+(\.[0-9]+)?$`) | Manifest format revision, currently `"1.2"`. Minor revisions forward-compatible. |
| `audbot_schema_version` | integer ≥ 1 | Structural schema version, currently `3` (Section 2). |
| `source_session_name` | string, non-empty | Name of the archived source folder. Extractors use it as the default restored-folder name. |
| `created_at` | string | Archive creation time, UTC ISO 8601 with trailing `Z`, seconds precision (`YYYY-MM-DDTHH:MM:SSZ`). |
| `audbot_creator` | string, non-empty | Free-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_power` | enum: `fast` \| `normal` \| `high` \| `very_high` \| `extra_high` | WavPack effort level used at write time (Section 6.1). Informative for readers: `.wv` files self-describe; any WavPack decoder handles any level. |
| `incompressible_tar` | object | Integrity + statistics for member 2 (Section 4.2). |
| `compressible_tar_zst` | object | Integrity + statistics for member 3 (Section 4.2). |
| `audio_files` | array | Per-audio-file record: what was done to each audio file and how to restore it (Section 4.3). |
| `audio_summary` | object | Aggregates: `total_audio_files` (integer), `by_treatment` (map treatment → count), `by_format` (map source format → count). |
| `data_summary` | object | Aggregates: `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):

| Field | Type | Semantics |
|---|---|---|
| `filename` | const `"incompressible.tar"` | Member name in the outer tar. |
| `sha256` | string, `^[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_count` | integer ≥ 0 | Files inside the bundle. |
| `uncompressed_total_bytes` | integer ≥ 0 | Sum 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_category` | object | File-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):

| Field | Type | Semantics |
|---|---|---|
| `filename` | const `"compressible.tar.zst"` | Member name in the outer tar. |
| `sha256` | string, `^[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_count` | integer ≥ 0 | Files inside the bundle. |
| `stored_size_bytes` | integer ≥ 0 | Size of `compressible.tar.zst` after zstd. |
| `uncompressed_total_bytes` | integer ≥ 0 | Sum of source bytes before zstd. Doubles as a decompression budget for readers (Section 8.3). |
| `zstd_level` | integer const `3` | Compression level used. Informative — decoders don't need it. |
| `by_top_level_dir` | object | Map 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:

| Field | Type | Semantics |
|---|---|---|
| `relative_path` | string, non-empty | The 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). |
| `treatment` | enum | What the writer did — one of the three treatments below. |
| `audbot_path` | string, non-empty | The 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:

| `treatment` | Meaning | Restore action |
|---|---|---|
| `compressed_wavpack` | PCM 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_compressed` | Already-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_pcm` | Audio 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:

```json
{
  "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`](https://audiobottle.app/schemas/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:

- **WAV / BWF (including RF64 and BW64, the >4 GB variants):** all RIFF chunks survive exactly — `fmt`, `bext` (BWF broadcast metadata), `iXML`, `LIST`/`INFO`, `JUNK` padding, proprietary DAW chunks, and any trailer data. `wvunpack` reproduces the original WAV/BWF file **byte-for-byte** — the strongest possible restoration guarantee, verifiable by hashing.
- **AIFF / AIFF-C:** the container is ingested natively, so marker, comment and instrument chunks (`MARK`, `COMT`, `INST`) survive exactly. Restoration is **byte-for-byte**, same guarantee and same verification as WAV.
- **CAF (PCM payloads):** ingested natively; restoration is **byte-for-byte**, same guarantee as WAV.

### 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:

| Layer | Detects | Cost |
|---|---|---|
| Outer tar structure | Truncation, non-tar data | Fails at open — milliseconds |
| `manifest.json` parse + schema | Manifest corruption, wrong format family | Kilobytes read |
| `audbot_format` / `audbot_schema_version` | Future-format archives | Two field checks |
| **Two SHA-256 digests** | Any content corruption, per bundle | One streaming read of each member |
| WavPack MD5 (`wvunpack -v`) | Per-audio-file damage localization | Optional, 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

- Any tar implementation (pax-aware; every mainstream one is).
- Any zstd decoder.
- Any SHA-256 implementation.
- The **`wvunpack`** CLI (or libwavpack) — the only audio-specific dependency; open source, builds on macOS, Windows, Linux. It reconstructs every compressed container (WAV, AIFF, CAF, W64) natively; no other converter is needed.

### 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 (step below).
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:

- **Tar member paths:** reject absolute paths, `..` traversal, and symlink escapes before writing (standard tar-slip defense). AudioBottle's writer never emits such entries; your reader must not assume every writer is honest.
- **Manifest restore targets:** `relative_path` and `audbot_path` are untrusted manifest fields that your reader turns into filesystem paths. Before invoking `wvunpack` (or copying a passthrough file), reject values containing NUL or newline bytes, absolute paths, or `..` segments, and assert the resolved output stays inside your scratch directory — otherwise a crafted manifest is an arbitrary-file-write primitive.
- **Decompression budget:** the manifest's `uncompressed_total_bytes` fields bound expected output; treat gross overruns as an error (zstd-bomb defense).
- **Digests before content:** verify SHA-256 over stored bytes *before* zstd-decoding or delivering anything (Section 7).

### 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:

- **`wvunpack`** — official builds exist for both platforms; ship the right binary per platform. Because WavPack reconstructs every compressed container natively, no platform-specific audio converter is needed anywhere in the restore path.

### 8.5 Sketch (Python, illustrative)

```python
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

- **This document is versioned with the format.** Each published revision states which `audbot_format`, `audbot_version`, and `audbot_schema_version` it documents (see the header table). The published copy at `audiobottle.app` is the canonical current edition.
- **Additive manifest changes** (new fields) bump `audbot_schema_version` and `audbot_version`, extend the published schema, and add the fields to Section 4 of this document — in the same release. Older readers refuse newer archives with a clear "please update" message (Section 2); newer readers read all older **production** archives. (The pre-release `v1`/`v2` schemas predate the first production archive and sit below every reader's accepted window — Section 2.)
- **Incompatible container changes** (member layout, compression scheme, digest scheme) require a **new `audbot_format` string** — hypothetically `split-v2` — with its own schema `$id` and its own container-layout section in a revised edition of this document. `split-v1` remains frozen and permanently documented: an archive written today stays readable against this specification forever.
- **The maintenance contract:** the shipped JSON Schemas, the writer/reader implementation, and this white paper change **together, in the same release**, and AudioBottle's test suite enforces that the published document matches the shipped schemas. A format spec that lies is worse than none — third-party extractors are built on it.

### 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:

- **Internal identifiers keep the historical short name `audbot`.** The manifest fields (`audbot_format`, `audbot_version`, `audbot_schema_version`, `audbot_creator`) and the manifest schema URL (`…/audbot-manifest-split-v1.json`) use `audbot` **verbatim** — they are shipped, load-bearing identifiers and will not be renamed. Do not "correct" them to `audiobottle` in a reader.
- **Readers must identify archives by content, not filename.** The identity check is `audbot_format` in the manifest (Section 2). Pre-release AudioBottle builds used the extension `.audbot` for the identical container; a conforming reader accepts any filename whose content passes the manifest checks.

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_version` | `audbot_schema_version` | Change |
|---|---|---|
| `1.0` | `1` (implicit — the field did not exist yet) | Initial `split-v1` manifest (pre-release; never shipped in production). |
| `1.1` | `2` | Added 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.2` | `3` | WavPack-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 | `$id` | Role |
|---|---|---|
| Manifest (`split-v1`) | `https://audiobottle.app/schemas/audbot-manifest-split-v1.json` | Normative shape of `manifest.json` — the file a reader validates against (Section 4). |
| Inventory (`v1`) | `https://audiobottle.app/schemas/inventory/v1.json` | Build-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.

- **Extractors are free.** Implementing, distributing, and using software that reads, verifies, or restores `.audiobottle` archives is unrestricted — no fee, no royalty, no registration, no permission required. This document and the two published JSON Schemas may be used and reproduced for that purpose. AudioBottle's own cross-platform extractor is free of charge.
- **Archivers are licensed.** Creating `.audiobottle` archives is the commercial function of [AudioBottle](https://audiobottle.app), available under a paid license. This specification is published to guarantee reading freedom, not as an invitation to implement competing writers: producing `.audiobottle` archives with other software requires the author's prior written permission.
- **Specification text.** This document and the schemas are © 2026 Christophe Darlot. Reproduction and translation are permitted for the purpose of implementing or documenting readers, with attribution.

---

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