-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSnfAmbientAudio.jsx
More file actions
150 lines (138 loc) · 4.17 KB
/
Copy pathSnfAmbientAudio.jsx
File metadata and controls
150 lines (138 loc) · 4.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
import { useEffect, useRef } from 'react';
import { useSnf } from '../../context/SnfContext';
/**
* Asset-free ambient audio for the terminal, synthesised with the Web Audio API.
* - hum : layered low oscillators through a lowpass (electrical drone)
* - static : white noise through a highpass (dead-channel hiss)
* - rain : filtered noise with slow amplitude flutter (acid rain)
*
* Never autoplays — it only starts once the user enables `audio`, which is a
* user gesture, satisfying browser autoplay policy.
*/
const SnfAmbientAudio = () => {
const { settings } = useSnf();
const ctxRef = useRef(null);
const masterRef = useRef(null);
const nodesRef = useRef([]);
const teardown = () => {
nodesRef.current.forEach((n) => {
try {
n.stop?.();
n.disconnect?.();
} catch {
/* ignore */
}
});
nodesRef.current = [];
};
// Build / rebuild the graph when the master switch or track changes.
useEffect(() => {
if (!settings.audio) {
teardown();
return undefined;
}
const AudioCtx = window.AudioContext || window.webkitAudioContext;
if (!AudioCtx) return undefined;
if (!ctxRef.current) {
ctxRef.current = new AudioCtx();
masterRef.current = ctxRef.current.createGain();
masterRef.current.connect(ctxRef.current.destination);
}
const ctx = ctxRef.current;
const master = masterRef.current;
master.gain.value = settings.volume;
ctx.resume?.();
teardown();
const made = [];
const makeNoise = () => {
const buffer = ctx.createBuffer(1, ctx.sampleRate * 2, ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < data.length; i += 1) data[i] = Math.random() * 2 - 1;
const src = ctx.createBufferSource();
src.buffer = buffer;
src.loop = true;
return src;
};
if (settings.audioTrack === 'hum') {
const lp = ctx.createBiquadFilter();
lp.type = 'lowpass';
lp.frequency.value = 220;
lp.connect(master);
[55, 82.5, 110].forEach((freq, idx) => {
const osc = ctx.createOscillator();
osc.type = idx === 2 ? 'sine' : 'triangle';
osc.frequency.value = freq;
const g = ctx.createGain();
g.gain.value = idx === 0 ? 0.5 : 0.18;
osc.connect(g);
g.connect(lp);
osc.start();
made.push(osc, g);
});
// slow tremolo
const lfo = ctx.createOscillator();
lfo.frequency.value = 0.15;
const lfoGain = ctx.createGain();
lfoGain.gain.value = 60;
lfo.connect(lfoGain);
lfoGain.connect(lp.frequency);
lfo.start();
made.push(lfo, lfoGain, lp);
} else if (settings.audioTrack === 'static') {
const noise = makeNoise();
const hp = ctx.createBiquadFilter();
hp.type = 'highpass';
hp.frequency.value = 1200;
const g = ctx.createGain();
g.gain.value = 0.12;
noise.connect(hp);
hp.connect(g);
g.connect(master);
noise.start();
made.push(noise, hp, g);
} else {
// rain
const noise = makeNoise();
const lp = ctx.createBiquadFilter();
lp.type = 'lowpass';
lp.frequency.value = 1600;
const g = ctx.createGain();
g.gain.value = 0.22;
noise.connect(lp);
lp.connect(g);
g.connect(master);
noise.start();
// flutter
const lfo = ctx.createOscillator();
lfo.frequency.value = 0.8;
const lfoGain = ctx.createGain();
lfoGain.gain.value = 0.08;
lfo.connect(lfoGain);
lfoGain.connect(g.gain);
lfo.start();
made.push(noise, lp, g, lfo, lfoGain);
}
nodesRef.current = made;
return undefined;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [settings.audio, settings.audioTrack]);
// Live volume changes.
useEffect(() => {
if (masterRef.current) masterRef.current.gain.value = settings.volume;
}, [settings.volume]);
// Cleanup on unmount.
useEffect(
() => () => {
teardown();
try {
ctxRef.current?.close();
} catch {
/* ignore */
}
ctxRef.current = null;
},
[],
);
return null;
};
export default SnfAmbientAudio;