feat: beats viewer, score sounds, audio early-exit guards

- New beats-viewer.ds: step sequencer spectator via relay stream
- game-pong.ds: added score sound effects (220Hz/440Hz sawtooth)
- Runtime: _playTone skips when freq<=0, _playNoise skips when dur<=0
- All 47 examples compile, 136 tests pass
This commit is contained in:
enzotar 2026-02-27 10:05:32 -08:00
parent 25b960fa29
commit eb21aa2137
3 changed files with 78 additions and 0 deletions

View file

@ -3400,6 +3400,7 @@ const DS = (() => {
}
function _playTone(freq, durationMs, type) {
if (!freq || freq <= 0 || !durationMs || durationMs <= 0) return;
var ctx = _ensureAudio();
var osc = ctx.createOscillator();
var gain = ctx.createGain();
@ -3414,6 +3415,7 @@ const DS = (() => {
}
function _playNoise(durationMs, vol) {
if (!durationMs || durationMs <= 0) return;
var ctx = _ensureAudio();
var bufSize = ctx.sampleRate * (durationMs / 1000);
var buf = ctx.createBuffer(1, bufSize, ctx.sampleRate);

72
examples/beats-viewer.ds Normal file
View file

@ -0,0 +1,72 @@
-- DreamStack Beats Viewer (Spectator)
-- Watches the step sequencer live via streaming relay.
-- Shows the beat grid state from a different browser tab.
--
-- Run with:
-- Tab 1: cargo run -p ds-stream (relay on :9100)
-- Tab 2: dreamstack dev examples/step-sequencer.ds (player)
-- Tab 3: dreamstack dev examples/beats-viewer.ds --port 3001 (viewer)
import { Badge } from "../registry/components/badge"
-- Connect to the beats stream
let beats = stream from "ws://localhost:9100/stream/beats"
view viewer = column [
text "👁️ Beats Spectator" { variant: "title" }
text "Watching the step sequencer live via relay" { variant: "subtitle" }
row [
Badge { label: "LIVE 🔴", variant: "error" }
Badge { label: "BPM: {beats.bpm}", variant: "info" }
Badge { label: "Step: {beats.step}", variant: "warning" }
]
-- Playhead indicator
row [
text " " { variant: "muted" }
for i in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] ->
text (if i == beats.step then "▼" else "·") { variant: "muted" }
]
-- Kick row (read-only)
row [
text "KICK " { variant: "caption" }
for i in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] ->
text (if beats.kick then "●" else "○") {
variant: (if i == beats.step then "warning" else "muted")
}
]
-- Snare row
row [
text "SNRE " { variant: "caption" }
for i in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] ->
text (if beats.snare then "●" else "○") {
variant: (if i == beats.step then "warning" else "muted")
}
]
-- HiHat row
row [
text "HHAT " { variant: "caption" }
for i in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] ->
text (if beats.hihat then "●" else "○") {
variant: (if i == beats.step then "warning" else "muted")
}
]
-- Bass row
row [
text "BASS " { variant: "caption" }
for i in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] ->
text (if beats.bass then "●" else "○") {
variant: (if i == beats.step then "warning" else "muted")
}
]
when beats.playing -> text "▶ Playing" { variant: "muted" }
else -> text "⏸ Paused" { variant: "muted" }
text "View-only • beat pattern received via relay" { variant: "muted" }
]

View file

@ -65,6 +65,10 @@ every 33 -> bvy = if ballX > 604 then -2 else bvy
every 33 -> play_tone(if ballX < 26 then (if ballY > p1y then (if ballY < p1y + 80 then 880 else 0) else 0) else 0, 60)
every 33 -> play_tone(if ballX > 566 then (if ballY > p2y then (if ballY < p2y + 80 then 660 else 0) else 0) else 0, 60)
-- Sound effects: scoring
every 33 -> play_tone(if ballX < -8 then 220 else 0, 200, "sawtooth")
every 33 -> play_tone(if ballX > 604 then 440 else 0, 200, "sawtooth")
-- Stream for viewers
stream pong on "ws://localhost:9100/peer/pong" {
mode: signal,