Skip to content

Commit 02f6a7e

Browse files
committed
Add Audio SFX style based slider clamps - PR_26145_009-audio-sfx-style-based-slider-clamps
1 parent 21a28b5 commit 02f6a7e

4 files changed

Lines changed: 252 additions & 17 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Audio / SFX Playground V2 Style Slider Clamps
2+
3+
PR: `PR_26145_009-audio-sfx-style-based-slider-clamps`
4+
5+
## Scope
6+
7+
- Added Sound Style based min/max clamps for every Audio / SFX Playground V2 slider.
8+
- Kept clamp definitions centralized in `SfxControlPanel.js`.
9+
- Preserved waveform availability; Sound Style does not disable or hide waveform options.
10+
- Updated Audio / SFX toolState validation and schema frequency range to support Pure Tone `20-20000 Hz`.
11+
- Preserved current step values, including Duration, Release, and Sweep step `5`.
12+
13+
## Style Clamp Coverage
14+
15+
Every Sound Style defines min/max clamps for:
16+
17+
- Frequency
18+
- Duration
19+
- Attack
20+
- Release
21+
- Volume
22+
- Sweep
23+
- Amount
24+
- Decay
25+
- Brightness
26+
27+
Requested frequency examples are present:
28+
29+
- Pure Tone: `20-20000 Hz`
30+
- Atari-style: `40-4000 Hz`
31+
- TTL Arcade: `80-2500 Hz`
32+
- Vector Arcade: `100-6000 Hz`
33+
34+
## Targeted Static Validation
35+
36+
- `node --check` over `tools/audio-sfx-playground-v2/js/**/*.js`: PASS
37+
- `JSON.parse` for `tools/schemas/tools/audio-sfx-playground-v2.schema.json`: PASS
38+
- HTML/CSS static guard:
39+
- no inline event handlers: PASS
40+
- no `<style>` blocks: PASS
41+
- no inline `<script>` blocks: PASS
42+
- CSS non-empty: PASS
43+
- `git diff --check -- tools/audio-sfx-playground-v2 tools/schemas/tools/audio-sfx-playground-v2.schema.json`: PASS
44+
- Git reported LF-to-CRLF working-copy warnings only.
45+
46+
## Targeted Node Validation
47+
48+
- `SfxControlPanel.js` contains requested frequency clamp examples: PASS
49+
- Atari-style updates all slider min/max values: PASS
50+
- Atari-style clamps out-of-range current values into its slider ranges: PASS
51+
- Sound Style does not disable waveform select: PASS
52+
- Serializer accepts exported frequency values up to `20000`: PASS
53+
- Schema frequency range is `20-20000`: PASS
54+
55+
## Focused Playwright Validation
56+
57+
Ran a focused Playwright validation through a temporary local HTTP server using local browser binaries from `.ms-playwright`.
58+
59+
- Tool launches without console errors: PASS
60+
- Sound Style order and disabled divider are preserved: PASS
61+
- Each Sound Style updates all slider min/max values: PASS
62+
- Out-of-range current values are clamped inside the selected style ranges: PASS
63+
- Duration, Release, and Sweep preserve step `5`: PASS
64+
- All waveform options remain visible, enabled, and selectable for each style: PASS
65+
- Copy JSON after style selection produces a valid Audio / SFX toolState payload: PASS
66+
67+
## V8 Coverage
68+
69+
- `(100%) tools/audio-sfx-playground-v2/js/controls/SfxControlPanel.js - covered by focused style slider clamp Playwright validation`
70+
- `(100%) tools/audio-sfx-playground-v2/js/services/ToolStateSerializer.js - covered by focused style slider clamp Playwright validation`
71+
72+
Coverage is advisory per project instructions.
73+
74+
## Workspace V2
75+
76+
Command:
77+
78+
```powershell
79+
$env:PLAYWRIGHT_BROWSERS_PATH='.ms-playwright'; npm.cmd run test:workspace-v2
80+
```
81+
82+
Result: TIMEOUT after 900 seconds.
83+
84+
Observed progress before timeout:
85+
86+
- 72 tests discovered.
87+
- Tests 1-70 began running.
88+
- 68 observed as passed.
89+
- 2 observed as failed before timeout:
90+
- Object Vector Studio V2 layout shell/schema-only palette gate test.
91+
- Object Vector Studio V2 compact geometry layout/selected palette state test.
92+
- Timeout occurred before tests 71-72 reported.
93+
94+
The observed failures are outside the Audio / SFX Playground V2 scope.
95+
96+
## Full Samples Smoke
97+
98+
Skipped per request because this PR only impacts Audio / SFX Playground V2 slider behavior.

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

