Skip to content

Commit 0ed5299

Browse files
committed
Refine Audio SFX playback layout and focused test gate - PR_26145_023-audio-sfx-playback-layout-and-test-gate
1 parent 057c3b7 commit 0ed5299

9 files changed

Lines changed: 183 additions & 46 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# PR_26145_023 Audio / SFX V8 Coverage
2+
3+
Coverage source: focused Playwright/Chromium validation for Audio / SFX playback layout and Stop All behavior.
4+
5+
Changed runtime JavaScript files:
6+
7+
(100%) tools/audio-sfx-playground-v2/js/AudioSfxPlaygroundV2App.js - 18081/18081 bytes used in focused playback layout validation
8+
(100%) tools/audio-sfx-playground-v2/js/bootstrap.js - 4991/4991 bytes used in focused playback layout validation
9+
(100%) tools/audio-sfx-playground-v2/js/controls/ActionNavControl.js - 4048/4048 bytes used in focused playback layout validation
10+
(100%) tools/audio-sfx-playground-v2/js/controls/SfxControlPanel.js - 27599/27599 bytes used in focused playback layout validation
11+
(100%) tools/audio-sfx-playground-v2/js/services/AudioSfxEngine.js - 6419/6419 bytes used in focused playback layout validation
12+
13+
Thresholds: none enforced. Missing changed runtime JS files would be reported as WARN.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# PR_26145_023 Audio / SFX Playback Layout
2+
3+
Date: 2026-05-25
4+
5+
## Targeted validation
6+
7+
- `node --check` passed for changed Audio / SFX JavaScript files:
8+
- `tools/audio-sfx-playground-v2/js/AudioSfxPlaygroundV2App.js`
9+
- `tools/audio-sfx-playground-v2/js/bootstrap.js`
10+
- `tools/audio-sfx-playground-v2/js/controls/ActionNavControl.js`
11+
- `tools/audio-sfx-playground-v2/js/controls/SfxControlPanel.js`
12+
- `tools/audio-sfx-playground-v2/js/services/AudioSfxEngine.js`
13+
- HTML guard passed for `tools/audio-sfx-playground-v2/index.html`: no inline `<script>`, `<style>`, or inline event handlers.
14+
- CSS static brace validation passed for `tools/audio-sfx-playground-v2/styles/audioSfxLayoutDensity.css`.
15+
- Existing Audio / SFX schema JSON parse passed.
16+
- Serializer validation confirmed `playbackMode: "loop"` still round-trips.
17+
18+
## npm run test:workspace-v2
19+
20+
Command:
21+
22+
```text
23+
$env:PLAYWRIGHT_BROWSERS_PATH='0'; npm.cmd run test:workspace-v2
24+
```
25+
26+
Result: `65 passed, 7 failed`.
27+
28+
Per this PR's test-gate instruction, the full suite was run once and was not rerun after unrelated failures. Failing tests:
29+
30+
- `Workspace Manager V2 bootstrap › captures Input Mapping V2 mouse drag from live input and keeps visual capture states observable`
31+
- `Workspace Manager V2 bootstrap › shows Object Vector Studio V2 layout shell and schema-only palette gate`
32+
- `Workspace Manager V2 bootstrap › compacts Object Vector Studio V2 geometry layouts and selected palette state`
33+
- `Workspace Manager V2 bootstrap › resolves game manifest schema refs from the game schema during repo discovery`
34+
- `Workspace Manager V2 bootstrap › enables object vector and collision tools only from manifest geometry without fallback defaults`
35+
- `Workspace Manager V2 bootstrap › uses header lifecycle controls and launches tools from fixed Workspace Manager V2 tiles`
36+
- `Workspace Manager V2 bootstrap › owns temporary UAT manifest seeding and launches Asset Manager V2 through session context`
37+
38+
Notes:
39+
40+
- No dirty sample/game manifest files were present in `git status`.
41+
- `games/AITargetDummy/game.manifest.json` was left untouched. It already contains an Audio / SFX payload without `playbackMode`, which contributes to the Active Game discovery failures above.
42+
43+
## Focused Playwright validation
44+
45+
Ran focused Playwright scripts against `tools/audio-sfx-playground-v2/index.html` with Chromium and a stub Web Audio API.
46+
47+
Validated:
48+
49+
- Status Clear is positioned on the Status accordion header before the accordion X.
50+
- Sound Design header contains Play, Stop, and Stop All before the accordion X.
51+
- Play, Stop, and Stop All are disabled until an SFX tile is selected.
52+
- Adding/selecting an SFX tile enables Play, Stop, and Stop All.
53+
- Looping checkbox appears under Noise Controls after the noise sliders.
54+
- Open accordion section overflow is hidden for `section.accordion-v2.tool-starter__accordion.is-open[data-accordion-v2-open="true"]`.
55+
- Density label `white-space` is no longer `nowrap`.
56+
- Wave/Noise control spacing uses the tightened CSS gaps.
57+
- One Shot playback stops after one cycle.
58+
- Looping playback continues until Stop.
59+
- Stop stops the selected SFX playback.
60+
- Stop All stops all active named SFX playback.
61+
- Status Clear clears the log from the header action.
62+
- Exported JSON preserves `playbackMode`.
63+
- Workspace dirty behavior is preserved: Play, Stop, and Stop All do not mark dirty; Looping edits do mark dirty.
64+
- No console errors.
65+
66+
Result: focused impacted validation passed.
67+
68+
## Full samples smoke
69+
70+
Skipped. This PR only impacts Audio / SFX Playground V2 UI/playback behavior.
71+
72+
## Manual validation
73+
74+
1. Open Audio / SFX Playground V2.
75+
2. Confirm Play, Stop, and Stop All appear in the Sound Design header and are disabled before selecting a tile.
76+
3. Add an SFX tile and confirm the three playback actions enable.
77+
4. Confirm One Shot playback finishes on its own.
78+
5. Check Looping, click Play, then confirm Stop ends the selected SFX.
79+
6. Start multiple named looping sounds and confirm Stop All ends all active playback.
80+
7. Confirm Clear appears in the Status header and clears the Status log.

