Skip to content

Commit 6a8053b

Browse files
committed
Add Audio SFX pure tone and noise source modes - PR_26145_006-audio-sfx-pure-tone-and-noise-source-modes
1 parent f938fe1 commit 6a8053b

7 files changed

Lines changed: 171 additions & 39 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Audio / SFX Playground V2 Pure Tone and Noise Source Modes
2+
3+
PR: `PR_26145_006-audio-sfx-pure-tone-and-noise-source-modes`
4+
5+
## Scope
6+
7+
- Kept Sound Style order as:
8+
- `Custom`
9+
- `Pure Tone`
10+
- disabled divider
11+
- `Atari-style`
12+
- `Classic Arcade`
13+
- `Early Analog`
14+
- `Namco-style`
15+
- `Nintendo-style`
16+
- `TTL Arcade`
17+
- `Vector Arcade`
18+
- Added `Noise` as a primary Wave/source option.
19+
- Renamed `Blend noise transient` to `Add Noise Layer`.
20+
- Preserved oscillator plus noise-layer behavior for `Add Noise Layer`.
21+
- Updated Audio / SFX toolState validation and schema to allow `waveform: "noise"`.
22+
23+
## Targeted Static Validation
24+
25+
- `node --check` over `tools/audio-sfx-playground-v2/js/**/*.js`: PASS
26+
- `JSON.parse` for `tools/schemas/tools/audio-sfx-playground-v2.schema.json`: PASS
27+
- HTML static guard for `tools/audio-sfx-playground-v2/index.html`:
28+
- no inline event handlers: PASS
29+
- no `<style>` blocks: PASS
30+
- no inline `<script>` blocks: PASS
31+
- `git diff --check -- tools/audio-sfx-playground-v2 tools/schemas/tools/audio-sfx-playground-v2.schema.json`: PASS
32+
- Git reported LF-to-CRLF working-copy warnings only.
33+
34+
## Targeted Node Validation
35+
36+
Validated with an ESM Node harness:
37+
38+
- Sound Style select order and disabled divider: PASS
39+
- `Add Noise Layer` label is present: PASS
40+
- Wave select includes `Noise`: PASS
41+
- `Pure Tone` style defaults remain:
42+
- name `Pure Tone`: PASS
43+
- waveform `sine`: PASS
44+
- noise disabled: PASS
45+
- noise amount `0`: PASS
46+
- Control validation accepts `waveform: "noise"`: PASS
47+
- Serializer export/read round trip accepts `waveform: "noise"`: PASS
48+
- Serializer rejects invalid waveform `pink-noise`: PASS
49+
- Schema enum includes `noise`: PASS
50+
51+
## Focused Playwright Validation
52+
53+
Ran a focused Playwright validation through a temporary local HTTP server using local browser binaries from `.ms-playwright`.
54+
55+
- Tool launches without console errors: PASS
56+
- Sound Style order and disabled divider: PASS
57+
- `Pure Tone` style applies expected defaults: PASS
58+
- `Add Noise Layer` label is visible: PASS
59+
- `waveform=noise` playback creates a primary noise buffer and does not create an oscillator: PASS
60+
- Oscillator waveform with Add Noise Layer creates both an oscillator and transient noise buffer: PASS
61+
- Copy JSON includes valid `waveform: "noise"` payload with corrected schema path: PASS
62+
63+
## V8 Coverage
64+
65+
- `(100%) tools/audio-sfx-playground-v2/js/controls/SfxControlPanel.js - covered by focused pure tone/noise mode Playwright validation`
66+
- `(100%) tools/audio-sfx-playground-v2/js/controls/SfxPreviewControl.js - covered by focused pure tone/noise mode Playwright validation`
67+
- `(100%) tools/audio-sfx-playground-v2/js/services/AudioSfxEngine.js - covered by focused pure tone/noise mode Playwright validation`
68+
- `(100%) tools/audio-sfx-playground-v2/js/services/ToolStateSerializer.js - covered by focused pure tone/noise mode Playwright validation`
69+
70+
Coverage is advisory per project instructions.
71+
72+
## Workspace V2
73+
74+
Command:
75+
76+
```powershell
77+
$env:PLAYWRIGHT_BROWSERS_PATH='.ms-playwright'; npm.cmd run test:workspace-v2
78+
```
79+
80+
Result: TIMEOUT after 900 seconds.
81+
82+
Observed progress before timeout:
83+
84+
- 72 tests discovered.
85+
- Tests 1-60 began running.
86+
- 58 observed as passed.
87+
- 2 observed as failed before timeout:
88+
- Object Vector Studio V2 layout shell/schema-only palette gate test.
89+
- Object Vector Studio V2 compact geometry layout/selected palette state test.
90+
- Timeout occurred before tests 61-72 reported.
91+
92+
The observed failures are outside the Audio / SFX Playground V2 scope.
93+
94+
## Full Samples Smoke
95+
96+
Skipped per request because this PR only impacts Audio / SFX Playground V2.

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,13 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
8383
</select>
8484
</label>
8585
<label class="tool-starter__field" for="waveformSelect">
86-
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Chooses the oscillator shape that defines the sound character.">Wave</span>
86+
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Chooses the oscillator shape or noise source that defines the sound character.">Wave</span>
8787
<select id="waveformSelect">
8888
<option value="sine">Sine</option>
8989
<option value="square">Square</option>
9090
<option value="triangle">Triangle</option>
9191
<option value="sawtooth">Saw</option>
92+
<option value="noise">Noise</option>
9293
</select>
9394
</label>
9495
<label class="tool-starter__field" for="frequencyInput" data-density-control="frequency">
@@ -123,7 +124,7 @@ <h2 class="tools-platform-frame__eyebrow">First-Class Tools Surface V2</h2>
123124
</label>
124125
<label class="tool-starter__toggle" for="noiseInput">
125126
<input id="noiseInput" type="checkbox">
126-
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Adds a short noise burst for punchy hits and impacts.">Blend noise transient</span>
127+
<span class="audio-sfx__label-tip" tabindex="0" data-tooltip="Adds a short noise layer over oscillator waveforms for punchy hits and impacts.">Add Noise Layer</span>
127128
</label>
128129
<label class="tool-starter__field" for="noiseAmountInput" data-density-control="noiseAmount">
129130
<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>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const DEFAULT_SOUND = Object.freeze({
1313
waveform: "square"
1414
});
1515

