#JMX MIDI files

This document specifies the custom MIDI recording format written by Jamcorder. The current format version is JMX 1.0.

Purpose: help people understand exactly what Jamcorder records and develop their own tools to parse and work with MIDI files produced by Jamcorder.

A JMX recording is a valid Standard MIDI File (SMF) with one track and a .mid extension. Normal MIDI channel events and SysEx messages use standard SMF encodings. Jamcorder adds JSON-bearing meta events for recording identity, time recovery, bookmarks, chunk indexing, summary data, and reverse traversal.

#Design goals

JMX extends Standard MIDI Files to make continuous, user-facing music capture practical:

This specification describes the bytes stored in the file. For network calls that list, download, or stream recordings, see the Jamcorder device API.

Privacy: a JMX file can contain the device and performer names and UUIDs, the original device-side path, wall-clock timestamps, firmware build hashes, fabrication information, and lifetime playing statistics. Treat a raw JMX file as device metadata as well as musical data.

#Contents

#Format overview

The file has four conceptual layers:

LayerPurpose
Standard MIDI headerIdentifies an SMF format-0 file with one track.
Standard MIDI eventsStores channel voice messages, SysEx, tempo, and end-of-track.
JMX meta eventsStores JSON metadata, bookmarks, skipped time, stone boundaries, and summaries.
Backward-length markersMake variable-size meta and SysEx events seekable in reverse.

At a high level, a newly created recording looks like this:

MThd
MTrk
  jmxStoneHdr          first stone and full recovery snapshot
  jmxBlm
  jmxAsset             small, display-friendly asset summary
  jmxBlm
  MIDI events ...
  jmxEof               renewable summary and current-stone pointer
  jmxBlm
  End of Track

As recording continues, the writer removes the old renewable trailer, appends new events, and writes a new trailer. When a stone reaches approximately 45 KiB, the next segment begins with a footer/header pair:

  ... previous stone data
  jmxStoneFooter
  jmxBlm
  jmxStoneHdr          next stone
  jmxBlm
  Set Tempo
  jmxBlm
  ... next stone data

The tempo event is itself a standard meta event and therefore receives a JMX backward-length marker.

#Real recording used in examples

The concrete examples in this document are taken from an actual JMX 1.0 recording, Jmx-A00074-Apr-24-2025.mid. Its relevant properties are:

PropertyValue
File size111,298 bytes
SHA-256999e819a18de19159a72f0b8e24fc31433b44453008cf5d20e3665ecca6cf2a7
SMF format0
Tracks1
Division458 ticks per quarter note
Track length111,276 bytes
Stones3
jmxSkip events50
jmxBlm events59
Compressed duration1,987,223 ms
Skipped-time total343,795 ms
Recorded Note On count10,545
Stored non-trailer event count24,214

Names, UUIDs, offsets, timestamps, counters, and firmware information shown below are real sample values. The unique fabrication product key is explicitly redacted.

#Standard MIDI envelope

All fixed-width integers in SMF chunk headers are unsigned big-endian values. The initial 22 bytes are:

OffsetSizeValueMeaning
04ASCII MThdHeader chunk identifier.
4400 00 00 06Header payload length: 6 bytes.
8200 00SMF format 0.
10200 01One track.
12201 CA458 ticks per quarter note.
144ASCII MTrkTrack chunk identifier.
184VariableTrack-data length, excluding MTrk and this length field.
22...VariableFirst delta-time and event.

The sample recording begins with these exact bytes:

4D 54 68 64 00 00 00 06 00 00 00 01 01 CA
4D 54 72 6B 00 01 B2 AC

The final two bytes of the first line are the division. The second line begins with MTrk; 00 01 B2 AC is the sample's 111,276-byte track length.

The track length at offset 18 is maintained as the file grows. A complete file must satisfy:

file size = 14 + 8 + track length

#Variable-length quantities

Delta-times and variable payload lengths use the standard MIDI variable-length quantity (VLQ): seven data bits per byte, most-significant group first, with bit 7 set on every byte except the last.

ValueEncoding
000
1277F
12881 00
16,383FF 7F

JMX writers emit an explicit status byte for every MIDI event. Running status is not used.

#Time model

