c64bridge 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +3 -3
  3. package/data/sound/sidwave.md +113 -36
  4. package/data/sound/sidwave.schema.json +138 -28
  5. package/dist/.tsbuildinfo +1 -1
  6. package/dist/c64Client.js +83 -93
  7. package/dist/chargen.js +0 -4
  8. package/dist/config.js +26 -6
  9. package/dist/device.js +130 -24
  10. package/dist/diagnostics.js +14 -5
  11. package/dist/logger.js +3 -2
  12. package/dist/loggingHttpClient.js +12 -1
  13. package/dist/mcp-server.js +19 -9
  14. package/dist/petscii.js +0 -5
  15. package/dist/redact.js +166 -0
  16. package/dist/sidwave.js +453 -61
  17. package/dist/sidwaveCompiler.js +1424 -269
  18. package/dist/tools/audio.js +70 -12
  19. package/dist/tools/developer.js +17 -9
  20. package/dist/tools/errors.js +7 -0
  21. package/dist/tools/graphics.js +4 -4
  22. package/dist/tools/input.js +10 -13
  23. package/dist/tools/machineControl.js +7 -7
  24. package/dist/tools/memory.js +6 -6
  25. package/dist/tools/meta/audio.js +8 -2
  26. package/dist/tools/meta/config.js +5 -4
  27. package/dist/tools/meta/configInventory.js +25 -2
  28. package/dist/tools/meta/diagnostics.js +2 -1
  29. package/dist/tools/meta/graphics.js +1 -1
  30. package/dist/tools/meta/memory.js +4 -4
  31. package/dist/tools/meta/program.js +12 -8
  32. package/dist/tools/printer.js +4 -4
  33. package/dist/tools/programRunners.js +8 -8
  34. package/dist/tools/registry/batch.js +1 -7
  35. package/dist/tools/registry/disk.js +7 -5
  36. package/dist/tools/storage.js +21 -17
  37. package/dist/tools/streaming.js +2 -2
  38. package/dist/tools/translation/assembler.js +44 -12
  39. package/dist/tools/translation/basicTokenizer.js +5 -1
  40. package/dist/tools/translation/opcodeInventory.js +1 -1
  41. package/dist/tools/vice.js +1 -1
  42. package/dist/vice/mockServer.js +92 -5
  43. package/dist/vice/readiness.js +4 -9
  44. package/dist/vice/viceClient.js +18 -0
  45. package/doc/audit/secret-handling.md +61 -0
  46. package/mcp/schemas/c64_config.schema.json +19 -2
  47. package/mcp/schemas/c64_disk.schema.json +2 -2
  48. package/mcp/schemas/c64_input.schema.json +3 -3
  49. package/mcp/schemas/c64_program.schema.json +2 -2
  50. package/mcp/schemas/c64_vice.schema.json +3 -3
  51. package/mcp/server.json +1 -1
  52. package/mcp/tools.json +33 -12
  53. package/mcp.json +1 -1
  54. package/package.json +5 -2
  55. package/scripts/mockC64Server.mjs +15 -1
  56. package/scripts/run-coverage.mjs +31 -1
  57. package/scripts/update-readme.ts +3 -2
  58. package/server.json +20 -3
package/AGENTS.md CHANGED
@@ -90,7 +90,7 @@ If a workflow description starts listing tool calls outside `.github/skills`, tr
90
90
 
91
91
  ## Capabilities
92
92
 
93
- - Program runners: `c64_program` (`upload_run_basic`, `upload_run_asm`, `run_prg`, `run_crt`, `bundle_run`, `batch_run`)
93
+ - Program runners: `c64_program` (`upload_run_basic`, `upload_run_asm`, `load_prg`, `run_prg`, `run_crt`, `bundle_run`, `batch_run`)
94
94
  - Fast demo workflow: `c64_program` (`cross_platform_greeting`) for one-call greetings on VICE and/or C64U with screenshot capture and verification
95
95
  - Screen & memory: `c64_memory` (`read`, `write`, `read_screen`, `wait_for_text`)
96
96
  - System control: `c64_system` (`pause`, `resume`, `reset`, `reboot`, `poweroff`, `power_cycle`, `menu`, tasks)
package/README.md CHANGED
@@ -717,7 +717,7 @@ Grouped entry point for configuration reads/writes, diagnostics, and snapshots.
717
717
  | --- | --- | --- | --- | --- | --- | --- |
718
718
  | `batch_update` | Apply multiple configuration updates in a single request. | — | — | ✅ | ✅ | ✅ |
719
719
  | `diff` | Compare the current configuration with a snapshot. | `path` | — | ✅ | ✅ | ✅ |
