Skip to content

feat: read wave from the language, and accept the harmonics form - #20

Merged
spacedevin merged 3 commits into
mainfrom
feat/wave-harmonics
Aug 28, 2026
Merged

feat: read wave from the language, and accept the harmonics form#20
spacedevin merged 3 commits into
mainfrom
feat/wave-harmonics

Conversation

@spacedevin

Copy link
Copy Markdown
Member

Draft — blocked on the deckfile 1.5.0 release from spacedevin/deck#16. The pin is bumped ahead of it, so CI will not go green until that publishes.

wave <name> <32 hex nibbles> started life here, as a fork of the .deck parser. When the fork was undone it survived as a registered dialect extension — but it was never actually GBA-specific: deck-player registered its own independent copy, and conformance/profiles.json still listed it under gba.extensions.topLevel. A statement every host implements is a language feature, so it has been adopted into the language proper.

What changed here

The hex validation and nibble unpacking are gone from this crate. The parser hands over 32 resolved levels whichever spelling wrote them, so the bake only packs two levels per byte:

for w in &program.waves {
    let mut packed = [0u8; 16];
    for i in 0..16 {
        packed[i] = ((w.levels[i * 2] as u8) << 4) | (w.levels[i * 2 + 1] as u8);
    }
    named_waves.insert(w.name.clone(), packed);
}

Songs can now write a table as harmonic amplitudes instead of digits:

wave organ 8beffecbbbbaa9888776554444310014
wave organ harmonics 1 0.5 0.33 0.2

Nothing about the ROM changes

The additive sum happens in the parser at bake time — the only place it can, since the device is no_std on ARM7TDMI with no FPU and no libm. DeckSong.waves: &'static [[u8; 16]] is unchanged, psg::wave_table still copies 16 bytes verbatim into WAVE_RAM, and the size delta is zero. A harmonics table costs exactly what a hex one did.

Verification

  • Corpus digest identical across all 54 decks. DECK_CORPUS_DIGEST=1 cargo test -- --nocapture corpus before and after diffs clean — not a single note moved.
  • New test wave_harmonics_and_hex_pack_identically pins the thing that would otherwise be invisible: a harmonics table and its hex literal pack to byte-identical wave RAM. This is the hardware end of the language's equality guarantee.
  • cargo test — 11 pass.
  • profile_test::gba_profile_is_honest passes against the new 013-wave conformance case. Worth noting it earned its keep: it caught that my first draft of that case used two type wave tracks, which the LR35902's single wave channel cannot do. The case was fixed upstream rather than the profile loosened.

No .deck file in this repo currently uses a top-level wave statement — all 44 wave tracks use built-in wave_shape — so there is no existing content to regress.

`wave <name> <32 hex nibbles>` originated here, as a fork of the parser, and
survived as a registered dialect extension after the fork was undone. It was
never GBA-specific though — deck-player registered its own copy too — so it has
been adopted into the language proper (spacedevin/deck#16).

That deletes the hex validation and nibble unpacking from this crate. The parser
resolves both spellings to 32 levels, and all that is left here is packing two
levels per byte for wave RAM.

The additive sum lands at bake time, which is the only place it can: the device
is no_std on ARM7TDMI with no FPU and no libm, so there is no sin() on the far
side. Nothing about the ROM changes — still 16 bytes per table, copied verbatim
into WAVE_RAM, so a harmonics table costs exactly what a hex one did.

The corpus digest is unchanged across all 54 decks, and a new test pins the part
that would be invisible otherwise: a harmonics table and the hex literal it
resolves to pack to byte-identical wave RAM.

Blocked on the deckfile 1.5.0 release; the pin is bumped ahead of it, so CI will
not go green until that publishes.
The released crate, in place of the version pinned ahead of it. `wave` is core
grammar there, so this is what actually makes the branch build.

All 11 tests pass against the published crate, and the corpus digest across all
54 decks is byte-identical to the pre-change baseline.
@spacedevin
spacedevin marked this pull request as ready for review August 27, 2026 23:44
@spacedevin

Copy link
Copy Markdown
Member Author

Unblocked and out of draft — pinned to the released deckfile 1.5.3.

  • builds against the published crate (no path override)
  • cargo test — 11 pass
  • corpus digest across all 54 decks byte-identical to the pre-change baseline
  • wave_harmonics_and_hex_pack_identically passes against the real crate: a harmonics table and its hex literal produce the same 16 bytes of wave RAM

CI runs `cargo fmt --check` on every crate and `cargo clippy --all-targets -D
warnings` on this one; the wave changes passed neither.

Formatting was mechanical. Clippy found two needless range loops and one real
problem: `assert!(b >> 4 <= 15 && (b & 0x0f) <= 15)` is always true, because a
nibble of a u8 cannot exceed 15. The assertion proved nothing.

Replaced with the invariant actually worth pinning — that packing is lossless
and high-nibble-first. Getting that order wrong still yields 16 plausible bytes,
so nothing downstream would catch it; the ROM would just play a scrambled
waveform.

Tests still 11/11 and the corpus digest is unchanged across all 54 decks.
@spacedevin
spacedevin merged commit 72ec53e into main Aug 28, 2026
65 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant