Version 1.0 · Release Candidate · 2026-10-29

oTAPE file format
specification.

A cassette is not just audio. It is audio, sides, artwork, packaging, protection and memory.
Extension
.otape
Media type
application/vnd.otape.cassette
Apple UTI
org.otape.cassette
Editor
Jan Osio

§1. Introduction

oTAPE stores one cassette in one file. The file is a portable object you can own, lend, record over, protect, flip and collect, the digital equivalent of a compact cassette in its case.

An .otape file contains:

§1.1 Design principles

  1. A cassette is not a playlist. Each side is one continuous recording. Silence between songs, spoken intros and the click of a pause button are part of the recording.
  2. Physical behaviour is preserved. A cassette has a fixed length, two sides that share one tape, a position you reach by winding, and a record tab that can be broken off.
  3. The file is the object. It is self-contained and works offline. It never refers to the files that were recorded from.
  4. Listening does not change the object. Playback position, ratings and library data stay with the player, not in the file.
  5. Open and simple. A ZIP container, one JSON manifest, standard audio and image formats. A developer can write a basic player in an afternoon.
  6. Distinctive by design. Every cassette can look unique, either with images in fixed physical proportions or with a compact procedural description any player can draw.

§1.2 Non-goals

oTAPE is not a DRM system, not a streaming format and not a multitrack audio container. It does not describe how a player's user interface must look.


§2. Conventions and terminology

The key words MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT, RECOMMENDED, MAY and OPTIONAL are to be interpreted as described in RFC 2119 and RFC 8174 when, and only when, they appear in all capitals.

TermMeaning
CassetteOne oTAPE object, i.e. one .otape file.
SideA or B. Each side is one continuous recording on the same tape.
Side lengthPlaying time of one side in seconds, fixed by the cassette type (side_length_seconds).
TimelineThe frame grid used for all positions in a cassette, at timeline_rate frames per second.
FrameOne sample instant on the timeline (one sample per channel).
Recorded lengthrecorded_frames: the high-water mark of recording on a side. Everything after it is blank tape.
Head positionWhere the tape is, as a frame offset from the start of the current side.
ReaderSoftware that opens oTAPE files. Player, recorder and editor are kinds of reader (§16).
WriterSoftware that creates or modifies oTAPE files.
mmMillimetres. All physical sizes in this document are in millimetres.

JSON terms (object, array, string, number, null) are as in RFC 8259. Paths are archive paths (§3.3).


§3. Container

§3.1 ZIP

An .otape file is a ZIP archive (PKWARE APPNOTE 6.3.x).

§3.2 The mimetype entry

Writers MUST write an entry named mimetype:

This puts the media type at a fixed offset (byte 38) of the file, so tools can recognise an oTAPE file without unzipping it. EPUB and OpenDocument use the same technique. Readers SHOULD NOT reject a file because mimetype is missing or not first. The manifest is authoritative.

§3.3 Paths

Every path in the manifest refers to an entry in the same archive and MUST:

Paths are case-sensitive. Writers SHOULD use lowercase ASCII paths.

§3.4 Layout

example.otape
├── mimetype                    REQUIRED  first entry, stored
├── manifest.json               REQUIRED  the cassette (§6)
├── audio/
│   ├── side-a.m4a              REQUIRED if side A has been recorded on
│   └── side-b.m4a              REQUIRED if side B has been recorded on
├── artwork/                    OPTIONAL  images (§12)
│   ├── cassette-face-a.png
│   ├── cassette-face-b.png
│   ├── label-a.png
│   ├── label-b.png
│   ├── jcard-front.png         (or .jpg)
│   ├── jcard-spine.png
│   ├── jcard-flap.png
│   ├── booklet-01.png …        booklet panels in reading order
│   └── thumbnail.png
├── docs/                       OPTIONAL  booklet.html, notes.md (§12.7)
└── extras/                     OPTIONAL  implementation data (§15.3)

The names under audio/ and artwork/ are RECOMMENDED names. Readers MUST use the paths given in the manifest and MUST NOT guess a file from its name.

§3.5 Limits

ItemLimit
manifest.jsonMUST NOT exceed 1 MiB.
ImageMUST NOT exceed 8192 px on either side and SHOULD NOT exceed 32 MiB.
Booklet panelsSHOULD NOT exceed 32.
Archive entriesSHOULD NOT exceed 1024.

