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/inventory/v1.json |
| Canonical plain-text edition | format.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:
- 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
.audiobottlefile 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):
"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):
- If
audbot_formatis missing → the file is not a valid archive of this family. Refuse with an integrity error. - If
audbot_formatis 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. - If
audbot_schema_versionis 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 opaqueadditionalPropertiesrejection. - If
audbot_schema_versionis missing or below3→ the archive is a pre-release development artifact whose compressed entries av3reader 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. - 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.jsonis 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 notmanifest.json. - Exactly three members, named exactly
manifest.json,incompressible.tar,compressible.tar.zst. These names areconst-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, Goarchive/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
.wvfiles (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 (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:
{
"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:
- WAV / BWF (including RF64 and BW64, the >4 GB variants): all RIFF chunks survive exactly —
fmt,bext(BWF broadcast metadata),iXML,LIST/INFO,JUNKpadding, proprietary DAW chunks, and any trailer data.wvunpackreproduces 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
wvunpackCLI (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
- Open the file as a tar archive. Failure ⇒ not an archive / structurally corrupt.
- Read the first member. Its name must be
manifest.json; parse as UTF-8 JSON. - Check
audbot_format. Missing ⇒ integrity error. Not a known value ⇒ refuse: "created by a newer version — please update" (Section 2). - Check
audbot_schema_version. Missing or below 3 ⇒ refuse: pre-release artifact (Section 2). Greater than the highest you support ⇒ refuse with "please update". - Validate the manifest against the published schema for the format (Section 10).
- Preflight (recommended):
incompressible_tar.uncompressed_total_bytes + compressible_tar_zst.uncompressed_total_bytesapproximates the restored size — check destination free space before writing. - 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.
- Unpack
incompressible.tar(plain tar) andcompressible.tar.zst(zstd-decode, then untar) into a scratch directory. - Restore audio per
audio_files[].treatment(Section 4.3):wvunpackeverycompressed_wavpackentry's.wvwith the output path set to the entry's exactrelative_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. Validaterelative_pathfirst (Section 8.3). - 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_pathandaudbot_pathare untrusted manifest fields that your reader turns into filesystem paths. Before invokingwvunpack(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_bytesfields 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)
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, andaudbot_schema_versionit documents (see the header table). The published copy ataudiobottle.appis the canonical current edition. - Additive manifest changes (new fields) bump
audbot_schema_versionandaudbot_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-releasev1/v2schemas 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_formatstring — hypotheticallysplit-v2— with its own schema$idand its own container-layout section in a revised edition of this document.split-v1remains 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) useaudbotverbatim — they are shipped, load-bearing identifiers and will not be renamed. Do not "correct" them toaudiobottlein a reader. - Readers must identify archives by content, not filename. The identity check is
audbot_formatin the manifest (Section 2). Pre-release AudioBottle builds used the extension.audbotfor 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
.audiobottlearchives 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
.audiobottlearchives is the commercial function of AudioBottle, available under a paid license. This specification is published to guarantee reading freedom, not as an invitation to implement competing writers: producing.audiobottlearchives 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.