Skip to content

Commit 756d26b

Browse files
committed
Add Audio SFX style descriptions and recommended slider zones - PR_26145_011-audio-sfx-style-descriptions-and-recommended-zones
1 parent 1487836 commit 756d26b

5 files changed

Lines changed: 246 additions & 1 deletion

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# PR_26145_011 Audio SFX Style Descriptions
2+
3+
## Scope
4+
5+
- Updated `tools/audio-sfx-playground-v2` only.
6+
- Added short Sound Style descriptions displayed near the Sound Style control.
7+
- Added Recommended Zone indicators to slider tracks using per-style recommended zones inside each active slider clamp range.
8+
- Preserved Sound Style min/max clamps, waveform availability, slider focus behavior, and single-line slider rows.
9+
- No `start_of_day` folders were modified.
10+
11+
## Targeted Static Validation
12+
13+
PASS:
14+
15+
- `Get-ChildItem -Recurse -File tools/audio-sfx-playground-v2/js -Filter *.js | ForEach-Object { node --check $_.FullName }`
16+
- HTML/CSS/JS static guard:
17+
- no `<style>` blocks in `tools/audio-sfx-playground-v2/index.html`
18+
- no inline `<script>` blocks
19+
- no inline event handlers
20+
- `#styleDescription` is present and wired through `bootstrap.js`
21+
- `STYLE_DESCRIPTIONS` and `RECOMMENDED_ZONE_SPAN` exist in `SfxControlPanel.js`
22+
- recommended zone CSS variables and range-track rules exist in `audioSfxLayoutDensity.css`
23+
- `git diff --check -- tools/audio-sfx-playground-v2`
24+
- PASS with Git LF/CRLF warnings only for `index.html` and `audioSfxLayoutDensity.css`.
25+
26+
## Focused Playwright Validation
27+
28+
PASS using a local repo HTTP server and Chromium:
29+
30+
- Audio / SFX Playground V2 launched at `/tools/audio-sfx-playground-v2/index.html`.
31+
- No console errors and no page errors.
32+
- Style descriptions update correctly:
33+
- Custom: `Full-range design mode for building a sound without a style preset.`
34+
- Atari-style: `Softer analog-style arcade tones with midrange sweeps.`
35+
- TTL Arcade: `Harsh digital logic-style arcade sounds.`
36+
- Vector Arcade: `Clean vector-display era synth tones.`
37+
- Recommended zone stylesheet rule loaded for the range track.
38+
- Recommended zone data and CSS variables update by style:
39+
- Custom Frequency: `20-20000 Hz`, `0%` to `100%`
40+
- Atari-style Frequency: `40-1074 Hz`, `0%` to `26.1%`
41+
- Atari-style Sweep: `-976--64 cents`, `9.3%` to `47.3%`
42+
- TTL Arcade Frequency: `841-1519 Hz`
43+
- Vector Arcade Frequency: `100-1246 Hz`
44+
- Slider clamps still work:
45+
- Atari-style Frequency min/max stayed `40` / `4000`.
46+
- TTL Arcade Frequency min/max stayed `80` / `2500`.
47+
- Vector Arcade Frequency min/max stayed `100` / `6000`.
48+
- Switching from Pure Tone after setting Frequency to `20000 Hz` visibly returned to a TTL-safe value: `1180 Hz [80-2500 Hz]`.
49+
- Waveform options remained enabled, including `noise`.
50+
- Slider focus retention passed:
51+
- Clicking `#durationInput` left focus on `durationInput`.
52+
- `ArrowRight` changed duration from `395` to `400`, preserving the 5 ms step.
53+
- All 9 density slider rows kept label, slider, and value output on one aligned row without overlap.
54+
55+
V8 coverage entries captured:
56+
57+
- `tools/audio-sfx-playground-v2/js/controls/SfxControlPanel.js`
58+
- `tools/audio-sfx-playground-v2/js/bootstrap.js`
59+
60+
## Workspace V2 Validation
61+
62+
Command:
63+
64+
```powershell
65+
$env:PLAYWRIGHT_BROWSERS_PATH='.ms-playwright'; npm.cmd run test:workspace-v2
66+
```
67+
68+
Result:
69+
70+
- Timed out after 900 seconds.
71+
- Observed 70 passing tests before timeout.
72+
- Observed failing/problem tests before timeout:
73+
- `Workspace Manager V2 bootstrap > shows Object Vector Studio V2 layout shell and schema-only palette gate`
74+
- `Workspace Manager V2 bootstrap > compacts Object Vector Studio V2 geometry layouts and selected palette state`
75+
- `Workspace Manager V2 bootstrap > owns temporary UAT manifest seeding and launches Asset Manager V2 through session context`
76+
- Test 72 was not reached before the command timeout.
77+
78+
These observed failures/timeouts are outside the Audio / SFX Playground V2 scope and match the existing Workspace V2 problem areas seen during the prior Audio SFX PR validation.
79+
80+
## Full Samples Smoke
81+
82+
Skipped per BUILD request because this PR only impacts Audio / SFX Playground V2 UI behavior.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
8383
<option value="vector-arcade">Vector Arcade</option>
8484
</select>
8585
</label>
86+
<p id="styleDescription" class="tool-starter__hint audio-sfx__style-description" aria-live="polite"></p>
8687
<label class="tool-starter__field" for="waveformSelect">
8788
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Chooses the oscillator shape or noise source; every waveform remains selectable for every Sound Style.">Wave</span>
8889
<select id="waveformSelect">

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+
styleDescription: requireElement("#styleDescription"),
6465
styleProfileSelect: requireElement("#styleProfileSelect"),
6566
validationMessage: requireElement("#sfxValidationMessage"),
6667
volumeInput: requireElement("#volumeInput"),

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

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,31 @@ const SLIDER_RANGE_UNITS = Object.freeze({
3434
volume: ""
3535
});
3636

