#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:
- Standard MIDI compatibility. This is the primary design goal. Every JMX recording is a valid format-0 Standard MIDI File with a
.midextension, so ordinary music software can open and play it. Software that does not understand JMX can ignore the custom metadata; JMX-aware apps use that metadata to provide the additional capabilities below. - Extensible. JMX features are self-identifying meta events with UTF-8 JSON payloads. Readers can preserve unknown events and fields, allowing metadata to evolve without replacing the underlying MIDI container.
- Efficient through long silence. A Jamcorder may remain ready to record all day. Long quiet periods are omitted so they do not produce huge, mostly empty MIDI timelines.
jmxSkiprecords how much real time was removed. - Bookmarkable.
jmxBookmarkgives a musically interesting moment a stable index, UUID, source, and wall-clock time. Bookmarks mark passage ends so an app can present or derive the surrounding performance. - Chunkable for fast recent-music access. A recording is divided into linked stones of roughly 45 KiB. The app can locate and download the newest stone—or walk backward through recent stones—without transferring the entire daily recording.
- Connected to real-world time. Wall-clock anchors include Unix time, local UTC offset, and the source that established the device clock. This lets users find music by calendar time and lets readers reconstruct sessions across restarts, timezone changes, corrected clocks, and skipped silence.
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
- Design goals
- Format overview
- Real recording used in examples
- Standard MIDI envelope
- Time model
- JMX meta-event framing
- Custom event catalog
- Stones and random access
- Renewable file trailer
- Backward-length markers
- Recorded MIDI events
- File naming and asset identity
- Reader requirements
- Compatibility and current v1 quirks
#Format overview
The file has four conceptual layers:
| Layer | Purpose |
|---|---|
| Standard MIDI header | Identifies an SMF format-0 file with one track. |
| Standard MIDI events | Stores channel voice messages, SysEx, tempo, and end-of-track. |
| JMX meta events | Stores JSON metadata, bookmarks, skipped time, stone boundaries, and summaries. |
| Backward-length markers | Make 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 TrackAs 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 dataThe 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:
| Property | Value |
|---|---|
| File size | 111,298 bytes |
| SHA-256 | 999e819a18de19159a72f0b8e24fc31433b44453008cf5d20e3665ecca6cf2a7 |
| SMF format | 0 |
| Tracks | 1 |
| Division | 458 ticks per quarter note |
| Track length | 111,276 bytes |
| Stones | 3 |
jmxSkip events | 50 |
jmxBlm events | 59 |
| Compressed duration | 1,987,223 ms |
| Skipped-time total | 343,795 ms |
| Recorded Note On count | 10,545 |
| Stored non-trailer event count | 24,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:
| Offset | Size | Value | Meaning |
|---|---|---|---|
| 0 | 4 | ASCII MThd | Header chunk identifier. |
| 4 | 4 | 00 00 00 06 | Header payload length: 6 bytes. |
| 8 | 2 | 00 00 | SMF format 0. |
| 10 | 2 | 00 01 | One track. |
| 12 | 2 | 01 CA | 458 ticks per quarter note. |
| 14 | 4 | ASCII MTrk | Track chunk identifier. |
| 18 | 4 | Variable | Track-data length, excluding MTrk and this length field. |
| 22 | ... | Variable | First 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 ACThe 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.
| Value | Encoding |
|---|---|
| 0 | 00 |
| 127 | 7F |
| 128 | 81 00 |
| 16,383 | FF 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 tickJMX-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:
- Playback time is the sum of SMF delta-times. This is the silence-compressed musical timeline and corresponds to
totalMillisandtmptOffset. - Elapsed time is playback time plus the
millisvalues of precedingjmxSkipevents. Wall-clock anchors in stone, skip, reopen, bookmark, footer, and EOF events can be used to correct clock or timezone changes.
#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>
00The 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
| Token | Meta type | Purpose |
|---|---|---|
jmxAsset | 0x01 | Minimal asset summary for humans and generic MIDI tools. |
jmxTrackReopened | 0x7F | Records that an existing daily asset was reopened. |
jmxSkip | 0x7F | Records wall-clock silence omitted from the MIDI timeline. |
jmxBookmark | 0x7F | Marks the end of a user-selected passage. |
jmxStoneHdr | 0x7F | Begins an indexed stone and captures recovery state. |
jmxStoneFooter | 0x7F | Closes the preceding stone. |
jmxEof | 0x7F | Renewable whole-file summary and latest-stone pointer. |
jmxBlm | 0x7F | Fixed-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"
}| Field | Type | Meaning |
|---|---|---|
time | string | Local creation time formatted as YYYY-MM-DD HH:MM:SS UTC±offset; UTC itself is written as UTC. |
assetIdx | number | Device-local monotonically increasing asset number. |
jamcorderName | string | Device display name at asset creation. |
performerName | string | Performer display name at asset creation. |
jmxVersion | string | JMX 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
}| Field | Required | Meaning |
|---|---|---|
millis | Yes | Milliseconds removed from the wall-clock timeline. |
unixtime | No | Wall-clock anchor when the event was written. |
localOffset | No | Local 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:
| Field | Meaning |
|---|---|
jmxVersion | JMX schema version. |
stoneIdx | One-based stone number within the asset. |
stoneUuid | UUID assigned to this stone. Some JMX 1.0 files contain a duplicate key; see the compatibility note below. |
time | Wall-clock anchor and the source that established device time. |
asset | Stable asset identity, asset counter, and original device-side path. |
identities | Device and performer IDs and display names at this boundary. |
position | File, message, and compressed-time offsets at the stone start. |
prevStone | Previous stone offsets and, in JMX 1.1, its UUID; omitted for the first stone. |
tempo | Timebase constants: ticks and microseconds per quarter note. |
sound | Whether recorded piano state is silent or noisy, plus duration in milliseconds. |
notes | Depressed-note arrays keyed as ch0, ch1, and so on; empty channels are omitted. |
lifestats | Device lifetime playing totals at this boundary. |
firmwareInfo | Running build identity and hash; fields may be absent if unavailable. |
fabInfo | Provisioned manufacturing information plus physical device ID; schema is provisioning-dependent. |
Position fields are unsigned byte/count values:
| Field | Meaning |
|---|---|
fileOffset | Absolute byte offset of the first delta-time byte of this jmxStoneHdr. |
msgOffset | Count of stored non-trailer events before this header, including prior automatic jmxBlm events. |
tmptOffset | Silence-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
}| Field | Meaning |
|---|---|
fileOffset | Absolute byte offset where this jmxEof starts. |
totalMillis | Sum of non-trailer SMF delta-times in milliseconds; skipped wall-clock silence is excluded. |
totalNotes | Count of recorded Note On messages with velocity greater than zero. |
totalMsgs | Count of non-trailer stored events, including JMX events and their automatic markers. |
stoneOffsetPrev | Absolute byte offset of the latest stone header. |
stoneIdxPrev | One-based index of the latest stone. |
stoneUuidPrev | UUID of the latest stone. |
unixtime | Wall-clock time when the trailer was generated. |
localOffset | UTC 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 00The 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 terminatorIts 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:
| Family | Encoding |
|---|---|
| Channel voice | Standard delta, status, and one or two data bytes. |
| SysEx | Delta, F0, VLQ byte count, payload beginning after F0 and ending in F7; followed by jmxBlm. |
| Standard meta | Standard delta, FF, type, VLQ length, payload; followed by jmxBlm except End of Track. |
| JMX meta | The 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>.midExample:
/JAMC/2025/60b2d69c-333d-9b33-6c3b-9b38886c102d/Jmx-A00074-Apr-24-2025.midassetIdx 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:
- Verify
MThd, a six-byte header payload, format 0, one track, and division 458. - Read the
MTrklength as big-endian and confirm that the declared track end matches the file size. - Parse every delta and payload length as a MIDI VLQ. Do not use running status assumptions for files produced by Jamcorder.
- Recognize
jmxAssetasFF 01with an ASCIIjmxAssetprefix. - Recognize other JMX events as
FF 7Fwhose payload begins with00 6A 6D 78. - Find the first
{, stop JSON text at the first NUL, and parse JSON without depending on indentation or property order. - Require a final zero-delta
FF 2F 00. Read the precedingjmxEofthrough its marker and validate its offsets. - Treat unknown JMX tokens, JSON fields, time sources, and firmware metadata as forward-compatible extensions.
- 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.
- 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 NoneThe 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
- In
jmxStoneHdr,stoneUuid: JMX 1.0 may write this key twice in stone headers after the first stone: once for the current stone and again for the previous stone. Readers should ignorestoneUuidin JMX 1.0 files because duplicate JSON keys are ambiguous. JMX 1.1 writes the previous stone UUID toprevStone.stoneUuidinstead. - In
jmxStoneHdr,prevStone.msgOffset: JMX 1.0 may store a note count here instead of a message count. Readers should useprevStone.fileOffsetto navigate to the previous stone. JMX 1.1 stores the correct message count. - In
jmxEof,totalNotes: JMX 1.0 counts every Note On message, including velocity-zero messages that represent Note Off. Readers that need an accurate count should calculate it from the MIDI events. JMX 1.1 counts only Note On messages with velocity greater than zero.