Readers MAY refuse files that exceed these limits.


§4. Identification

Value
Format nameoTAPE (written with a lowercase o)
File extension.otape
Media typeapplication/vnd.otape.cassette (intended for IANA registration)
Apple Uniform Type Identifierorg.otape.cassette, conforming to public.data, public.content, public.zip-archive
MagicPK\x03\x04 at offset 0 and application/vnd.otape.cassette at offset 38 (§3.2)

Applications that own the format SHOULD register the extension with their platform's document-type mechanism. On Apple platforms that is UTExportedTypeDeclarations plus CFBundleDocumentTypes.


§5. Cassette model

§5.1 Cassette types

typeside_length_secondsMinutes per side
C46138023
C60180030
C90270045
C120360060
custom1 … 5400any, up to 90

For the standard types side_length_seconds MUST equal the value in the table. Readers MUST reject a mismatch. A custom cassette is labelled like a real one, with its total length in minutes rounded: C + round(2 × side_length_seconds / 60), e.g. 37 minutes per side → C74.

§5.2 Identity

FieldRule
cassette.idREQUIRED. RFC 4122 UUID, lowercase recommended. Identifies this cassette. A writer MUST generate a new id for a new blank cassette and for a dub (§13.2). Copying the file does not create a new cassette.
serial_numberOptional collector's number chosen by the creator, e.g. OT-000001. Not guaranteed unique.
created_atREQUIRED. RFC 3339 timestamp, UTC recommended (2026-10-28T22:00:00Z).
created_withName and version of the creating application.

The id is an identity, not a protection mechanism. Two copies of a file are the same cassette.

§5.3 Timeline

All positions and lengths in a cassette, including recorded_frames, marker frame, history start_frame and end_frame, are integers on one timeline:

Using integer frames instead of seconds makes positions exact. A recording and the silence after it line up to the sample.

§5.4 Head position and flipping

Both sides share one physical tape. A head position p frames into side A is the same piece of tape as L − p frames into side B, where L is the side length in frames.

Players that simulate flipping SHOULD apply this mapping: at the end of side A you flip to the start of side B. Players MAY instead keep independent positions per side if they don't simulate the tape physically.

§5.5 Blank tape

Any part of a side that has never been recorded is blank tape and plays as silence. This includes every frame from recorded_frames to the end of the side, and all of a side whose audio is null. Players MAY render blank tape with a faint tape hiss.


§6. Manifest

manifest.json is a UTF-8 JSON document without a byte-order mark. Its root is an object.

§6.1 Top level

KeyTypeReq.Description
formatstringyesAlways "oTAPE".
format_versionstringyes"MAJOR.MINOR", "1.0" for this document (§15).
cassetteobjectyesIdentity and metadata (§6.2).
sidesobjectyesExactly the keys "A" and "B" (§7).
appearanceobjectnoProcedural look of shell and label (§10).
jcardobjectnoText and colours of the paper insert (§11).
artworkobjectnoImage slots (§12).
bookletobjectnoFold-out panels and documents (§12.7).
historyarraynoEvents in the cassette's life (§13).
extensionsobjectnoImplementation data (§15.3).

All keys use snake_case. Side keys are the uppercase letters "A" and "B" everywhere they appear.

§6.2 cassette

KeyTypeReq.Description
idstring (UUID)yes§5.2
typestringyesC46, C60, C90, C120 or custom (§5.1)
side_length_secondsintegeryes§5.1
created_atstringyesRFC 3339
timeline_rateintegerno44100 (default) or 48000 (§5.3)
titlestringnoName of the cassette, written on the label
creatorstringnoWho made it
serial_numberstringnoCollector's number, ≤ 64 characters
descriptionstringnoOne or two sentences
genrestringnoFree text
notesstringnoShort notes. Long text goes in docs/notes.md.
locationstringnoWhere it was recorded
date_recordedstringnoFree-form date, e.g. 2026-10-28 or Summer 2002
modified_atstringnoRFC 3339, set on every save
created_with / modified_withstringnoApplication name and version
generationintegerno0 = original, 1 = first dub, and so on
source_cassette_idstring (UUID)noFor a dub: the cassette it was copied from

§6.3 Example

A minimal valid manifest (blank C60):