Jamcorder event timestamps originate as milliseconds since device boot. The writer stores the non-negative difference from the preceding recorded event as the SMF delta-time.

The file uses 458 ticks per quarter note and tempo events with 458,000 microseconds per quarter note. Together these define exactly one millisecond per tick:

458,000 microseconds / 458 ticks = 1,000 microseconds per tick

JMX-aware readers should treat one delta tick as one millisecond. A standard MIDI reader that applies the SMF default tempo before seeing a JMX tempo event may calculate a slightly different duration, because a newly created file is not guaranteed to begin with a Set Tempo event.

#Silence compression

When silence skipping is enabled, long silent periods are not represented in full by SMF delta-time. A jmxSkip event records the omitted wall-clock duration in its millis field. The resumed performance is kept about 500 milliseconds after the preceding recorded activity on the compressed SMF timeline.

Use two timelines when needed:

#JMX meta-event framing

Every JMX event is an SMF meta event preceded by a normal delta-time VLQ. The meta payload contains an ASCII event token immediately followed by a JSON object and a NUL byte.

#Asset event framing

jmxAsset uses SMF Text meta type 0x01 so basic MIDI applications can display the small asset summary:

<delta VLQ>
FF 01
<payload-length VLQ>
"jmxAsset"
<UTF-8 JSON object>
00

#All other JMX event framing

Other JMX events use meta type 0x7F with a leading zero byte:

<delta VLQ>
FF 7F
<payload-length VLQ>
00
<ASCII event token>
<UTF-8 JSON object>
00

