Sources: three projects saved on an MPC Sample, in testdata/device/ of bajdev/mpckit (first committed as demo-projects/), analysed 2026-11-10.
DemoBlank.xpj: new empty project.DemoFile.xpj: 6 factory samples on pads A1-A4 and B1-B2, two sequences with recorded notes.Demo02.xpj: one non-factory sample on pad 1, and a copy on pad 5 with tuning, start and end changed.Everything below is observed in those files unless marked as an open question. Three projects written with these recipes have been loaded and played on the device (see section 7).
.xpj reader/writer is its own Go package with no UI code.Intended use: set up new kits on the desktop, then move to the device to create sequences and tracks.
In:
.xpj plus its _[ProjectData] folder.UI (mockup agreed 2026-11-10, "mpckit main screen mockup" artifact):
2.0 or later:
| Item | Observed |
|---|---|
| File | gzip stream; about 47-52 KB on disk, about 3 MB uncompressed |
| Header | five text lines: ACVS, 1.3.0.12, SerialisableProjectData, json, Linux |
| Body | one JSON object, {"data": {...}}, 66 top-level keys under data |
| Layout | one key or value per line, no indentation, ": " after keys, ASCII only |
| Product | originalCreatorProductIdentifier and lastSavedProductIdentifier are both AC50; data.version is 28 |
| Audio | sibling folder <ProjectName>_[ProjectData]/ holding the sample files; file names keep their spaces |
| Project name | not stored in the file; the name is the file name plus the folder name |
| Empty project | the device creates the data folder even when it holds no samples |
tracks[0].colour (a different colour per project)engineMode ("Sample" or "Sequence": the mode the device was in when saved)assignableEnvelopeFollowerAssignments.currentControlIndex (varies with no pattern; treat as noise)Drum 001 (all 128 pads), Submix 1-Submix 8, Out 1/2, Out 3/4.Out 1/2 (LP Filter, Pad-Controlled FXs, Flex Beat Player, Color Compressor) exist even in the blank project. Their settings are opaque text blobs.Paths are relative to data. pad[n] means tracks[0].program.drum.instruments[n], where n = bank x 16 + pad - 1 (pad 5 of bank A is n = 4, confirmed). layer means pad[n].layersv[0].
Check: applying these recipes to DemoBlank reproduces Demo02 (ignoring recorded notes) with 4 differences in 141,561 values: the track colour, the noise field above, one float rounding (fixed by rule below), and one LoopCrossfadeLength.
Rename the .xpj and the _[ProjectData] folder together. Nothing inside the file changes.
path.samples[] and tracks[0].samples[]:
{version: 1, name, path, loadImpl: 0, metadata: {tempo: 0.0, rootNote: 60, tune: 0.0, key}}layer.sampleName and layer.sampleFile.layer.sliceInfo.End to the frame count minus 1 (true for all 8 samples seen).pad[n].tempo to 20.0 (the value the device writes when the sample has no tempo).layer.sliceInfo.LoopCrossfadeLength to -1 (what the device wrote for an untouched non-factory sample).The device gave the second pad its own copy of the file (Linn 9000 kick001.wav, byte-identical to the first) and its own pool entry. Sharing also works: a test project with two pads pointing at one file and one pool entry loaded and played (section 7), so mpckit can store each file once.
A pad's settings hold nothing that depends on its slot: all 128 blank pads are identical. So:
pad[n] object into the target slot. Both pads can point at the same file and pool entry (sharing is device-tested).pad[n] objects.Recorded notes follow the slot, not the sample (a note stores 36 + n). Moving a pad that has notes in a sequence changes what those notes play. Decided for 1.0: warn before moving a pad that has recorded notes, and leave the notes untouched. Rewriting notes to follow the pad belongs with sequence editing, a later feature.
Three fields on the layer, written together:
layer.coarseTune (semitones, integer; demo: 12)layer.fineTune (cents, integer; demo: 40)layer.pitch = coarse + fine / 100, stored as a 32-bit float (demo: the device wrote 12.399999618530274; mpckit writes the shortest form of the same float64, 12.399999618530273)layer.sliceInfo.Start and layer.sliceInfo.LoopStart = start frame (demo: 328)layer.sliceInfo.End = end frame (demo: 3777)layer.sampleStart and sampleEnd stay 0.One field each: seq.name, seq.bpm (seq = sequences[].value).
Exactly 26 fields, confirmed by comparing a 2-bar and a 4-bar sequence:
seq.lengthBars, seq.loopEndBar = barsseq.lengthPulses, seq.loopEndPulses = bars x 3840 (4/4, 960 pulses per beat)endPulses and loopEndPulses on each of the 11 clips in seq.trackClipMaps[0]Copy the blank sequence, set sequences[].key to the slot (0-based), then name, BPM and length as above. Only sequences that exist are stored, and the device does not write them in slot order.
map[string]any with json.Marshal.| Property | Path | Seen | Proposed |
|---|---|---|---|
| Project name | file name and _[ProjectData] folder name |
DemoFile | 1.0 |
| Master tempo | masterTempo, masterTempoEnabled |
128.0, false in all three | 2.0 (sequences carry their own BPM) |
| Project key | key |
"C Major" | 2.0 |
| Info tags | info.title, artist, originalArtist, album, year, genre, copyright, notes |
all empty | 2.0 |
| Quantise | quantiser.enabled, timeDivision, swingLevel |
true, 5, 0.0 | 2.0 |
| Master volume | mixer.volume |
0.7079 | 2.0 |
| Track colour | tracks[0].colour |
differs per project | preserve |
| Saved mode | engineMode |
"Sample", "Sequence" | preserve |
| Property | Path | Seen | Proposed |
|---|---|---|---|
| Name | samples[].name |
"Linn 9000 kick" | 1.0 |
| File | samples[].path |
"Linn 9000 kick.wav" | 1.0 |
| Load mode | samples[].loadImpl |
0 for one-shots and the user sample; 1 for three factory loops | 1.0, write 0 |
| Tempo | samples[].metadata.tempo |
0.0 for the user sample; 20-114 for factory | 1.0, write 0.0 |
| Root note, tune | samples[].metadata.rootNote, tune |
60, 0.0 | 1.0, defaults |
| Key | samples[].metadata.key |
"C Natural Minor" on the user sample | 1.0, default |
| Property | Path | Proposed |
|---|---|---|
| Sample | layer.sampleName, layer.sampleFile |
1.0 |
| Slice end | layer.sliceInfo.End |
1.0, computed |
| Pad tempo | pad[n].tempo |
1.0, computed |
| Tune | layer.coarseTune, fineTune, pitch |
1.0 |
| Start, end | layer.sliceInfo.Start, LoopStart, End |
2.0, with waveform view |
| Property | Path | Seen | Proposed |
|---|---|---|---|
| Mute group | pad[n].whichMuteGroup |
0 none; 1 on both hats in DemoFile | 1.0 |
| Level, pan | pad[n].mixable.volume, pan |
0.7079 (unity), 0.5 centre | 2.0 (not yet seen changed) |
| Trigger mode | pad[n].triggerMode |
0; 3 on the looped pad | 2.0 until mapped |
| Layer loop | layer.loopMode, sliceInfo.LoopMode |
0/1 | 2.0 until mapped |
| One-shot | pad[n].zonePlayTime |
1 on every pad | 2.0 |
| Mono / voices | pad[n].monophonic, polyphony |
true, 1 | 2.0 |
| Mute, solo | pad[n].mixable.mute, solo |
false | 2.0 |
| Property | Path |
|---|---|
| Amp envelope | pad[n].synthSection.ampEnvelope.Attack, Hold, Decay, Sustain, Release, curves |
| Filter | synthSection.filterData.value0/1: filterType, filterCutoff, filterResonance, filterEnvelopeAmount |
| Other envelopes | synthSection.filterEnvelope, pitchEnvelope, auxEnvelope |
| LFOs | synthSection.lfoData.value0/1 |
| Velocity response | synthSection.velocitySensitivity, velocityToPitch, velocityToPan, velocityToStart |
| Pad effects | pad[n].padEffects.padEffectsData.value0-7 (fxType, fxValue) |
| Sends, routing | pad[n].mixable.sends[4], audioRoute |
| Stretch, chop | pad[n].bpmLock, warpEnable, stretchPercentage, chopProperties.* |
| Loop points | layer.loopStart, loopEnd, loopCrossfadeLength |
| Velocity range, randomise | layer.velocityStart, velocityEnd, pitchRandom, VolumeRandom, PanRandom, OffsetRandom |
| Pad colours | tracks[0].program.programPads |
| Property | Path | Seen | Proposed |
|---|---|---|---|
| Slot | sequences[].key |
0, 1 | 1.0 |
| Name | seq.name |
"Intro", "Sequence 02" | 1.0 |
| BPM | seq.bpm, seq.tempoEnable |
105, 110, 128; true | 1.0 |
| Length | seq.lengthBars plus the 25 dependent fields |
2, 4 | 1.0 |
| Time signature | seq.timeSignatureTrack.timeSignatures[] |
4/4 | 2.0 |
| Transpose | seq.transposition |
0 | 2.0 |
| Notes | ...eventList.events[] type 3: time, note, velocity, length, probability, ratchet |
up to 32 notes | preserve |
| Automation | events type 1: automation.parameter, note, value |
parameter 131 only | preserve |
| Property | Path | Proposed |
|---|---|---|
| Songs | songs[0-31]: name, ignoreTempo, items |
2.0 |
| Submix and output tracks | tracks[1-10], mixer.submixes, mixer.outputs |
preserve |
| Insert effects | ...mixable.inserts.effects[].plugin (description, state blob) |
preserve |
| Q-Link, XY, crossfader assignments | qlink*, assignable* |
preserve |
| Scenes, snapshots | scene, parameterSnapshotterData |
preserve |
| UI state | tui*, mpcControlSurfaceBehaviour, padPerformSettings, arpeggiatorProperties |
preserve |
atem (JSON: default slice, bar count, key), smpl, and on factory files meta. The non-factory kick has atem and smpl too, so the device adds them itself when it saves.C Natural Minor) matches the atem chunk in its file.sliceInfo.End is in the file's own frames at its own sample rate. Details and remaining unknowns are in the build plan, section 7.mpckit-device-tests.zip holds three projects built from DemoBlank with the recipes above. Each has the kick on A1, the kick tuned +5 on B1, sequence 1 "Verse" (96 BPM, 8 bars) and sequence 2 "Chorus" (96 BPM, 4 bars).
| Project | Difference | Answers |
|---|---|---|
MpckitT1 |
device-saved WAVs, one file per pad | does the device load a file written by our writer? |
MpckitT2 |
plain WAVs with the Akai chunks removed | can users bring any ordinary WAV? |
MpckitT3 |
both pads point at one file and one pool entry | can pads share a sample? |
Results (2026-11-10, on the device): all three projects loaded, and pads A1 and B1 played in each. So the writer's output is accepted, plain WAVs without Akai chunks work, and two pads can share one file and one pool entry.
Tuning confirmed: B1 was written as +5 semitones and the device shows +5.
Sequences confirmed: the device shows "Verse" (96 BPM, 8 bars) and "Chorus" (96 BPM, 4 bars).
Project name confirmed: the device shows MpckitT1, taken from the file and folder name.
Every item on the original 1.0 list (project name, pad sample assignment, sequence name, BPM, bar length, adding a sequence) plus pad tuning has now been written by these recipes and accepted by the device.
pad[n].mixable.triggerMode values (0 and 3 seen) and how they pair with loopMode.currentSequence a slot number or an index into sequences[]? Every file seen has 0, and slot 0 is stored second, so the files cannot tell. mpckit treats it as a slot (build plan 4.3); checkpoint B4 should remove the current sequence and check which one the device opens.