{
  "format": "oTAPE",
  "format_version": "1.0",
  "cassette": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "title": "Night Drive",
    "type": "C60",
    "side_length_seconds": 1800,
    "created_at": "2026-10-28T22:00:00Z"
  },
  "sides": {
    "A": { "write_protect": "intact", "audio": null },
    "B": { "write_protect": "intact", "audio": null }
  }
}

A complete example with every field is in examples/manifests/manifest-full.json. Complete example cassettes are in examples/files/.


§7. Sides and audio

§7.1 Side object

KeyTypeReq.Description
write_protectstringyesintact, broken or taped_over (§8)
audioobject or nullyes¹The recording (§7.2). null for a blank side.
titlestringnoSide title written on the label, e.g. "Outbound"
markersarraynoTrack markers (§7.4)

¹ Writers MUST write the key, using null for a blank side. Readers MUST treat a missing audio like null.

§7.2 Audio object

KeyTypeReq.Description
filepathyesThe audio entry, e.g. audio/side-a.m4a
codecstringyes§7.3
recorded_framesintegeryesRecorded length on the timeline. MUST be ≤ the side length in frames.
sample_rateintegernoSample rate of the audio file. SHOULD equal timeline_rate.
channelsintegerno1 or 2. Mono is played on both channels.
bit_rateintegernoFor lossy codecs, in bits per second
duration_secondsnumbernoInformative: recorded_frames / timeline_rate, three decimals

An audio object with recorded_frames = 0 is equivalent to null. A side has at most one audio file. The audio is one continuous recording from frame 0 to recorded_frames.

§7.3 Codecs

codecContainerExtensionStatus in 1.0
aac-lcMPEG-4 (M4A).m4aREQUIRED for every player. RECOMMENDED for writers.
alacMPEG-4 (M4A).m4aOptional
flacFLAC.flacOptional
pcmWAVE, 16- or 24-bit integer.wavOptional

Rules for audio files:

§7.4 Track markers

Markers name places on a side. They are informative: the side stays one continuous recording.

KeyTypeReq.Description
frameintegeryesPosition on the timeline
titlestringyese.g. a song title
kindstringnotrack (default), voice or note. Unknown values are treated as note.

Markers SHOULD be sorted by frame. Players MAY show the marker nearest before the head, e.g. "now playing". Players SHOULD NOT offer to jump to a marker instantly. Reaching a position by winding is part of the medium. A player that does offer jumping SHOULD animate it as winding.

§7.5 Playback


§8. Write protection

A real cassette has one record tab per side. Breaking it off stops a deck from recording that side. Covering the hole with tape makes the side recordable again. oTAPE models this as a physical property of the medium.

This is not DRM. It protects a finished tape from accidental recording, as on a real cassette. Anyone can tape over the hole.

§8.1 States

write_protectMeaningRecording allowed
intactTab in placeyes
brokenTab broken offno
taped_overHole covered with tapeyes

§8.2 Transitions

intact ──break off──▶ broken ──cover with tape──▶ taped_over
                         ▲                            │
                         └────── remove the tape ─────┘

A broken tab never becomes intact again. Writers MUST NOT change broken or taped_over to intact. Every change SHOULD be logged as a write_protect_changed history event (§13).

§8.3 Behaviour

A conforming recorder:


§9. Recording

oTAPE files are produced by recording in real time. The format doesn't require that, but its rules follow from it.


§10. Appearance

appearance describes the cassette in a compact form that any player can draw. A cassette with no images still looks like itself. Images (§12) replace the drawn parts where present.

§10.1 Structure

"appearance": {
  "preset": "smoke",
  "shell": { "color": "#3A3F478C", "finish": "transparent" },
  "label": { "style": "stripes", "paper": "#FAFAF5", "ink": "#202020", "accent": "#E4572E", "font": "pen" }
}
KeyTypeDescription
presetstringInformative name of the design the creator started from. Readers MUST NOT depend on it.
shell.colorcolourPlastic colour. The alpha channel gives the shell's opacity.
shell.finishstringopaque or transparent. A transparent shell shows the tape packs and mechanism through the plastic.
label.stylestring§10.3
label.papercolourLabel paper
label.inkcolourLettering and printed marks
label.accentcolourStripes, bands and the top rule
label.fontstringLettering role, §10.5

Defaults when absent: shell #1B1B1D opaque, paper #F3EEDC, ink #1D2B53, accent #C8102E, style ruled, font marker.

§10.2 Colours

