BLINDAUDIOTESTLISTEN. COMPARE. DECIDE.

.batest Specification

This page renders a copy of the .batest file format specification, pinned to release v1.0. View the full specification and changelog on GitHub for the authoritative, versioned source.

Blind Audio Test File Format Specification (.batest)

Version: 1.0 Status: Stable — first public release

This document is the authoritative specification of the .batest file format, an open, ZIP-based container format for storing reproducible blind audio comparison tests.

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.

Overview

A .batest file is a ZIP container that fully describes a blind audio comparison test: the audio tracks being compared, the metadata needed to reproduce the listening experience, and optional supporting material (cover images, documentation, external references). A conforming .batest file is self-contained and MUST be playable offline, without any network access, by any implementation that supports the test type it contains.

This specification describes the format independently of any single application or implementation. Test results (a listener's actual responses) are explicitly out of scope — see Test Results.

Design Goals

File Extension and Container Format

The format uses the .batest file extension. A .batest file MUST be a valid ZIP archive.

Directory Structure

MyTest.batest
│
├── manifest.json
├── test.json
├── required/
│   ├── track1.flac        (converted from WAV)
│   ├── track2.mp3         (lossy source, kept as-is)
│   └── track3.flac
├── assets/
│   ├── cover.jpg
│   ├── setup.jpg
│   ├── room.png
│   ├── measurements.pdf
│   └── intro.mp4
└── resources/

manifest.json

manifest.json is a lightweight, redundant summary of the test. It allows a file browser or import dialog to display key information without unzipping and parsing test.json.

{
  "formatVersion": 1,
  "createdWith": "Blind Audio Test 1.0.0",
  "createdAt": "2026-07-10T18:30:00Z",
  "title": "Example Test",
  "comparisonCategory": "microphones",
  "testType": "abx",
  "models": [
    { "manufacturer": "Neumann", "model": "U87" },
    { "manufacturer": "AKG", "model": "C414" }
  ]
}

All fields listed above are REQUIRED.

test.json

test.json is the primary, authoritative description of the test.

{
  "id": "UUIDv7",
  "creatorUuid": null,
  "title": "Example Test",
  "description": "...",
  "comparisonCategory": "microphones",
  "content": {
    "type": "vocals",
    "genres": ["indie-pop"],
    "tags": ["soft", "breathy"]
  },
  "recording": {
    "environment": "studio",
    "room": {
      "type": "vocal-booth",
      "sizeSqm": 6,
      "acoustics": "dampened"
    },
    "signalChain": [
      { "type": "preamp", "manufacturer": "Millennia", "model": "HV-3C" },
      { "type": "converter", "manufacturer": "RME", "model": "ADI-2 Pro FS" }
    ],
    "tags": ["Studio XYZ", "Stereo XY", "close-miking", "professional-recording"]
  },
  "tracks": [],
  "backingTrack": {
    "file": "required/background.flac",
    "gainDb": -12
  },
  "loudnessMatching": {
    "mode": "integrated-lufs",
    "reference": "loudest",
    "version": 1
  },
  "trackLengthMode": "shortest",
  "testTypeConfig": {}
}

description and assets are omitted from the example above; see Schema Hygiene Convention and Assets for when each is present versus omitted.

Top-Level Fields

content and recording

content and recording are both OPTIONAL and exist at both test level and track level (see Track Object Schema for the track-level variant). They describe the audio content and recording setup for context — e.g. "what is being listened to, and how was it captured" — and have no effect on playback, randomization, or scoring. Each key is omitted entirely when there is no content to add, rather than being present with a null or empty value.

content describes what is being compared:

recording describes the recording setup that is shared across all tracks in the test (e.g., when comparing microphones, the room, performer/instrument, and everything downstream of the microphone — preamp, converter, etc. — is typically identical across tracks; only the microphone itself differs, and that distinguishing detail belongs at track level instead):

At track level, content and recording use the exact same shape as above, but represent only a delta: only fields that differ from track to track belong there. A consuming application MUST merge test-level and track-level values per field, with the track-level value winning when present and the test-level value applying otherwise. Arrays such as signalChain and tags are NOT merged element-by-element: if a track provides its own signalChain, that array replaces the test-level signalChain for that track. How a replaced/combined chain is presented for display is left to the application; the format itself only stores the two arrays separately.

A typical usage in a microphone comparison test: content is entirely test-level (identical across tracks), while each track sets its own recording.signalChain with a single "microphone" entry — the one thing that actually differs between tracks. All other recording details (room, preamp, converter) stay at test level, since they are identical for every take.

backingTrack

backingTrack is an OPTIONAL, single audio track that plays continuously in the background throughout the test (e.g. a full music production over which the individually compared elements — instrument takes, effects, etc. — are layered). It is not part of the blind comparison itself: it plays unchanged, in parallel, for every track being evaluated.

This field MUST always be present in test.json; it is null when the test does not use a backing track (see Schema Hygiene Convention).

"backingTrack": {
  "file": "required/background.flac",
  "gainDb": -12
}

loudnessMatching

loudnessMatching describes the loudness-matching behavior used for the test. The key is OPTIONAL and is omitted entirely when loudness matching was not enabled for the test — there is no enabled: false representation; the block's mere presence in test.json means it was enabled.

"loudnessMatching": {
  "mode": "integrated-lufs",
  "reference": "loudest",
  "version": 1
}

Implementations MUST accept files that predate this convention and may still contain a legacy enabled and/or target field inside the block; both MUST be ignored on import, since the block's presence already conveys "enabled", and no known implementation ever wrote a target value other than matching relative to reference.

trackLengthMode

trackLengthMode determines how tracks with different durations are handled during playback:

This field is OPTIONAL and is only meaningful — and only ever written — when the test's tracks actually have different durations; it is omitted entirely when all tracks have the same length (or there is only one track). Implementations MUST assume "shortest" when the key is absent but the tracks do have different lengths.

trackLengthMode does not apply to testTypeConfig.rating: a Rating test presents one track at a time rather than playing multiple tracks in sync, so this field is never written for that test type, regardless of whether the tracks have different durations. Implementations MUST ignore this key if present on a Rating test (e.g. from a file exported by an older implementation).

Track Object Schema

Each entry in tracks[] follows this schema, independent of test type, so the same schema is used for A/B, A/B/X, and multitrack Ranking tests alike.

{
  "id": 0,
  "filename": "track1.flac",
  "originalFilename": "Vocal.wav",
  "manufacturer": "neumann",
  "model": "u87",
  "label": null,
  "recording": {
    "signalChain": [
      { "type": "microphone", "manufacturer": "Neumann", "model": "U 87 Ai" }
    ]
  },
  "originalFormat": "wav",
  "storedFormat": "flac",
  "originalSampleRate": 48000,
  "originalBitDepth": 24,
  "durationSeconds": 187.4,
  "integratedLufs": -18.7
}

Format Conversion Rule

testTypeConfig

Test-type-specific data lives in a namespaced object, keyed by test type, so the fields shared across the format (tracks, loudnessMatching, playback, randomization, assets, resources) remain identical across all test types, and new test types can be added without breaking older parsers — unknown testTypeConfig keys MUST be ignored by implementations that do not support them. The object's single key is the test type; test.json has no separate top-level testType field (that field exists only in manifest.json, see manifest.json).

This specification defines the following test types: A/B, A/B/X, A/B/X→A/B, Ranking, and Rating.

A/B:

"testTypeConfig": {
  "ab": {
    "testRounds": 2
  }
}

A/B/X:

"testTypeConfig": {
  "abx": {
    "testRounds": 8
  }
}

A/B/X→A/B:

This combined procedure runs two phases back to back, each with its own independent round count, so it uses two fields instead of a single testRounds:

"testTypeConfig": {
  "abx-then-ab": {
    "abxRounds": 8,
    "abRounds": 2
  }
}

Ranking:

"testTypeConfig": {
  "ranking": {
    "testRounds": 3
  }
}

For ab, abx, and ranking, testRounds is the number of rounds the listener completes. For abx-then-ab, abxRounds and abRounds are the round counts for the A/B/X phase and the following A/B phase, respectively. All fields shown above are REQUIRED for their respective test type.

Rating:

"testTypeConfig": {
  "rating": {
    "ratingMode": "identified",
    "ratingCategories": [
      {
        "id": 0,
        "label": "Brightness",
        "scaleType": "numeric",
        "scaleMin": 1,
        "scaleMax": 5,
        "scaleMinLabel": "Dull",
        "scaleMaxLabel": "Bright"
      },
      {
        "id": 1,
        "label": "Low-End",
        "scaleType": "bipolar",
        "scaleRadius": 5,
        "centerLabel": "Just right",
        "negativeLabel": "Too little",
        "positiveLabel": "Too much"
      }
    ]
  }
}

Additional test types, such as MUSHRA, are under consideration for a future version of this specification but are not yet defined. See the project README for status.

Assets

Assets are optional local files that help explain or document the test but never change the listening test itself.

Supported asset types: Images (JPG, PNG, WebP), Videos (MP4 recommended), PDF documents.

test.json references assets as follows:

"assets": {
  "cover": "assets/cover.jpg",
  "items": [
    { "type": "image", "file": "assets/setup.jpg", "label": "Microphone setup" },
    { "type": "video", "file": "assets/intro.mp4", "label": "Introduction" },
    { "type": "pdf", "file": "assets/measurements.pdf", "label": "Measurement report" }
  ]
}

The assets key itself is OPTIONAL and is omitted entirely from test.json when neither cover nor items has actual content. When assets is present, it MUST contain only the sub-field(s) that actually have content — cover alone, items alone, or both — never an empty placeholder for the field that has no content. For example, a test with only a cover image but no additional assets exports as:

"assets": {
  "cover": "assets/cover.jpg"
}

Missing assets MUST NOT prevent the test from running.

Resources

resources is an OPTIONAL array of external references that are not embedded in the container (e.g. YouTube videos, manufacturer pages, whitepapers, AES papers, Git repositories). The key is omitted entirely when empty.

"resources": [
  { "type": "link", "title": "Manufacturer page", "url": "https://..." },
  { "type": "paper", "title": "AES Convention Paper 12345", "url": "https://..." }
]

Test Results

Results are never stored inside .batest. A test's results are linked to the test by its id (see Identifier Stability) in whatever backend or storage system an implementation uses; this specification does not define a results format.

Data Integrity and Security

File Integrity

File integrity is covered by the ZIP container's own built-in per-entry CRC32 checksum, which detects corruption during storage or transfer. Implementations SHOULD rely on this mechanism, together with standard ZIP validation, when reading a .batest file.

Zip-Slip Protection

When extracting a .batest container, implementers MUST validate every entry path before writing it to disk. Entries containing ../, absolute paths, or that otherwise resolve outside the intended extraction directory MUST be rejected. This is a standard ZIP extraction vulnerability known as "Zip Slip", and applies regardless of file extension.

Canonical Serialization for Hashing

Implementers who need to compute a hash over the content of test.json or manifest.json — for example, to detect whether the payload has changed, for caching, or for synchronization between systems — SHOULD compute that hash over the canonical serialization of the JSON content, as defined by RFC 8785, the JSON Canonicalization Scheme (JCS), rather than over the raw file bytes.

Different JSON serializers can produce different key ordering, whitespace, or number formatting for otherwise identical data. Hashing raw bytes would therefore cause semantically identical content to produce different hashes depending on which tool or library wrote the file. Canonicalizing first removes this variation. JCS implementations already exist for most common languages, including JavaScript/ TypeScript, Python, and Rust, so implementers do not need to write their own canonicalizer.

This recommendation applies to any tooling built around the format (validators, sync tools, caches, etc.); it is not a requirement for the container format itself, and does not affect how test.json or manifest.json are written to disk inside a .batest file.

Schema Hygiene Convention

Optional fields in manifest.json and test.json follow a consistent convention: when an optional field has no meaningful value, its key is omitted from the JSON entirely — it is never written as null, an empty object {}, or an empty array []. What counts as "empty" depends on the field's type: for object-typed fields it means an empty object or no meaningful sub-fields set; for array-typed fields it means an empty array; for string-typed fields it means no value present; for number-typed fields it means no measurement or value available.

Implementations MUST accept both the omitted-key form and an explicit null (or empty {}/[]) for these fields, since files exported by older implementations may still contain the legacy representation.

This convention applies to (among others): description, assets (and its cover and items sub-fields), playback, randomization, resources, content, recording (at both test level and track level), comparisonCategoryOther, comparisonSubcategory, comparisonSubcategoryOther, and, on each track object, manufacturer, model, manufacturerOther, modelOther, notes, and integratedLufs.

A small number of fields are exceptions and are instead always present with an explicit null value when unset, rather than being omitted: creatorUuid, backingTrack (both at top level), label (on each track object), and scaleMinLabel/scaleMaxLabel (on numeric rating categories). originalBitDepth (on each track object) is likewise always present, but is null for a different reason: not because the value is unset, but because the concept of bit depth does not apply to lossy-compressed formats.

Identifier Stability

test.json's top-level id field is the test's unique, permanent identifier (a UUIDv7). This identifier MUST be treated as stable and immutable: once assigned to a test, it MUST NOT change, even across subsequent edits to any other field in test.json. Systems that reference a test externally (e.g. to associate results with it, per Test Results) rely on id remaining constant for the lifetime of the test.

Versioning and Compatibility

Guiding Principle

A .batest file represents a fully reproducible listening test. Required assets make the test executable. Optional assets (including embedded videos) provide additional context. External resources complement the package without increasing its size unnecessarily.