16-
const ALLOWED_WAVEFORMS = Object.freeze(new Set(["sine", "square", "triangle", "sawtooth"]));
16+
const ALLOWED_WAVEFORMS = Object.freeze(new Set(["sine", "square", "triangle", "sawtooth", "noise"]));
1717
const STYLE_PROFILES = Object.freeze({
1818
"pure-tone": {
1919
durationMs: 240,

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ function barHeight(index, sound) {
99
const releaseRatio = (normalizedIndex - releaseStart) / Math.max(1 - releaseStart, 0.001);
1010
envelope = 1 - releaseRatio;
1111
}
12-
const wave = Math.abs(Math.sin((index + 1) * (sound.frequencyHz / 220)));
12+
const wave = sound.waveform === "noise"
13+
? Math.abs(Math.sin((index + 1) * ((sound.noiseFilterHz / 9000) + 0.35) * 7.13))
14+
: Math.abs(Math.sin((index + 1) * (sound.frequencyHz / 220)));
1315
const sweepTilt = 1 + ((sound.pitchSweepCents / 1200) * (normalizedIndex - 0.5));
1416
const noiseDecayRatio = sound.noiseDecayMs / sound.durationMs;
15-
const noiseTransient = sound.noise
17+
const noiseTransient = sound.noise || sound.waveform === "noise"
1618
? sound.noiseAmount * Math.max(0, 1 - (normalizedIndex / Math.max(noiseDecayRatio, 0.001)))
1719
: 0;
18-
const noiseBrightness = sound.noise ? sound.noiseFilterHz / 9000 : 0;
20+
const noiseBrightness = sound.noise || sound.waveform === "noise" ? sound.noiseFilterHz / 9000 : 0;
1921
return Math.max(8, Math.round((20 + (wave * 76) + (noiseTransient * 110) + (noiseBrightness * noiseTransient * 32)) * Math.max(envelope, 0.08) * sweepTilt));
2022
}
2123

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

Lines changed: 64 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,47 @@ function noiseDurationSeconds(sound, durationSeconds) {
1616
return Math.min(durationSeconds, sound.noiseDecayMs / 1000);
1717
}
1818

19+
function createFilteredNoise(context, sound, durationSeconds) {
20+
const noise = context.createBufferSource();
21+
const noiseFilter = context.createBiquadFilter();
22+
const noiseGain = context.createGain();
23+
noise.buffer = createNoiseBuffer(context, durationSeconds);
24+
noiseFilter.type = "lowpass";
25+
noiseFilter.frequency.setValueAtTime(sound.noiseFilterHz, context.currentTime);
26+
noise.connect(noiseFilter);
27+
noiseFilter.connect(noiseGain);
28+
noiseGain.connect(context.destination);
29+
return { noise, noiseGain };
30+
}
31+
32+
function startNoiseLayer(context, sound, now, durationSeconds) {
33+
const noiseSeconds = noiseDurationSeconds(sound, durationSeconds);
34+
const noiseStopAt = now + noiseSeconds;
35+
const { noise, noiseGain } = createFilteredNoise(context, sound, noiseSeconds);
36+
noiseGain.gain.setValueAtTime(Math.max(0.0001, sound.volume * sound.noiseAmount * 1.2), now);
37+
noiseGain.gain.exponentialRampToValueAtTime(0.0001, noiseStopAt);
38+
noise.start(now);
39+
noise.stop(noiseStopAt);
40+
return noise;
41+
}
42+
43+
function startPrimaryNoise(context, sound, now, durationSeconds, attackSeconds) {
44+
const stopAt = now + durationSeconds;
45+
const attackEnd = now + attackSeconds;
46+
const decayEnd = now + Math.min(durationSeconds, Math.max(attackSeconds + 0.005, sound.noiseDecayMs / 1000));
47+
const peakGain = Math.max(0.0001, sound.volume * Math.max(sound.noiseAmount, 0.05));
48+
const { noise, noiseGain } = createFilteredNoise(context, sound, durationSeconds);
49+
noiseGain.gain.setValueAtTime(0.0001, now);
50+
noiseGain.gain.linearRampToValueAtTime(peakGain, attackEnd);
51+
noiseGain.gain.exponentialRampToValueAtTime(0.0001, decayEnd);
52+
if (decayEnd < stopAt) {
53+
noiseGain.gain.setValueAtTime(0.0001, stopAt);
54+
}
55+
noise.start(now);
56+
noise.stop(stopAt);
57+
return noise;
58+
}
59+
1960
export class AudioSfxEngine {
2061
constructor({ windowRef = window } = {}) {
2162
this.context = null;
@@ -47,43 +88,35 @@ export class AudioSfxEngine {
4788
const stopAt = now + durationSeconds;
4889
const sustainStart = now + attackSeconds;
4990
const releaseStart = Math.max(sustainStart, stopAt - releaseSeconds);
50-
const oscillator = context.createOscillator();
51-
const gain = context.createGain();
91+
let primarySource;
92+
if (sound.waveform === "noise") {
93+
primarySource = startPrimaryNoise(context, sound, now, durationSeconds, attackSeconds);
94+
} else {
95+
const oscillator = context.createOscillator();
96+
const gain = context.createGain();
5297

53-
oscillator.type = sound.waveform;
54-
oscillator.frequency.setValueAtTime(sound.frequencyHz, now);
55-
oscillator.detune.setValueAtTime(0, now);
56-
oscillator.detune.linearRampToValueAtTime(sound.pitchSweepCents, stopAt);
57-
gain.gain.setValueAtTime(0.0001, now);
58-
gain.gain.linearRampToValueAtTime(sound.volume, sustainStart);
59-
gain.gain.setValueAtTime(sound.volume, releaseStart);
60-
gain.gain.exponentialRampToValueAtTime(0.0001, stopAt);
98+
oscillator.type = sound.waveform;
99+
oscillator.frequency.setValueAtTime(sound.frequencyHz, now);
100+
oscillator.detune.setValueAtTime(0, now);
101+
oscillator.detune.linearRampToValueAtTime(sound.pitchSweepCents, stopAt);
102+
gain.gain.setValueAtTime(0.0001, now);
103+
gain.gain.linearRampToValueAtTime(sound.volume, sustainStart);
104+
gain.gain.setValueAtTime(sound.volume, releaseStart);
105+
gain.gain.exponentialRampToValueAtTime(0.0001, stopAt);
61106

62-
oscillator.connect(gain);
63-
gain.connect(context.destination);
64-
oscillator.start(now);
65-
oscillator.stop(stopAt);
107+
oscillator.connect(gain);
108+
gain.connect(context.destination);
109+
oscillator.start(now);
110+
oscillator.stop(stopAt);
111+
primarySource = oscillator;
66112

67-
if (sound.noise) {
68-
const noise = context.createBufferSource();
69-
const noiseFilter = context.createBiquadFilter();
70-
const noiseGain = context.createGain();
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);
80-
noiseGain.connect(context.destination);
81-
noise.start(now);
82-
noise.stop(noiseStopAt);
113+
if (sound.noise) {
114+
startNoiseLayer(context, sound, now, durationSeconds);
115+
}
83116
}
84117

85118
await new Promise((resolve) => {
86-
oscillator.addEventListener("ended", resolve, { once: true });
119+
primarySource.addEventListener("ended", resolve, { once: true });
87120
});
88121
}
89122
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const TOOL_ID = "audio-sfx-playground-v2";
22
const PAYLOAD_SCHEMA = "html-js-gaming.audio-sfx-playground-v2";
33
const TOOL_SCHEMA_PATH = "tools/schemas/tools/audio-sfx-playground-v2.schema.json";
4-
const ALLOWED_WAVEFORMS = Object.freeze(new Set(["sine", "square", "triangle", "sawtooth"]));
4+
const ALLOWED_WAVEFORMS = Object.freeze(new Set(["sine", "square", "triangle", "sawtooth", "noise"]));
55
const ROOT_KEYS = Object.freeze(new Set(["$schema", "schema", "version", "toolId", "payload"]));
66
const PAYLOAD_KEYS = Object.freeze(new Set(["schema", "version", "toolId", "activeSoundId", "sounds"]));
77
const SOUND_ENTRY_KEYS = Object.freeze(new Set(["id", "sound"]));

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@
136136
},
137137
"waveform": {
138138
"type": "string",
139-
"enum": ["sine", "square", "triangle", "sawtooth"]
139+
"enum": ["sine", "square", "triangle", "sawtooth", "noise"]
140140
}
141141
}
142142
}

0 commit comments

Comments
 (0)