720
- | `get` | Read a configuration category or specific item. | `category` | `item` | ✅ | ✅ | ✅ |
720
+ | `get` | Read a configuration category or specific item. | `category` | `item`, `includeSecretValues=false` | ✅ | ✅ | ✅ |
721
721
  | `info` | Retrieve Ultimate hardware information and status. | — | — | ✅ | ✅ | ✅ |
722
722
  | `list` | List configuration categories reported by the firmware. | — | — | ✅ | ✅ | ✅ |
723
723
  | `load_flash` | Load configuration from flash storage. | — | — | ✅ | ✅ | |
@@ -764,7 +764,7 @@ Grouped entry point for disk mounts, listings, image creation, and program disco
764
764
  | `find_and_run` | Search for a PRG/CRT by name substring and run the first match. | `nameContains` | `root="/"`, `extensions`, `caseInsensitive=true`, `sort="discovered"`, `waitMs=0`, `captureCandidates=10` | ✅ | ✅ | |
765
765
  | `list_drives` | List Ultimate drive slots and their mounted images. | — | — | ✅ | ✅ | ✅ |
766
766
  | `mount` | Mount a disk image with optional verification and retries. Supports verification. | `drive`, `image` | `type`, `attachmentMode`, `driveMode`, `verify=false`, `powerOnIfNeeded=true`, `resetAfterMount=true`, `maxRetries=2`, `retryDelayMs=500` | ✅ | ✅ | ✅ |
767
- | `unmount` | Remove the mounted image from an Ultimate drive slot. | `drive` | — | ✅ | ✅ | ✅ |
767
+ | `unmount` | Remove the mounted image from an Ultimate drive slot. | `drive` | — | ✅ | ✅ | |
768
768
 
769
769
  #### c64_drive
770
770
 
@@ -809,7 +809,7 @@ Cross-platform PETSCII typing plus native Ultimate keyboard and joystick events.
809
809
 
810
810
  | Operation | Description | Required Inputs | Optional Inputs | C64U | U2 | VICE |
811
811
  | --- | --- | --- | --- | --- | --- | --- |
812
- | `joystick` | Simulate joystick input. On C64U/U64 this uses machine:input; VICE uses Binary Monitor Joyport Set. | `port`, `controls`, `action` | `durationMs=80` | ✅ | | ✅ |
812
+ | `joystick` | Simulate joystick input. On C64U/U64 this uses machine:input. On VICE it attaches VICE's I/O simulation joyport device while pressed; that device also pulls CIA lines 5-7 low, so port 1 produces phantom keys (e.g. SHIFT+C=) and port 2 reads $1E for up instead of $7E. Prefer port 2 on VICE and test bits 0-4. | `port`, `controls`, `action` | `durationMs=80` | ✅ | | ✅ |
813
813
  | `key` | Tap a single key or hold it for a duration. | `key` | `durationMs=0`, `count=1` | ✅ | ✅ | ✅ |
814
814
  | `keyboard` | Send physical C64 keyboard matrix events through machine:input (a C64U firmware version that provides it, or U64 3.15+). | `inputs`, `transition` | — | ✅ | | |
815
815
  | `release_all` | Release every key and joystick control injected through machine:input (a C64U firmware version that provides it, or U64 3.15+). | — | — | ✅ | | |
@@ -1,18 +1,11 @@
1
1
  # SIDWAVE Format Specification
2
2
 
3
- This document defines **SIDWAVE**, a declarative format for describing structured, modern C64 SID compositions.
4
- SIDWAVE files are authored in YAML (optionally JSON) and compiled by an MCP server into `.SID` or `.PRG` binaries for playback on the Commodore 64 via the Ultimate64 REST API.
5
-
6
- ---
7
-
8
- ## Purpose
9
-
10
- SIDWAVE provides a human- and machine-readable way to describe complex, atmospheric, and modern electronic music for the MOS 6581/8580 SID chip.
11
- It captures instruments, modulation, and structure symbolically, allowing large language models to generate music that the MCP expands into precise SID register sequences.
3
+ This document defines **SIDWAVE**, a declarative format for describing structured C64 SID compositions.
4
+ SIDWAVE files are authored in YAML (or JSON) and compiled by the MCP server (`music_compile_and_play`) into a `.PRG` or `.SID` (PSID v2) binary with a built-in frame-based player.
12
5
 
13
6
  Pipeline:
14
7
 
15
- LLM → SIDWAVE (YAML) → MCP Compiler → SID Binary (.sid/.prg) → Ultimate64 REST → C64 Playback
8
+ LLM → SIDWAVE (YAML) → MCP compiler → PRG or PSID → C64 / Ultimate / VICE playback
16
9
 
