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.
- package/AGENTS.md +1 -1
- package/README.md +3 -3
- package/data/sound/sidwave.md +113 -36
- package/data/sound/sidwave.schema.json +138 -28
- package/dist/.tsbuildinfo +1 -1
- package/dist/c64Client.js +83 -93
- package/dist/chargen.js +0 -4
- package/dist/config.js +26 -6
- package/dist/device.js +130 -24
- package/dist/diagnostics.js +14 -5
- package/dist/logger.js +3 -2
- package/dist/loggingHttpClient.js +12 -1
- package/dist/mcp-server.js +19 -9
- package/dist/petscii.js +0 -5
- package/dist/redact.js +166 -0
- package/dist/sidwave.js +453 -61
- package/dist/sidwaveCompiler.js +1424 -269
- package/dist/tools/audio.js +70 -12
- package/dist/tools/developer.js +17 -9
- package/dist/tools/errors.js +7 -0
- package/dist/tools/graphics.js +4 -4
- package/dist/tools/input.js +10 -13
- package/dist/tools/machineControl.js +7 -7
- package/dist/tools/memory.js +6 -6
- package/dist/tools/meta/audio.js +8 -2
- package/dist/tools/meta/config.js +5 -4
- package/dist/tools/meta/configInventory.js +25 -2
- package/dist/tools/meta/diagnostics.js +2 -1
- package/dist/tools/meta/graphics.js +1 -1
- package/dist/tools/meta/memory.js +4 -4
- package/dist/tools/meta/program.js +12 -8
- package/dist/tools/printer.js +4 -4
- package/dist/tools/programRunners.js +8 -8
- package/dist/tools/registry/batch.js +1 -7
- package/dist/tools/registry/disk.js +7 -5
- package/dist/tools/storage.js +21 -17
- package/dist/tools/streaming.js +2 -2
- package/dist/tools/translation/assembler.js +44 -12
- package/dist/tools/translation/basicTokenizer.js +5 -1
- package/dist/tools/translation/opcodeInventory.js +1 -1
- package/dist/tools/vice.js +1 -1
- package/dist/vice/mockServer.js +92 -5
- package/dist/vice/readiness.js +4 -9
- package/dist/vice/viceClient.js +18 -0
- package/doc/audit/secret-handling.md +61 -0
- package/mcp/schemas/c64_config.schema.json +19 -2
- package/mcp/schemas/c64_disk.schema.json +2 -2
- package/mcp/schemas/c64_input.schema.json +3 -3
- package/mcp/schemas/c64_program.schema.json +2 -2
- package/mcp/schemas/c64_vice.schema.json +3 -3
- package/mcp/server.json +1 -1
- package/mcp/tools.json +33 -12
- package/mcp.json +1 -1
- package/package.json +5 -2
- package/scripts/mockC64Server.mjs +15 -1
- package/scripts/run-coverage.mjs +31 -1
- package/scripts/update-readme.ts +3 -2
- 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
|
|
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+). | — | — | ✅ | | |
|
package/data/sound/sidwave.md
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
# SIDWAVE Format Specification
|
|
2
2
|
|
|
3
|
-
This document defines **SIDWAVE**, a declarative format for describing structured
|
|
4
|
-
SIDWAVE files are authored in YAML (
|
|
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
|
|
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
|
-
- **
|
|
16
|
+
- **Version field:** `sidwave: 1.0`
|
|
24
17
|
- **Schema file:** `sidwave.schema.json`
|
|
25
|
-
- **
|
|
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
|
|
26
|
+
## Example
|
|
30
27
|
|
|
31
28
|
```yaml
|
|
32
|
-
# yaml-language-server: $schema=https://
|
|
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:
|
|
54
|
-
filter_mod: { sweep: +500 over 8
|
|
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:
|
|
59
|
+
type: arpeggio
|
|
63
60
|
notes: [C3, Eb3, G3, Bb3]
|
|
64
61
|
frame_rate: 2
|
|
65
62
|
fx:
|
|
66
|
-
pwm_sweep: { speed:
|
|
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:
|
|
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:
|
|
86
|
+
layers: { v1: A–, v2: "-", v3: "-" }
|
|
90
87
|
```
|
|
91
88
|
|
|
92
89
|
---
|
|
93
90
|
|
|
94
|
-
##
|
|
91
|
+
## Song Fields
|
|
95
92
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
-
|
|
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://
|
|
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
|
-
"
|
|
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": "
|
|
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", "
|
|
25
|
-
"adsr": { "type": "array", "items": { "type": "
|
|
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
|
-
{
|
|
31
|
-
"type":
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
|
|
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":
|
|
100
|
+
"additionalProperties": false
|
|
51
101
|
}
|
|
52
102
|
},
|
|
53
103
|
"timeline": {
|
|
54
104
|
"type": "array",
|
|
55
105
|
"minItems": 1,
|
|
56
|
-
"items": {
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
|
|
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
|
}
|