tools/audio-sfx-playground-v2/index.html

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -86,21 +86,10 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
8686
<option value="vector-arcade">Vector Arcade</option>
8787
</select>
8888
</label>
89-
<label class="tool-starter__field" for="playbackModeSelect">
90-
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="One Shot plays once; Loop repeats until Stop is clicked.">Playback</span>
91-
<select id="playbackModeSelect">
92-
<option value="oneShot">One Shot</option>
93-
<option value="loop">Loop</option>
94-
</select>
95-
</label>
9689
<p id="styleDescription" class="tool-starter__hint audio-sfx__style-description" aria-live="polite"></p>
9790
<ul id="styleExamples" class="audio-sfx__style-examples" aria-label="Example sound ideas"></ul>
9891
<p id="settingsHelper" class="tool-starter__hint audio-sfx__settings-helper" aria-live="polite"></p>
9992
<p id="sfxValidationMessage" class="tool-starter__hint">Ready to audition.</p>
100-
<div class="audio-sfx__shape-actions">
101-
<button id="toolPlayButton" type="button">Play</button>
102-
<button id="toolStopButton" type="button">Stop</button>
103-
</div>
10493
</div>
10594
</section>
10695
</aside>
@@ -111,6 +100,11 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
111100
<span>Sound Design</span>
112101
<span class="accordion-v2__icon" aria-hidden="true">+</span>
113102
</button>
103+
<div class="audio-sfx__accordion-actions audio-sfx__design-actions" aria-label="Sound Design playback actions">
104+
<button id="toolPlayButton" type="button">Play</button>
105+
<button id="toolStopButton" type="button">Stop</button>
106+
<button id="toolStopAllButton" type="button">Stop All</button>
107+
</div>
114108
<div id="soundDesignContent" class="accordion-v2__content">
115109
<div class="audio-sfx__control-grid">
116110
<section class="audio-sfx__control-group" aria-labelledby="waveControlsHeading">
@@ -156,7 +150,7 @@ <h3 id="waveControlsHeading">Wave Controls</h3>
156150
<output id="pitchSweepValue" for="pitchSweepInput"></output>
157151
</label>
158152
</section>
159-
<section class="audio-sfx__control-group" aria-labelledby="noiseControlsHeading">
153+
<section class="audio-sfx__control-group audio-sfx__control-group--noise" aria-labelledby="noiseControlsHeading">
160154
<h3 id="noiseControlsHeading">Noise Controls</h3>
161155
<label class="tool-starter__toggle" for="noiseInput">
162156
<input id="noiseInput" type="checkbox">
@@ -177,6 +171,10 @@ <h3 id="noiseControlsHeading">Noise Controls</h3>
177171
<input id="noiseFilterInput" type="range">
178172
<output id="noiseFilterValue" for="noiseFilterInput"></output>
179173
</label>
174+
<label class="tool-starter__toggle audio-sfx__looping-toggle" for="loopingInput">
175+
<input id="loopingInput" type="checkbox">
176+
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Repeats the selected SFX until Stop is clicked.">Looping</span>
177+
</label>
180178
</section>
181179
</div>
182180
</div>
@@ -215,15 +213,15 @@ <h3 id="noiseControlsHeading">Noise Controls</h3>
215213
</div>
216214
</section>
217215