17
10
  ---
18
11
 
@@ -20,16 +13,20 @@ LLM → SIDWAVE (YAML) → MCP Compiler → SID Binary (.sid/.prg) → Ultimate6
20
13
 
21
14
  - **Format name:** SIDWAVE
22
15
  - **File suffix:** `.sidwave.yaml` (or `.sid.yaml` for brevity)
23
- - **Schema field:** `sidwave: 1.0`
16
+ - **Version field:** `sidwave: 1.0`
24
17
  - **Schema file:** `sidwave.schema.json`
25
- - **Primary serialization:** YAML 1.2 (JSON also valid)
18
+ - **Serialization:** YAML 1.2 or JSON
19
+
20
+ Unknown fields, unknown pattern names, invalid notes and out-of-range values are rejected with an error that names the path, for example `Invalid SIDWAVE at $.timeline[1].layers.v2: pattern "X" is not defined for voice 2 (known: B)`.
21
+
22
+ > YAML pitfall: inside a flow map, a bare `-` starts a list. Write silent layers as `v2: "-"`, not `v2: -`.
26
23
 
27
24
  ---
28
25
 
29
- ## Example SIDWAVE File
26
+ ## Example
30
27
 
31
28
  ```yaml
32
- # yaml-language-server: $schema=https://example.com/sidwave.schema.json
29
+ # yaml-language-server: $schema=https://raw.githubusercontent.com/chrisgleissner/c64bridge/refs/heads/main/data/sound/sidwave.schema.json
33
30
  sidwave: 1.0
34
31
  title: "Echoes of Tomorrow"
35
32
  author: "Generated by MCP"
@@ -50,8 +47,8 @@ voices:
50
47
  motif: "Cmin9 pad motif"
51
48
  transpositions: [0, 5, 7]
52
49
  fx:
53
- vibrato: { depth: 2, rate: 5 }
54
- filter_mod: { sweep: +500 over 8 steps }
50
+ vibrato: { depth: 0.25, rate: 5, delay: 10 }
51
+ filter_mod: { sweep: "+500 over 8 beats" }
55
52
 
56
53
  - id: 2
57
54
  name: ArpLayer
@@ -59,11 +56,11 @@ voices:
59
56
  pulse_width: 0.3
60
57
  patterns:
61
58
  B:
62
- type: "arpeggio"
59
+ type: arpeggio
63
60
  notes: [C3, Eb3, G3, Bb3]
64
61
  frame_rate: 2
65
62
  fx:
66
- pwm_sweep: { speed: 6, depth: 8 }
63
+ pwm_sweep: { speed: 1, depth: 0.2 }
67
64
 
68
65
  - id: 3
69
66
  name: Bass
@@ -77,7 +74,7 @@ voices:
77
74
  timeline:
78
75
  - section: "Intro"
79
76
  bars: 16
80
- layers: { v1: A, v2: -, v3: C }
77
+ layers: { v1: A, v2: "-", v3: C }
81
78
  - section: "Build"
82
79
  bars: 32
83
80
  layers: { v1: A↑, v2: B, v3: C }
@@ -86,46 +83,126 @@ timeline:
86
83
  layers: { v1: A↓, v2: B, v3: C }
87
84
  - section: "Fade"
88
85
  bars: 32
89
- layers: { v1: A–, v2: -, v3: - }
86
+ layers: { v1: A–, v2: "-", v3: "-" }
90
87
  ```
91
88
 
92
89
  ---
93
90
 
94
- ## MCP Responsibilities
91
+ ## Song Fields
95
92
 
96
- 1. Parse SIDWAVE YAML or JSON input.
97
- 2. Expand patterns, transpositions, and modulation into frame-accurate SID register writes.
98
- 3. Compile to valid `.SID` or `.PRG` binaries with proper init and play routines.
99
- 4. Send the binary to Ultimate64 via:
93
+ | Field | Meaning |
94
+ | --- | --- |
95
+ | `sidwave` | Format version, `1.0`. |
96
+ | `title`, `author` | Written to the PSID header (Latin-1, 32 characters). |
97
+ | `tempo` | Beats (quarter notes) per minute, 30–220. Default 100. |
98
+ | `mode` | `PAL` (985248 Hz clock, 50.12 frames/s) or `NTSC` (1022727 Hz clock, 59.83 frames/s). Default `PAL`. |
99
+ | `length_bars` | Optional song length. Shorter than the timeline truncates it; longer repeats the timeline from the start. Default: the sum of the timeline's bars. |
100
+ | `global_fx` | `reverb_sim` and `filter_env`, described below. |
101
+ | `voices` | One to three voices with ids 1–3. Voices that are not declared stay silent. |
102
+ | `timeline` | Sections played in order. The song loops forever after the last section. |
100
103
 
