diff --git a/docs/midi-vectors/out-abs-same.json b/docs/midi-vectors/out-abs-same.json new file mode 100644 index 00000000..8b49e6bd --- /dev/null +++ b/docs/midi-vectors/out-abs-same.json @@ -0,0 +1,12 @@ +{ + "contract": "docs/midi-spec.md#device--host", + "says": "in `abs` only a position that differs from the last one sent goes out", + "features": ["midi"], + "steps": [ + { "send": { "hand": { "knob": 2, "turn": 1 } }, "expect": { "out": [{ "midi": [176, 25, 65], "within": 20 }] } }, + { "send": { "hand": { "knob": 2, "turn": 1 } }, "expect": { "out": [{ "midi": "", "within": 20 }] } }, + { "send": { "hand": { "knob": 2, "turn": -1 } }, "expect": { "out": [{ "midi": "", "within": 20 }] } }, + { "send": { "wait": 100 }, "expect": { "out": [{ "midi": "" }] } }, + { "send": { "hand": { "knob": 2, "turn": 1 } }, "expect": { "out": [{ "midi": [176, 25, 66], "within": 20 }] } } + ] +} diff --git a/docs/midi-vectors/out-buttons.json b/docs/midi-vectors/out-buttons.json new file mode 100644 index 00000000..937eca17 --- /dev/null +++ b/docs/midi-vectors/out-buttons.json @@ -0,0 +1,13 @@ +{ + "contract": "docs/midi-spec.md#device--host", + "says": "Encoder button 1–4 pressed", + "features": ["midi"], + "steps": [ + { "send": { "hand": { "knob": 1, "down": true } }, "expect": { "out": [{ "midi": [144, 60, 127], "within": 20 }] } }, + { "send": { "wait": 500 }, "expect": { "out": [{ "midi": "" }] } }, + { "send": { "hand": { "knob": 1, "down": false } }, "expect": { "out": [{ "midi": [128, 60, ""], "within": 20 }] } }, + { "send": { "hand": { "knob": 4, "down": true } }, "expect": { "out": [{ "midi": [144, 63, 127], "within": 20 }] } }, + { "send": { "wait": 100 } }, + { "send": { "hand": { "knob": 4, "down": false } }, "expect": { "out": [{ "midi": [128, 63, ""], "within": 20 }, { "midi": "" }] } } + ] +} diff --git a/docs/midi-vectors/out-cc-abs.json b/docs/midi-vectors/out-cc-abs.json new file mode 100644 index 00000000..6e43938b --- /dev/null +++ b/docs/midi-vectors/out-cc-abs.json @@ -0,0 +1,15 @@ +{ + "contract": "docs/midi-spec.md#device--host", + "says": "the value is a virtual position `0..127` the panel keeps per knob — starts at 64, moves by that knob's `outMul` steps per `outDiv` detents, clamps at the ends", + "features": ["midi"], + "steps": [ + { "send": { "hand": { "knob": 1, "turn": 3 } }, "expect": { "out": [{ "midi": [176, 24, 67], "within": 20 }] } }, + { "send": { "hand": { "knob": 2, "turn": -2 } }, "expect": { "out": [{ "midi": [176, 25, 62], "within": 20 }] } }, + { "send": { "wait": 100 } }, + { "send": { "hand": { "knob": 1, "turn": 62 } }, "expect": { "out": [{ "midi": [176, 24, 127], "within": 20 }] } }, + { "send": { "hand": { "knob": 2, "turn": -63 } }, "expect": { "out": [{ "midi": [176, 25, 0], "within": 20 }] } }, + { "send": { "wait": 100 } }, + { "send": { "hand": { "knob": 1, "turn": -1 } }, "expect": { "out": [{ "midi": [176, 24, 126], "within": 20 }] } }, + { "send": { "hand": { "knob": 2, "turn": 1 } }, "expect": { "out": [{ "midi": [176, 25, 1], "within": 20 }] } } + ] +} diff --git a/docs/midi-vectors/out-cc-div.json b/docs/midi-vectors/out-cc-div.json new file mode 100644 index 00000000..f315fa36 --- /dev/null +++ b/docs/midi-vectors/out-cc-div.json @@ -0,0 +1,13 @@ +{ + "contract": "docs/midi-spec.md#settings", + "says": "The encoders have 24 detents a turn, so `1` is 24 steps per turn and `4` is 6.", + "features": ["midi"], + "steps": [ + { "send": { "http": "POST /api/midi?outDiv=4&knob=1" } }, + { "send": { "hand": { "knob": 1, "turn": 24 } }, "expect": { "out": [{ "midi": [176, 24, 70], "within": 20 }] } }, + { "send": { "hand": { "knob": 2, "turn": 24 } }, "expect": { "out": [{ "midi": [176, 25, 88], "within": 20 }] } }, + { "send": { "wait": 100 } }, + { "send": { "hand": { "knob": 1, "turn": 3 } }, "expect": { "out": [{ "midi": "", "within": 100 }] } }, + { "send": { "hand": { "knob": 1, "turn": 1 } }, "expect": { "out": [{ "midi": [176, 24, 71], "within": 20 }] } } + ] +} diff --git a/docs/midi-vectors/out-cc-mul.json b/docs/midi-vectors/out-cc-mul.json new file mode 100644 index 00000000..b38e2538 --- /dev/null +++ b/docs/midi-vectors/out-cc-mul.json @@ -0,0 +1,12 @@ +{ + "contract": "docs/midi-spec.md#settings", + "says": "outbound sensitivity the other way: steps per detent, `1..8`.", + "features": ["midi"], + "steps": [ + { "send": { "http": "POST /api/midi?outMul=8&knob=4" } }, + { "send": { "hand": { "knob": 4, "turn": 2 } }, "expect": { "out": [{ "midi": [176, 27, 80], "within": 20 }] } }, + { "send": { "hand": { "knob": 3, "turn": 2 } }, "expect": { "out": [{ "midi": [176, 26, 66], "within": 20 }] } }, + { "send": { "wait": 100 } }, + { "send": { "hand": { "knob": 4, "turn": -1 } }, "expect": { "out": [{ "midi": [176, 27, 72], "within": 20 }] } } + ] +} diff --git a/docs/midi-vectors/out-cc-rel.json b/docs/midi-vectors/out-cc-rel.json new file mode 100644 index 00000000..4112487d --- /dev/null +++ b/docs/midi-vectors/out-cc-rel.json @@ -0,0 +1,12 @@ +{ + "contract": "docs/midi-spec.md#device--host", + "says": "`64 ± steps` since the previous message (binary offset)", + "features": ["midi"], + "steps": [ + { "send": { "http": "POST /api/midi?outMode=rel" } }, + { "send": { "hand": { "knob": 3, "turn": -2 } }, "expect": { "out": [{ "midi": [176, 26, 62], "within": 20 }] } }, + { "send": { "wait": 100 } }, + { "send": { "hand": { "knob": 3, "turn": 5 } }, "expect": { "out": [{ "midi": [176, 26, 69], "within": 20 }] } }, + { "send": { "hand": { "knob": 1, "turn": 1 } }, "expect": { "out": [{ "midi": [176, 24, 65], "within": 20 }] } } + ] +} diff --git a/docs/midi-vectors/out-hand-only.json b/docs/midi-vectors/out-hand-only.json new file mode 100644 index 00000000..b4aeed98 --- /dev/null +++ b/docs/midi-vectors/out-hand-only.json @@ -0,0 +1,10 @@ +{ + "contract": "docs/midi-spec.md#device--host", + "says": "Encoder 1–4 turned by a hand.", + "features": ["midi"], + "steps": [ + { "send": { "http": "POST /api/params?p1=900" } }, + { "send": { "wait": 100 }, "expect": { "out": [{ "midi": "" }], "status": { "params": [900, "", "", ""] } } }, + { "send": { "hand": { "knob": 1, "turn": 1 } }, "expect": { "out": [{ "midi": [176, 24, 65], "within": 20 }] } } + ] +} diff --git a/docs/midi-vectors/out-no-echo.json b/docs/midi-vectors/out-no-echo.json new file mode 100644 index 00000000..dfa213a2 --- /dev/null +++ b/docs/midi-vectors/out-no-echo.json @@ -0,0 +1,13 @@ +{ + "contract": "docs/midi-spec.md#device--host", + "says": "a CC or note the host itself sent is never reflected back", + "features": ["midi"], + "steps": [ + { "send": { "midi": [176, 20, 100] } }, + { "send": { "midi": [176, 25, 65] } }, + { "send": { "midi": [144, 62, 127] } }, + { "send": { "wait": 100 }, "expect": { "out": [{ "midi": "" }], "status": { "paramActive": [true, false, false, false] }, "input": { "given": { "deltas": [0, 1, 0, 0], "presses": [0, 0, 1, 0] } } } }, + { "send": { "midi": [128, 62, 0] } }, + { "send": { "wait": 100 }, "expect": { "out": [{ "midi": "" }] } } + ] +} diff --git a/docs/midi-vectors/out-pacing.json b/docs/midi-vectors/out-pacing.json new file mode 100644 index 00000000..3566af9c --- /dev/null +++ b/docs/midi-vectors/out-pacing.json @@ -0,0 +1,11 @@ +{ + "contract": "docs/midi-spec.md#device--host", + "says": "at most one message per knob every 50 ms (≤ 20/s per knob, `PF_MIDI_OUT_MIN_INTERVAL_MS`), trailing-edge so the final value always arrives", + "features": ["midi"], + "steps": [ + { "send": { "hand": { "knob": 1, "turn": 1 } }, "expect": { "out": [{ "midi": [176, 24, 65], "within": 20 }] } }, + { "send": { "hand": { "knob": 1, "turn": 1 } }, "expect": { "out": [{ "midi": "", "within": 20 }] } }, + { "send": { "hand": { "knob": 1, "turn": 1 } }, "expect": { "out": [{ "midi": "", "within": 10 }] } }, + { "send": { "wait": 50 }, "expect": { "out": [{ "midi": [176, 24, 67] }, { "midi": "" }] } } + ] +} diff --git a/docs/midi-vectors/out-program-change.json b/docs/midi-vectors/out-program-change.json new file mode 100644 index 00000000..37a1cc42 --- /dev/null +++ b/docs/midi-vectors/out-program-change.json @@ -0,0 +1,10 @@ +{ + "contract": "docs/midi-spec.md#device--host", + "says": "The pattern changed (by anyone: knob, console, OSC, MIDI, a show), for `n ≤ 127`", + "features": ["midi"], + "steps": [ + { "send": { "http": "GET /api/patterns/select?index=2" }, "expect": { "out": [{ "midi": [192, 2], "within": 20 }] } }, + { "send": { "midi": [192, 5] }, "expect": { "out": [{ "midi": [192, 5], "within": 50 }] } }, + { "send": { "wait": 100 }, "expect": { "out": [{ "midi": "" }] } } + ] +} diff --git a/docs/midi-vectors/out-rel-sum.json b/docs/midi-vectors/out-rel-sum.json new file mode 100644 index 00000000..dc90b33e --- /dev/null +++ b/docs/midi-vectors/out-rel-sum.json @@ -0,0 +1,17 @@ +{ + "contract": "docs/midi-spec.md#device--host", + "says": "in `rel` the steps sum between messages and a sum leaving ±63 is sent at once", + "features": ["midi"], + "steps": [ + { "send": { "http": "POST /api/midi?outMode=rel" } }, + { "send": { "hand": { "knob": 4, "turn": 1 } }, "expect": { "out": [{ "midi": [176, 27, 65], "within": 20 }] } }, + { "send": { "hand": { "knob": 4, "turn": 2 } }, "expect": { "out": [{ "midi": "", "within": 20 }] } }, + { "send": { "hand": { "knob": 4, "turn": 3 } }, "expect": { "out": [{ "midi": "", "within": 10 }] } }, + { "send": { "wait": 50 }, "expect": { "out": [{ "midi": [176, 27, 69] }, { "midi": "" }] } }, + { "send": { "wait": 100 } }, + { "send": { "hand": { "knob": 4, "turn": 1 } }, "expect": { "out": [{ "midi": [176, 27, 65], "within": 20 }] } }, + { "send": { "hand": { "knob": 4, "turn": 40 } }, "expect": { "out": [{ "midi": "", "within": 20 }] } }, + { "send": { "hand": { "knob": 4, "turn": 40 } }, "expect": { "out": [{ "midi": [176, 27, 127], "within": 20 }] } }, + { "send": { "wait": 60 }, "expect": { "out": [{ "midi": [176, 27, 81] }, { "midi": "" }] } } + ] +} diff --git a/docs/rest-api-vectors/midi-caps.json b/docs/rest-api-vectors/midi-caps.json new file mode 100644 index 00000000..3809b9f2 --- /dev/null +++ b/docs/rest-api-vectors/midi-caps.json @@ -0,0 +1,9 @@ +{ + "contract": "docs/rest-api.md#conventions-for-future-additions", + "says": "A feature's endpoints exist only when that feature is composed in — the route itself is absent otherwise, so clients probe `caps` rather than expecting a soft 404.", + "features": ["midi"], + "steps": [ + { "send": { "http": "GET /api/status" }, "expect": { "reply": { "body": { "caps": ["patterns", "params", "sleep", "midi"] } } } }, + { "send": { "http": "GET /api/midi" }, "expect": { "reply": { "status": 200, "headers": { "content-type": "application/json" } } } } + ] +} diff --git a/docs/rest-api-vectors/midi-get.json b/docs/rest-api-vectors/midi-get.json new file mode 100644 index 00000000..85f5214f --- /dev/null +++ b/docs/rest-api-vectors/midi-get.json @@ -0,0 +1,19 @@ +{ + "contract": "docs/midi-spec.md#settings", + "says": "channel, `outDiv`, `outMul`, `outMode`, `host`, session state, counters", + "features": ["midi"], + "steps": [ + { + "send": { "http": "GET /api/midi" }, + "expect": { + "reply": { + "status": 200, + "body": { + "channel": 1, "outDiv": [1, 1, 1, 1], "outMul": ["", "", "", ""], "outMode": "abs", "host": "", + "rtpPeers": "", "rtpPeer": "", "rx": "", "tx": "" + } + } + } + } + ] +} diff --git a/docs/rest-api-vectors/midi-host.json b/docs/rest-api-vectors/midi-host.json new file mode 100644 index 00000000..a2b1de4c --- /dev/null +++ b/docs/rest-api-vectors/midi-host.json @@ -0,0 +1,11 @@ +{ + "contract": "docs/midi-spec.md#settings", + "says": "Empty string clears it.", + "features": ["midi"], + "steps": [ + { "send": { "http": "POST /api/midi?host=192.168.0.23" }, "expect": { "reply": { "status": 200 } } }, + { "send": { "http": "GET /api/midi" }, "expect": { "reply": { "body": { "host": "192.168.0.23" } }, "status": { "midi": { "host": "192.168.0.23" } } } }, + { "send": { "http": { "request": "POST /api/midi", "form": "host=" } }, "expect": { "reply": { "status": 200 } } }, + { "send": { "http": "GET /api/midi" }, "expect": { "reply": { "body": { "host": "" } } } } + ] +} diff --git a/docs/rest-api-vectors/midi-knob.json b/docs/rest-api-vectors/midi-knob.json new file mode 100644 index 00000000..11545bf9 --- /dev/null +++ b/docs/rest-api-vectors/midi-knob.json @@ -0,0 +1,12 @@ +{ + "contract": "docs/midi-spec.md#settings", + "says": "Apply `outMul`/`outDiv` to one knob; without it, to all four.", + "features": ["midi"], + "steps": [ + { "send": { "http": "POST /api/midi?outDiv=4&knob=3" }, "expect": { "reply": { "status": 200 } } }, + { "send": { "http": "POST /api/midi?outMul=2&knob=2" }, "expect": { "reply": { "status": 200 } } }, + { "send": { "http": "GET /api/midi" }, "expect": { "reply": { "body": { "outDiv": [1, 1, 4, 1], "outMul": [1, 2, 1, 1] } } } }, + { "send": { "http": "POST /api/midi?outDiv=2" } }, + { "send": { "http": "GET /api/midi" }, "expect": { "reply": { "body": { "outDiv": [2, 2, 2, 2], "outMul": [1, 1, 1, 1] } } } } + ] +} diff --git a/docs/rest-api-vectors/midi-out-div.json b/docs/rest-api-vectors/midi-out-div.json new file mode 100644 index 00000000..8963ab1f --- /dev/null +++ b/docs/rest-api-vectors/midi-out-div.json @@ -0,0 +1,11 @@ +{ + "contract": "docs/midi-spec.md#settings", + "says": "outbound sensitivity: detents per outbound step, `1..16`, persisted on the panel.", + "features": ["midi"], + "steps": [ + { "send": { "http": "POST /api/midi?outDiv=16" }, "expect": { "reply": { "status": 200 } } }, + { "send": { "http": "GET /api/midi" }, "expect": { "reply": { "body": { "outDiv": [16, 16, 16, 16] } }, "status": { "midi": { "outDiv": [16, 16, 16, 16] } } } }, + { "send": { "http": "POST /api/midi?outDiv=1" }, "expect": { "reply": { "status": 200 } } }, + { "send": { "http": "GET /api/midi" }, "expect": { "reply": { "body": { "outDiv": [1, 1, 1, 1] } } } } + ] +} diff --git a/docs/rest-api-vectors/midi-out-mode.json b/docs/rest-api-vectors/midi-out-mode.json new file mode 100644 index 00000000..467a3081 --- /dev/null +++ b/docs/rest-api-vectors/midi-out-mode.json @@ -0,0 +1,12 @@ +{ + "contract": "docs/midi-spec.md#settings", + "says": "knobs out as a virtual position (default) or as relative steps.", + "features": ["midi"], + "steps": [ + { "send": { "http": "GET /api/midi" }, "expect": { "reply": { "body": { "outMode": "abs" } } } }, + { "send": { "http": "POST /api/midi?outMode=rel" }, "expect": { "reply": { "status": 200 } } }, + { "send": { "http": "GET /api/midi" }, "expect": { "reply": { "body": { "outMode": "rel" } }, "status": { "midi": { "outMode": "rel" } } } }, + { "send": { "http": { "request": "POST /api/midi", "form": "outMode=abs" } }, "expect": { "reply": { "status": 200 } } }, + { "send": { "http": "GET /api/midi" }, "expect": { "reply": { "body": { "outMode": "abs" } }, "status": { "midi": { "outMode": "abs" } } } } + ] +} diff --git a/docs/rest-api-vectors/midi-out-mul.json b/docs/rest-api-vectors/midi-out-mul.json new file mode 100644 index 00000000..b2e21522 --- /dev/null +++ b/docs/rest-api-vectors/midi-out-mul.json @@ -0,0 +1,13 @@ +{ + "contract": "docs/midi-spec.md#settings", + "says": "Setting one of `outMul`/`outDiv` above 1 resets the other to 1", + "features": ["midi"], + "steps": [ + { "send": { "http": "POST /api/midi?outDiv=4" } }, + { "send": { "http": "GET /api/midi" }, "expect": { "reply": { "body": { "outDiv": [4, 4, 4, 4] } } } }, + { "send": { "http": "POST /api/midi?outMul=8" }, "expect": { "reply": { "status": 200 } } }, + { "send": { "http": "GET /api/midi" }, "expect": { "reply": { "body": { "outDiv": [1, 1, 1, 1], "outMul": [8, 8, 8, 8] } } } }, + { "send": { "http": "POST /api/midi?outDiv=2" } }, + { "send": { "http": "GET /api/midi" }, "expect": { "reply": { "body": { "outDiv": [2, 2, 2, 2], "outMul": [1, 1, 1, 1] } } } } + ] +} diff --git a/docs/rest-api-vectors/midi-rx-detail.json b/docs/rest-api-vectors/midi-rx-detail.json new file mode 100644 index 00000000..835d68aa --- /dev/null +++ b/docs/rest-api-vectors/midi-rx-detail.json @@ -0,0 +1,37 @@ +{ + "contract": "docs/midi-spec.md#status", + "says": "Unreleased 1.2 adds `rxDetail` to both `midi` in `/api/status` and the root of `GET /api/midi`.", + "features": ["midi"], + "steps": [ + { + "send": { "http": "GET /api/status" }, + "expect": { + "reply": { + "body": { + "midi": { + "rxDetail": { + "worker": "", "abs": ["", "", "", ""], "rel": ["", "", "", ""], + "queueDropped": "", "txQueueDropped": "", "packetGaps": "", "parseErrors": "", "bufferOverflows": "", + "pollMaxGapUs": "", "pollMaxUs": "", "queueMaxAgeUs": "", "stackMin": "" + } + } + } + } + } + }, + { + "send": { "http": "GET /api/midi" }, + "expect": { + "reply": { + "body": { + "rxDetail": { + "worker": "", "abs": ["", "", "", ""], "rel": ["", "", "", ""], + "queueDropped": "", "txQueueDropped": "", "packetGaps": "", "parseErrors": "", "bufferOverflows": "", + "pollMaxGapUs": "", "pollMaxUs": "", "queueMaxAgeUs": "", "stackMin": "" + } + } + } + } + } + ] +} diff --git a/docs/rest-api-vectors/midi-status.json b/docs/rest-api-vectors/midi-status.json new file mode 100644 index 00000000..5037a54a --- /dev/null +++ b/docs/rest-api-vectors/midi-status.json @@ -0,0 +1,24 @@ +{ + "contract": "docs/midi-spec.md#status", + "says": "`GET /api/status` on a build with this feature lists `\"midi\"` in `caps` and carries", + "features": ["midi"], + "steps": [ + { + "send": { "http": "GET /api/status" }, + "expect": { + "reply": { + "status": 200, + "body": { + "caps": ["patterns", "params", "sleep", "midi"], + "midi": { + "runtime": "", "channel": "", "outMode": "", "host": "", + "outDiv": ["", "", "", ""], + "outMul": ["", "", "", ""], + "rtpPeers": "", "rtpPeer": "", "rx": "", "tx": "" + } + } + } + } + } + ] +}