Selaa lähdekoodia

Record checkpoint A results; use device limits

Device checkpoint A passed on 2026-11-10. Mute groups are 0-16 and BPM
is 20-999 on the device; tuning limits and extensible WAV headers are
confirmed. Record the results in the docs and the pack README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude 2 päivää sitten
vanhempi
sitoutus
1ab4fedb98

+ 6 - 4
docs/build-plan.md

@@ -102,7 +102,7 @@ Pool rules:
 - Entries that came with a loaded project keep their metadata untouched. New entries use `loadImpl` 0, tempo 0.0, root note 60, tune 0.0, key "C Natural Minor" (the values the device wrote for the non-factory sample).
 - Limit: 128 samples. Refuse the 129th with a clear message.
 
-Validation at the edge of the model: WAV must be uncompressed (integer PCM or 32-bit float), mono or stereo, at most 20 minutes; `sliceInfo.End` is the file's own frame count minus 1 at its own sample rate; project and sequence names non-empty and free of characters that are illegal in file names on Windows or FAT; bars at least 1; BPM positive.
+Validation at the edge of the model: WAV must be uncompressed (integer PCM or 32-bit float), mono or stereo, at most 20 minutes; `sliceInfo.End` is the file's own frame count minus 1 at its own sample rate; project and sequence names non-empty and free of characters that are illegal in file names on Windows or FAT; bars at least 1; BPM 20-999 (the device's range); mute group 0-16.
 
 ### 4.4 Export (`internal/export`)
 
@@ -206,6 +206,8 @@ Run by hand on the MPC Sample. Record results in the format doc.
 4. WAV formats: covered by the separate WAV test pack below, which can be run before any Go code exists.
 5. A sequence at 1 bar and one at a long length (for example 64 bars); BPM at the device's lowest and highest.
 
+Results (2026-11-10, on the device): all passed. Tuning limits confirmed; mute groups run from none to 16; BPM from 20 to 999; 1 and 64-bar sequences work; extensible-header WAVs play. Details in the format doc, section 7, and `tools/checkpoint-a/README.md`. M3 is done.
+
 **WAV test pack (`mpckit-wav-tests.zip`, built 2026-11-10 with the prototype writer):**
 
 Akai's published spec for the device: file import at 16 or 24-bit and 44.1, 48 or 96 kHz; internal processing at 44.1 kHz, 32-bit float. That describes importing through the device's own browser. mpckit places files straight into the project folder, so the test checks whether the device still converts them when it loads a project.
@@ -240,7 +242,7 @@ Conclusions:
 
 1.0 rule for assigning a file: accept any uncompressed WAV (integer PCM or 32-bit float), mono or stereo. Refuse compressed WAVs (ADPCM, MP3-in-WAV), more than two channels, and anything over 20 minutes.
 
-Not yet tested: WAVs with the "extensible" header that many audio tools write for 24-bit files; sample rates above 96 kHz or below 22.05 kHz; 64-bit float. Accept the extensible header when it declares PCM or float inside, and add one such file to checkpoint A.
+Not yet tested: sample rates above 96 kHz or below 22.05 kHz; 64-bit float (refused in 1.0). The extensible header that many audio tools write was confirmed in checkpoint A (PCM inside, 16 and 24-bit, mono and stereo); mpckit accepts it when it declares PCM or float inside.
 
 **B (after M8), round trips through the app:**
 1. Load `DemoFile` (recorded notes), change nothing, export: sequences still play the same on the device.
@@ -262,9 +264,9 @@ Not yet tested: WAVs with the "extensible" header that many audio tools write fo
 | Firmware update changes the format | The header version (`1.3.0.12`) and `data.version` (28) are checked on load; warn when they differ from the tested values. |
 | WAV variants the device rejects or plays wrongly | Largely retired by the WAV test pack (section 7): all eleven formats played correctly. Remaining unknowns are listed there. Akai's spec caps a sample at 20 minutes; refuse longer files. |
 | The app's preview cannot decode a WAV the device accepts | Each system's webview has its own audio decoder, and 8-bit, 32-bit integer or extensible files may fail in some. Try the webview's decoder first; if it fails, fall back to a Go route that decodes the PCM itself and returns 32-bit float samples for the frontend to play. `internal/wav` therefore needs a small decoder as well as the header reader. |
