Skip to content

Commit 3a365a4

Browse files
committed
Enhance Audio SFX noise behavior and refine slider resolution - PR_26145_003-audio-sfx-noise-enhancements-and-slider-resolution
1 parent 5a88f7c commit 3a365a4

9 files changed

Lines changed: 256 additions & 20 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Audio / SFX Playground V2 Noise and Slider Resolution Validation
2+
3+
PR: `PR_26145_003-audio-sfx-noise-enhancements-and-slider-resolution`
4+
5+
Playwright impacted: Yes.
6+
7+
## Scope
8+
9+
- Replaced simple noise mix behavior with a transient arcade-noise layer.
10+
- Added Audio / SFX Playground V2 controls for:
11+
- Noise Amount
12+
- Noise Decay
13+
- Noise Brightness
14+
- Updated Audio / SFX payload/schema fields for the new noise parameters.
15+
- Updated slider resolution:
16+
- Duration step: 5 ms
17+
- Release step: 5 ms
18+
- Sweep step: 5 cents
19+
- Duration max: 2000 ms
20+
21+
## Targeted Validation
22+
23+
PASS: JavaScript syntax validation
24+
25+
Command:
26+
27+
```powershell
28+
Get-ChildItem -Recurse -File tools/audio-sfx-playground-v2/js -Filter *.js | ForEach-Object { node --check $_.FullName }
29+
```
30+
31+
PASS: HTML/CSS static validation
32+
33+
Checked Audio / SFX Playground V2 HTML/CSS for empty files, inline event handlers, `<style>` blocks, and inline `<script>` blocks.
34+
35+
PASS: Schema JSON parse validation
36+
37+
Command:
38+
39+
```powershell
40+
Get-Content -Raw tools/schemas/tools/audio-sfx-playground-v2.schema.json | ConvertFrom-Json | Out-Null
41+
```
42+
43+
PASS: Diff whitespace validation
44+
45+
Command:
46+
47+
```powershell
48+
git diff --check -- tools/audio-sfx-playground-v2 tools/schemas
49+
```
50+
51+
PASS: Targeted Node behavior validation
52+
53+
Validated with an inline Node module script:
54+
55+
- Schema requires `noiseAmount`, `noiseDecayMs`, and `noiseFilterHz`.
56+
- Schema allows duration up to `2000` ms.
57+
- Schema enforces 5 ms duration increments.
58+
- Schema enforces 5 ms release increments.
59+
- Schema enforces 5 cent sweep increments.
60+
- Serializer preserves the new noise fields.
61+
- Serializer rejects non-5-cent sweep values.
62+
- Audio engine starts a noise buffer source when noise is enabled.
63+
- Noise stops at the configured decay time instead of full sound duration.
64+
- Noise brightness reaches the `BiquadFilterNode.frequency` path.
65+
- Noise amount reaches the transient noise gain path with audible gain.
66+
- Noise gain decays transiently.
67+
68+
PASS: Targeted Playwright validation
69+
70+
Validated with a temporary local HTTP server, Chromium, and an injected fake `AudioContext`:
71+
72+
- Audio / SFX Playground V2 launched without console errors.
73+
- Noise Amount, Noise Decay, and Noise Brightness controls were visible.
74+
- Duration slider step was `5`.
75+
- Duration slider max was `2000`.
76+
- Release slider step was `5`.
77+
- Sweep slider step was `5`.
78+
- Clicking Play with noise enabled started the transient noise layer.
79+
- Noise stopped at the configured decay time.
80+
- Noise Brightness reached the engine filter frequency.
81+
- Noise Amount reached the audible transient gain path.
82+
83+
## Workspace V2 Validation
84+
85+
PARTIAL: `npm.cmd run test:workspace-v2`
86+
87+
Command:
88+
89+
```powershell
90+
$env:PLAYWRIGHT_BROWSERS_PATH='.ms-playwright'
91+
npm.cmd run test:workspace-v2
92+
```
93+
94+
Result:
95+
96+
- Workspace V2 Playwright launched with local Chromium.
97+
- The run timed out after 15 minutes at test 67 of 72.
98+
- 65 tests passed before timeout.
99+
- 2 unrelated Object Vector Studio V2 tests were marked failed before timeout:
100+
- `shows Object Vector Studio V2 layout shell and schema-only palette gate`
101+
- `compacts Object Vector Studio V2 geometry layouts and selected palette state`
102+
- The timeout/failures are outside the Audio / SFX Playground V2 synthesis-control scope.
103+
104+
Expected Playwright pass behavior:
105+
106+
- Audio / SFX Playground V2 launches without console errors.
107+
- Noise controls visibly affect the playback graph.
108+
- Noise can be clearly heard during playback through the transient noise gain path.
109+
- Duration/Release/Sweep sliders use 5-unit steps.
110+
- Duration reaches 2000 ms.
111+
112+
Expected Playwright fail behavior:
113+
114+
- Fail if noise controls are missing or disconnected from playback.
115+
- Fail if noise is mixed uniformly for the full duration instead of as a transient.
116+
- Fail if duration, release, or sweep slider steps regress.
117+
- Fail if duration max does not reach 2000 ms.
118+
- Fail if console errors occur.
119+
120+
## Coverage
121+
122+
Playwright V8 coverage was not collected during the focused inline browser validation.
123+
124+
WARN: `tools/audio-sfx-playground-v2/js/AudioSfxPlaygroundV2App.js` - changed runtime JavaScript; coverage unavailable.
125+
WARN: `tools/audio-sfx-playground-v2/js/bootstrap.js` - changed runtime JavaScript; coverage unavailable.
126+
WARN: `tools/audio-sfx-playground-v2/js/controls/SfxControlPanel.js` - changed runtime JavaScript; coverage unavailable.
127+
WARN: `tools/audio-sfx-playground-v2/js/controls/SfxPreviewControl.js` - changed runtime JavaScript; coverage unavailable.
128+
WARN: `tools/audio-sfx-playground-v2/js/services/AudioSfxEngine.js` - changed runtime JavaScript; coverage unavailable.
129+
WARN: `tools/audio-sfx-playground-v2/js/services/ToolStateSerializer.js` - changed runtime JavaScript; coverage unavailable.
130+
131+
## Full Samples Smoke Test
132+
133+
Skipped. This PR only impacts Audio / SFX Playground V2 synthesis controls.

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
8383
</label>
8484
<label class="tool-starter__field" for="durationInput" data-density-control="duration">
8585
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Controls the total playback length of the effect.">Duration</span>
86-
<input id="durationInput" type="range" min="60" max="1400" step="10">
86+
<input id="durationInput" type="range" min="60" max="2000" step="5">
8787
<output id="durationValue" for="durationInput"></output>
8888
</label>
8989
<label class="tool-starter__field" for="attackInput" data-density-control="attack">
@@ -93,7 +93,7 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
9393
</label>
9494
<label class="tool-starter__field" for="releaseInput" data-density-control="release">
9595
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Controls how long the sound fades out at the end.">Release</span>
96-
<input id="releaseInput" type="range" min="20" max="700" step="10">
96+
<input id="releaseInput" type="range" min="20" max="700" step="5">
9797
<output id="releaseValue" for="releaseInput"></output>
9898
</label>
9999
<label class="tool-starter__field" for="volumeInput" data-density-control="volume">
@@ -103,13 +103,28 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
103103
</label>
104104
<label class="tool-starter__field" for="pitchSweepInput" data-density-control="sweep">
105105
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Bends pitch over time in cents, positive upward and negative downward.">Sweep</span>
106-
<input id="pitchSweepInput" type="range" min="-1200" max="1200" step="25">
106+
<input id="pitchSweepInput" type="range" min="-1200" max="1200" step="5">
107107
<output id="pitchSweepValue" for="pitchSweepInput"></output>
108108
</label>
109109
<label class="tool-starter__toggle" for="noiseInput">
110110
<input id="noiseInput" type="checkbox">
111111
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Adds a short noise burst for punchy hits and impacts.">Blend noise transient</span>
112112
</label>
113+
<label class="tool-starter__field" for="noiseAmountInput" data-density-control="noiseAmount">
114+
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Sets how loud the arcade noise transient is when noise is enabled.">Noise Amount</span>
115+
<input id="noiseAmountInput" type="range" min="0" max="1" step="0.01">
116+
<output id="noiseAmountValue" for="noiseAmountInput"></output>
117+
</label>
118+
<label class="tool-starter__field" for="noiseDecayInput" data-density-control="noiseDecay">
119+
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Controls how quickly the noise burst fades out.">Noise Decay</span>
120+
<input id="noiseDecayInput" type="range" min="20" max="600" step="5">
121+
<output id="noiseDecayValue" for="noiseDecayInput"></output>
122+
</label>
123+
<label class="tool-starter__field" for="noiseFilterInput" data-density-control="noiseFilter">
124+
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Raises or lowers the transient brightness filter.">Noise Brightness</span>
125+
<input id="noiseFilterInput" type="range" min="400" max="9000" step="50">
126+
<output id="noiseFilterValue" for="noiseFilterInput"></output>
127+
</label>
113128
<p id="sfxValidationMessage" class="tool-starter__hint">Ready to audition.</p>
114129
<div class="audio-sfx__shape-actions">
115130
<button id="toolPlayButton" type="button">Play</button>

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ function cloneSound(sound) {
55
frequencyHz: sound.frequencyHz,
66
name: sound.name,
77
noise: sound.noise,
8+
noiseAmount: sound.noiseAmount,
9+
noiseDecayMs: sound.noiseDecayMs,
10+
noiseFilterHz: sound.noiseFilterHz,
811
pitchSweepCents: sound.pitchSweepCents,
912
releaseMs: sound.releaseMs,
1013
volume: sound.volume,

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ window.addEventListener("DOMContentLoaded", () => {
5050
frequencyValue: requireElement("#frequencyValue"),
5151
deleteButton: requireElement("#deleteSfxButton"),
5252
nameInput: requireElement("#nameInput"),
53+
noiseAmountInput: requireElement("#noiseAmountInput"),
54+
noiseAmountValue: requireElement("#noiseAmountValue"),
55+
noiseDecayInput: requireElement("#noiseDecayInput"),
56+
noiseDecayValue: requireElement("#noiseDecayValue"),
57+
noiseFilterInput: requireElement("#noiseFilterInput"),
58+
noiseFilterValue: requireElement("#noiseFilterValue"),
5359
noiseInput: requireElement("#noiseInput"),
5460
pitchSweepInput: requireElement("#pitchSweepInput"),
5561
pitchSweepValue: requireElement("#pitchSweepValue"),

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ const DEFAULT_SOUND = Object.freeze({
44
frequencyHz: 880,
55
name: "Coin",
66
noise: false,
7+
noiseAmount: 0.65,
8+
noiseDecayMs: 95,
9+
noiseFilterHz: 5200,
710
pitchSweepCents: 700,
811
releaseMs: 90,
912
volume: 0.42,
@@ -37,6 +40,12 @@ export class SfxControlPanel {
3740
frequencyInput,
3841
frequencyValue,
3942
nameInput,
43+
noiseAmountInput,
44+
noiseAmountValue,
45+
noiseDecayInput,
46+
noiseDecayValue,
47+
noiseFilterInput,
48+
noiseFilterValue,
4049
noiseInput,
4150
pitchSweepInput,
4251
pitchSweepValue,
@@ -56,6 +65,12 @@ export class SfxControlPanel {
5665
this.frequencyInput = frequencyInput;
5766
this.frequencyValue = frequencyValue;
5867
this.nameInput = nameInput;
68+
this.noiseAmountInput = noiseAmountInput;
69+
this.noiseAmountValue = noiseAmountValue;
70+
this.noiseDecayInput = noiseDecayInput;
71+
this.noiseDecayValue = noiseDecayValue;
72+
this.noiseFilterInput = noiseFilterInput;
73+
this.noiseFilterValue = noiseFilterValue;
5974
this.noiseInput = noiseInput;
6075
this.pitchSweepInput = pitchSweepInput;
6176
this.pitchSweepValue = pitchSweepValue;
@@ -77,6 +92,9 @@ export class SfxControlPanel {
7792
this.durationInput,
7893
this.frequencyInput,
7994
this.nameInput,
95+
this.noiseAmountInput,
96+
this.noiseDecayInput,
97+
this.noiseFilterInput,
8098
this.noiseInput,
8199
this.pitchSweepInput,
82100
this.releaseInput,
@@ -100,6 +118,9 @@ export class SfxControlPanel {
100118
this.frequencyInput.value = String(sound.frequencyHz);
101119
this.nameInput.value = sound.name;
102120
this.noiseInput.checked = sound.noise;
121+
this.noiseAmountInput.value = String(sound.noiseAmount);
122+
this.noiseDecayInput.value = String(sound.noiseDecayMs);
123+
this.noiseFilterInput.value = String(sound.noiseFilterHz);
103124
this.pitchSweepInput.value = String(sound.pitchSweepCents);
104125
this.releaseInput.value = String(sound.releaseMs);
105126
this.volumeInput.value = String(sound.volume);
@@ -111,6 +132,9 @@ export class SfxControlPanel {
111132
this.attackValue.textContent = `${Math.round(toNumber(this.attackInput))} ms`;
112133
this.durationValue.textContent = `${Math.round(toNumber(this.durationInput))} ms`;
113134
this.frequencyValue.textContent = `${Math.round(toNumber(this.frequencyInput))} Hz`;
135+
this.noiseAmountValue.textContent = toNumber(this.noiseAmountInput).toFixed(2);
136+
this.noiseDecayValue.textContent = `${Math.round(toNumber(this.noiseDecayInput))} ms`;
137+
this.noiseFilterValue.textContent = `${Math.round(toNumber(this.noiseFilterInput))} Hz`;
114138
this.pitchSweepValue.textContent = `${Math.round(toNumber(this.pitchSweepInput))} cents`;
115139
this.releaseValue.textContent = `${Math.round(toNumber(this.releaseInput))} ms`;
116140
this.volumeValue.textContent = toNumber(this.volumeInput).toFixed(2);
@@ -130,7 +154,10 @@ export class SfxControlPanel {
130154
const release = readRange(this.releaseInput);
131155
const volume = readRange(this.volumeInput);
132156
const pitchSweep = readRange(this.pitchSweepInput);
133-
const failed = [frequency, duration, attack, release, volume, pitchSweep].find((result) => !result.ok);
157+
const noiseAmount = readRange(this.noiseAmountInput);
158+
const noiseDecay = readRange(this.noiseDecayInput);
159+
const noiseFilter = readRange(this.noiseFilterInput);
160+
const failed = [frequency, duration, attack, release, volume, pitchSweep, noiseAmount, noiseDecay, noiseFilter].find((result) => !result.ok);
134161
if (failed) {
135162
return { valid: false, message: failed.message };
136163
}
@@ -145,6 +172,9 @@ export class SfxControlPanel {
145172
frequencyHz: Math.round(frequency.value),
146173
name,
147174
noise: this.noiseInput.checked,
175+
noiseAmount: Number(noiseAmount.value.toFixed(2)),
176+
noiseDecayMs: Math.round(noiseDecay.value),
177+
noiseFilterHz: Math.round(noiseFilter.value),
148178
pitchSweepCents: Math.round(pitchSweep.value),
149179
releaseMs: Math.round(release.value),
150180
volume: Number(volume.value.toFixed(2)),

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ function barHeight(index, sound) {
1111
}
1212
const wave = Math.abs(Math.sin((index + 1) * (sound.frequencyHz / 220)));
1313
const sweepTilt = 1 + ((sound.pitchSweepCents / 1200) * (normalizedIndex - 0.5));
14-
const noiseLift = sound.noise ? 0.18 : 0;
15-
return Math.max(8, Math.round((20 + (wave * 76) + (noiseLift * 100)) * Math.max(envelope, 0.08) * sweepTilt));
14+
const noiseDecayRatio = sound.noiseDecayMs / sound.durationMs;
15+
const noiseTransient = sound.noise
16+
? sound.noiseAmount * Math.max(0, 1 - (normalizedIndex / Math.max(noiseDecayRatio, 0.001)))
17+
: 0;
18+
const noiseBrightness = sound.noise ? sound.noiseFilterHz / 9000 : 0;
19+
return Math.max(8, Math.round((20 + (wave * 76) + (noiseTransient * 110) + (noiseBrightness * noiseTransient * 32)) * Math.max(envelope, 0.08) * sweepTilt));
1620
}
1721

1822
export class SfxPreviewControl {

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

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ function createNoiseBuffer(context, durationSeconds) {
1212
return buffer;
1313
}
1414

15+
function noiseDurationSeconds(sound, durationSeconds) {
16+
return Math.min(durationSeconds, sound.noiseDecayMs / 1000);
17+
}
18+
1519
export class AudioSfxEngine {
1620
constructor({ windowRef = window } = {}) {
1721
this.context = null;
@@ -62,14 +66,20 @@ export class AudioSfxEngine {
6266

6367
if (sound.noise) {
6468
const noise = context.createBufferSource();
69+
const noiseFilter = context.createBiquadFilter();
6570
const noiseGain = context.createGain();
66-
noise.buffer = createNoiseBuffer(context, Math.min(durationSeconds, 0.18));
67-
noiseGain.gain.setValueAtTime(sound.volume * 0.36, now);
68-
noiseGain.gain.exponentialRampToValueAtTime(0.0001, now + Math.min(durationSeconds, 0.18));
69-
noise.connect(noiseGain);
71+
const noiseSeconds = noiseDurationSeconds(sound, durationSeconds);
72+
const noiseStopAt = now + noiseSeconds;
73+
noise.buffer = createNoiseBuffer(context, noiseSeconds);
74+
noiseFilter.type = "lowpass";
75+
noiseFilter.frequency.setValueAtTime(sound.noiseFilterHz, now);
76+
noiseGain.gain.setValueAtTime(Math.max(0.0001, sound.volume * sound.noiseAmount * 1.2), now);
77+
noiseGain.gain.exponentialRampToValueAtTime(0.0001, noiseStopAt);
78+
noise.connect(noiseFilter);
79+
noiseFilter.connect(noiseGain);
7080
noiseGain.connect(context.destination);
7181
noise.start(now);
72-
noise.stop(now + Math.min(durationSeconds, 0.18));
82+
noise.stop(noiseStopAt);
7383
}
7484

7585
await new Promise((resolve) => {

0 commit comments

Comments
 (0)