101
- ```
102
- POST /v1/attachment:sid
103
- Content-Type: application/octet-stream
104
- Body = compiled binary
105
- ```
104
+ The legacy shape with a `song: { title, author, tempo, mode, length_bars, global_fx }` block next to `voices` and `timeline` is also accepted.
105
+
106
+ Time is 4/4: a bar has four beats. The player runs once per video frame of a real C64 (50.1245 Hz on PAL, 59.8262 Hz on NTSC). Every onset is placed on the nearest frame of its exact time (`beat × 60 / tempo × frame rate`), so long songs do not drift; individual notes may be one frame longer or shorter than the average.
107
+
108
+ ## Voices
109
+
110
+ | Field | Meaning |
111
+ | --- | --- |
112
+ | `id` | SID voice 1, 2 or 3. |
113
+ | `name` | Label only. |
114
+ | `waveform` | `pulse`, `sawtooth` (`saw`), `triangle` (`tri`) or `noise`. Default `pulse`. |
115
+ | `pulse_width` | Pulse duty cycle: a fraction (0 < x ≤ 1, e.g. `0.3`) or a 12-bit value 1–4094. Default 2048 (50 %). |
116
+ | `adsr` | `[attack, decay, sustain, release]`, each 0–15 (SID register nibbles). Default `[2, 2, 10, 3]`. |
117
+ | `patterns` | Named patterns used by the timeline. |
118
+
119
+ ## Patterns
120
+
121
+ Every pattern is a list of **entries**. Each entry lasts `length` beats (default 1; fractions allowed, e.g. `0.25` for sixteenth notes). The pattern repeats until its section ends; a note that would run past the section end is cut there.
122
+
123
+ - **groove** — `groove: [C2, "-", C2, "~", G1]`: one note per entry. `-` (or `.`) is a rest, `~` ties the entry to the previous note (no retrigger). A tie holds whatever note is sounding right before it, also across a section boundary (a section that starts with `~` continues the last note of the previous section). After a rest, or at the very start of the song, a tie is a rest.
124
+ - **arpeggio** — `type: arpeggio`, `notes: [...]`. Without `frame_rate`, the notes are played one per entry (a broken chord). With `frame_rate: N`, all notes form one entry that sounds as a fast chord arpeggio, switching to the next chord tone every N frames (`frame_rate: 1` = one tone per frame, about 50 per second on PAL); each entry retriggers the chord.
125
+ - **motif** — `motif: "..."` with tokens separated by spaces or commas:
126
+ - note names (`C4`, `Eb3`, `F#2`),
127
+ - integers: semitones above `root` (default `C3`), e.g. `"0 4 7 12"`,
128
+ - chord symbols, expanded to their chord tones in the octave of `root`: `C`, `Cm`/`Cmin`, `Cdim`, `Caug`, `Csus2`, `Csus4`, `C6`, `Cm6`, `C7`, `Cmaj7`, `Cm7`, `Cdim7`, `Cm7b5`, `C9`, `Cmaj9`, `Cm9`/`Cmin9`, `Cadd9`,
129
+ - `-` rests and `~` ties.
130
+ Words that are not notes or chord symbols (such as `pad` or `motif`) are ignored. `"Cmin9 pad motif"` therefore plays C3, Eb3, G3, Bb3, D4, one per entry.
131
+
132
+ `transpositions: [0, 5, 7]` transposes successive repetitions of the pattern within a section by the given semitones, cycling; the cycle restarts at each section.
133
+
134
+ Notes range from C0 to B7. At the PAL clock B7 exceeds the SID's 16-bit frequency register, so the highest PAL note is A#7. Notes that leave the range after transposition are rejected.
135
+
136
+ ### Pattern effects (`fx`)
137
+
138
+ | Effect | Fields | Behaviour |
139
+ | --- | --- | --- |
140
+ | `vibrato` | `depth` (semitones, 0.03–6), `rate` (Hz, 0.2–12), `delay` (frames, optional) | Triangle pitch modulation of ±`depth` semitones around the note (the upward and downward swings are computed separately, so both reach `depth` to within the SID's frequency resolution, about 3 cents in the lowest octave), starting `delay` frames after the note. The upward swing must stay within the SID's highest frequency: on PAL a vibrato on A#7 can be at most 0.54 semitones deep, on NTSC B7 at most 0.19; deeper vibratos on such notes (including the highest note of a chord arpeggio) are rejected. The player samples the vibrato once per frame, so rates are limited to 12 Hz (about a quarter of the frame rate); faster rates would alias or cancel out. |
141
+ | `pwm_sweep` | `depth` (fraction ≤ 1 or 12-bit units), `speed` (cycles per second) | Pulse width moves linearly between `pulse_width − depth` and `pulse_width + depth`, restarting at the base on each note. Pulse waveform only. |
142
+ | `slide` | `depth` (semitones, ±24, non-zero), `speed` (frames, 1–255) | Each note starts `depth` semitones below the target (above when negative) and glides to it linearly in frequency, arriving after `speed` frames. The start note must itself be playable (C0..A#7 on PAL, C0..B7 on NTSC); otherwise the document is rejected. |
143
+ | `filter_mod` | `sweep: "+500 over 8 steps"` (units: `steps`, `beats`, `bars`, `frames`; a step is one entry), or `{ sweep: 500, beats: 8 }` | Routes the voice through the filter while the pattern plays. At each repetition of the pattern the cutoff offset restarts at 0 and ramps to `sweep` (cutoff units, −2047..2047) over the given time, then holds. The resulting cutoff is clamped to 0..2047. The offset is removed when the section ends. |
144
+
145
+ ## Timeline
146
+
147
+ Each section has `bars` (whole number ≥ 1), an optional `section` name, and `layers` mapping `v1`/`v2`/`v3` to a pattern reference:
148
+
149
+ - `"-"`, `.`, `rest` or an omitted layer: the voice is silent.
150
+ - `A`: play pattern `A` of that voice.
151
+ - Modifiers after the name: `↑` (or `^`) one octave up, `↓` one octave down, `+N` / `-N` N semitones (e.g. `A+5`), and a trailing `–`, `—` or `-` fades the voice out over the section.
152
+
153
+ Fading lowers the voice's sustain level linearly to 0 across the section. When every sounding voice of a section fades, the master volume also fades to 0 and returns to full volume when the section ends.
154
+
155
+ ## Global effects (`global_fx`)
156
+
157
+ | Effect | Fields | Behaviour |
158
+ | --- | --- | --- |
159
+ | `filter_env` | `cutoff_base` (0–2047), `sweep` (±2047), `speed` (beats per one-way sweep), `resonance` (0–15, default 8), `type` (`lowpass` default, `bandpass`, `highpass`), `voices` (default all) | Enables the SID filter for the listed voices. The cutoff moves back and forth between `cutoff_base` and `cutoff_base + sweep`, each direction taking `speed` beats. Without `sweep`/`speed` the cutoff stays at `cutoff_base`. The sweep restarts from `cutoff_base` each time the song loops, so every pass sounds the same. `filter_mod` offsets are added on top. |
160
+ | `reverb_sim` | `depth` (0 < x ≤ 1), `delay_frames` (1–50) | In sections that leave a voice free, the lowest free voice echoes the lowest sounding voice `delay_frames` later, with its sustain level scaled by `depth`. The SID has no per-voice volume, so the echo's attack peak is as loud as the original; only the sustained part is quieter. Sections that use all three voices have no echo. |
161
+
162
+ Without `filter_env`, a `filter_mod` uses a low-pass filter with cutoff 1024 and resonance 8 as its base. The SID has a single filter, so all routed voices share it; when several patterns with `filter_mod` start at once, the most recent start sets the ramp.
163
+
164
+ ---
165
+
166
+ ## Player
167
+
168
+ The compiler arranges the timeline into per-voice note events and encodes them as compact command streams (note, instrument, wait, gate-off, hard restart, sustain, volume, filter ramp, loop). A 6502 player reads these streams once per video frame and computes per-frame effects itself.
169
+
170
+ - **Hard restart:** two frames before every new note the voice's gate is released and its ADSR is set to zero, so each note starts a fresh envelope (this avoids the SID's ADSR delay bug). A note therefore sounds with the gate on until two frames before the next note, so every note needs at least 3 frames (one sounding frame plus the hard restart). Entries shorter than 3 frames are rejected, which limits very short entries at fast tempos (for example 1/16 notes are fine at 220 BPM, 1/32 notes are not). A note that is cut to fewer than 3 frames by the end of a section is dropped.
171
+ - **Gate:** a rest releases the gate at its start, so the note decays with its release rate.
172
+ - **Instruments:** each voice/pattern/effect combination becomes an instrument with its own ADSR, waveform, pulse settings, vibrato, arpeggio table, slide and filter routing (up to 63).
173
+ - **Memory:** the program loads at $0801 and must end below $D000 (about 50 KB). Very long, dense songs are rejected with a validation error rather than truncated. The player uses zero-page bytes $FB/$FC and restores them after every call.
174
+ - **CPU time:** typical songs take 20–65 raster lines per frame (about 1,400–4,100 cycles). The worst case, all three voices starting notes with vibrato, slide, arpeggio, pulse sweep and filter ramp in the same frame, takes about 9,800 cycles (155 raster lines, half a PAL frame), mostly for the arithmetic that scales vibrato and slide to each note's pitch (an 8×8 multiply from a 1 KB table of squares).
175
+
176
+ ### Output formats
177
+
178
+ - **PSID (`output: sid`):** PSID v2 with init at $0810, play at $0813, vertical-blank timing, and the PAL or NTSC clock flag from `mode`. The SID model is left unspecified.
179
+ - **PRG (`output: prg`):** `10 SYS2061`. The program detects whether the machine is PAL or NTSC, programs CIA 1 timer A for the song's frame rate on that clock, installs an IRQ handler, and returns to BASIC while the music plays. Pitch and tempo are therefore correct on either machine type.
180
+ - Before returning, it moves the start of BASIC and the variable area behind the player (like `NEW` with a raised start of BASIC), so direct-mode commands, variables and new program lines work without touching the player. The compile result reports `basicFreeBytes`. A song whose PRG ends at or above $A000 leaves no BASIC memory: BASIC commands that need memory then fail with `?OUT OF MEMORY` while the music keeps playing, and the compile result carries a warning.
181
+ - Because the KERNAL's IRQ (keyboard scan, cursor blink, jiffy clock) now runs at the song's frame rate instead of 60 Hz, `TI`/`TI$` runs about 16.5 % slow for a PAL song (0.3 % for NTSC), and key repeat and cursor blink slow down accordingly while the music plays.
182
+
183
+ ### Not supported
184
+
185
+ Time signatures other than 4/4, per-note velocity, portamento between consecutive notes (the `slide` effect is a slide into each note), multiple subtunes, multispeed (more than one player call per frame), and combined waveforms.
106
186
 
107
187
  ---
108
188
 
109
189
  ## Authoring and Validation
110
190
 
111
- - SIDWAVE files are plain YAML and open cleanly in any standard editor.
112
- - Validation uses JSON Schema:
191
+ - Validation uses JSON Schema (`sidwave.schema.json`); the compiler applies the same rules and additionally checks note ranges after transposition and minimum note durations.
113
192
 
114
193
  ```json
115
194
  {
116
195
  "yaml.schemas": {
117
- "https://example.com/sidwave.schema.json": "*.sidwave.yaml"
196
+ "https://raw.githubusercontent.com/chrisgleissner/c64bridge/refs/heads/main/data/sound/sidwave.schema.json": "*.sidwave.yaml"
118
197
  }
119
198
  }
120
199
  ```
121
200
 
122
- This enables autocomplete, hover info, and validation in VS Code or similar IDEs.
123
-
124
201
  ---
125
202
 
126
203
  ## LLM Prompt Template
127
204
 
128
205
  > Compose a 3–5 minute atmospheric, modern electronic SID composition using the **SIDWAVE** format.
129
206
  > Use three voices with expressive modulation (PWM, filter sweeps, arpeggios, evolving pads).
130
- > Output valid YAML conforming to `sidwave.schema.json`.
207
+ > Output valid YAML conforming to `sidwave.schema.json`; quote silent layers as `"-"`.
131
208
  > Avoid direct note repetition; reuse and transpose patterns.
@@ -2,63 +2,173 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://raw.githubusercontent.com/chrisgleissner/c64bridge/refs/heads/main/data/sound/sidwave.schema.json",
4
4
  "title": "SIDWAVE Composition Schema",
5
+ "description": "See sidwave.md for the meaning of every field. The compiler also checks note ranges after transposition and minimum note durations.",
5
6
  "type": "object",
6
7
  "required": ["sidwave", "title", "tempo", "mode", "voices", "timeline"],
7
8
  "properties": {
8
9
  "sidwave": { "type": ["number", "string"], "enum": [1, "1.0", "1"] },
9
10
  "title": { "type": "string" },
10
11
  "author": { "type": "string" },
11
- "tempo": { "type": "number", "minimum": 30, "maximum": 220 },
12
+ "tempo": { "type": "number", "minimum": 30, "maximum": 220, "description": "Beats (quarter notes) per minute." },
12
13
  "mode": { "type": "string", "enum": ["PAL", "NTSC"] },
13
- "global_fx": { "type": "object", "additionalProperties": true },
14
+ "length_bars": { "type": "integer", "minimum": 1, "description": "Truncates or repeats the timeline to this many 4/4 bars." },
15
+ "global_fx": {
16
+ "type": "object",
17
+ "properties": {
18
+ "reverb_sim": {
19
+ "type": "object",
20
+ "required": ["depth", "delay_frames"],
21
+ "properties": {
22
+ "depth": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 },
23
+ "delay_frames": { "type": "integer", "minimum": 1, "maximum": 50 }
24
+ },
25
+ "additionalProperties": false
26
+ },
27
+ "filter_env": {
28
+ "type": "object",
29
+ "required": ["cutoff_base"],
30
+ "properties": {
31
+ "cutoff_base": { "type": "number", "minimum": 0, "maximum": 2047 },
32
+ "sweep": { "type": "number", "minimum": -2047, "maximum": 2047 },
33
+ "speed": { "type": "number", "exclusiveMinimum": 0, "maximum": 1024, "description": "Beats per one-way sweep." },
34
+ "resonance": { "type": "integer", "minimum": 0, "maximum": 15 },
35
+ "type": { "type": "string", "enum": ["lowpass", "bandpass", "highpass"] },
36
+ "voices": { "type": "array", "items": { "type": "integer", "minimum": 1, "maximum": 3 } }
37
+ },
38
+ "additionalProperties": false
39
+ }
40
+ },
41
+ "additionalProperties": false
42
+ },
14
43
  "voices": {
15
44
  "type": "array",
16
45
  "minItems": 1,
46
+ "maxItems": 3,
17
47
  "items": {
18
48
  "type": "object",
19
49
  "required": ["id"],
20
50
  "properties": {
21
- "id": { "type": "number", "minimum": 1, "maximum": 3 },
51
+ "id": { "type": "integer", "minimum": 1, "maximum": 3 },
22
52
  "name": { "type": "string" },
23
53
  "waveform": { "type": "string", "enum": ["pulse", "sawtooth", "triangle", "noise", "saw", "tri"] },
24
- "pulse_width": { "type": "number", "minimum": 0 },
25
- "adsr": { "type": "array", "items": { "type": "number", "minimum": 0, "maximum": 15 }, "minItems": 4, "maxItems": 4 },
54
+ "pulse_width": { "type": "number", "exclusiveMinimum": 0, "exclusiveMaximum": 4095, "description": "Fraction (0 < x <= 1) or 12-bit value." },
55
+ "adsr": { "type": "array", "items": { "type": "integer", "minimum": 0, "maximum": 15 }, "minItems": 4, "maxItems": 4 },
26
56
  "patterns": {
27
57
  "type": "object",
28
58
  "additionalProperties": {
29
59
  "oneOf": [
30
- { "type": "object", "required": ["type", "notes"], "properties": {
31
- "type": { "const": "arpeggio" },
32
- "notes": { "type": "array", "items": { "type": "string" }, "minItems": 1 },
33
- "frame_rate": { "type": "number", "minimum": 1 },
34
- "fx": { "type": "object", "additionalProperties": true }
35
- }, "additionalProperties": false },
36
- { "type": "object", "properties": {
37
- "length": { "type": "number", "minimum": 1 },
38
- "motif": { "type": "string" },
39
- "transpositions": { "type": "array", "items": { "type": "number" } },
40
- "fx": { "type": "object", "additionalProperties": true }
41
- }, "additionalProperties": false },
42
- { "type": "object", "required": ["groove"], "properties": {
43
- "groove": { "type": "array", "items": { "type": "string" }, "minItems": 1 },
44
- "fx": { "type": "object", "additionalProperties": true }
45
- }, "additionalProperties": false }
60
+ {
61
+ "type": "object",
62
+ "required": ["notes"],
63
+ "properties": {
64
+ "type": { "const": "arpeggio" },
65
+ "notes": { "type": "array", "items": { "$ref": "#/$defs/note" }, "minItems": 1 },
66
+ "frame_rate": { "type": "integer", "minimum": 1, "maximum": 50, "description": "Frames per chord tone; makes the notes a fast chord arpeggio." },
67
+ "length": { "$ref": "#/$defs/length" },
68
+ "transpositions": { "$ref": "#/$defs/transpositions" },
69
+ "fx": { "$ref": "#/$defs/fx" }
70
+ },
71
+ "additionalProperties": false
72
+ },
73
+ {
74
+ "type": "object",
75
+ "required": ["motif"],
76
+ "properties": {
77
+ "motif": { "type": "string", "description": "Notes, semitone offsets from root, chord symbols, '-' rests and '~' ties." },
78
+ "root": { "$ref": "#/$defs/note" },
79
+ "length": { "$ref": "#/$defs/length" },
80
+ "transpositions": { "$ref": "#/$defs/transpositions" },
81
+ "fx": { "$ref": "#/$defs/fx" }
82
+ },
83
+ "additionalProperties": false
84
+ },
85
+ {
86
+ "type": "object",
87
+ "required": ["groove"],
88
+ "properties": {
89
+ "groove": { "type": "array", "items": { "anyOf": [{ "$ref": "#/$defs/note" }, { "enum": ["-", ".", "~", "_", "r", "rest"] }] }, "minItems": 1 },
90
+ "length": { "$ref": "#/$defs/length" },
91
+ "transpositions": { "$ref": "#/$defs/transpositions" },
92
+ "fx": { "$ref": "#/$defs/fx" }
93
+ },
94
+ "additionalProperties": false
95
+ }
46
96
  ]
47
97
  }
48
98
  }
49
99
  },
50
- "additionalProperties": true
100
+ "additionalProperties": false
51
101
  }
52
102
  },
53
103
  "timeline": {
54
104
  "type": "array",
55
105
  "minItems": 1,
56
- "items": { "type": "object", "required": ["bars", "layers"], "properties": {
57
- "section": { "type": "string" },
58
- "bars": { "type": "number", "minimum": 1 },
59
- "layers": { "type": "object", "additionalProperties": { "type": "string" } }
60
- }, "additionalProperties": false }
106
+ "items": {
107
+ "type": "object",
108
+ "required": ["bars", "layers"],
109
+ "properties": {
110
+ "section": { "type": "string" },
111
+ "bars": { "type": "integer", "minimum": 1 },
112
+ "layers": {
113
+ "type": "object",
114
+ "propertyNames": { "pattern": "^(v|voice)?[1-3]$" },
115
+ "additionalProperties": { "type": ["string", "null"], "description": "Pattern name with optional ↑ ↓ +N -N modifiers and a trailing – for fade; \"-\" is silence." }
116
+ }
117
+ },
118
+ "additionalProperties": false
119
+ }
61
120
  }
62
121
  },