Colours are sRGB strings #RRGGBB or #RRGGBBAA (hex, case-insensitive). Alpha FF is opaque.

§10.3 Label styles

styleDrawing
ruledAccent rule 2.2 mm high along the top edge. Hairlines in ink at 25 % opacity at y = 10.5, 15.5 and 31.5 mm, from x = 11.0 to 86.5.
stripesFour stripes 1.6 mm high, 2.2 mm apart, in the bottom band from y = 33.0: accent, accent 75 %, ink 85 %, accent 50 %.
kraftBrown paper texture. Use paper as the base, with fine darker fibres.
gradientThe bottom band (y 33.0–42.0) filled left to right: accent → #FF5A8C → #FFD966.
plainPaper only.

Unknown styles MUST be drawn as plain.

§10.4 Procedural label layout

All values are in label coordinates (mm from the label's top-left corner). See §12.5 and templates/label.svg.

ElementBox (x, y, w, h)Content
Side letter2.5, 3.0, 6.5, 8.0Outlined box with A or B, heavy sans, cap height ≈ 4.5 mm, in ink
Title line11.0, 3.5, 75.5, 7.0cassette.title in the label font role, baseline y = 9.5, cap height ≈ 4 mm, left-aligned
Side title line11.0, 10.5, 75.5, 4.5sides.X.title at ≈ 65 % of the title size, baseline y = 14.0
Bottom band0, 33.0, 89.0, 9.0Stripes or gradient by style
Type badge70.0, 35.0, 16.5, 5.0e.g. C60, wide heavy sans, cap height ≈ 2.6 mm, right-aligned at x = 86.5. Paper colour on stripes and gradient, ink otherwise.

Text that doesn't fit SHOULD be truncated with an ellipsis rather than shrunk below 60 %.

§10.5 Lettering (font roles)

oTAPE names a role rather than a typeface, so every platform can draw it with a font it has.

RoleLookmacOS (reference)Open alternative
markerFelt-tip handwriting, the classic mixtapeMarker FeltPermanent Marker
penBallpoint handwritingNoteworthyCaveat
typewriterTyped labelAmerican TypewriterSpecial Elite, Courier Prime
sansClean printsystem sans (SF Pro)Inter
serifBook printBaskervilleLibre Baskerville
stencilBold condensed / stencilFutura Condensed ExtraBoldBig Shoulders Stencil

Unknown roles MUST be drawn as sans.


§11. J-card and case

The J-card is the folded paper insert in the case. From outside you see the front, the spine and, folded behind, the back flap.

§11.1 Structure

"jcard": {
  "card_color": "#1C1033",
  "ink": "#FFE8A3",
  "font": "stencil",
  "front": { "title": "Night Drive", "subtitle": "a mixtape for the A4" },
  "spine": { "text": "NIGHT DRIVE · 2026" },
  "flap": { "text": "Dolby B · Type II · Jan Osio" },
  "tracklist": { "A": ["Intro", "Midnight City"], "B": ["Sunrise"] },
  "dedication": "For everyone still awake."
}
KeyDescriptionDefault
card_colorPaper colour#F4EEDF
inkText colour#1F2A44
fontLettering role for titles and dedication (§10.5). Track lists use sans.marker
front.titleCover titlecassette.title
front.subtitleLine under the titlenone
spine.textWhat you read on the shelfuppercase front.title
flap.textShort text on the back flapnone
tracklist.A / .BPrinted track lists, one string per linenone
dedicationHandwritten notenone

The printed tracklist is what the creator wrote on the card. It is independent of the markers (§7.4). Writers MAY fill it from the markers.

§11.2 Procedural J-card layout

PanelSizeContent
Front63.5 × 101.6Inside a 3.0 mm safe area: title (font role, cap ≈ 5 mm), subtitle (sans ≈ 2.4 mm), creator, then the track lists (SIDE A, SIDE B, sans ≈ 2 mm), then the dedication
Spine12.7 × 101.6spine.text, wide bold sans, cap ≈ 3 mm, centred, reading top to bottom (§11.3)
Flap15.9 × 101.6flap.text, sans ≈ 2 mm, centred, reading top to bottom

§11.3 Reading direction

The spine and flap are stored and described upright: the panel is portrait, with its top at the top of the front cover. Their text is rotated 90° clockwise and reads top to bottom, so it reads correctly with the case lying front-up and the spine towards you. Creators supplying images SHOULD follow this.

§11.4 Case

The standard case is 109.0 × 70.0 × 17.0 mm and the cassette is 100.4 × 63.8 × 12.0 mm. Players that draw the case in 3D SHOULD use these sizes. The J-card front is seen through the lid, and the spine runs along the long edge.


§12. Artwork

§12.1 General rules

§12.2 Slots

Slot (artwork.*)Recommended pathPhysical size (mm)AspectRecommended pxMinimum pxAlpha
cassette_face_aartwork/cassette-face-a.png100.4 × 63.81.57372410 × 15311205 × 766required for holes
cassette_face_bartwork/cassette-face-b.png100.4 × 63.81.57372410 × 15311205 × 766required for holes
label_aartwork/label-a.png89.0 × 42.02.11902136 × 10081068 × 504keep-out transparent
label_bartwork/label-b.png89.0 × 42.02.11902136 × 10081068 × 504keep-out transparent
jcard_frontartwork/jcard-front.jpg63.5 × 101.60.62501524 × 2438762 × 1219no
jcard_spineartwork/jcard-spine.png12.7 × 101.60.1250305 × 2438152 × 1219no
jcard_flapartwork/jcard-flap.png15.9 × 101.60.1565382 × 2438191 × 1219no
booklet.panels[]artwork/booklet-NN.png63.5 × 101.60.62501524 × 2438762 × 1219no
thumbnailartwork/thumbnail.pngn/a1.00001024 × 1024256 × 256yes

Pixel sizes are the physical size × px/mm, rounded to the nearest integer.

The thumbnail is a square preview for file browsers, Quick Look and library views: the cassette (and/or case) on a transparent background. Writers SHOULD regenerate it on save so it shows the current look.

Unknown slot names MAY appear in artwork. Readers ignore them, and writers keep them (§15.2).

§12.3 Layering

A player draws the cassette face from back to front:

  1. Mechanism, drawn by the player and animated: tape packs (radius from the head position, Appendix B.3), the tape path along the head opening, and the white drive hubs, which turn with the reels.
  2. Shell face: cassette_face_a/_b, or the procedural shell (§10.1). The window and both hub holes are cut out.
  3. Label: label_a/_b, or the procedural label (§10.4). The keep-out zone (§12.5) is cut out.
  4. Glass: a subtle reflection over the window. Optional.

Because the mechanism is underneath, images MUST be transparent wherever the reels should show. A transparent (finish: transparent) shell image uses partial alpha so the tape packs show through the plastic.

§12.4 Cassette face

Cassette face template

Coordinates in mm from the top-left of the face, cassette upright, head opening at the bottom:

FeatureGeometry
Outline100.4 × 63.8, corner radius 3.5
Window (transparent)x 30.2–70.2, y 21.5–33.5 (40.0 × 12.0), corner radius 1.5
Hub centres(29.0, 27.5) and (71.4, 27.5), 42.4 apart
Hub holes (transparent)Ø 17.0 around each hub centre
Visible drive hubØ 12.0, drawn by the player
Label positionx 5.7, y 3.5 (89.0 × 42.0)
Head openingTrapezoid (17.0, 63.8) – (23.6, 49.5) – (76.8, 49.5) – (83.4, 63.8)
ScrewsØ 2.4 at (3.0, 3.0), (97.4, 3.0), (3.0, 60.8), (97.4, 60.8), (50.2, 52.6)

Side B's face has the same layout. The cassette is turned over, not mirrored.

§12.5 Label

Label template

§12.6 J-card panels

J-card template

Unfolded outside, left to right: flap 15.9 | spine 12.7 | front 63.5, all 101.6 mm high, 92.1 mm wide in total. Each panel is a separate image.

§12.7 Booklet and documents

"booklet": {
  "panels": ["artwork/booklet-01.png", "artwork/booklet-02.png"],
  "html": "docs/booklet.html",
  "notes": "docs/notes.md"
}

If both panels and html exist, the panels are the canonical printed look and the HTML is an alternate reading view.

§12.8 Templates

SVG templates at 1:1 millimetre scale are in templates/: cassette-face.svg, label.svg, jcard-outside.svg and booklet-panel.svg. They import into vector tools at real size.

Every dimension in this section is also published as JSON in geometry/otape-geometry-1.0.json, for implementations that draw cassettes. The templates are generated from that file by tools/make_templates.py. If the JSON and this text ever disagree, this text is normative.


§13. History

history records the cassette's life as a collector's object. It is optional, and readers MUST NOT need it to play the cassette.

§13.1 Event object

KeyTypeReq.Description
typestringyes§13.2
timestampstringyesRFC 3339
appstringnoApplication name and version
side"A" / "B"per type
otherper type

Events are in chronological order, and writers append to the end.

§13.2 Event types

typeExtra fieldsMeaning
creatednoneThe blank cassette came into being
recordedside, start_frame, end_frame, source, source_formatOne recording pass
write_protect_changedside, stateThe record tab changed (§8)
erasedsideA side was bulk-erased (recorded_frames reset to 0)
dubbedsource_cassette_idThis cassette was made by copying another one
editedwhat (string)Metadata or artwork changed (optional, may be coalesced)

source is one of file, line, radio, microphone, dub or generated. source_format is free text, e.g. mp3.

Readers MUST ignore event types they don't know, and writers MUST keep them (§15.2).


§14. Medium state and local state

Belongs to the cassette (in the file)Belongs to the player (not in the file)
audio, recorded length, markerslast head position and side
write protectionfavourites, ratings, play counts
appearance, artwork, J-card, bookletlibrary, shelves, tags
identity, metadata, historycounter resets, volume

Playing a cassette MUST NOT modify the file. Players SHOULD remember the side and head position per cassette.id, so a cassette put back into the deck resumes where it was, the way a real tape stays wound where you left it.


§15. Versioning and extensibility

§15.1 Versions

format_version is "MAJOR.MINOR".

§15.2 Preservation

Unknown fields, unknown slots, unknown history events and unknown archive entries MUST be ignored by readers. Writers modifying an existing cassette MUST preserve them unchanged, unless the user removes them explicitly. This lets cassettes pass between applications without losing anything.

§15.3 Extensions

Implementation-specific data goes in extensions, keyed by a reverse-DNS identifier the implementer controls:

"extensions": { "org.example.player": { "shelf_position": 12 } }

Files go under extras/<identifier>/…. Extensions MUST NOT change the meaning of standard fields.


§16. Conformance

§16.1 Reader (validator)

A conforming reader:

  1. opens the ZIP container (§3), checking paths (§3.3) and limits (§3.5);
  2. checks format = oTAPE and that it supports the major version;
  3. validates the required fields: cassette.id, type with matching side_length_seconds, created_at, and both sides with a valid write_protect;
  4. for each side with audio, checks that file exists and that recorded_frames ≤ the side length in frames;
  5. rejects the file if any of the above fails, naming the reason.

The JSON Schema schema/otape-manifest-1.0.schema.json covers the structural part. Rules spanning several fields, such as recorded_frames against the side length or entries existing in the archive, need code. tools/otape_validate.py implements all of them, together with the artwork and container checks.

§16.2 Player

A conforming player is a reader that additionally:

§16.3 Recorder

A conforming recorder is a player that additionally:

§16.4 Editor

A conforming editor changes metadata, appearance, J-card or artwork without re-encoding audio, enforces artwork proportions (§12.1) and preserves unknown data.


§17. Security considerations


§18. Reference material

ItemLocation
JSON Schemaschema/otape-manifest-1.0.schema.json
Geometry (JSON)geometry/otape-geometry-1.0.json
Example manifestsexamples/manifests/. Files named invalid-* must be rejected by a conforming reader.
Example cassettesexamples/files/
Artwork templates (SVG, mm)templates/
Validatortools/otape_validate.py (Python 3, standard library; jsonschema optional)
Reference writertools/make_examples.py, a minimal writer that builds the example cassettes
Reference implementationoTape Studio for macOS by Jan Osio. A full recorder and player whose test suite loads the example cassettes and checks its geometry against the JSON file.

§Appendix A: Changes from the 1.0 draft

The first draft of this specification was reviewed against a working recorder. These inconsistencies were resolved for 1.0:

#Draft1.0Why
1Both audio/side-a.m4a and side-b.m4a requiredAudio is required only for a recorded side. A blank side has "audio": null.A new cassette is blank. Storing encoded silence is wasteful and says nothing.
2Per-side data spread over audio.side_a, write_protect.side_a, track_markers.side_a, with naming side_a_write_protect in another sectionAll per-side data in one object: sides.A, sides.BOne place per side and one naming scheme. Side keys are "A"/"B", as in history.
3duration_seconds (float) as the length; marker time in secondsInteger frames on one timeline_rate: recorded_frames, marker frame; seconds are informativeFloats can't place a recording or a silence exactly. Frames are exact.
4Side capacity and recorded length mixedcassette.side_length_seconds = capacity; sides.*.audio.recorded_frames = what was recordedThey are different things: a C60 side is 30 minutes even if 12 are recorded.
5"Playback position is counted separately per side"One tape: position on B = L − position on A (§5.4); stored locally, not in the fileA real tape has one position. Flipping at the end of A puts you at the start of B.
6tape_override_applied event, separate from the stateOnly write_protect_changed with stateTaping over is a state change. One event type covers all three transitions.
7broken → intact not excludedA broken tab can never become intact (§8.2)It is physically impossible.
8case-front.png and booklet-cover.pngjcard_front is the cover. Booklet panels are the fold-out.Both draft files described the same panel.
9case-back.png (no defined meaning)Removed. The flap is jcard_flap, and inner panels are booklet panels.A standard case has no printed back. The visible parts are the front, spine and flap.
10Artwork without sizesPhysical sizes, aspect ratios, px/mm, keep-out and safe areas, layering (§12)Without fixed proportions, artwork can't be exchanged between applications.
11booklet.mode (panels or html)No mode. Panels are canonical, and HTML is an optional alternate view.Both can coexist, and a mode field adds a way to be inconsistent.
12specification URL in every manifestNot required. The version identifies the specification.The URL is not stable yet, and files must not depend on a website.
13recorded_from per cassette (mp3, radio, mic, aux, dub)source per recorded event (file, line, radio, microphone, dub, generated) and source_format (e.g. mp3)One cassette can mix sources, and mp3 is a file format, not a source.
14format_version "uses semver"MAJOR.MINOR with explicit rules (§15)A file format has no patch level. Compatibility rules are stated precisely.
15MIME application/vnd.otape.cassette only proposedPlus a mimetype first entry (§3.2)Identifies a file by its bytes without unzipping.
16Procedural look unspecified (images only)appearance and jcard with drawing rules and font rolesA cassette must look like itself even without images.
17extras/waveform-side-a.json with no formatextras/<reverse-dns>/… plus extensionsLets implementations add data without colliding.

§Appendix B: Geometry reference

All values below, and those of §12, are also in geometry/otape-geometry-1.0.json.

§B.1 Summary of physical sizes (mm)

ObjectWidthHeightDepth
Cassette shell100.463.812.0
Label89.042.0n/a
J-card front63.5101.6n/a
J-card spine12.7101.6n/a
J-card flap15.9101.6n/a
J-card unfolded (outside)92.1101.6n/a
Booklet panel63.5101.6n/a
Case109.070.017.0

§B.2 Pixel sizes

Object24 px/mm (recommended)12 px/mm (minimum)
Cassette face2410 × 15311205 × 766
Label2136 × 10081068 × 504
J-card front / booklet panel1524 × 2438762 × 1219
J-card spine305 × 2438152 × 1219
J-card flap382 × 2438191 × 1219

§B.3 Reels and tape packs

For realistic reels, players MAY use this model (all lengths in cm):

On a C60 this gives a full pack radius of about 2.47 cm. The take-up reel turns fast at first and slows down as it fills.

§Appendix C: Reading an oTAPE file step by step

open ZIP
  └ if first entry is "mimetype": expect "application/vnd.otape.cassette" (informative)
read manifest.json  → JSON object (≤ 1 MiB)
  ├ format == "oTAPE"                    else: not an oTAPE file
  ├ major(format_version) == 1           else: needs newer app
  ├ cassette.id is UUID
  ├ (type, side_length_seconds) valid    (§5.1)
  ├ rate = cassette.timeline_rate ?? 44100
  └ for side in A, B:
       write_protect ∈ {intact, broken, taped_over}
       if audio != null and audio.recorded_frames > 0:
           path safe and present in archive
           recorded_frames ≤ side_length_seconds × rate
           decode, resample to rate if needed, trim/pad to recorded_frames
       else: side is blank tape
draw: appearance + artwork (§10, §12), jcard (§11)
restore local head position for cassette.id (§14)