37+
const RECOMMENDED_ZONE_SPAN = Object.freeze({
38+
attackMs: 0.36,
39+
durationMs: 0.34,
40+
frequencyHz: 0.28,
41+
noiseAmount: 0.36,
42+
noiseDecayMs: 0.34,
43+
noiseFilterHz: 0.32,
44+
pitchSweepCents: 0.38,
45+
releaseMs: 0.34,
46+
volume: 0.3
47+
});
48+
49+
const STYLE_DESCRIPTIONS = Object.freeze({
50+
custom: "Full-range design mode for building a sound without a style preset.",
51+
"pure-tone": "Clean oscillator tones with little noise and broad pitch control.",
52+
"noise-only": "Transient noise bursts for impacts, hits, glitches, and static.",
53+
"atari-style": "Softer analog-style arcade tones with midrange sweeps.",
54+
"classic-arcade": "Punchy cabinet-era blips, zaps, and short mixed noise accents.",
55+
"early-analog": "Warm early synth effects with slower envelopes and wider motion.",
56+
"namco-style": "Bright melodic arcade pings with compact timing and clear pitch.",
57+
"nintendo-style": "Crisp console-style square tones with tight envelopes.",
58+
"ttl-arcade": "Harsh digital logic-style arcade sounds.",
59+
"vector-arcade": "Clean vector-display era synth tones."
60+
});
61+
3762
const STYLE_CLAMPS = Object.freeze({
3863
"pure-tone": Object.freeze({
3964
attackMs: Object.freeze({ min: 0, max: 120 }),
@@ -292,6 +317,10 @@ function formatRangeNumber(soundKey, value) {
292317
return String(Math.round(value));
293318
}
294319

320+
function formatPercent(value) {
321+
return `${Math.round(value * 1000) / 10}%`;
322+
}
323+
295324
export class SfxControlPanel {
296325
constructor({
297326
addButton,
@@ -314,6 +343,7 @@ export class SfxControlPanel {
314343
pitchSweepValue,
315344
releaseInput,
316345
releaseValue,
346+
styleDescription,
317347
styleProfileSelect,
318348
validationMessage,
319349
volumeInput,
@@ -341,6 +371,7 @@ export class SfxControlPanel {
341371
this.pitchSweepValue = pitchSweepValue;
342372
this.releaseInput = releaseInput;
343373
this.releaseValue = releaseValue;
374+
this.styleDescription = styleDescription;
344375
this.styleProfileSelect = styleProfileSelect;
345376
this.validationMessage = validationMessage;
346377
this.volumeInput = volumeInput;
@@ -449,20 +480,23 @@ export class SfxControlPanel {
449480
this.releaseInput.value = String(sound.releaseMs);
450481
this.volumeInput.value = String(sound.volume);
451482
this.waveformSelect.value = sound.waveform;
483+
this.syncStyleDescription();
452484
this.syncOutputs();
453485
}
454486

455487
applyStyleProfile() {
456488
const styleKey = this.styleProfileSelect.value;
457489
if (styleKey === "custom") {
458490
this.applySliderLimits("custom", true);
491+
this.syncStyleDescription();
459492
this.syncOutputs();
460493
return true;
461494
}
462495
const profile = STYLE_PROFILES[styleKey];
463496
if (!profile) {
464497
this.styleProfileSelect.value = "custom";
465498
this.applySliderLimits("custom", true);
499+
this.syncStyleDescription();
466500
return false;
467501
}
468502
this.applySliderLimits(styleKey, true);
@@ -478,11 +512,13 @@ export class SfxControlPanel {
478512
this.releaseInput.value = String(profile.releaseMs);
479513
this.volumeInput.value = String(profile.volume);
480514
this.waveformSelect.value = profile.waveform;
515+
this.syncStyleDescription();
481516
this.syncOutputs();
482517
return true;
483518
}
484519

485520
syncOutputs() {
521+
this.syncRecommendedZones();
486522
this.attackValue.textContent = this.valueWithRange("attackMs", `${Math.round(toNumber(this.attackInput))} ms`);
487523
this.durationValue.textContent = this.valueWithRange("durationMs", `${Math.round(toNumber(this.durationInput))} ms`);
488524
this.frequencyValue.textContent = this.valueWithRange("frequencyHz", `${Math.round(toNumber(this.frequencyInput))} Hz`);
@@ -499,12 +535,73 @@ export class SfxControlPanel {
499535
}
500536

501537
rangeText(soundKey) {
502-
const limits = this.activeSliderLimits[soundKey];
538+
return this.formatRangeText(soundKey, this.activeSliderLimits[soundKey]);
539+
}
540+
541+
formatRangeText(soundKey, limits) {
503542
const unit = SLIDER_RANGE_UNITS[soundKey];
504543
const range = `${formatRangeNumber(soundKey, limits.min)}-${formatRangeNumber(soundKey, limits.max)}`;
505544
return unit ? `${range} ${unit}` : range;
506545
}
507546

547+
syncStyleDescription() {
548+
const styleKey = this.styleProfileSelect.value || "custom";
549+
this.styleDescription.textContent = STYLE_DESCRIPTIONS[styleKey] || STYLE_DESCRIPTIONS.custom;
550+
}
551+
552+
syncRecommendedZones() {
553+
SLIDER_INPUTS.forEach((item) => {
554+
const input = this[item.inputProperty];
555+
const zone = this.recommendedZoneFor(item.soundKey);
556+
const limits = this.activeSliderLimits[item.soundKey];
557+
const start = this.percentForValue(limits, zone.min);
558+
const end = this.percentForValue(limits, zone.max);
559+
input.style.setProperty("--audio-sfx-zone-start", formatPercent(start));
560+
input.style.setProperty("--audio-sfx-zone-end", formatPercent(end));
561+
input.dataset.recommendedZone = this.formatRangeText(item.soundKey, zone);
562+
input.title = `Recommended Zone: ${input.dataset.recommendedZone}`;
563+
});
564+
}
565+
566+
recommendedZoneFor(soundKey) {
567+
const styleKey = this.styleProfileSelect.value || "custom";
568+
const limits = this.activeSliderLimits[soundKey];
569+
if (styleKey === "custom") {
570+
return { min: limits.min, max: limits.max };
571+
}
572+
const profile = STYLE_PROFILES[styleKey] || {};
573+
const centerSource = Object.hasOwn(profile, soundKey) ? profile[soundKey] : DEFAULT_SOUND[soundKey];
574+
const center = this.clampNumber(Number(centerSource), limits);
575+
const totalRange = Math.max(0, limits.max - limits.min);
576+
const span = Math.max(
577+
SLIDER_LIMITS[soundKey].step,
578+
totalRange * (RECOMMENDED_ZONE_SPAN[soundKey] || 0.34)
579+
);
580+
const halfSpan = span / 2;
581+
let min = this.clampNumber(center - halfSpan, limits);
582+
let max = this.clampNumber(center + halfSpan, limits);
583+
if (min === max) {
584+
min = this.clampNumber(center - SLIDER_LIMITS[soundKey].step, limits);
585+
max = this.clampNumber(center + SLIDER_LIMITS[soundKey].step, limits);
586+
}
587+
return { min, max };
588+
}
589+
590+
percentForValue(limits, value) {
591+
const range = limits.max - limits.min;
592+
if (range <= 0) {
593+
return 0;
594+
}
595+
return (this.clampNumber(value, limits) - limits.min) / range;
596+
}
597+
598+
clampNumber(value, limits) {
599+
if (!Number.isFinite(value)) {
600+
return limits.min;
601+
}
602+
return Math.min(limits.max, Math.max(limits.min, value));
603+
}
604+
508605
validate() {
509606
const name = this.nameInput.value.trim();
510607
if (!name) {

tools/audio-sfx-playground-v2/styles/audioSfxLayoutDensity.css

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,70 @@ body[data-tool-id="audio-sfx-playground-v2"] #sfxShapeContent .tool-starter__fie
4545
}
4646

4747
body[data-tool-id="audio-sfx-playground-v2"] #sfxShapeContent .tool-starter__field[data-density-control] input[type="range"] {
48+
--audio-sfx-zone-start: 0%;
49+
--audio-sfx-zone-end: 100%;
50+
-webkit-appearance: none;
51+
appearance: none;
52+
background: transparent;
4853
height: 24px;
4954
min-width: 0;
5055
padding-block: 0;
5156
width: 100%;
5257
}
5358

59+
body[data-tool-id="audio-sfx-playground-v2"] #sfxShapeContent .tool-starter__field[data-density-control] input[type="range"]::-webkit-slider-runnable-track {
60+
height: 8px;
61+
border: 1px solid var(--tool-starter-surface-border);
62+
border-radius: 999px;
63+
background:
64+
linear-gradient(
65+
90deg,
66+
rgba(255, 255, 255, 0.1) 0%,
67+
rgba(255, 255, 255, 0.1) var(--audio-sfx-zone-start),
68+
rgba(134, 239, 172, 0.55) var(--audio-sfx-zone-start),
69+
rgba(134, 239, 172, 0.55) var(--audio-sfx-zone-end),
70+
rgba(255, 255, 255, 0.1) var(--audio-sfx-zone-end),
71+
rgba(255, 255, 255, 0.1) 100%
72+
);
73+
}
74+
75+
body[data-tool-id="audio-sfx-playground-v2"] #sfxShapeContent .tool-starter__field[data-density-control] input[type="range"]::-webkit-slider-thumb {
76+
-webkit-appearance: none;
77+
appearance: none;
78+
width: 16px;
79+
height: 16px;
80+
margin-top: -5px;
81+
border: 2px solid var(--tool-starter-panel);
82+
border-radius: 999px;
83+
background: var(--tool-starter-accent);
84+
box-shadow: 0 0 0 1px var(--tool-starter-surface-border);
85+
}
86+
87+
body[data-tool-id="audio-sfx-playground-v2"] #sfxShapeContent .tool-starter__field[data-density-control] input[type="range"]::-moz-range-track {
88+
height: 8px;
89+
border: 1px solid var(--tool-starter-surface-border);
90+
border-radius: 999px;
91+
background:
92+
linear-gradient(
93+
90deg,
94+
rgba(255, 255, 255, 0.1) 0%,
95+
rgba(255, 255, 255, 0.1) var(--audio-sfx-zone-start),
96+
rgba(134, 239, 172, 0.55) var(--audio-sfx-zone-start),
97+
rgba(134, 239, 172, 0.55) var(--audio-sfx-zone-end),
98+
rgba(255, 255, 255, 0.1) var(--audio-sfx-zone-end),
99+
rgba(255, 255, 255, 0.1) 100%
100+
);
101+
}
102+
103+
body[data-tool-id="audio-sfx-playground-v2"] #sfxShapeContent .tool-starter__field[data-density-control] input[type="range"]::-moz-range-thumb {
104+
width: 14px;
105+
height: 14px;
106+
border: 2px solid var(--tool-starter-panel);
107+
border-radius: 999px;
108+
background: var(--tool-starter-accent);
109+
box-shadow: 0 0 0 1px var(--tool-starter-surface-border);
110+
}
111+
54112
body[data-tool-id="audio-sfx-playground-v2"] #sfxShapeContent .tool-starter__field[data-density-control] output {
55113
color: var(--tool-starter-muted);
56114
font-size: 0.78rem;
@@ -68,6 +126,12 @@ body[data-tool-id="audio-sfx-playground-v2"] #sfxValidationMessage {
68126
line-height: 1.25;
69127
}
70128

129+
body[data-tool-id="audio-sfx-playground-v2"] .audio-sfx__style-description {
130+
min-height: 30px;
131+
margin: -1px 0 0 74px;
132+
line-height: 1.28;
133+
}
134+
71135
body[data-tool-id="audio-sfx-playground-v2"] .audio-sfx__shape-actions {
72136
display: flex;
73137
justify-content: flex-end;

0 commit comments

Comments
 (0)