218-
<section class="accordion-v2 tool-starter__accordion is-open" data-accordion-v2-open="true">
216+
<section class="accordion-v2 tool-starter__accordion audio-sfx__status-accordion is-open" data-accordion-v2-open="true">
219217
<button class="accordion-v2__header tool-starter__status-accordion-header" type="button" aria-expanded="true" aria-controls="statusLogContent">
220218
<span>Status</span>
221219
<span class="accordion-v2__icon" aria-hidden="true">+</span>
222220
</button>
221+
<div class="audio-sfx__accordion-actions audio-sfx__status-actions" aria-label="Status actions">
222+
<button id="clearStatusButton" class="tool-starter__status-clear-button" type="button">Clear</button>
223+
</div>
223224
<div id="statusLogContent" class="accordion-v2__content">
224-
<div class="tool-starter__status-header-actions">
225-
<button id="clearStatusButton" class="tool-starter__status-clear-button" type="button">Clear</button>
226-
</div>
227225
<textarea id="statusLog" readonly rows="10" aria-label="Status log"></textarea>
228226
</div>
229227
</section>

tools/audio-sfx-playground-v2/js/AudioSfxPlaygroundV2App.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export class AudioSfxPlaygroundV2App {
108108
onToolPlay: () => {
109109
void this.play();
110110
},
111+
onToolStopAll: () => this.stopAll(),
111112
onToolStop: () => this.stop(),
112113
onWorkspaceCopyManifest: () => this.statusLog.write("Copy manifest action ready for workspace wiring."),
113114
onWorkspaceExportManifest: () => this.statusLog.write("Export manifest action ready for workspace wiring."),
@@ -187,11 +188,13 @@ export class AudioSfxPlaygroundV2App {
187188
this.preview.clear(validation.message);
188189
this.inspector.showObject({ error: validation.message });
189190
this.actionNav.setToolActionsEnabled(false);
191+
this.actionNav.setPlaybackActionsEnabled(false);
190192
return;
191193
}
192194
this.preview.render(validation.value);
193195
this.inspector.showObject(toolState);
194196
this.actionNav.setToolActionsEnabled(true);
197+
this.actionNav.setPlaybackActionsEnabled(Boolean(this.activeSoundId));
195198
}
196199

197200
handleEditorChange() {
@@ -352,9 +355,14 @@ export class AudioSfxPlaygroundV2App {
352355
});
353356
this.controls.setDeleteEnabled(Boolean(this.activeSoundId));
354357
this.controls.setRenameEnabled(Boolean(this.activeSoundId));
358+
this.actionNav.setPlaybackActionsEnabled(Boolean(this.activeSoundId));
355359
}
356360

