Skip to content

Commit 98b88cb

Browse files
fm modulation
1 parent 63404a0 commit 98b88cb

File tree

2 files changed

+143
-12
lines changed

2 files changed

+143
-12
lines changed

fm-processor.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
class FmProcessor extends AudioWorkletProcessor {
2+
constructor() {
3+
super(...arguments);
4+
this._phase = 0;
5+
}
6+
static get parameterDescriptors() {
7+
return [
8+
{
9+
name: "frequency",
10+
defaultValue: 440,
11+
minValue: 0.01,
12+
maxValue: 20000,
13+
automationRate: "a-rate",
14+
},
15+
{
16+
name: "level",
17+
defaultValue: 1,
18+
minValue: 0,
19+
maxValue: 1000,
20+
automationRate: "a-rate",
21+
},
22+
{
23+
name: "gain",
24+
defaultValue: 1,
25+
minValue: 0,
26+
maxValue: 1,
27+
automationRate: "a-rate",
28+
},
29+
];
30+
}
31+
process(inputs, outputs, parameters) {
32+
const blockSize = outputs[0][0].length;
33+
const levelValues = parameters.level;
34+
// mix down all input channels to mono
35+
const processedInput = new Float32Array(blockSize);
36+
for (const input of inputs) {
37+
const channelCount = input.length;
38+
for (const channel of input) {
39+
console.assert(channel.length === blockSize);
40+
for (let i = 0; i < channel.length; i++) {
41+
processedInput[i] += channel[i] / channelCount * levelValues[i];
42+
}
43+
}
44+
}
45+
const result = new Float32Array(blockSize);
46+
const frequencyValues = parameters.frequency;
47+
const gainValues = parameters.gain;
48+
for (let i = 0; i < blockSize; i++) {
49+
const frequency = frequencyValues[i];
50+
const phaseIncrement = frequency / sampleRate;
51+
this._phase += phaseIncrement;
52+
result[i] = gainValues[i] * Math.sin(this._phase * Math.PI * 2 + processedInput[i] * Math.PI * 2);
53+
}
54+
for (const output of outputs) {
55+
for (const channel of output) {
56+
for (let i = 0; i < channel.length; i++) {
57+
channel[i] = result[i];
58+
}
59+
}
60+
}
61+
return true;
62+
}
63+
}
64+
registerProcessor('fm-processor', FmProcessor);

index.html

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,31 @@
44
<meta charset="utf-8">
55
<title>Hear the Traffic (2025)</title>
66
<style>
7-
7+
body {
8+
font-family: system-ui, sans-serif;
9+
}
10+
#guage {
11+
margin-block: 1rem;
12+
display: block;
13+
padding: 0;
14+
inline-size: 100%;
15+
}
16+
#toggle-sound {
17+
font: inherit;
18+
}
819
</style>
920
</head>
1021
<body>
1122
<h1>Hear the Traffic</h1>
1223
<p>By 真空 (2025)</p>
1324
<button id="toggle-sound">Unmute/Mute</button>
25+
<meter id="guage" min="0" max="255"></meter>
1426
<script>
1527
const toggleButton = document.querySelector('#toggle-sound');
28+
const guage = document.querySelector('#guage');
1629
let sound = false;
1730

18-
let realData = new Array(120).fill(0);
31+
let absData = new Array(120).fill(0);
1932

2033
toggleButton.addEventListener('click', () => {
2134
if (sound) {
@@ -29,21 +42,33 @@ <h1>Hear the Traffic</h1>
2942

3043
let ac = null;
3144
let osc = null;
32-
let freq = 30;
45+
let dynamicRange = 3;
46+
let worklet = null;
47+
let gain = null;
48+
let gainGain = null;
49+
let analyser = null;
50+
51+
let analyserArray = null;
3352

3453
const update = () => {
3554
fetch('https://metrics.nc.menhera.org/_traffic.json').then(async (res) => {
3655
if (!res.ok) return;
3756
const data = await res.json();
38-
freq = data[0] * 2;
39-
realData = [0, ... data.slice(7, 126)];
57+
dynamicRange = data[0];
58+
absData = [0, ... data.slice(1)];
4059

4160
if (osc) {
42-
let len = realData.length;
61+
let len = absData.length;
62+
const realData = new Array(len).fill(0);
4363
const imagData = new Array(len).fill(0);
64+
for (let i = 0; i < len; i++) {
65+
let phase = Math.random() * 2 * Math.PI;
66+
realData[i] = absData[i] * Math.cos(phase);
67+
imagData[i] = absData[i] * Math.sin(phase);
68+
}
4469
const periodicWave = ac.createPeriodicWave(realData, imagData);
45-
osc.frequency.value = freq;
4670
osc.setPeriodicWave(periodicWave);
71+
gain.gain.value = 33 * dynamicRange;
4772
}
4873
}).catch((e) => {
4974
console.error(e);
@@ -54,25 +79,67 @@ <h1>Hear the Traffic</h1>
5479

5580
function sound_start() {
5681
ac = new AudioContext();
82+
analyser = ac.createAnalyser();
83+
analyser.fftSize = 8192;
84+
const bufferLength = analyser.frequencyBinCount;
85+
analyserArray = new Uint8Array(bufferLength);
5786
osc = ac.createOscillator();
58-
osc.frequency.value = freq;
59-
let len = realData.length;
87+
osc.frequency.value = 0.0667;
88+
let len = absData.length;
89+
const realData = new Array(len).fill(0);
6090
const imagData = new Array(len).fill(0);
91+
for (let i = 0; i < len; i++) {
92+
let phase = Math.random() * 2 * Math.PI;
93+
realData[i] = absData[i] * Math.cos(phase);
94+
imagData[i] = absData[i] * Math.sin(phase);
95+
}
6196
const periodicWave = ac.createPeriodicWave(realData, imagData);
6297
osc.setPeriodicWave(periodicWave);
63-
osc.connect(ac.destination);
64-
ac.resume();
65-
osc.start();
98+
osc.connect(analyser);
99+
ac.audioWorklet.addModule('fm-processor.js').then(() => {
100+
worklet = new AudioWorkletNode(ac, 'fm-processor');
101+
const freqParam = worklet.parameters.get('frequency');
102+
const gainParam = worklet.parameters.get('gain');
103+
gain = new GainNode(ac);
104+
gainGain = new GainNode(ac);
105+
gainGain.gain.value = 0.2;
106+
freqParam.value = 440;
107+
osc.connect(gain);
108+
osc.connect(gainGain);
109+
gain.connect(freqParam);
110+
gainParam.value = -0.5;
111+
gainGain.connect(gainParam);
112+
gain.gain.value = dynamicRange * 33;
113+
worklet.connect(ac.destination);
114+
ac.resume();
115+
osc.start();
116+
});
66117
}
67118

68119
function sound_stop() {
69120
if (!ac) return;
70121
ac.close();
71122
ac = null;
72123
osc = null;
124+
analyser = null;
125+
}
126+
127+
function draw() {
128+
requestAnimationFrame(draw);
129+
130+
if (!analyser) return;
131+
analyser.getByteTimeDomainData(analyserArray);
132+
let sum = 0;
133+
analyserArray.forEach(v => {
134+
sum += v;
135+
});
136+
let value = sum / analyserArray.length;
137+
guage.value = value;
73138
}
74139

75140
update();
141+
142+
draw();
76143
</script>
77144
</body>
78145
</html>

0 commit comments

Comments
 (0)