Skip to content

Commit db24d7b

Browse files
committed
Tighten Audio SFX center layout and preserve tile names across style changes - PR_26145_014-audio-sfx-center-column-tight-layout
1 parent e456184 commit db24d7b

6 files changed

Lines changed: 166 additions & 30 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Audio / SFX Playground V2 Center Column Tight Layout
2+
3+
PR: `PR_26145_014-audio-sfx-center-column-tight-layout`
4+
5+
## Targeted Static Validation
6+
7+
- `node --check tools/audio-sfx-playground-v2/js/bootstrap.js` - PASS
8+
- `node --check tools/audio-sfx-playground-v2/js/AudioSfxPlaygroundV2App.js` - PASS
9+
- `node --check tools/audio-sfx-playground-v2/js/controls/SfxControlPanel.js` - PASS
10+
- HTML external script/style guard for `tools/audio-sfx-playground-v2/index.html` - PASS
11+
- `git diff --check -- tools/audio-sfx-playground-v2` - PASS, with existing LF/CRLF working-copy warnings only
12+
13+
## Focused Playwright Validation
14+
15+
Command: custom Playwright validation using the repo test server and installed Microsoft Edge at `C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe`.
16+
17+
Result: PASS
18+
19+
Validated:
20+
21+
- Audio / SFX Playground V2 launches with no console or page errors.
22+
- Center-column Sound Design top gap is reduced: grid top is 9px below content top.
23+
- Sound Design content uses available width efficiently: grid width is 604px inside a 620px content area.
24+
- Wave Controls and Noise Controls are top-aligned.
25+
- Noise Controls are not pushed lower by unused top margin.
26+
- All slider rows remain single-line with no clipping or output wrapping.
27+
- Rename button is centered below the Name row.
28+
- Sound Style changes do not rename the selected SFX tile.
29+
- Pending Name edits do not rename the selected SFX tile until Rename is clicked.
30+
- Slider focus remains on the active slider after click, and ArrowRight changes Duration by the 5ms step.
31+
- Right-column Wave Preview, Output Summary, and Status accordions still collapse and expand.
32+
33+
Measured layout highlights:
34+
35+
- `#soundDesignContent`: 620px wide
36+
- `.audio-sfx__control-grid`: 604px wide
37+
- Wave group: 297px wide
38+
- Noise group: 297px wide
39+
- Slider row height: 28px for all measured slider rows
40+
41+
## Playwright V8 Coverage
42+
43+
- `(100%) tools/audio-sfx-playground-v2/js/AudioSfxPlaygroundV2App.js - covered by focused launch, rename, style, slider, accordion, preview/export-state flows`
44+
- `(100%) tools/audio-sfx-playground-v2/js/bootstrap.js - covered by focused tool launch`
45+
- `(100%) tools/audio-sfx-playground-v2/js/controls/SfxControlPanel.js - covered by style, slider, name, rename, and validation flows`
46+
47+
Changed runtime JavaScript files were present in focused coverage.
48+
49+
## Required Workspace V2 Test
50+
51+
Command: `npm.cmd run test:workspace-v2`
52+
53+
Result: FAIL due environment browser install, not this PR's tool behavior.
54+
55+
Observed:
56+
57+
- 72 tests collected.
58+
- 1 test passed.
59+
- 71 tests failed before executing page assertions because Playwright could not launch its managed Chromium executable.
60+
- Error: `Executable doesn't exist at C:\Users\DavidQ\AppData\Local\ms-playwright\chromium-1217\chrome-win64\chrome.exe`
61+
- Playwright requested: `npx playwright install`
62+
63+
Note: PowerShell blocked the direct `npm` shim because local script execution is disabled, so the package script was run through `npm.cmd`.
64+
65+
## Full Samples Smoke
66+
67+
Skipped. This PR only impacts Audio / SFX Playground V2 layout and rename behavior.
68+
69+
## Manual Validation Steps
70+
71+
1. Open `tools/audio-sfx-playground-v2/index.html` through the repo test server or Workspace V2 tile.
72+
2. Confirm Sound Design uses the center column width with Wave Controls and Noise Controls side by side.
73+
3. Confirm Noise Controls begin at the same vertical level as Wave Controls.
74+
4. Add the default `Coin` SFX tile.
75+
5. Change Sound Style to `TTL Arcade`; confirm the tile remains named `Coin`.
76+
6. Edit Name to `Renamed Coin`; confirm the tile remains `Coin` until Rename is clicked.
77+
7. Click Rename; confirm the active tile becomes `Renamed Coin`.
78+
8. Click a slider and press arrow keys; confirm focus stays on that slider and values move by the configured step.
79+
9. Collapse and expand Wave Preview, Output Summary, and Status.
80+
81+
Expected outcome: no clipping, no slider wrapping, no console errors, no style-driven tile renaming.

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
6767
<button id="addSfxButton" type="button" aria-label="Add SFX">+</button>
6868
<button id="deleteSfxButton" type="button" aria-label="Delete selected SFX">&#128465;</button>
6969
</div>
70+
<div class="audio-sfx__rename-row">
71+
<button id="renameSfxButton" type="button">Rename</button>
72+
</div>
7073
<label class="tool-starter__field" for="styleProfileSelect">
7174
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Changes recommended slider operating ranges and clamps current values; waveform choices are never disabled by style.">Sound Style</span>
7275
<select id="styleProfileSelect">

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

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ export class AudioSfxPlaygroundV2App {
8585
this.controls.mount({
8686
onAdd: () => this.addCurrentSound(),
8787
onChange: () => this.handleEditorChange(),
88-
onDelete: () => this.deleteCurrentSound()
88+
onDelete: () => this.deleteCurrentSound(),
89+
onRename: () => this.renameCurrentSound()
8990
});
9091
this.tileList.mount({
9192
onSelect: (soundId) => this.selectSound(soundId)
@@ -97,7 +98,7 @@ export class AudioSfxPlaygroundV2App {
9798
}
9899

99100
currentToolState() {
100-
const validation = this.controls.validate();
101+
const validation = this.controls.validate({ nameOverride: this.activeSoundName() });
101102
if (!validation.valid) {
102103
this.controls.showMessage(validation.message, true);
103104
return { toolState: null, validation };
@@ -124,13 +125,8 @@ export class AudioSfxPlaygroundV2App {
124125
}
125126

126127
handleEditorChange() {
127-
const validation = this.controls.validate();
128-
if (validation.valid && this.activeSoundId && this.hasDuplicateSoundName(validation.value.name, this.activeSoundId)) {
129-
this.statusLog.error(duplicateSoundNameMessage(validation.value.name));
130-
this.controls.showMessage("Name must be unique.", true);
131-
return;
132-
}
133-
if (validation.valid && this.updateActiveSound(validation.value)) {
128+
const validation = this.controls.validate({ nameOverride: this.activeSoundName() });
129+
if (validation.valid && this.updateActiveSound(this.soundForActiveEditorValue(validation.value))) {
134130
this.renderSoundList();
135131
}
136132
this.refreshPreview();
@@ -141,6 +137,10 @@ export class AudioSfxPlaygroundV2App {
141137
return this.soundEntries.some((entry) => entry.id !== excludedSoundId && normalizeSoundName(entry.sound.name) === normalizedName);
142138
}
143139

140+
activeSoundName() {
141+
return this.soundEntries.find((entry) => entry.id === this.activeSoundId)?.sound.name || "";
142+
}
143+
144144
createSoundEntry(sound) {
145145
const entry = {
146146
id: `sfx-${this.nextSoundNumber}`,
@@ -161,6 +161,17 @@ export class AudioSfxPlaygroundV2App {
161161
return entry;
162162
}
163163

164+
soundForActiveEditorValue(sound) {
165+
const entry = this.soundEntries.find((candidate) => candidate.id === this.activeSoundId);
166+
if (!entry) {
167+
return sound;
168+
}
169+
return {
170+
...sound,
171+
name: entry.sound.name
172+
};
173+
}
174+
164175
ensureActiveSoundEntry(sound) {
165176
const activeEntry = this.soundEntries.find((entry) => entry.id === this.activeSoundId);
166177
const excludedSoundId = activeEntry?.id || "";
@@ -195,6 +206,30 @@ export class AudioSfxPlaygroundV2App {
195206
this.statusLog.write(`Added ${entry.sound.name}.`);
196207
}
197208

209+
renameCurrentSound() {
210+
const entry = this.soundEntries.find((candidate) => candidate.id === this.activeSoundId);
211+
if (!entry) {
212+
this.statusLog.error("Select a saved SFX tile before renaming.");
213+
this.controls.setRenameEnabled(false);
214+
return;
215+
}
216+
const nextName = this.controls.currentName();
217+
if (!nextName) {
218+
this.statusLog.error("Name is required.");
219+
this.controls.showMessage("Name is required.", true);
220+
return;
221+
}
222+
if (this.hasDuplicateSoundName(nextName, entry.id)) {
223+
this.statusLog.error(duplicateSoundNameMessage(nextName));
224+
this.controls.showMessage("Name must be unique.", true);
225+
return;
226+
}
227+
entry.sound.name = nextName;
228+
this.renderSoundList();
229+
this.refreshPreview();
230+
this.statusLog.write(`Renamed SFX to ${entry.sound.name}.`);
231+
}
232+
198233
selectSound(soundId) {
199234
const entry = this.soundEntries.find((candidate) => candidate.id === soundId);
200235
if (!entry) {
@@ -214,6 +249,7 @@ export class AudioSfxPlaygroundV2App {
214249
soundEntries: this.soundEntries
215250
});
216251
this.controls.setDeleteEnabled(Boolean(this.activeSoundId));
252+
this.controls.setRenameEnabled(Boolean(this.activeSoundId));
217253
}
218254

219255
async play() {
@@ -224,8 +260,9 @@ export class AudioSfxPlaygroundV2App {
224260
return;
225261
}
226262
try {
227-
await this.audioEngine.play(validation.value);
228-
this.statusLog.write(`Played ${validation.value.name}.`);
263+
const sound = this.soundForActiveEditorValue(validation.value);
264+
await this.audioEngine.play(sound);
265+
this.statusLog.write(`Played ${sound.name}.`);
229266
} catch (error) {
230267
this.statusLog.error(`Audio playback failed: ${error.message}`);
231268
}
@@ -251,11 +288,11 @@ export class AudioSfxPlaygroundV2App {
251288
}
252289

253290
exportPayload() {
254-
const validation = this.controls.validate();
291+
const validation = this.controls.validate({ nameOverride: this.activeSoundName() });
255292
if (!validation.valid) {
256293
return { valid: false, message: validation.message, toolState: null, json: "" };
257294
}
258-
const activeSoundEntry = this.ensureActiveSoundEntry(validation.value);
295+
const activeSoundEntry = this.ensureActiveSoundEntry(this.soundForActiveEditorValue(validation.value));
259296
if (!activeSoundEntry.valid) {
260297
return { valid: false, message: activeSoundEntry.message, toolState: null, json: "" };
261298
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ window.addEventListener("DOMContentLoaded", () => {
6161
pitchSweepValue: requireElement("#pitchSweepValue"),
6262
releaseInput: requireElement("#releaseInput"),
6363
releaseValue: requireElement("#releaseValue"),
64+
renameButton: requireElement("#renameSfxButton"),
6465
settingsHelper: requireElement("#settingsHelper"),
6566
styleDescription: requireElement("#styleDescription"),
6667
styleExamples: requireElement("#styleExamples"),

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

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,6 @@ const STYLE_PROFILES = Object.freeze({
194194
"pure-tone": {
195195
durationMs: 240,
196196
frequencyHz: 660,
197-
name: "Pure Tone",
198197
noise: false,
199198
noiseAmount: 0,
200199
noiseDecayMs: 80,
@@ -207,7 +206,6 @@ const STYLE_PROFILES = Object.freeze({
207206
"noise-only": {
208207
durationMs: 260,
209208
frequencyHz: 880,
210-
name: "Noise Only",
211209
noise: false,
212210
noiseAmount: 0.72,
213211
noiseDecayMs: 120,
@@ -220,7 +218,6 @@ const STYLE_PROFILES = Object.freeze({
220218
"atari-style": {
221219
durationMs: 180,
222220
frequencyHz: 520,
223-
name: "Atari Blip",
224221
noise: true,
225222
noiseAmount: 0.48,
226223
noiseDecayMs: 75,
@@ -233,7 +230,6 @@ const STYLE_PROFILES = Object.freeze({
233230
"classic-arcade": {
234231
durationMs: 220,
235232
frequencyHz: 880,
236-
name: "Classic Zap",
237233
noise: true,
238234
noiseAmount: 0.65,
239235
noiseDecayMs: 95,
@@ -246,7 +242,6 @@ const STYLE_PROFILES = Object.freeze({
246242
"early-analog": {
247243
durationMs: 360,
248244
frequencyHz: 340,
249-
name: "Analog Bloom",
250245
noise: false,
251246
noiseAmount: 0.22,
252247
noiseDecayMs: 140,
@@ -259,7 +254,6 @@ const STYLE_PROFILES = Object.freeze({
259254
"namco-style": {
260255
durationMs: 170,
261256
frequencyHz: 1040,
262-
name: "Namco Ping",
263257
noise: false,
264258
noiseAmount: 0.15,
265259
noiseDecayMs: 70,
@@ -272,7 +266,6 @@ const STYLE_PROFILES = Object.freeze({
272266
"nintendo-style": {
273267
durationMs: 260,
274268
frequencyHz: 760,
275-
name: "Nintendo Pop",
276269
noise: true,
277270
noiseAmount: 0.36,
278271
noiseDecayMs: 90,
@@ -285,7 +278,6 @@ const STYLE_PROFILES = Object.freeze({
285278
"ttl-arcade": {
286279
durationMs: 130,
287280
frequencyHz: 1180,
288-
name: "TTL Tick",
289281
noise: true,
290282
noiseAmount: 0.72,
291283
noiseDecayMs: 50,
@@ -298,7 +290,6 @@ const STYLE_PROFILES = Object.freeze({
298290
"vector-arcade": {
299291
durationMs: 300,
300292
frequencyHz: 420,
301-
name: "Vector Sweep",
302293
noise: false,
303294
noiseAmount: 0.12,
304295
noiseDecayMs: 120,
@@ -356,6 +347,7 @@ export class SfxControlPanel {
356347
pitchSweepValue,
357348
releaseInput,
358349
releaseValue,
350+
renameButton,
359351
settingsHelper,
360352
styleDescription,
361353
styleExamples,
@@ -386,6 +378,7 @@ export class SfxControlPanel {
386378
this.pitchSweepValue = pitchSweepValue;
387379
this.releaseInput = releaseInput;
388380
this.releaseValue = releaseValue;
381+
this.renameButton = renameButton;
389382
this.settingsHelper = settingsHelper;
390383
this.styleDescription = styleDescription;
391384
this.styleExamples = styleExamples;
@@ -396,22 +389,23 @@ export class SfxControlPanel {
396389
this.waveformSelect = waveformSelect;
397390
}
398391

399-
mount({ onAdd, onChange, onDelete }) {
392+
mount({ onAdd, onChange, onDelete, onRename }) {
400393
this.applySliderLimits();
401394
this.loadSound(DEFAULT_SOUND);
402395
this.addButton.addEventListener("click", onAdd);
403396
this.deleteButton.addEventListener("click", onDelete);
397+
this.renameButton.addEventListener("click", onRename);
404398
this.styleProfileSelect.addEventListener("change", () => {
405399
if (this.applyStyleProfile()) {
406400
onChange();
407401
}
408402
});
409403
this.setDeleteEnabled(false);
404+
this.setRenameEnabled(false);
410405
[
411406
this.attackInput,
412407
this.durationInput,
413408
this.frequencyInput,
414-
this.nameInput,
415409
this.noiseAmountInput,
416410
this.noiseDecayInput,
417411
this.noiseFilterInput,
@@ -520,7 +514,6 @@ export class SfxControlPanel {
520514
this.attackInput.value = String(DEFAULT_SOUND.attackMs);
521515
this.durationInput.value = String(profile.durationMs);
522516
this.frequencyInput.value = String(profile.frequencyHz);
523-
this.nameInput.value = profile.name;
524517
this.noiseInput.checked = profile.noise;
525518
this.noiseAmountInput.value = String(profile.noiseAmount);
526519
this.noiseDecayInput.value = String(profile.noiseDecayMs);
@@ -675,8 +668,8 @@ export class SfxControlPanel {
675668
return Math.min(limits.max, Math.max(limits.min, value));
676669
}
677670

678-
validate() {
679-
const name = this.nameInput.value.trim();
671+
validate({ nameOverride = "" } = {}) {
672+
const name = (nameOverride || this.nameInput.value).trim();
680673
if (!name) {
681674
return { valid: false, message: "Name is required." };
682675
}
@@ -726,4 +719,12 @@ export class SfxControlPanel {
726719
setDeleteEnabled(isEnabled) {
727720
this.deleteButton.disabled = !isEnabled;
728721
}
722+
723+
currentName() {
724+
return this.nameInput.value.trim();
725+
}
726+
727+
setRenameEnabled(isEnabled) {
728+
this.renameButton.disabled = !isEnabled;
729+
}
729730
}

0 commit comments

Comments
 (0)