-| Mute group count unknown | Checkpoint A3; the UI list is sized from the result. |
+| Mute group count | Resolved (checkpoint A): none and 1-16. The UI list offers Off, 1-16. |
 | Shortening sequences with notes | Checkpoint B5; until then warn and leave notes untouched. |
-| Tuning limits taken from a community toolkit | Checkpoint A2. |
+| Tuning limits taken from a community toolkit | Resolved (checkpoint A): ±24 semitones and ±90 cents confirmed on the device. |
 | Very long or unusual file names | Keep names as they are; reject only characters illegal on Windows/FAT. Revisit if the device truncates. |
 | File drop quirks per system | Dialog-based assign is always available as the fallback. |
 

+ 12 - 3
docs/xpj-format-and-scope.md

@@ -165,7 +165,7 @@ Copy the blank sequence, set `sequences[].key` to the slot (0-based), then name,
 
 | Property | Path | Seen | Proposed |
 |---|---|---|---|
-| Mute group | `pad[n].whichMuteGroup` | 0 none; 1 on both hats in DemoFile | 1.0 |
+| Mute group | `pad[n].whichMuteGroup` | 0 none; 1 on both hats in DemoFile; device offers 0-16 | 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 |
@@ -238,15 +238,24 @@ Sequences confirmed: the device shows "Verse" (96 BPM, 8 bars) and "Chorus" (96
 
 Project name confirmed: the device shows `MpckitT1`, taken from the file and folder name.
 
+### Checkpoint A (2026-11-10)
+
+Four projects written by the Go writer (`tools/checkpoint-a`, built from commit `f36cda3`). All loaded and passed on the device:
+
+- **Kit** (`MpckitKit`): samples on A1-A4 and B1 play. Tuning at -24 and +24 semitones, -90 and +90 cents, and both combined (-24/-90, +24/+90) loads and plays as expected. Two hats in mute group 1 choke each other. Sequences of 1 and 64 bars work.
+- **Mute groups** (`MpckitMute`): the device offers none and 1-16. Pads written with group 16 choke each other; so do pads written with group 32, though editing such a pad's group on the device starts it at 16. mpckit writes 0-16 only.
+- **Tempo** (`MpckitTempo`): sequences at 20, 30, 300 and 999 BPM all work. The device's tempo control runs from 20 to 999, and mpckit uses the same range.
+- **WAV headers** (`MpckitWav`): 48 kHz 24-bit stereo, 44.1 kHz 16-bit stereo and mono, all with the extensible header, play the same as a plain-header control.
+
 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.
 
 ## 8. Open questions
 
 1. Device-test a moved and a copied pad.
-1a. Confirm the tuning limits on the device. The sliders use -24 to +24 semitones and -90 to +90 cents, taken from a community toolkit (MPC Sample Toolkit) that reports them as tested on hardware. We have only confirmed +5 and +12.40 ourselves.
+1a. ~~Confirm the tuning limits on the device.~~ Confirmed 2026-11-10 (checkpoint A): -24 to +24 semitones and -90 to +90 cents.
 2. Level and pan have not been seen changed; confirm they are written to `pad[n].mixable`.
 3. `triggerMode` values (0 and 3 seen) and how they pair with `loopMode`.
 4. When a sequence with notes is shortened, are notes past the new end kept or removed?
-5. WAVs with the "extensible" header and sample rates outside 22.05-96 kHz are untested. (Other WAV bit depths and sample rates are confirmed; non-WAV formats are out of 1.0.)
+5. Sample rates outside 22.05-96 kHz and 64-bit float are untested. (The extensible header was confirmed in checkpoint A; other WAV bit depths and sample rates are confirmed; non-WAV formats are out of 1.0.)
 6. What key and tempo to write for a sample mpckit imports (the device detects them; mpckit cannot yet).
 7. Is `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.

+ 9 - 3
internal/project/project.go

@@ -35,9 +35,15 @@ const (
 	MinFine, MaxFine     = -90, 90
 )
 
-// MaxMuteGroup is the highest mute group the UI offers. Not yet confirmed
-// on the device (build plan checkpoint A3).
-const MaxMuteGroup = 32
+// MaxMuteGroup is the highest mute group the device offers; 0 is off.
+// Device checkpoint A3, 2026-11-10.
+const MaxMuteGroup = 16
+
+// BPM range of the device's tempo control (device checkpoint A5, 2026-11-10).
+const (
+	MinBPM = 20.0
+	MaxBPM = 999.0
+)
 
 // DefaultBPM is used for a new sequence when no sequence has been edited.
 const DefaultBPM = 120.0

+ 5 - 1
internal/project/project_test.go

@@ -6,6 +6,7 @@ import (
 	"encoding/binary"
 	"fmt"
 	"io"
+	"math"
 	"os"
 	"path/filepath"
 	"strconv"
@@ -407,6 +408,7 @@ func TestTuneAndMuteGroup(t *testing.T) {
 	if padOf(t, p, 3).MuteGroup != 1 {
 		t.Error("mute group")
 	}
+	must(t, p.SetMuteGroup(3, 16))
 	if err := p.SetMuteGroup(3, MaxMuteGroup+1); err == nil {
 		t.Error("mute group out of range accepted")
 	}
@@ -462,11 +464,13 @@ func TestSequences(t *testing.T) {
 		name string
 		bpm  float64
 		bars int
-	}{{"", 120, 2}, {"a/b", 120, 2}, {"ok", 0, 2}, {"ok", -1, 2}, {"ok", 120, 0}, {"ok", 120, MaxBars + 1}} {
+	}{{"", 120, 2}, {"a/b", 120, 2}, {"ok", 0, 2}, {"ok", -1, 2}, {"ok", 19.9, 2}, {"ok", 999.5, 2}, {"ok", math.NaN(), 2}, {"ok", math.Inf(1), 2}, {"ok", 120, 0}, {"ok", 120, MaxBars + 1}} {
 		if err := p.SetSequence(4, bad.name, bad.bpm, bad.bars); err == nil {
 			t.Errorf("SetSequence(%q, %v, %d) accepted", bad.name, bad.bpm, bad.bars)
 		}
 	}
+	must(t, p.SetSequence(4, "Min", MinBPM, 1))
+	must(t, p.SetSequence(4, "Max", MaxBPM, MaxBars))
 	if err := p.SetSequence(9, "x", 120, 2); err == nil {
 		t.Error("missing sequence accepted")
 	}

+ 4 - 5
internal/project/sequences.go

@@ -2,7 +2,6 @@ package project
 
 import (
 	"fmt"
-	"math"
 	"sort"
 
 	"git.monoexp.com/bajdev/mpckit/internal/xpj"
@@ -14,8 +13,8 @@ const (
 	eventNote       = 3
 )
 
-// MaxBars is the longest sequence mpckit writes. Not confirmed on the device
-// (build plan checkpoint A5).
+// MaxBars is the longest sequence mpckit writes. 64 bars is confirmed on the
+// device (checkpoint A5); its own maximum is not yet known.
 const MaxBars = 999
 
 // Sequence is a read-only view of one sequence.
@@ -124,8 +123,8 @@ func (p *Project) SetSequence(slot int, name string, bpm float64, bars int) erro
 	if err := ValidName(name); err != nil {
 		return fmt.Errorf("sequence name: %w", err)
 	}
-	if !(bpm > 0) || math.IsInf(bpm, 0) {
-		return fmt.Errorf("BPM must be a positive number")
+	if !(bpm >= MinBPM && bpm <= MaxBPM) {
+		return fmt.Errorf("BPM must be %g-%g", MinBPM, MaxBPM)
 	}
 	if bars < 1 || bars > MaxBars {
 		return fmt.Errorf("length must be 1-%d bars", MaxBars)

+ 20 - 0
pull-requests/checkpoint-a-results/checkpoint-a-results-and-device-limits.md

@@ -0,0 +1,20 @@
+# Checkpoint A results and device limits
+
+Branch: `checkpoint-a-results` → `main`
+
+## What changed
+
+Device checkpoint A passed on 2026-11-10 (run by bajdev; pack built from `f36cda3`), which completes M3. This records the results and uses the limits it found.
+
+- **Mute groups**: `MaxMuteGroup` 32 → **16**. The device offers none and 1-16. Pads written with 32 still choked, but the device treats the field as 1-16, so mpckit writes only 0-16.
+- **BPM**: was "any positive number"; now **20-999** (`MinBPM`, `MaxBPM`), the device's tempo control range. All four test tempos (20, 30, 300, 999) worked.
+- **Bars**: comment updated (64 bars confirmed). `MaxBars` stays 999 because the device maximum is still unknown.
+- **Pack builder**: `MpckitMute` now uses groups 16 and 1, since 32 is refused. The README notes what the 2026-11-10 run used.
+- **Docs**:
+  - Format doc: new "Checkpoint A" results in section 7; mute group range added to section 5; open question 1a (tuning limits) closed; question 5 narrowed (extensible header confirmed).
+  - Build plan: results under checkpoint A ("M3 is done"); validation rule for BPM and mute group; the WAV "not yet tested" note and the mute-group and tuning risk rows marked resolved.
+  - `tools/checkpoint-a/README.md`: results table filled in.
+
+## Testing
+
+`go vet ./...` and `go test ./...` pass. The tests now check that group 16 and BPM 20/999 are accepted and that group 17, BPM 19.9 and 999.5, NaN and infinity are refused. `go run ./tools/checkpoint-a` still builds the pack.

+ 10 - 8
tools/checkpoint-a/README.md

@@ -47,11 +47,13 @@ Sequences: 1 "One Bar" (120 BPM, 1 bar), 2 "Long" (120 BPM, 64 bars).
 
 ### MpckitMute: high mute group numbers (A3)
 
-A1 and A2 (hats) in group 16; A3 and A4 in group 32.
+A1 and A2 (hats) in group 16; A3 and A4 in group 1.
 
 1. Does the project load?
 2. What group does each pad show? Do A1/A2 choke each other, and A3/A4?
 
+The 2026-11-10 run used groups 16 and 32, built from commit `f36cda3`. Since the device's limit turned out to be 16, mpckit now refuses 32 and this project uses 1 and 16.
+
 ### MpckitTempo: BPM extremes (A5)
 
 Kick on A1. Sequences: 1 "Slow" 30 BPM, 2 "Fast" 300 BPM, 3 "Very Slow" 20 BPM, 4 "Very Fast" 999 BPM (2 bars each).
@@ -72,14 +74,14 @@ Kick on A1. Sequences: 1 "Slow" 30 BPM, 2 "Fast" 300 BPM, 3 "Very Slow" 20 BPM,
 
 ## Results
 
-To fill in after the device run, then copy into the format doc (section 7) and the build plan (section 7, checkpoint A).
+Run on the MPC Sample on 2026-11-10 by bajdev (pack built from commit `f36cda3`). All four projects loaded and passed. Also recorded in the format doc (section 7) and the build plan (section 7).
 
 | Project | Loads | Result |
 |---|---|---|
-| MpckitKit | | |
-| MpckitMute | | |
-| MpckitTempo | | |
-| MpckitWav | | |
+| MpckitKit | yes | All pads, tuning extremes, the group-1 choke and the 1 and 64-bar sequences work as expected. |
+| MpckitMute | yes | A1/A2 (group 16) choke each other; A3/A4 (written as group 32) choke each other. Editing A3 or A4's group on the device starts it at 16. |
+| MpckitTempo | yes | All four sequences work, including 20 and 999 BPM. |
+| MpckitWav | yes | All four pads work and sound the same: extensible headers are accepted. |
 
-Device mute group range: 
-Device BPM range: 
+Device mute group range: none (0) to 16.
+Device BPM range: 20 to 999 (the tempo control's limits).

+ 3 - 2
tools/checkpoint-a/main.go

@@ -136,11 +136,12 @@ func build(src, work string) ([]*project.Project, error) {
 			seqs: []seq{{"One Bar", 120, 1}, {"Long", 120, 64}},
 		},
 		{
-			// A3: does the device accept mute groups 16 and 32 in a file?
+			// A3: the lowest and highest mute groups. The 2026-11-10 run
+			// used 16 and 32 (see README); mpckit now refuses 32.
 			name: "MpckitMute",
 			pads: []pad{
 				{label: "A1", file: hat1, mute: 16}, {label: "A2", file: hat2, mute: 16},
-				{label: "A3", file: hat1, mute: 32}, {label: "A4", file: hat2, mute: 32},
+				{label: "A3", file: hat1, mute: 1}, {label: "A4", file: hat2, mute: 1},
 			},
 		},
 		{