PSY6 is a browser-based psytrance groovebox: pooled-voice audio engine, worker-timed scheduler, deterministic pattern model. Built on psy-foundation — the shared musical infrastructure of the PSY device family.
LOCAL · DETERMINISTIC · NO SERVER · NO TELEMETRY · NO BUILD STEP
| Path | What it is |
|---|---|
index.html |
The PSY6 device — standalone groovebox (power-on screen, Perform/Sequencer/Sound/Mixer/Self-Gate tabs, CO-PILOT panel, section arranger). Self-contained by design. |
worklets/psy-engine.js |
PSY6 real-time audio engine — single AudioWorkletProcessor (transport, ring-buffer event queue, preallocated voice pool, master chain). |
worklets/psy-dsp.js |
PSY6 DSP primitives — Moog ladder, polyBLEP saw/square, saturation, phaser, bus EQ (AudioWorkletProcessors). |
soundBank.js, factory-presets.js |
Factory preset data used by the device. |
foundation/ |
psy-foundation — shared packages (music, material, learning, dsp, scheduler, transport, protocol, device-sdk, analysis, fixtures, composition). Single source of truth for musical primitives. The device consumes foundation/learning/bandit.mjs (contextual bandit with abstention) for the CO-PILOT. See FOUNDATION_API.md. |
tests/ |
Bun test suite for foundation packages. |
playground/ |
The PSY6 browser playground (deployed to Cloudflare Pages as project psy6). |
data/ |
scales / motifs / rhythms / presets / styles JSON. |
samples/ |
Drum one-shot sample manifest + WAVs. |
tools/verify.mjs |
Repository verification gates (syntax + document structure) — run by CI before deploy. |
# Device (ES modules — needs an HTTP origin, not file://):
npx serve . # then visit /
# Playground (what Cloudflare Pages deploys):
npx serve . # then visit /playground/No bundler, no install, no account. Everything runs locally in your browser.
bun test # 336 tests across 28 files — 336 pass / 0 fail (326537 expect() calls)
node tools/verify.mjs # syntax + structure gates (CI runs this before deploy) — GREEN
bun tools/e2e.mjs # headless-Chrome Self-Gate evidence (CI job `gates`) — JSON outSuite breakdown (all runnable with bun test):
| File | Tests | Covers |
|---|---|---|
tests/voice-stealing.test.ts |
11 | worklet priority-tier voice allocation |
tests/determinism.test.ts |
18 | per-bar seeding, groove templates, micro timing |
tests/master-oversampling.test.ts |
4 | 2x oversampled master saturation + aliasing benchmark + non-silent-output guard |
tests/foundation-primitives.test.ts |
13 | foundation PRNG / fnv1a / scale tables (pinned vectors) |
tests/soundbank.test.ts |
4 | sound bank coherence |
tests/copilot.test.ts |
18 | co-pilot contextual bandit: context building, reward mapping, serialization round-trip, determinism, foundation extension |
tests/arranger.test.ts |
13 | section arranger: bar-quantized advance, persistence, manual override, paused transport + v0.6.0 timeline editor ops (move/insert), share round-trip, PLAY SONG offline-sim, song info |
tests/sidechain.test.ts |
10 | kick-triggered sidechain: envelope shape, overlap continuity, project round-trip |
tests/sends.test.ts |
10 | BPM-synced delay divisions, feedback clamps, deterministic IR, project round-trip |
tests/bounce.test.ts |
8 | bounce schedule determinism, WAV header/data integrity, clipping |
tests/midi.test.ts |
20 | MIDI IN core: note routing, CC learn + round-trip, param dispatch, CC0/CC123 rules, provider injection |
tests/capture.test.ts |
7 | live capture: buffer growth accounting, bar-quantization math, bounce-encoder reuse |
tests/stems.test.ts |
6 | stem discovery, per-track schedule determinism, full-mix hash unchanged |
tests/share.test.ts |
12 | share links: canonical ordering, round-trip, determinism, learner survival, size guards |
tests/limits.test.ts |
14 | v0.5.0 ceilings: 16 tracks / 128 steps / 64 scenes, mixed loopLen, addTrack, step-alias regression, legacy byte-stability |
tests/scenes.test.ts |
14 | scene bank: add/duplicate/clear/reorder/rename/color/bars/fill, chain over 32+ scenes, launch semantics, persistence |
tests/params.test.ts |
12 | param registry completeness + clamps, recordPoint/quantStep math, applyLanes state-vs-lock, MIDI→lane mapping |
tests/composer.test.ts |
41 | composer determinism, 7-section structure, length ±5%, step invariants, 20-seed project-wide uniqueness, output integrity + v0.7.0 section variants (pairwise ≥ 0.15, KICK-SACRED bound, pinned form fingerprint, pinned legacy hashes) + FOREST/HI-TECH recipes + v0.9.0 chord-progressions (harmonic invariant via the shared expansion, diversity, rhythm byte-identity) + 12/20-min growth (extended chain, allocateBars regression, SONG_HARD_MAX_SEC refusal) |
tests/evolution.test.ts |
15 | v0.9.0 per-bar evolution: OFF byte-identity contract (pinned post-P1 schedule), ON diff ≥200, replay determinism, intensity-0 == OFF, op hygiene (chord-root rolls, lane precedence, clamps), live absBar mapping |
tests/library.test.ts |
7 | v0.9.0 song library: recipe round-trip (compose → recipeFromProject → composeRecipe byte-identical), CRUD + deterministic ids, JSON/save/loadProjectObj/share persistence, legacy null, G33 support |
tests/midifile.test.ts |
9 | v0.7.0 MIDI export: format-1 writer, VLQ multi-byte, stable ordering, dependency-free parse-back, .mid == WAV schedule note-for-note identity, byte-identical exports |
tests/follow.test.ts |
13 | v0.7.0 follow actions: model validation, followBars precedence, all modes' 20-transition simulations, prob=0 fallback, seeded replayability, JSON + share round-trips |
tests/mixsnap.test.ts |
14 | v0.8.0 scene mix snapshots: canonical validation/clamps, registry application, walk-order launch trace, persistence + share round-trips, composer energy-curve payloads (kick excluded), determinism incl. snapshots, form-fp unchanged |
tests/master.test.ts |
9 | v0.8.0 master section: ensureMaster backfill/clamps, 9 registry params apply, compOn rounding, project-level exclusion, MIDI denorm, legacy load idempotence, share round-trip |
tests/stems-song.test.ts |
6 | v0.8.0 song stems + section bounce: songStemTracks, stems memory caps (per-stem 10 min, 60 audio-minute budget), sectionFrames formula, songFrames unchanged |
tests/usability.test.ts |
7 | shortcut registry (no collisions, taskbook bindings), demo recipes recompose + boot |
tests/song.test.ts |
12 | v0.6.0 song render: phase rules == live-scheduler oracle, frame-count formula (pinned number), sections/fills, schedule determinism, duration guard, cancel contract |
tests/pwa.test.ts |
10 | v0.6.0 PWA: SW CACHE_VERSION == CHANGELOG latest, network-first + cleanup + claim pieces, manifest/icon integrity, deterministic icon generator |
The on-device Self-Gate (RUN SELF-GATE button, Tests tab) is also run by CI:
.github/workflows/ci-gates.yml job gates boots the real device in headless
Chrome (tools/e2e.mjs, autoplay bypass + fresh profile + no-store server) and
asserts the deterministic offline subset — machine-readable JSON per gate
(id, pass, evidence numbers) is uploaded as a CI artifact.
Honest subset classification (v0.4.0):
| Gates | Class | Where asserted |
|---|---|---|
G2, G5, G6, G8, G10, G16, G19 |
pure computation (hash/save-load/macro/pools/bandit/MIDI core/share codec) | CI + local |
G1-TECHNO, G1-PSYTRANCE, G1-TRANCE, G1-PROGRESSIVE |
deterministic OfflineAudioContext render | CI + local |
G9, G11, G12, G13, G14, G15, G18 |
deterministic OfflineAudioContext render (steal counters / sidechain / sends / bounce / drain / default-pool overload / stem isolation) | CI + local |
G17 (live capture), G25 (record song) |
realtime ScriptProcessor tap + real scheduler | run on-device; CI reports them as non-asserted info — local-only assertions |
G21, G22, G23, G24 |
v0.5.0/v0.6.0 offline+pure set (long patterns / automation / composer / song render) | CI + local |
G26 (MIDI export), G27 (follow actions) |
v0.7.0 offline+pure set (format-1 parse-back / seeded chain simulation) | CI + local |
G28 (scene mix snapshots), G29 (master EQ+glue), G30 (song stems + section bounce) |
v0.8.0 offline+pure set (snapshot RMS ratio + null-mix control / neutral tolerance + crest compression / stem frames + RMS ordering + slice equality) | CI + local |
G31 (chord progression engine), G32 (per-bar evolution), G33 (song library) |
v0.9.0 offline+pure set (0 chord-tone violations via the shared expansion + determinism + diversity / OFF==pin + ON diff ≥200 + replay + intensity-0 / recipes compose deterministically + save/share round-trips + canonical rebuild) | CI + local |
G14w, G15w (WORKLET engine reduced set) |
worklet offline render | local-only — worklet rendering is environment-sensitive in CI; exercised from the live site at release |
Gate-truth accounting (v0.9.0 — canonical inventory lives as a comment above
runSelfGate() in js/ui/tests.js): the device runs 32 MAIN entries, of
which 30 are hard (offline/pure — CI asserts all 30, including G24 song
render, G26 MIDI export, G27 follow actions, G28 snapshots, G29 master,
G30 stems/sections, G31 progressions, G32 evolution, G33 library) and
2 are evidence-only realtime (G17 live capture, G25 record song — they
run on-device every time, are reported as info in CI, and are exercised
from the production URL at every release). WORKLET: 3/3 reduced set. Numbering gaps G3/G4/G7/G20
never existed in any shipped commit (verified with git log -S across all
history) and are left unrenumbered.
Note: although G9/G14/G15 were originally labelled "realtime-ish", code
inspection (js/ui/tests.js) shows that in MAIN mode they run entirely through
OfflineAudioContext with fixed event schedules — they are deterministic
offline renders, and their criteria are inequalities/integer counters
(peak/rms thresholds, kicks===16), never bit-exact audio, so they are
stable across Chrome versions. CI has no realtime dependency.
CI layout: job verify (verify.mjs + bun test) → job gates
(headless Chrome e2e; blocking, one automatic retry of the driver before
going red — no continue-on-error anywhere).
bun test tests/master-oversampling.test.ts prints the numbers it asserts
against. Latest run — sawtooth sweep 12→16 kHz @ 44.1 kHz through the real
worklet MasterChain, alias-only band 16.5–22.05 kHz:
- native saturation: 68.5 dB alias-band energy
- 2x oversampled saturation: 60.4 dB
- reduction: 8.2 dB
Correction (v0.3.0): an earlier version of this document claimed a
79.6 dB reduction. That claim was vacuous: the oversampler never advanced
its input ring cursor (osInIdx), so the “oversampled” path re-read stale
input and produced a heavily-attenuated (near-silent) output — the old number
measured near-silence, not alias reduction. The cursor now advances, the
benchmark asserts the honest figure, and a non-silent-output guard test
(oversampled-path peak > 0.5, measured 0.729) prevents a silent-output
regression from faking the number again.
Device Self-Gate (Self-Gate tab → RUN SELF-GATE): 19/19 passed in the default MAIN engine (v0.4.0: + G16 MIDI, G17 live capture, G18 stems, G19 share links), including:
- G9 — 64 consecutive hats + kick on every 4th step under deliberate pool
overload (3-voice drum pool):
kicks=16/16 hats=64/64 tier0Steals=0 steals=70/0/2 peak=0.752(the kick is never dropped, zero tier-0 voice starvation). - G10 — the CO-PILOT learner ranks a consistently rewarded action above a
zero-reward one (
fillAvg=1.00(n=45) > varAvg=0.00(n=2), probeexploit fill) and abstains (DO_NOTHING) when every candidate's expected reward is below the threshold. - G11 (sidechain) — kick ducks
scAmount>0buses ≥60 % within the attack window, full recovery before the next kick, zero automation when everyscAmount=0:dipMin=72% recoveryMin=100% amt0Events=0 amt75Events=8. - G12 (sends) — send>0 → signal in the bus output, send=0 → silent tail;
BPM-synced delay times at 145 BPM for 1/8 / 3/16 / 1/4 =
206.9/310.3/413.8 ms; IR byte-identical to the canonical seeded PRNG. - G13 (bounce) — offline WAV render spans the exact scheduled sample
count, is non-silent, and its event schedule hash is identical across
renders:
samples=148192/148192 rms=0.089 schedIdentical=true. - G14/G15 — full-project render with zero residual events
(
peak=0.785 residualEvents=0) and overload at DEFAULT pools (tier0Steals=0 hatSteals=41 kicks=16/16 peak=1.016).
In the optional WORKLET engine the Self-Gate runs a reduced but real set:
3/3 passed (G2 deterministic build; G14w boot + sample-accurate queue
drain + all kicks voiced, peak=0.710 residualEvents=0 kicksVoiced=8/8;
G15w overload via worklet stats, tier0Victims=0 hatSteals=188 kicksVoiced=16/16 peak=0.938).
v0.7.0 closes the "complete, long, UNIQUE content" gap and adds standard interchange: no composed section ever repeats identically, two new styles, standard MIDI file export, and seeded follow actions for performance.
- SECTION VARIANTS — every section family used more than once in the
arranger derives variant scenes (
DROP,DROP 2,DROP 3, …) through deterministic seeded ops (hat density/offset swap, bass octave shifts + velocity re-jitter, lead motif re-processed through the foundation MotifTransformer, perc repositioning) plus a per-variant lane delta via the param registry (cutoff/res/detune/send curves open progressively across variants). KICK IS SACRED: kick patterns never move — velocity accents only (|Δvel| ≤ 0.1, asserted). Difference contract: every pair within a family (base included) reachesvariantStepDiff ≥ 0.15(documented union-normalized metric); measured minimum 0.306 across all styles and lengths. The form, total length and base patterns are unchanged (form fingerprint pinned to the v0.6.0 hashd0c5f32f032f2a88). - 5 COMPOSER STYLES — FULL-ON 145, DARK-PSY 148, PROGRESSIVE 138, FOREST ~150 (harmonicMinor, longer builds, denser hats, rolling forest bass) and HI-TECH ~155 (glitch perc density, energy variance, per-bar riser sweeps). Legacy recipes are byte-pinned; the new ones are full recipes in the same dict.
- EXPORT MIDI — standard format-1 MIDI file of the WHOLE arranger from
the SAME song expansion the offline WAV renderer walks (the
.midequals the WAV schedule, asserted note-for-note). 1 step = 120 ticks @ ppq 480; melodic tracks → channels 1–8, drums → channel 10 (GM); tempo meta from the project BPM. Trigger-map durations (1 step); the WAV is the authoritative sound. - FOLLOW ACTIONS (chain mode only) — per-scene
next / prev / random / scene → targetwith probability (miss → documentednextfallback) andafterBarsoverride. Random is SEEDED (fnv(projectSeed + ':' + transitionCounter)) — the same seed + start replays the identical sequence (G27 pins it). PRECEDENCE: PLAY SONG always follows the arranger and ignores follow actions.
- CHORD PROGRESSION ENGINE — every composed project now carries a
seeded chord progression (
p.harmony): 12 templates per style family (4/8-bar diatonic loops, mode-aware triad voicings), picked byfnv1a(seed+':prog'). Bass roots, lead-motif harmonization (nearest chord-tone snap) and pad/arp voicings follow the active bar's chord; kick/hats/perc/snare/fx are byte-identical to v0.8.0 (pinned digests). Harmonic invariant: 0 off-chord tonal notes across 69k+ audited notes via the shared songSteps expansion (G31). 10+ distinct progressions across 20 seeds per style. - PER-BAR EVOLUTION (Perform tab → arranger panel) — opt-in
(
p.evolution, default OFF) deterministic section morphing: hat density shifts, chord-root bass rolls, ±1-scale-degree lead contour, perc ghosts, cutoff/sendA creep — seeded per (evolution seed, song bar) through the existing event machinery. Precedence documented: snapshot launch → evolution → lane automation (lane-covered pairs win per-step). Live PLAY SONG keys evolution to the arranger position — the same bar morphs identically offline. Evolution-OFF determinism contract: OFF renders are BYTE-IDENTICAL to the pre-evolution engine (G32 pins the schedule hash); intensity-0 behaves exactly like OFF; ON replays hash-identically. - SONG LIBRARY — multi-song projects: the album stores composer
RECIPES (style/seed/length, ~100 bytes each), never snapshots; LOAD
re-renders the recipe in memory via the deterministic composer. ADD
CURRENT recovers the recipe of a composed project (free-form projects
honestly report "recipe unavailable"); COMPOSE NEW from the drawer keeps
the album (stash/restore); LOAD is confirm-if-dirty; the active song
badges "▶ playing" during PLAY SONG. The library rides save/export/
share/RESUME and
loadProjectObjrebuilds it canonically. The plain header COMPOSE starts a fresh project by design. - 12 AND 20 MINUTE FORMS — lengths 3/5/8/12/20 min (±5%, measured max error 0.48%). >8-min forms use an 11-section chain (DROP3, double-BREAK, BRIDGE, OUTRO2) with behavior mapping (DROP3 plays like DROP, BRIDGE like BREAK). 3/5/8-min outputs are byte-identical (pinned). Memory tiers: full-song bounce refuses >30 min before any Web Audio work (SONG_HARD_MAX_SEC); 10–30 min renders require explicit confirm and show the progress/cancel modal; stems and SECTION bounce stay capped at 10 min. Known storage limit (documented, honest): 12/20-min projects exceed the ~5 MB localStorage quota (12-min ≈ 3.8 MB, 20-min ≈ 6.4 MB JSON) — SAVE shows 'SAVE FAILED' by design; EXPORT (file) and SHARE are unaffected; RESUME of long forms is best-effort.
- SCENE MIX SNAPSHOTS — every scene can carry a mix identity
(
scene.mix: per-track vol/pan/sendA/sendB/scAmount, optional master params, optional note). Applied on EVERY launch path (instant click, the quantized bar-boundary launch that PLAY SONG / chain / follow actions / manual launches all share, and the offline song render) through ONE primitive (applySceneMix) with a glide anchored at the launch point. Precedence: the snapshot applies at the launch; per-step automation lanes evaluate on top — continuous automation wins per-step, exactly as documented in js/scenes.js. Null-mix scenes are byte-identical to legacy behavior (opt-in; G28 asserts the event schedule is unchanged). The scene bank has MIX→SCENE (capture the current mixer — mute/solo deliberately not captured), ×MIX, and a MIX badge. The composer populates snapshots from the section energy curve (INTRO low → DROP full/dry/bass-duck → BREAK spatial/duck-off → RISER swell → OUTRO fall; variants lean pan ±0.12; the kick level NEVER appears in a composer snapshot). Form fingerprint unchanged (d0c5f32f032f2a88); whole-project legacy hashes re-pinned (documented in CHANGELOG 0.8.0). - MASTER SECTION (Mixer tab → MASTER): EQ3 (low shelf 100 Hz / peak
1 kHz Q 0.8 / high shelf 8 kHz, ±12 dB) + glue compressor (−40..0 dB,
1..20:1, 1..100 ms, 20..1000 ms, makeup 0..24 dB, GLUE ON/OFF — bypass
removes the node from the chain). Neutral by default: existing projects
render identically within a measured 1.79e-7 max sample diff (G29).
All 9 params are registry params: lane-automatable (track −1),
ARM-AUTO-recordable, MIDI-learnable (
master.<param>), snapshot-able. - SONG STEMS (bounce modal → MODE SONG → STEMS checkbox): one
psy6-song-stem-<track>.wavper non-empty track through the SAME renderSong (trackFilter — isolation by not spawning other voices, no signal math). Sequential downloads with progress. Memory caps (songStemsGuard): per-stem ≤ 10 min with tail; total budget Σ stems × duration ≤ 60 audio-minutes — > 6 stems on a long song is exactly what this refuses (toast). - SECTION BOUNCE (Perform tab → timeline: click selects, shift+click
extends a contiguous range → BOUNCE SECTION):
psy6-section-<scene>-<idx> .wav— the selected arranger range as it appears in the song (slice of the full arrangement render; music window sample-exact vs the full render, G30 measured 2.98e-7). File = 0.05 s pre-roll + range music + 2-bar FX tail; formula asserted. Renders the full arrangement (single renderer) and slices — cost equals a SONG bounce; the 10-minute guard applies. SHARE note: the share hard cap moved 50 → 64 KB so a composed snapshot-bearing song still fits a share link.
The composer builds the arrangement; v0.6.0 makes the device able to export and record the actual song (previously BOUNCE only rendered the current pattern loop ×N — a composed 3-minute song bounced as a 26-second fragment).
- SONG BOUNCE (bounce modal → MODE
SONG, enabled when the arranger has sections): offline-renders the whole[scene,bars]chain through the SAME live machinery —stepEventsper-bar seeded groove, the live scene-launch phase rule (sc.step = sc.step % newLoop), per-scene auto-FILL, and the per-step automation player (applyLanes→syncMix/resolveMacros), so state-lane sweeps land on voices exactly as in playback. Documented frame formula:frames = ceil(sr·(0.05 + (Σbars·16 + 32)·(60/bpm/4)))— the +32 steps are a 2-bar FX release tail; the toast reports music length and with-tail length separately. Progress bar with section name; CANCEL aborts cleanly (never touches the live AudioContext). Outputpsy6-song-<bpm>bpm.wav. Render cap 10 minutes (memory guard, toast refusal beyond). - ARRANGER TIMELINE EDITOR (Perform tab): visual blocks, width ∝ bars,
scene color; click-select → bars ±, reorder ◀▶, insert-from-scene, DELETE;
total readout in sections/bars/mm:ss. All edits persist in
project.arranger(save/export/share round-trip). - PLAY SONG: jumps to section 0, quantized start when already playing, boots the transport when stopped; progress via the existing arranger state.
- RECORD SONG (transport row): captures the whole live PLAY SONG through
the existing master tap, auto-stops at the end of the final section +1 bar,
encodes with the existing WAV encoder →
psy6-song-live-<bpm>bpm.wav. - G24 (offline, CI-asserted): composed FULL-ON 3min seed 424242 → song render: frame count == formula (10,075,254 samples), all 7 sections RMS > 0.03 (measured 0.0665–0.1088), event schedule == pure oracle (evHash equality), determinism max sample diff 3.73e-7 < 1e-6 (Chrome float nondeterminism between runs is real and documented — the bound is honest).
- G25 (realtime, evidence-only): 4-bar two-section song recorded live → duration 9.381s vs 5-bar target 9.375s (skew 6 ms), RMS 0.082. G25 also exposed and fixed a real bug: a second capture in one page session included the first capture's audio (CaptureTap.start now resets state; completed captures retire their tap).
| Limit | v0.4.0 | v0.5.0 | How to reach it |
|---|---|---|---|
| Tracks | 8 | 16 | +TRACK button (Perform tab) |
| Steps per pattern-track | 32 | 128 | length select (Sequencer) |
| Scenes | 8 | 64 | +SCENE in the scene bank |
| Pattern length options | 4–32 | 8/16/32/64/128 | length select |
| Loop length cap | 96 | 1024 | automatic (LCM of track lengths) |
| Voice pools | 20 synth + 24 drum | unchanged | polyphony absorbed by priority stealing |
New projects still start 8 tracks / 16-step patterns / 8 scenes; legacy projects load and sound identically (fields backfilled; load→save byte-stable after one canonicalizing load).
64 scenes with inline rename, duplicate, clear, reorder (up/down), color tags, per-scene bars override (pre-fills the arranger), and a per-scene auto-FILL toggle (fires the existing FILL op at launch — instant or at the quantized bar boundary). Launch semantics unchanged: click = quantized, alt = instant, shift+click = assign.
Every automatable parameter (23 in the registry: synth sound, mixer, sidechain,
master, macros) has a lane. Lanes are (track, param) pairs; 'state' lanes
apply per-step through the registry (knob-equivalent), legacy 'lock' lanes
keep exact per-voice behavior. ARM-AUTO + per-lane arm records knob moves and
MIDI CC into armed lanes at the quantized playhead — multiple lanes at once.
Editor: param picker, lane list with live value readout, curve canvas + playhead.
COMPOSE (power screen + header) generates a complete unique arrangement —
INTRO→BUILD→DROP→BREAK→RISER→DROP2→OUTRO — from (seed, style, minutes):
FULL-ON 145 / DARK-PSY 148 / PROGRESSIVE 138 BPM, 3/5/8 minutes (length
error < 1 % by construction, ±5 % asserted), per-section patterns with
energy-scaled recipes, lead motif varied per section via foundation
MotifTransformer, fills, a 9th FX riser track, filter/send lane suggestions,
and the arranger chain pre-loaded. Same seed = byte-identical song; 20 seeds
→ all pairwise different. Overwrite protection: composing over a non-empty
project requires an explicit confirm and happens in a fresh in-memory project.
Keyboard shortcuts from a single tested registry (Space play/stop, arrows scene prev/next, 1-8 pads, Shift+1-8 track select, f fill, v variation, b bounce, ? help overlay, Esc close), tooltips on all header controls, two shipped demo songs (composer recipes — deterministic recomposition), touch targets ≥ 40 px on primary controls, 390 px layout verified.
Connect a MIDI controller in the Perform tab → notes play the currently
selected track (velocity = note-on velocity, MIDI note = pitch), CC learn
binds any of 57 parameters (macros, mixer incl. sendA/sendB/scAmount,
master) to a knob/fader, CC 123 = PANIC. Bindings persist in the project
(midiMap) and survive save/export/import/share. Honest scope: no MIDI
clock sync (out of scope, documented in the UI); note-off releases synth
voices only — drum one-shots are never cut mid-hit; master.vol is applied
by the MAIN engine only (the WORKLET engine persists but does not apply it);
Web MIDI needs a Chromium browser (graceful note elsewhere).
CAPTURE in the transport bar records the master output losslessly: starts on
the next bar, stops on the next bar after that (16-step quantization), and
downloads psy6-capture-<bpm>bpm.wav via the same WAV encoder as bounce.
The tap is parallel to the listening path — starting/stopping capture can
never stop playback. Honest scope: built on ScriptProcessorNode
(deprecated API, chosen deliberately: zero changes to the MAIN engine
graph; universally supported in Chrome/Firefox/Safari); quantization skew is
bounded by the 1024-frame callback (measured 6 ms on-device, tolerance
±50 ms). Capture gate G17 is realtime and asserted on-device only.
BOUNCE → MODE: STEMS renders one WAV per non-empty track
(psy6-stem-<trackName>.wav, sequential downloads) through the same
deterministic offline graph as the mix bounce. Isolation semantics are
honest: tracks with no scheduled events contribute exactly 0; a track's own
FX-delay/reverb tail and decay tail belong to its own stem.
SHARE copies a link (#p=<token>) containing the whole project — canonical
JSON key order, deflate-raw compressed, base64url — including the CO-PILOT
learner snapshot. Links are byte-identical for identical projects. A link is
NEVER auto-loaded: the power screen shows a consent banner (LOAD SHARE /
DISMISS). >6 KB links warn (browser URL limits), >50 KB are refused — use
EXPORT instead.
The power screen offers a choice of audio engine:
- MAIN (default) — the pooled Web Audio engine (
js/engine.js): preallocated synth/drum voice pools, per-track bus chain with kick-triggered sidechain ducking, BPM-synced delay + seeded-IR reverb sends, worker-timed lookahead scheduler. Full Self-Gate (15/15). - WORKLET (experimental) — the single-processor AudioWorklet engine
(
worklets/psy-engine.js, processorpsy-engine) driven through the adapter injs/worklet-engine.js. The MAIN thread schedules; the worklet fires events sample-accurately on the audio thread. Reduced Self-Gate (3/3).
WORKLET limitations (also listed on the power screen — nothing is faked): per-track sends collapse to per-BUS max; delay division not exposed (fixed 0.5 s buffer); per-track sidechain → single fixed bass-bus duck; synth editor params → worklet world params; worklet-internal reverb IR. What CANNOT map cleanly is skipped and documented, never approximated silently.
The device is PSY6. The engine worklets are worklets/psy-engine.js and
worklets/psy-dsp.js (registered processor names: psy-engine, moog-filter,
bl-saw, bl-square, saturation, phaser, bus-eq).
Historical documents in this repository (FOUNDATION_STATUS.md,
FOUNDATION_FREEZE.md) reference earlier devices in the family — PSY4 and
PSY5 — as provenance for design decisions. Those are historical records;
the current device and all engine code are PSY6.
psy-foundation (shared musical primitives)
↑
PSY6 device
├── model patterns, steps, scales, deterministic RNG
├── scheduler worker-timer + lookahead loop
├── engine pooled voices (synth 20 / drum 24), sidechain duck buses,
│ delay/reverb send buses, master chain ← MAIN (default)
├── worklet-engine model→worklet adapter ← WORKLET (experimental)
├── bounce offline WAV render (fresh graph, exact scheduling)
└── UI Perform · Sequencer · Sound · Mixer · Self-Gate
See ARCHITECTURE.md for the full specification and FOUNDATION_API.md for the versioned foundation API.
Three options — the device needs no build step and no secrets:
- GitHub Pages (live, zero secrets) —
.github/workflows/deploy-gh-pages.ymldeploys the repo root on every push tomain. Live URL: https://dudududi144-source.github.io/psy5/ (device at/, playground at/playground/). No repository secrets required. - Cloudflare Pages (needs the two secrets) —
.github/workflows/pages-deployment.yaml— pushes tomainthat touchplayground/**first run theverifygates, then deployplayground/to Cloudflare Pages (projectpsy6). Requires the repository secretsCLOUDFLARE_API_TOKENandCLOUDFLARE_ACCOUNT_IDto be configured. - Local (HTTP origin required for ES modules — not
file://):npx serve .— then visit/for the device and/playground/for the playground.
- One source of truth per piece of musical state — the device consumes
foundation/, it does not re-implement it. - Transport is not renderer, renderer is not UI.
- No device policy — PSY6 is built from foundation primitives.
- Every claim has evidence.
- The
process()hot path is allocation-free.
MIT.