63
- "additionalProperties": false
122
+ "additionalProperties": false,
123
+ "$defs": {
124
+ "note": { "type": "string", "pattern": "^[A-Ga-g][#b]?[0-7]$" },
125
+ "length": { "type": "number", "exclusiveMinimum": 0, "maximum": 64, "description": "Beats per entry (default 1)." },
126
+ "transpositions": { "type": "array", "items": { "type": "integer", "minimum": -48, "maximum": 48 }, "minItems": 1 },
127
+ "fx": {
128
+ "type": "object",
129
+ "properties": {
130
+ "vibrato": {
131
+ "type": "object",
132
+ "required": ["depth", "rate"],
133
+ "properties": {
134
+ "depth": { "type": "number", "minimum": 0.03, "maximum": 6, "description": "Semitones." },
135
+ "rate": { "type": "number", "minimum": 0.2, "maximum": 12, "description": "Hz; at most about a quarter of the frame rate." },
136
+ "delay": { "type": "integer", "minimum": 0, "maximum": 255, "description": "Frames." }
137
+ },
138
+ "additionalProperties": false
139
+ },
140
+ "pwm_sweep": {
141
+ "type": "object",
142
+ "required": ["depth", "speed"],
143
+ "properties": {
144
+ "depth": { "type": "number", "exclusiveMinimum": 0, "maximum": 2047 },
145
+ "speed": { "type": "number", "exclusiveMinimum": 0, "maximum": 25, "description": "Cycles per second." }
146
+ },
147
+ "additionalProperties": false
148
+ },
149
+ "slide": {
150
+ "type": "object",
151
+ "required": ["depth", "speed"],
152
+ "properties": {
153
+ "depth": { "type": "integer", "minimum": -24, "maximum": 24, "not": { "const": 0 } },
154
+ "speed": { "type": "integer", "minimum": 1, "maximum": 255, "description": "Frames until the slide reaches the note." }
155
+ },
156
+ "additionalProperties": false
157
+ },
158
+ "filter_mod": {
159
+ "type": "object",
160
+ "required": ["sweep"],
161
+ "properties": {
162
+ "sweep": { "type": ["string", "number"], "description": "e.g. \"+500 over 8 steps\" (steps, beats, bars or frames)." },
163
+ "steps": { "type": "number", "exclusiveMinimum": 0 },
164
+ "beats": { "type": "number", "exclusiveMinimum": 0 },
165
+ "bars": { "type": "number", "exclusiveMinimum": 0 },
166
+ "frames": { "type": "number", "exclusiveMinimum": 0 }
167
+ },
168
+ "additionalProperties": false
169
+ }
170
+ },
171
+ "additionalProperties": false
172
+ }
173
+ }
64
174
  }