The zero byte is part of the payload. The token and opening { identify the event and the start of its JSON. JSON may be pretty-printed or compact; parsers must not depend on whitespace. The terminal NUL is included in the meta payload length but is not part of the JSON text.

Readers should preserve unknown tokens and unknown JSON fields. New metadata can be added without changing the surrounding SMF framing.

#Custom event catalog

TokenMeta typePurpose
jmxAsset0x01Minimal asset summary for humans and generic MIDI tools.
jmxTrackReopened0x7FRecords that an existing daily asset was reopened.
jmxSkip0x7FRecords wall-clock silence omitted from the MIDI timeline.
jmxBookmark0x7FMarks the end of a user-selected passage.
jmxStoneHdr0x7FBegins an indexed stone and captures recovery state.
jmxStoneFooter0x7FCloses the preceding stone.
jmxEof0x7FRenewable whole-file summary and latest-stone pointer.
jmxBlm0x7FFixed-size backward-length marker for the preceding variable event.

#jmxAsset

This is deliberately small because some generic MIDI applications display the first text event.

{
  "time": "2025-04-24 16:01:14 UTC-7",
  "assetIdx": 74,
  "jamcorderName": "Chip's Jamcorder 🙂",
  "performerName": "Chip Weinberger",
  "jmxVersion": "1.0"
}
FieldTypeMeaning
timestringLocal creation time formatted as YYYY-MM-DD HH:MM:SS UTC±offset; UTC itself is written as UTC.
assetIdxnumberDevice-local monotonically increasing asset number.
jamcorderNamestringDevice display name at asset creation.
performerNamestringPerformer display name at asset creation.
jmxVersionstringJMX schema version recorded when the asset is created; current recordings use 1.0.

#jmxTrackReopened

Written when firmware reopens an existing asset after a restart or remount. The sample recording does not contain this event, so the following object is an illustrative schema example using the sample's time range and UTC offset.

{
  "unixtime": 1745538005,
  "localOffset": -420
}

unixtime is UTC seconds since the Unix epoch. localOffset is minutes east of UTC, so Chicago standard time is -360 and daylight time is -300.

#jmxSkip

{
  "millis": 2044,
  "unixtime": 1745535987,
  "localOffset": -420
}
FieldRequiredMeaning
millisYesMilliseconds removed from the wall-clock timeline.
unixtimeNoWall-clock anchor when the event was written.
localOffsetNoLocal UTC offset in minutes at that anchor.

The wall-clock fields are written periodically and when the clock, timezone, or time direction changes. They are intentionally omitted from many skip events to save space.

#jmxBookmark

The sample recording contains no bookmarks. This example therefore illustrates the wire schema rather than quoting an event from that file.

{
  "bookmarkIdx": 8,
  "bookmarkUuid": "bb5c9a0e-7a31-4d6f-8c22-91d04e3f7b62",
  "bookmarkSource": "byPianoTrigger",
  "unixtime": 1745538005,
  "localOffset": -420
}

A bookmark marks a passage end. A client can derive a passage start according to its own look-back policy. bookmarkIdx is a device-local counter; bookmarkUuid is the stable bookmark key.

#jmxStoneHdr

This is the richest event in the file. It starts a stone, identifies the asset, links to the previous stone, and snapshots enough state for incremental readers.

{
  "jmxVersion": "1.0",
  "stoneIdx": 3,
  "stoneUuid": "ffb9fe9f-b8a3-9085-cfab-0f3f1420608d",
  "time": {
    "timeSource": "sntp",
    "unixtime": 1745537690,
    "localOffset": -420
  },
  "asset": {
    "assetIdx": 74,
    "assetUuid": "aa9ff6a6-3874-b1a4-2282-b66f2da0cf21",
    "midiPath": "/JAMC/2025/60b2d69c-333d-9b33-6c3b-9b38886c102d/Jmx-A00074-Apr-24-2025.mid"
  },
  "identities": {
    "jamcorderUuid": "60b2d69c-333d-9b33-6c3b-9b38886c102d",
    "jamcorderName": "Chip's Jamcorder 🙂",
    "performerUuid": "eec97c90-d1e9-54cb-a2b3-b28f37886087",
    "performerName": "Chip Weinberger"
  },
  "position": {
    "fileOffset": 91797,
    "msgOffset": 20135,
    "tmptOffset": 1686472
  },
  "stoneUuid": "ff10787e-5481-e018-03c9-e8475c8b13e1",
  "prevStone": {
    "fileOffset": 45912,
    "msgOffset": 4192,
    "tmptOffset": 944653
  },
  "tempo": {
    "tpqn": 458,
    "mpqn": 458000
  },
  "sound": {
    "state": "noisy",
    "dur": 1408
  },
  "notes": {
    "ch0": [40, 52, 68, 71]
  },
  "lifestats": {
    "lifeMillisPlayed": 44815255,
    "lifeNotesPlayed": 215803
  },
  "firmwareInfo": {
    "appName": "jJfwmApp",
    "appVersion": "v1.3.1-1744874255",
    "compileDate": "Apr 17 2025",
    "compileTime": "00:17:38",
    "sha256": "def06876692e312e8bf14aa69df7313769d3b5743dc7651fcd48f70cff46bfaa"
  },
  "fabInfo": {
    "fabUnixtime": 1736983302,
    "fabProductKey": "[redacted]",
    "fabModelNumber": "JMX100",
    "fabPcbRev": "O",
    "fabTrain": "prod",
    "fabJmxGitBranchName": "master",
    "fabJmxGitCommitHash": "4fe533a34cb27680851928c1929377242d1a22f2",
    "fabJmxGitCommitUnixtime": 1736971570,
    "fabEspIdfVersion": "v5.1.4",
    "fabEspIdfGitBranchName": "user/chip/v5.1.4-production",
    "fabEspIdfGitCommitHash": "d7b0a45ddbddbac53afb4fc28168f9f9259dbb79",
    "fabEspIdfGitCommitUnixtime": 1715250918,
    "fabPhysicalId": "29bbbdeaed1f6b69f6259873d405e70c"
  }
}

This is the sample's JMX 1.0 third stone header as serialized, except for the redacted product key. The duplicate root stoneUuid properties are intentional in this example: the first is stone 3's UUID and the second is stone 2's UUID. JMX 1.1 instead stores the previous UUID as prevStone.stoneUuid. See Compatibility and current v1 quirks.

Root fields:

FieldMeaning
jmxVersionJMX schema version.
stoneIdxOne-based stone number within the asset.
stoneUuidUUID assigned to this stone. Some JMX 1.0 files contain a duplicate key; see the compatibility note below.
timeWall-clock anchor and the source that established device time.
assetStable asset identity, asset counter, and original device-side path.
identitiesDevice and performer IDs and display names at this boundary.
positionFile, message, and compressed-time offsets at the stone start.
prevStonePrevious stone offsets and, in JMX 1.1, its UUID; omitted for the first stone.
tempoTimebase constants: ticks and microseconds per quarter note.
soundWhether recorded piano state is silent or noisy, plus duration in milliseconds.
notesDepressed-note arrays keyed as ch0, ch1, and so on; empty channels are omitted.
lifestatsDevice lifetime playing totals at this boundary.
firmwareInfoRunning build identity and hash; fields may be absent if unavailable.
fabInfoProvisioned manufacturing information plus physical device ID; schema is provisioning-dependent.

Position fields are unsigned byte/count values:

FieldMeaning
fileOffsetAbsolute byte offset of the first delta-time byte of this jmxStoneHdr.
msgOffsetCount of stored non-trailer events before this header, including prior automatic jmxBlm events.
tmptOffsetSilence-compressed playback milliseconds before this header.

Known timeSource strings are unknown, factory, manual, app, sntp, and prevAsset.

#jmxStoneFooter

Closes the prior stone immediately before the next stone header.

{"unixtime":1745537690,"localOffset":-420}

#jmxEof

jmxEof is a renewable trailer, not an immutable final record. Its delta-time is zero and its fileOffset points to the beginning of its own event.

{
  "fileOffset": 111006,
  "totalMillis": 1987223,
  "totalNotes": 10545,
  "totalMsgs": 24214,
  "stoneOffsetPrev": 91797,
  "stoneIdxPrev": 3,
  "stoneUuidPrev": "ffb9fe9f-b8a3-9085-cfab-0f3f1420608d",
  "unixtime": 1745538005,
  "localOffset": -420
}
FieldMeaning
fileOffsetAbsolute byte offset where this jmxEof starts.
totalMillisSum of non-trailer SMF delta-times in milliseconds; skipped wall-clock silence is excluded.
totalNotesCount of recorded Note On messages with velocity greater than zero.
totalMsgsCount of non-trailer stored events, including JMX events and their automatic markers.
stoneOffsetPrevAbsolute byte offset of the latest stone header.
stoneIdxPrevOne-based index of the latest stone.
stoneUuidPrevUUID of the latest stone.
unixtimeWall-clock time when the trailer was generated.
localOffsetUTC offset in minutes at that time.

The current firmware parser requires the summary and stoneOffsetPrev to open a JMX asset efficiently. Readers should still validate that every offset falls inside the track data before seeking.

#Stones and random access

A stone is a contiguous byte range beginning at a jmxStoneHdr. Stones are created when the current segment grows beyond roughly 45 KiB. They support incremental sync, bounded-memory transfer, and fast access to recent recording data.

Stone numbering is one-based. The current stone is found by reading jmxEof.stoneOffsetPrev. Older stones are found by following jmxStoneHdr.prevStone.fileOffset until the field is absent or zero.

For a completed stone, its byte range is:

[this stone header fileOffset, next stone header fileOffset)

For the current stone, its stable data range is:

[current stone header fileOffset, jmxEof fileOffset)

The renewable jmxEof, its marker, and End of Track are not part of the stone. The HTTP stone-download endpoint returns this raw range with deflate content encoding. A downloaded stone is a track fragment, not a complete standalone SMF; it does not contain MThd, MTrk, or a track length.

#Renewable file trailer

Every committed JMX file ends in this exact logical sequence:

<jmxEof with delta 0>
<jmxBlm with delta 0>
00 FF 2F 00

The final four bytes are the standard zero-delta End of Track event. On append, the writer seeks to jmxEof.fileOffset, replaces the previous trailer with new events, creates a new summary and marker, writes End of Track again, and updates the track length.

This design keeps flushed snapshots structurally complete while allowing an active recording to remain append-only at the logical event layer. Concurrent readers should stop at jmxEof or End of Track and refetch the current footer after receiving new data; they must not hold a byte offset inside the renewable trailer across an append.

#Backward-length markers

Normal channel messages can be located backward because status bytes have bit 7 set while MIDI data bytes have it clear. Meta and SysEx payloads may contain any byte, so JMX writes a fixed-size jmxBlm immediately after each meta event except End of Track, and immediately after each SysEx event.

The jmxBlm meta event has a fixed size of 28 bytes from FF through its final byte, excluding its delta and payload-length VLQs. The complete event normally occupies 30 bytes because both VLQs are one byte:

00                   delta = 0
FF 7F                meta event, type 0x7F
1A                   payload length = 26
00                   JMX 0x7F prefix
6A 6D 78 42 6C 6D    ASCII "jmxBlm"
7B 22 62 22 3A ...   JSON beginning {"b":...}
00 ...               NUL terminator and zero padding
FD 00                reverse-search sentinel and low-byte terminator

Its JSON is:

{"b":1731}

b is the byte distance from the marker's FD byte backward to the first byte of the preceding event's delta-time. It includes the preceding event plus the portion of the marker before FD.

For forward parsing, jmxBlm is ordinary ignorable metadata. For reverse parsing, detect the trailing FD 00, parse b, and subtract it from the absolute byte offset of the FD byte to reach the previous event. Validate the target against the track start and confirm that forward parsing lands back at the expected boundary before trusting the offset.

#Recorded MIDI events

JMX stores these event families:

FamilyEncoding
Channel voiceStandard delta, status, and one or two data bytes.
SysExDelta, F0, VLQ byte count, payload beginning after F0 and ending in F7; followed by jmxBlm.
Standard metaStandard delta, FF, type, VLQ length, payload; followed by jmxBlm except End of Track.
JMX metaThe custom framing described above; followed by jmxBlm.

The recorder does not persist USB cable/misc events, System Common two- or three-byte messages, or single-byte System Real-Time messages. A SysEx message is coalesced before it is written and must end in F7.

Because input-source routing information is not part of normal SMF channel messages, the raw file preserves musical MIDI bytes but does not attach a source transport to every event.

#File naming and asset identity

Jamcorder stores recordings using this device-side pattern:

/JAMC/<year>/<jamcorderUuid>/Jmx-A<assetIdx>-<Mon>-<DD>-<YYYY>.mid

Example:

/JAMC/2025/60b2d69c-333d-9b33-6c3b-9b38886c102d/Jmx-A00074-Apr-24-2025.mid

assetIdx is printed with at least five digits. The date is the asset's local calendar date. A new asset is normally opened when recording resumes on a new day after at least three minutes of silence. The same day's file may be reopened after a device restart.

Use assetUuid from jmxStoneHdr as the stable identity stored inside the file. Use assetIdx and the path for device-local ordering and lookup. Do not assume a filename is globally unique without its Jamcorder UUID.

#Reader requirements

A robust JMX 1.x reader should:

  1. Verify MThd, a six-byte header payload, format 0, one track, and division 458.
  2. Read the MTrk length as big-endian and confirm that the declared track end matches the file size.
  3. Parse every delta and payload length as a MIDI VLQ. Do not use running status assumptions for files produced by Jamcorder.
  4. Recognize jmxAsset as FF 01 with an ASCII jmxAsset prefix.
  5. Recognize other JMX events as FF 7F whose payload begins with 00 6A 6D 78.
  6. Find the first {, stop JSON text at the first NUL, and parse JSON without depending on indentation or property order.
  7. Require a final zero-delta FF 2F 00. Read the preceding jmxEof through its marker and validate its offsets.
  8. Treat unknown JMX tokens, JSON fields, time sources, and firmware metadata as forward-compatible extensions.
  9. Keep all parsed byte offsets in at least 64-bit host integers even though JMX 1.x stores offsets and counters as 32-bit-oriented JSON numbers.
  10. Impose limits on meta lengths, JSON depth, strings, and offsets before allocating or seeking. Files and stone fragments are untrusted input.

#Minimal JMX event recognition

def classify_jmx_meta(meta_type: int, payload: bytes):
    if meta_type == 0x01 and payload.startswith(b"jmxAsset"):
        text = payload[len(b"jmxAsset"):].split(b"\0", 1)[0]
        return "jmxAsset", text

    if meta_type == 0x7F and payload.startswith(b"\x00jmx"):
        body = payload[1:].split(b"\0", 1)[0]
        brace = body.find(b"{")
        if brace >= 0:
            return body[:brace].decode("ascii"), body[brace:]

    return None

The example only recognizes framing. A production reader must also validate VLQs, track bounds, UTF-8, JSON limits, event-specific fields, and offsets.

#Compatibility and current v1 quirks