357361
async play() {
362+
if (!this.activeSoundId) {
363+
this.statusLog.error("Select a saved SFX tile before playback.");
364+
return;
365+
}
358366
const { validation } = this.currentToolState();
359367
if (!validation.valid) {
360368
this.statusLog.error(validation.message);
@@ -396,6 +404,15 @@ export class AudioSfxPlaygroundV2App {
396404
this.statusLog.write(`Stopped ${activeName}.`);
397405
}
398406

407+
stopAll() {
408+
const stoppedCount = this.audioEngine.stopAll();
409+
if (!stoppedCount) {
410+
this.statusLog.write("No active playback found.");
411+
return;
412+
}
413+
this.statusLog.write(`Stopped ${stoppedCount} active SFX playback${stoppedCount === 1 ? "" : "s"}.`);
414+
}
415+
399416
deleteCurrentSound() {
400417
const entryIndex = this.soundEntries.findIndex((entry) => entry.id === this.activeSoundId);
401418
if (entryIndex === -1) {

tools/audio-sfx-playground-v2/js/bootstrap.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ window.addEventListener("DOMContentLoaded", () => {
3939
toolImportJsonInput: requireElement("#toolImportJsonInput"),
4040
toolNav: requireElement(".tool-starter__tool__menu"),
4141
toolPlayButton: requireElement("#toolPlayButton"),
42+
toolStopAllButton: requireElement("#toolStopAllButton"),
4243
toolStopButton: requireElement("#toolStopButton"),
4344
workspaceCopyManifestButton: requireElement("#workspaceCopyManifestButton"),
4445
workspaceExportManifestButton: requireElement("#workspaceExportManifestButton"),
@@ -63,7 +64,7 @@ window.addEventListener("DOMContentLoaded", () => {
6364
noiseFilterInput: requireElement("#noiseFilterInput"),
6465
noiseFilterValue: requireElement("#noiseFilterValue"),
6566
noiseInput: requireElement("#noiseInput"),
66-
playbackModeSelect: requireElement("#playbackModeSelect"),
67+
loopingInput: requireElement("#loopingInput"),
6768
pitchSweepInput: requireElement("#pitchSweepInput"),
6869
pitchSweepValue: requireElement("#pitchSweepValue"),
6970
releaseInput: requireElement("#releaseInput"),

tools/audio-sfx-playground-v2/js/controls/ActionNavControl.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export class ActionNavControl {
99
toolImportJsonInput,
1010
toolNav,
1111
toolPlayButton,
12+
toolStopAllButton,
1213
toolStopButton,
1314
workspaceCopyManifestButton,
1415
workspaceExportManifestButton,
@@ -23,6 +24,7 @@ export class ActionNavControl {
2324
this.toolImportJsonInput = toolImportJsonInput;
2425
this.toolNav = toolNav;
2526
this.toolPlayButton = toolPlayButton;
27+
this.toolStopAllButton = toolStopAllButton;
2628
this.toolStopButton = toolStopButton;
2729
this.workspaceCopyManifestButton = workspaceCopyManifestButton;
2830
this.workspaceExportManifestButton = workspaceExportManifestButton;
@@ -36,13 +38,15 @@ export class ActionNavControl {
3638
onToolExportToolState,
3739
onToolImportJson,
3840
onToolPlay,
41+
onToolStopAll,
3942
onToolStop,
4043
onWorkspaceCopyManifest,
4144
onWorkspaceExportManifest,
4245
onWorkspaceImportManifest
4346
}) {
4447
this.applyLaunchMode();
4548
this.toolPlayButton.addEventListener("click", onToolPlay);
49+
this.toolStopAllButton.addEventListener("click", onToolStopAll);
4650
this.toolStopButton.addEventListener("click", onToolStop);
4751
this.toolImportJsonButton.addEventListener("click", () => {
4852
this.toolImportJsonInput.value = "";
@@ -79,12 +83,16 @@ export class ActionNavControl {
7983
}
8084

8185
setToolActionsEnabled(isEnabled) {
82-
this.toolPlayButton.disabled = !isEnabled;
83-
this.toolStopButton.disabled = !isEnabled;
8486
this.toolCopyJsonButton.disabled = !isEnabled;
8587
this.toolExportToolStateButton.disabled = !isEnabled;
8688
}
8789

90+
setPlaybackActionsEnabled(isEnabled) {
91+
this.toolPlayButton.disabled = !isEnabled;
92+
this.toolStopAllButton.disabled = !isEnabled;
93+
this.toolStopButton.disabled = !isEnabled;
94+
}
95+
8896
workspaceManagerUrl() {
8997
const url = new URL("../workspace-manager-v2/index.html", this.location.href);
9098
const params = new URLSearchParams(this.location.search);

tools/audio-sfx-playground-v2/js/controls/SfxControlPanel.js

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,6 @@ const DEFAULT_SOUND = Object.freeze({
191191
});
192192

193193
const ALLOWED_WAVEFORMS = Object.freeze(new Set(["sine", "square", "triangle", "sawtooth", "noise"]));
194-
const ALLOWED_PLAYBACK_MODES = Object.freeze(new Set(["oneShot", "loop"]));
195194
const STYLE_PROFILES = Object.freeze({
196195
"pure-tone": {
197196
durationMs: 240,
@@ -338,14 +337,14 @@ export class SfxControlPanel {
338337
frequencyInput,
339338
frequencyValue,
340339
nameInput,
340+
loopingInput,
341341
noiseAmountInput,
342342
noiseAmountValue,
343343
noiseDecayInput,
344344
noiseDecayValue,
345345
noiseFilterInput,
346346
noiseFilterValue,
347347
noiseInput,
348-
playbackModeSelect,
349348
pitchSweepInput,
350349
pitchSweepValue,
351350
releaseInput,
@@ -369,6 +368,7 @@ export class SfxControlPanel {
369368
this.durationValue = durationValue;
370369
this.frequencyInput = frequencyInput;
371370
this.frequencyValue = frequencyValue;
371+
this.loopingInput = loopingInput;
372372
this.nameInput = nameInput;
373373
this.noiseAmountInput = noiseAmountInput;
374374
this.noiseAmountValue = noiseAmountValue;
@@ -377,7 +377,6 @@ export class SfxControlPanel {
377377
this.noiseFilterInput = noiseFilterInput;
378378
this.noiseFilterValue = noiseFilterValue;
379379
this.noiseInput = noiseInput;
380-
this.playbackModeSelect = playbackModeSelect;
381380
this.pitchSweepInput = pitchSweepInput;
382381
this.pitchSweepValue = pitchSweepValue;
383382
this.releaseInput = releaseInput;
@@ -410,11 +409,11 @@ export class SfxControlPanel {
410409
this.attackInput,
411410
this.durationInput,
412411
this.frequencyInput,
412+
this.loopingInput,
413413
this.noiseAmountInput,
414414
this.noiseDecayInput,
415415
this.noiseFilterInput,
416416
this.noiseInput,
417-
this.playbackModeSelect,
418417
this.pitchSweepInput,
419418
this.releaseInput,
420419
this.volumeInput,
@@ -488,11 +487,11 @@ export class SfxControlPanel {
488487
this.durationInput.value = String(sound.durationMs);
489488
this.frequencyInput.value = String(sound.frequencyHz);
490489
this.nameInput.value = sound.name;
490+
this.loopingInput.checked = sound.playbackMode === "loop";
491491
this.noiseInput.checked = sound.noise;
492492
this.noiseAmountInput.value = String(sound.noiseAmount);
493493
this.noiseDecayInput.value = String(sound.noiseDecayMs);
494494
this.noiseFilterInput.value = String(sound.noiseFilterHz);
495-
this.playbackModeSelect.value = sound.playbackMode;
496495
this.pitchSweepInput.value = String(sound.pitchSweepCents);
497496
this.releaseInput.value = String(sound.releaseMs);
498497
this.volumeInput.value = String(sound.volume);
@@ -682,9 +681,6 @@ export class SfxControlPanel {
682681
if (!ALLOWED_WAVEFORMS.has(this.waveformSelect.value)) {
683682
return { valid: false, message: `Unsupported waveform: ${this.waveformSelect.value}.` };
684683
}
685-
if (!ALLOWED_PLAYBACK_MODES.has(this.playbackModeSelect.value)) {
686-
return { valid: false, message: `Unsupported playback mode: ${this.playbackModeSelect.value}.` };
687-
}
688684
const frequency = readRange(this.frequencyInput, this.activeSliderLimits.frequencyHz);
689685
const duration = readRange(this.durationInput, this.activeSliderLimits.durationMs);
690686
const attack = readRange(this.attackInput, this.activeSliderLimits.attackMs);
@@ -712,7 +708,7 @@ export class SfxControlPanel {
712708
noiseAmount: Number(noiseAmount.value.toFixed(2)),
713709
noiseDecayMs: Math.round(noiseDecay.value),
714710
noiseFilterHz: Math.round(noiseFilter.value),
715-
playbackMode: this.playbackModeSelect.value,
711+
playbackMode: this.loopingInput.checked ? "loop" : "oneShot",
716712
pitchSweepCents: Math.round(pitchSweep.value),
717713
releaseMs: Math.round(release.value),
718714
volume: Number(volume.value.toFixed(2)),

tools/audio-sfx-playground-v2/js/services/AudioSfxEngine.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,4 +186,10 @@ export class AudioSfxEngine {
186186
this.activePlaybacks.delete(soundName);
187187
return true;
188188
}
189+
190+
stopAll() {
191+
const soundNames = Array.from(this.activePlaybacks.keys());
192+
soundNames.forEach((soundName) => this.stop(soundName));
193+
return soundNames.length;
194+
}
189195
}

0 commit comments

Comments
 (0)