Skip to content

Commit acfabce

Browse files
committed
feat: replace vortex with sample
1 parent 5ad72be commit acfabce

5 files changed

Lines changed: 77 additions & 69 deletions

File tree

assets/audio/ATTRIBUTION.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,9 @@
55
"Vibing Over Venus" Kevin MacLeod (incompetech.com)
66
Licensed under Creative Commons: By Attribution 4.0 License
77
http://creativecommons.org/licenses/by/4.0/
8+
9+
## vortex_loop.wav — "0ktober_hyperspace"
10+
11+
"0ktober_hyperspace" by 0ktober
12+
https://freesound.org/people/0ktober/sounds/188831/
13+
Licensed under Creative Commons 0 (CC0) — Public Domain

assets/audio/vortex_loop.wav

16 MB
Binary file not shown.

src/lib/audio/AudioEngine.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
import type { SoundId, VolumeSettings } from './types'
11-
import { SOUND_CATEGORIES, SOUND_ASSETS, SYNTH_ONE_SHOTS, SYNTH_LOOPS } from './registry'
11+
import { SOUND_CATEGORIES, SOUND_ASSETS, SOUND_LOOP_VOLUMES, SYNTH_ONE_SHOTS, SYNTH_LOOPS } from './registry'
1212

1313
export class AudioEngine {
1414
private ctx: AudioContext | null = null
@@ -109,14 +109,15 @@ export class AudioEngine {
109109
const ctx = this.ensureContext()
110110
const cat = SOUND_CATEGORIES[id]
111111
const dest = this.getDestForCategory(cat)
112+
const effectiveVolume = volume * (SOUND_LOOP_VOLUMES[id] ?? 1)
112113

113114
// Per-loop gain node for individual volume control
114115
const loopGain = ctx.createGain()
115116
if (fadeInSeconds > 0) {
116117
loopGain.gain.setValueAtTime(0, ctx.currentTime)
117-
loopGain.gain.linearRampToValueAtTime(volume, ctx.currentTime + fadeInSeconds)
118+
loopGain.gain.linearRampToValueAtTime(effectiveVolume, ctx.currentTime + fadeInSeconds)
118119
} else {
119-
loopGain.gain.setValueAtTime(volume, ctx.currentTime)
120+
loopGain.gain.setValueAtTime(effectiveVolume, ctx.currentTime)
120121
}
121122
loopGain.connect(dest)
122123

src/lib/audio/registry.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,13 @@ export const SOUND_CATEGORIES: Record<SoundId, SoundCategory> = {
2828
* ui_click: new URL('../../../assets/audio/click.wav', import.meta.url).href
2929
*/
3030
export const SOUND_ASSETS: Partial<Record<SoundId, string>> = {
31-
music_menu: new URL('../../../assets/audio/music_menu.mp3', import.meta.url).href
31+
music_menu: new URL('../../../assets/audio/music_menu.mp3', import.meta.url).href,
32+
vortex_loop: new URL('../../../assets/audio/vortex_loop.wav', import.meta.url).href
33+
}
34+
35+
/** Per-sound volume overrides for asset-based loops (0–1). */
36+
export const SOUND_LOOP_VOLUMES: Partial<Record<SoundId, number>> = {
37+
vortex_loop: 0.3
3238
}
3339

3440
export const SYNTH_ONE_SHOTS: Partial<Record<SoundId, SynthOneShot>> = {

src/lib/audio/synthVortex.ts

Lines changed: 60 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,87 +2,82 @@ import type { SynthLoop } from './types'
22
import { loopTeardown } from './synthUtils'
33

44
/**
5-
* Builds a vortex tunnel sound. The core character is rushing air with
6-
* resonant sweeps that create a sense of forward motion. Tonal elements
7-
* sit underneath as a subtle drone.
8-
* - 'normal': smooth rushing wind with warm resonance
9-
* - 'error': harsher, more turbulent — same tunnel, gone wrong
5+
* Builds a vortex loop — a quiet, low-frequency rumble designed for
6+
* long listening sessions (30+ minutes during downloads/compiles).
7+
* - 'normal': gentle low rumble, barely-there presence
8+
* - 'error': same character but darker and slightly more prominent
109
*/
10+
/** Master volume multiplier for the vortex loop — tweak this to dial in overall level. */
11+
const VORTEX_VOLUME = 1.0
12+
1113
function buildVortexLoop(ctx: AudioContext, dest: AudioNode, variant: 'normal' | 'error'): () => void {
1214
const nodes: AudioNode[] = []
1315
const gains: GainNode[] = []
1416
const t = ctx.currentTime
1517
const isError = variant === 'error'
18+
const v = VORTEX_VOLUME
1619

17-
// --- Layer 1: Rushing air (primary) ---
18-
const wind = ctx.createBufferSource()
19-
const windBuf = ctx.createBuffer(2, ctx.sampleRate * 4, ctx.sampleRate)
20+
// --- Layer 1: Noise rumble through a steep lowpass ---
21+
const rumble = ctx.createBufferSource()
22+
const rumbleBuf = ctx.createBuffer(2, ctx.sampleRate * 4, ctx.sampleRate)
2023
for (let ch = 0; ch < 2; ch++) {
21-
const data = windBuf.getChannelData(ch)
24+
const data = rumbleBuf.getChannelData(ch)
2225
for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1
2326
}
24-
wind.buffer = windBuf
25-
wind.loop = true
26-
27-
const windBP = ctx.createBiquadFilter()
28-
windBP.type = 'bandpass'
29-
windBP.frequency.setValueAtTime(isError ? 600 : 680, t)
30-
windBP.Q.setValueAtTime(0.8, t)
31-
32-
const windGain = ctx.createGain()
33-
windGain.gain.setValueAtTime(0.14, t)
34-
wind.connect(windBP).connect(windGain).connect(dest)
35-
wind.start()
36-
nodes.push(wind, windBP, windGain)
37-
gains.push(windGain)
38-
39-
// --- Layer 2: High whistle / tunnel resonance ---
40-
const whistle = ctx.createBufferSource()
41-
const whistleBuf = ctx.createBuffer(1, ctx.sampleRate * 4, ctx.sampleRate)
42-
const whistleData = whistleBuf.getChannelData(0)
43-
for (let i = 0; i < whistleData.length; i++) whistleData[i] = Math.random() * 2 - 1
44-
whistle.buffer = whistleBuf
45-
whistle.loop = true
46-
47-
const whistleBP = ctx.createBiquadFilter()
48-
whistleBP.type = 'bandpass'
49-
whistleBP.frequency.setValueAtTime(isError ? 1500 : 1800, t)
50-
whistleBP.Q.setValueAtTime(2, t)
51-
52-
// Offset sweep so the two bands don't move in sync
53-
const whistleLfo = ctx.createOscillator()
54-
const whistleDepth = ctx.createGain()
55-
whistleLfo.type = 'sine'
56-
whistleLfo.frequency.setValueAtTime(isError ? 0.073 : 0.06, t)
57-
whistleDepth.gain.setValueAtTime(isError ? 700 : 600, t)
58-
whistleLfo.connect(whistleDepth).connect(whistleBP.frequency)
59-
whistleLfo.start()
60-
61-
const whistleGain = ctx.createGain()
62-
whistleGain.gain.setValueAtTime(isError ? 0.05 : 0.04, t)
63-
whistle.connect(whistleBP).connect(whistleGain).connect(dest)
64-
whistle.start()
65-
nodes.push(whistle, whistleBP, whistleLfo, whistleDepth, whistleGain)
66-
gains.push(whistleGain)
67-
68-
// --- Layer 3: Subtle tonal undertone ---
69-
const drone = ctx.createOscillator()
70-
const droneGain = ctx.createGain()
71-
drone.type = 'sine'
72-
drone.frequency.setValueAtTime(isError ? 50 : 60, t)
73-
droneGain.gain.setValueAtTime(isError ? 0.07 : 0.04, t)
74-
drone.connect(droneGain).connect(dest)
75-
drone.start()
76-
nodes.push(drone, droneGain)
77-
gains.push(droneGain)
27+
rumble.buffer = rumbleBuf
28+
rumble.loop = true
29+
30+
const rumbleLP = ctx.createBiquadFilter()
31+
rumbleLP.type = 'lowpass'
32+
rumbleLP.frequency.setValueAtTime(isError ? 140 : 160, t)
33+
rumbleLP.Q.setValueAtTime(2.5, t) // resonant peak adds body
34+
35+
const rumbleGain = ctx.createGain()
36+
rumbleGain.gain.setValueAtTime(0.35 * v, t)
37+
rumble.connect(rumbleLP).connect(rumbleGain).connect(dest)
38+
rumble.start()
39+
nodes.push(rumble, rumbleLP, rumbleGain)
40+
gains.push(rumbleGain)
41+
42+
// --- Layer 2: Sub-bass weight ---
43+
const sub = ctx.createOscillator()
44+
const subGain = ctx.createGain()
45+
sub.type = 'triangle'
46+
sub.frequency.setValueAtTime(isError ? 35 : 42, t)
47+
subGain.gain.setValueAtTime(0.12 * v, t)
48+
sub.connect(subGain).connect(dest)
49+
sub.start()
50+
nodes.push(sub, subGain)
51+
gains.push(subGain)
52+
53+
// --- Layer 3: Slow filter cutoff drift for organic movement ---
54+
const driftLfo = ctx.createOscillator()
55+
const driftDepth = ctx.createGain()
56+
driftLfo.type = 'sine'
57+
driftLfo.frequency.setValueAtTime(0.035, t)
58+
driftDepth.gain.setValueAtTime(40, t) // ±40 Hz cutoff drift
59+
driftLfo.connect(driftDepth).connect(rumbleLP.frequency)
60+
driftLfo.start()
61+
nodes.push(driftLfo, driftDepth)
62+
63+
// --- Layer 4: Very slow amplitude breathing ---
64+
const breathLfo = ctx.createOscillator()
65+
const breathDepth = ctx.createGain()
66+
breathLfo.type = 'sine'
67+
breathLfo.frequency.setValueAtTime(0.025, t)
68+
breathDepth.gain.setValueAtTime(0.04 * v, t)
69+
breathLfo.connect(breathDepth).connect(rumbleGain.gain)
70+
breathLfo.connect(breathDepth).connect(subGain.gain)
71+
breathLfo.start()
72+
nodes.push(breathLfo, breathDepth)
7873

7974
return loopTeardown(gains, nodes, 0.5)
8075
}
8176

82-
/** Rushing-through-a-tunnel vortex loop. */
77+
/** Quiet low rumble for the loading vortex. */
8378
export const synthVortexLoop: SynthLoop = (ctx, dest) => buildVortexLoop(ctx, dest, 'normal')
8479

85-
/** Harsher, turbulent variant of the vortex for error states. */
80+
/** Slightly darker/louder variant for error states. */
8681
export const synthVortexError: SynthLoop = (ctx, dest) => buildVortexLoop(ctx, dest, 'error')
8782

8883
/** Warm energy hum for portal hover — like standing near something powerful. */

0 commit comments

Comments
 (0)