feat: play .deck in the browser, and publish the docs - #14
Merged
Conversation
Two additions, both host-side, so the language package stays what it says it is. @spacedevin/deck-player (packages/player) — Web Audio playback. The parser is deliberately parse-only: absent optionals stay null and nothing is clamped, because that is host policy. This package is that host — defaults, clamps, synthesis, transport, plus a <deck-player> element. The synths are a source-level port of Deckard's, which are already Tish and already pure, and are cross-checked against tish-gba's build-time bake. That means real hardware behaviour rather than an impression of it: the four DMG duty tables, a genuine 15/7-bit LFSR, 4-bit wave RAM, the 64/32 Hz floors, the GBA's 256-step DAC staircase and ~16 kHz mixing roll-off. All 62 songs in the tish-gba corpus parse with zero errors and zero substituted generators. Three fidelity bugs found on the way. An unrecognised duty produced a constant -1.0 buffer — a silent channel — where the bake falls back to 50%. Notes past `* N` are legal and extend the pattern (the bake computes the span as max(note ends, bars*4)); rejecting them silenced 10 of those 62 songs. And the GBA host extensions `wave` and `layer` were unregistered, so a sixth of the corpus reported "unexpected top-level: wave" and played a generic saw instead of its own wave table. Deviations from Deckard, both deliberate: voices return their cleanup instead of arming a per-note setTimeout, since wall-clock timers are wrong for an offline render; and the reverb impulse is seeded, so two renders of one song match. Players are exclusive by default — starting one stops the others — and share a single page-level AudioContext. Docs site (site/, .github/workflows/pages.yml) — renders the markdown already in this repo to spacedevin.github.io/deck. Adding a page means adding a .md file; sources are read in place, because docs/*.md are package exports and a content/ copy would fork the canonical text. `.deck` highlighting comes from this package's own isKeyword/isStepToken/classifyLine rather than a second keyword list, and a block gets a play button when it actually parses into something audible. llms.txt and llms-full.txt are generated from the same pages. docs/EXAMPLES.md is new: eight complete runnable songs, one per idea, all clean and all playable on the site. Both packages release in lockstep from one tag; the player needs its own npm trusted-publisher entry before the first publish.
A dev landing on the readme should hear the language before reading about it, so a complete three-track song is now the first thing after the tagline — melody as notes on a beat grid, drums as a step pattern, both chip synths. On the docs site it renders with a play button, since a block earns one by parsing into something audible. Also fixes the release job to pin the player's dependency with a caret rather than an exact version. `tish build` inlines the parser into dist/deck-player.js, so the dependency only matters to a consumer compiling from src/index.tish via the `tish` export condition; an exact pin would have forced npm to install a second copy of @spacedevin/deck alongside a consumer's own.
HOST.md carried an "AST shape (summary)" table that covered the top-level parseProgram fields and stopped there — it never documented the typed body rows from parseTrackBody, which is the shape a host actually consumes. Until now the only complete statement of it was rust/facade.rs, in Rust. docs/AST.md documents both halves: every top-level field, every body-row kind with its fields, bar selectors, gen blocks, and the two rules that shape all of it — the parser never throws, and null means "the source didn't say" rather than "the default", because applying defaults is host policy. Shapes were derived by running the parser over the conformance corpus rather than written from memory, so the samples are real output. HOST.md now links here instead of keeping a second copy that could drift. Also exported as `@spacedevin/deck/ast`, alongside a new `./examples`.
spacedevin
added a commit
that referenced
this pull request
Aug 27, 2026
…#17) The Rust crate job has been posting four `##[error]` annotations on green runs: ENOENT: opendir '…/crate/target/package/deckfile-0.1.0/tests/trybuild' ENOENT: opendir '…/crate/target/package/deckfile-0.1.0/tests/target' They come from `Post Cache cargo`, not from any build step. `cargo publish --dry-run` leaves an extracted copy of the crate under `target/package/`, and that copy has a `tests/` directory (holding `conformance.rs`). rust-cache prunes `target/` before saving it, sees `tests/`, and goes looking for the trybuild layout — `tests/target` and `tests/trybuild` — which this crate has never had. It only surfaces on a cache MISS, because rust-cache skips the prune entirely on a hit. That is why it looks sporadic: it appeared on #14, not on #15, and again on #16, each time a change to the emitted crate moved the cache key. Nothing should be caching the output of a dry-run publish, so remove it once the check has served its purpose. The check itself is unchanged. Worth fixing rather than muting: a green run that reports four errors teaches everyone to skim past annotations, which is when a real one gets missed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two additions, both host-side, so the language package stays what it says it is.
@spacedevin/deck-playerWeb Audio playback, as a second package in this repo. The parser is deliberately parse-only — absent optionals stay
nulland nothing is clamped, because that is host policy. This package is that host: defaults, clamps, synthesis, transport, plus a<deck-player>element.The synths are a source-level port of Deckard's, which are already Tish and already pure functions, cross-checked against tish-gba's build-time bake. That means real hardware behaviour rather than an impression of it — the four DMG duty tables, a genuine 15/7-bit LFSR, 4-bit wave RAM, the 64/32 Hz frequency floors, the GBA's 256-step DAC staircase and ~16 kHz mixing roll-off.
All 62 songs in the tish-gba corpus parse with zero errors and zero substituted generators.
Three fidelity bugs found on the way
dutyproduced a constant-1.0buffer — a silent channel — where the bake falls back to 50%.* Nare legal and extend the pattern (the bake computes the span asmax(note ends, bars*4)). Rejecting them silenced 10 of those 62 songs.waveandlayerwere unregistered, so a sixth of the corpus reportedunexpected top-level: waveand played a generic saw instead of its own wave table.Deliberate deviations from Deckard
setTimeout— wall-clock timers are wrong for anOfflineAudioContextrender and untestable in Node.AudioContext.Unported generators (
matrixFm,patch,nes2a03, …) fall back to a plain oscillator and say so insong.substitutions.ttsVocal/meSpeakVocalare out of scope — they need the Web Speech API.Docs site
site/renders the markdown already in this repo to https://spacedevin.github.io/deck/. Adding a page means adding a.mdfile — no route, no nav entry. Sources are read in place, becausedocs/*.mdare package exports and acontent/copy would fork the canonical text..deckhighlighting comes from this package's ownisKeyword/isStepToken/classifyLinerather than a second keyword list, so it can't drift from the language.note <midi> <startBeat>notation never offers one it can't honour.llms.txtandllms-full.txtare generated from the same pages, so they can't fall behind.docs/EXAMPLES.mdis new: eight complete runnable songs, one per idea, all clean and all playable on the site.Tests
44 player tests (Song IR over the conformance corpus, pure timing math, and a recording fake
AudioContextasserting the scheduled graph). The language package is untouched and still passes at 100% line/function/statement coverage. No conformance cases were added — that corpus is the cross-implementation contract and a new case would force every profile to declare its position.Before merge
Both packages release in lockstep from one tag.
@spacedevin/deck-playerneeds its own npm trusted-publisher entry before the first publish, since trusted publishers are per package.Pages is already set to
build_type: workflow, so merging this deploys the site.