Lines changed: 151 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const SLIDER_LIMITS = Object.freeze({
22
attackMs: Object.freeze({ min: 0, max: 250, step: 5, defaultValue: 5 }),
33
durationMs: Object.freeze({ min: 60, max: 2000, step: 5, defaultValue: 180 }),
4-
frequencyHz: Object.freeze({ min: 80, max: 1800, step: 1, defaultValue: 880 }),
4+
frequencyHz: Object.freeze({ min: 20, max: 20000, step: 1, defaultValue: 880 }),
55
noiseAmount: Object.freeze({ min: 0, max: 1, step: 0.01, defaultValue: 0.65 }),
66
noiseDecayMs: Object.freeze({ min: 20, max: 600, step: 5, defaultValue: 95 }),
77
noiseFilterHz: Object.freeze({ min: 400, max: 9000, step: 50, defaultValue: 5200 }),
@@ -22,6 +22,108 @@ const SLIDER_INPUTS = Object.freeze([
2222
Object.freeze({ soundKey: "volume", inputProperty: "volumeInput" })
2323
]);
2424

25+
const STYLE_CLAMPS = Object.freeze({
26+
"pure-tone": Object.freeze({
27+
attackMs: Object.freeze({ min: 0, max: 120 }),
28+
durationMs: Object.freeze({ min: 80, max: 2000 }),
29+
frequencyHz: Object.freeze({ min: 20, max: 20000 }),
30+
noiseAmount: Object.freeze({ min: 0, max: 0.25 }),
31+
noiseDecayMs: Object.freeze({ min: 20, max: 240 }),
32+
noiseFilterHz: Object.freeze({ min: 1200, max: 9000 }),
33+
pitchSweepCents: Object.freeze({ min: -1200, max: 1200 }),
34+
releaseMs: Object.freeze({ min: 20, max: 700 }),
35+
volume: Object.freeze({ min: 0, max: 1 })
36+
}),
37+
"noise-only": Object.freeze({
38+
attackMs: Object.freeze({ min: 0, max: 80 }),
39+
durationMs: Object.freeze({ min: 60, max: 1200 }),
40+
frequencyHz: Object.freeze({ min: 80, max: 4000 }),
41+
noiseAmount: Object.freeze({ min: 0.2, max: 1 }),
42+
noiseDecayMs: Object.freeze({ min: 20, max: 600 }),
43+
noiseFilterHz: Object.freeze({ min: 400, max: 9000 }),
44+
pitchSweepCents: Object.freeze({ min: -400, max: 400 }),
45+
releaseMs: Object.freeze({ min: 20, max: 500 }),
46+
volume: Object.freeze({ min: 0, max: 1 })
47+
}),
48+
"atari-style": Object.freeze({
49+
attackMs: Object.freeze({ min: 0, max: 80 }),
50+
durationMs: Object.freeze({ min: 60, max: 900 }),
51+
frequencyHz: Object.freeze({ min: 40, max: 4000 }),
52+
noiseAmount: Object.freeze({ min: 0, max: 1 }),
53+
noiseDecayMs: Object.freeze({ min: 20, max: 450 }),
54+
noiseFilterHz: Object.freeze({ min: 400, max: 7000 }),
55+
pitchSweepCents: Object.freeze({ min: -1200, max: 1200 }),
56+
releaseMs: Object.freeze({ min: 20, max: 400 }),
57+
volume: Object.freeze({ min: 0, max: 1 })
58+
}),
59+
"classic-arcade": Object.freeze({
60+
attackMs: Object.freeze({ min: 0, max: 100 }),
61+
durationMs: Object.freeze({ min: 60, max: 1400 }),
62+
frequencyHz: Object.freeze({ min: 60, max: 5000 }),
63+
noiseAmount: Object.freeze({ min: 0, max: 1 }),
64+
noiseDecayMs: Object.freeze({ min: 20, max: 520 }),
65+
noiseFilterHz: Object.freeze({ min: 500, max: 8500 }),
66+
pitchSweepCents: Object.freeze({ min: -1200, max: 1200 }),
67+
releaseMs: Object.freeze({ min: 20, max: 500 }),
68+
volume: Object.freeze({ min: 0, max: 1 })
69+
}),
70+
"early-analog": Object.freeze({
71+
attackMs: Object.freeze({ min: 0, max: 180 }),
72+
durationMs: Object.freeze({ min: 100, max: 2000 }),
73+
frequencyHz: Object.freeze({ min: 30, max: 8000 }),
74+
noiseAmount: Object.freeze({ min: 0, max: 0.6 }),
75+
noiseDecayMs: Object.freeze({ min: 20, max: 600 }),
76+
noiseFilterHz: Object.freeze({ min: 400, max: 7000 }),
77+
pitchSweepCents: Object.freeze({ min: -1200, max: 1200 }),
78+
releaseMs: Object.freeze({ min: 40, max: 700 }),
79+
volume: Object.freeze({ min: 0, max: 1 })
80+
}),
81+
"namco-style": Object.freeze({
82+
attackMs: Object.freeze({ min: 0, max: 90 }),
83+
durationMs: Object.freeze({ min: 60, max: 900 }),
84+
frequencyHz: Object.freeze({ min: 120, max: 6500 }),
85+
noiseAmount: Object.freeze({ min: 0, max: 0.5 }),
86+
noiseDecayMs: Object.freeze({ min: 20, max: 360 }),
87+
noiseFilterHz: Object.freeze({ min: 1200, max: 9000 }),
88+
pitchSweepCents: Object.freeze({ min: -900, max: 1200 }),
89+
releaseMs: Object.freeze({ min: 20, max: 420 }),
90+
volume: Object.freeze({ min: 0, max: 1 })
91+
}),
92+
"nintendo-style": Object.freeze({
93+
attackMs: Object.freeze({ min: 0, max: 80 }),
94+
durationMs: Object.freeze({ min: 60, max: 1100 }),
95+
frequencyHz: Object.freeze({ min: 80, max: 5000 }),
96+
noiseAmount: Object.freeze({ min: 0, max: 0.9 }),
97+
noiseDecayMs: Object.freeze({ min: 20, max: 450 }),
98+
noiseFilterHz: Object.freeze({ min: 800, max: 9000 }),
99+
pitchSweepCents: Object.freeze({ min: -900, max: 1200 }),
100+
releaseMs: Object.freeze({ min: 20, max: 450 }),
101+
volume: Object.freeze({ min: 0, max: 1 })
102+
}),
103+
"ttl-arcade": Object.freeze({
104+
attackMs: Object.freeze({ min: 0, max: 60 }),
105+
durationMs: Object.freeze({ min: 60, max: 700 }),
106+
frequencyHz: Object.freeze({ min: 80, max: 2500 }),
107+
noiseAmount: Object.freeze({ min: 0, max: 1 }),
108+
noiseDecayMs: Object.freeze({ min: 20, max: 300 }),
109+
noiseFilterHz: Object.freeze({ min: 700, max: 9000 }),
110+
pitchSweepCents: Object.freeze({ min: -800, max: 800 }),
111+
releaseMs: Object.freeze({ min: 20, max: 260 }),
112+
volume: Object.freeze({ min: 0, max: 1 })
113+
}),
114+
"vector-arcade": Object.freeze({
115+
attackMs: Object.freeze({ min: 0, max: 140 }),
116+
durationMs: Object.freeze({ min: 80, max: 1600 }),
117+
frequencyHz: Object.freeze({ min: 100, max: 6000 }),
118+
noiseAmount: Object.freeze({ min: 0, max: 0.6 }),
119+
noiseDecayMs: Object.freeze({ min: 20, max: 500 }),
120+
noiseFilterHz: Object.freeze({ min: 500, max: 8500 }),
121+
pitchSweepCents: Object.freeze({ min: -1200, max: 1200 }),
122+
releaseMs: Object.freeze({ min: 20, max: 600 }),
123+
volume: Object.freeze({ min: 0, max: 1 })
124+
})
125+
});
126+
25127
const DEFAULT_SOUND = Object.freeze({
26128
attackMs: SLIDER_LIMITS.attackMs.defaultValue,
27129
durationMs: SLIDER_LIMITS.durationMs.defaultValue,
@@ -199,6 +301,7 @@ export class SfxControlPanel {
199301
volumeValue,
200302
waveformSelect
201303
}) {
304+
this.activeSliderLimits = SLIDER_LIMITS;
202305
this.addButton = addButton;
203306
this.attackInput = attackInput;
204307
this.attackValue = attackValue;
@@ -271,16 +374,41 @@ export class SfxControlPanel {
271374
return SLIDER_INPUTS.map((item) => this[item.inputProperty]);
272375
}
273376

274-
applySliderLimits() {
377+
applySliderLimits(styleKey = "custom", shouldClampValues = false) {
378+
this.activeSliderLimits = styleKey === "custom" ? SLIDER_LIMITS : this.sliderLimitsForStyle(styleKey);
275379
SLIDER_INPUTS.forEach((item) => {
276380
const input = this[item.inputProperty];
277-
const limits = SLIDER_LIMITS[item.soundKey];
381+
const limits = this.activeSliderLimits[item.soundKey];
278382
input.min = String(limits.min);
279383
input.max = String(limits.max);
280-
input.step = String(limits.step);
384+
input.step = String(SLIDER_LIMITS[item.soundKey].step);
385+
if (shouldClampValues) {
386+
input.value = String(this.clampSliderValue(input, limits));
387+
}
281388
});
282389
}
283390

391+
sliderLimitsForStyle(styleKey) {
392+
const clampProfile = STYLE_CLAMPS[styleKey] || {};
393+
return Object.freeze(Object.fromEntries(SLIDER_INPUTS.map((item) => {
394+
const baseLimits = SLIDER_LIMITS[item.soundKey];
395+
const clampLimits = clampProfile[item.soundKey] || baseLimits;
396+
return [item.soundKey, Object.freeze({
397+
max: clampLimits.max,
398+
min: clampLimits.min,
399+
step: baseLimits.step
400+
})];
401+
})));
402+
}
403+
404+
clampSliderValue(input, limits) {
405+
const value = toNumber(input);
406+
if (!Number.isFinite(value)) {
407+
return limits.min;
408+
}
409+
return Math.min(limits.max, Math.max(limits.min, value));
410+
}
411+
284412
focusSlider(input) {
285413
if (typeof input.focus === "function") {
286414
input.focus({ preventScroll: true });
@@ -289,6 +417,7 @@ export class SfxControlPanel {
289417

290418
loadSound(sound) {
291419
this.styleProfileSelect.value = "custom";
420+
this.applySliderLimits("custom");
292421
this.attackInput.value = String(sound.attackMs);
293422
this.durationInput.value = String(sound.durationMs);
294423
this.frequencyInput.value = String(sound.frequencyHz);
@@ -305,11 +434,19 @@ export class SfxControlPanel {
305434
}
306435

307436
applyStyleProfile() {
308-
const profile = STYLE_PROFILES[this.styleProfileSelect.value];
437+
const styleKey = this.styleProfileSelect.value;
438+
if (styleKey === "custom") {
439+
this.applySliderLimits("custom", true);
440+
this.syncOutputs();
441+
return true;
442+
}
443+
const profile = STYLE_PROFILES[styleKey];
309444
if (!profile) {
310445
this.styleProfileSelect.value = "custom";
446+
this.applySliderLimits("custom", true);
311447
return false;
312448
}
449+
this.applySliderLimits(styleKey, true);
313450
this.attackInput.value = String(DEFAULT_SOUND.attackMs);
314451
this.durationInput.value = String(profile.durationMs);
315452
this.frequencyInput.value = String(profile.frequencyHz);
@@ -346,15 +483,15 @@ export class SfxControlPanel {
346483
if (!ALLOWED_WAVEFORMS.has(this.waveformSelect.value)) {
347484
return { valid: false, message: `Unsupported waveform: ${this.waveformSelect.value}.` };
348485
}
349-
const frequency = readRange(this.frequencyInput, SLIDER_LIMITS.frequencyHz);
350-
const duration = readRange(this.durationInput, SLIDER_LIMITS.durationMs);
351-
const attack = readRange(this.attackInput, SLIDER_LIMITS.attackMs);
352-
const release = readRange(this.releaseInput, SLIDER_LIMITS.releaseMs);
353-
const volume = readRange(this.volumeInput, SLIDER_LIMITS.volume);
354-
const pitchSweep = readRange(this.pitchSweepInput, SLIDER_LIMITS.pitchSweepCents);
355-
const noiseAmount = readRange(this.noiseAmountInput, SLIDER_LIMITS.noiseAmount);
356-
const noiseDecay = readRange(this.noiseDecayInput, SLIDER_LIMITS.noiseDecayMs);
357-
const noiseFilter = readRange(this.noiseFilterInput, SLIDER_LIMITS.noiseFilterHz);
486+
const frequency = readRange(this.frequencyInput, this.activeSliderLimits.frequencyHz);
487+
const duration = readRange(this.durationInput, this.activeSliderLimits.durationMs);
488+
const attack = readRange(this.attackInput, this.activeSliderLimits.attackMs);
489+
const release = readRange(this.releaseInput, this.activeSliderLimits.releaseMs);
490+
const volume = readRange(this.volumeInput, this.activeSliderLimits.volume);
491+
const pitchSweep = readRange(this.pitchSweepInput, this.activeSliderLimits.pitchSweepCents);
492+
const noiseAmount = readRange(this.noiseAmountInput, this.activeSliderLimits.noiseAmount);
493+
const noiseDecay = readRange(this.noiseDecayInput, this.activeSliderLimits.noiseDecayMs);
494+
const noiseFilter = readRange(this.noiseFilterInput, this.activeSliderLimits.noiseFilterHz);
358495
const failed = [frequency, duration, attack, release, volume, pitchSweep, noiseAmount, noiseDecay, noiseFilter].find((result) => !result.ok);
359496
if (failed) {
360497
return { valid: false, message: failed.message };

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ function readSound(value, label) {
8383

8484
const attack = readNumber(value.attackMs, `${label}.attackMs`, 0, 250);
8585
const duration = readNumber(value.durationMs, `${label}.durationMs`, 60, 2000, 5);
86-
const frequency = readNumber(value.frequencyHz, `${label}.frequencyHz`, 80, 1800);
86+
const frequency = readNumber(value.frequencyHz, `${label}.frequencyHz`, 20, 20000);
8787
const noiseAmount = readNumber(value.noiseAmount, `${label}.noiseAmount`, 0, 1);
8888
const noiseDecay = readNumber(value.noiseDecayMs, `${label}.noiseDecayMs`, 20, 600, 5);
8989
const noiseFilter = readNumber(value.noiseFilterHz, `${label}.noiseFilterHz`, 400, 9000, 50);

tools/schemas/tools/audio-sfx-playground-v2.schema.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@
9090
},
9191
"frequencyHz": {
9292
"type": "integer",
93-
"minimum": 80,
94-
"maximum": 1800
93+
"minimum": 20,
94+
"maximum": 20000
9595
},
9696
"name": {
9797
"type": "string",

0 commit comments

Comments
 (0)