制作以太之光广播
Creating the Aetheryte Radio

原始链接: https://haz.ee/posts/aetheryte-radio.html

为了模拟有机且非重复的环境音效,作者使用 Web Audio API 重现了《最终幻想 XIV》的“呼啸”系统。 该实现由两部分组成: 1. **持续的嗡嗡声:** 使用循环的 `AudioBufferSourceNode` 作为稳定的背景基础。 2. **随机的呼啸声:** 系统不使用标准的循环,而是使用递归的 `setTimeout` 函数(`chooseWhir`)。每当一段短促的“呼啸”声结束时,系统会随机选择一个音频素材,应用随机的音高和增益,并在随机延迟后安排下一次播放。 通过即时创建和断开节点,而非使用固定循环,代码掩盖了重复性并创造了一种“生命的错觉”。这种方法以极低的性能开销,有效地模拟了复杂且动态的声景。

抱歉。
相关文章

原文

ffxiv plays the whirs at random intervals, pitches, and gain (volume). these are all added to create an illusion of life(?) that make it harder to detect when an asset is repeating. thankfully, ffxiv also provided values that gave the min and max values for each of the random parameters, so it was easy to translate into javascript:

const whirIndex = Math.floor(Math.random() * whirs.length);

const whirPlaybackRate = Math.random() * (1 - 0.794) + 0.794;

whirGainNode.gain.value = Math.random() * (1 - 0.6) + 0.6;

if you've ever built a graph by hand the flow is usually something like: allocate a node, set metadata, and connect. i do the following to setup the hum:

const humSource = audioContext.createBufferSource();
humSource.buffer = await loadSample(
    audioContext,
    isSafari ? "assets/hum.wav.opus.aac" : "assets/hum.wav.opus",
);
humSource.playbackRate.value = 0.63;
humSource.connect(humGainNode);
humSource.loop = true;
humSource.start(0);

it's actually ok that i connect the node before i set loop, because the node doesn't produce samples until i call start.

the other part of the ceremony is the whir loop. it's not actually a loop using conventional loop control flow. instead of using a while (true) with a random "sleep" in between, i instead use setTimeout to schedule the next whir at a random interval.

function chooseWhir() {
    const whirSource = audioContext.createBufferSource();
    const whirIndex = Math.floor(Math.random() * whirs.length);
    const whirPlaybackRate = Math.random() * (1 - 0.794) + 0.794;
    whirGainNode.gain.value = Math.random() * (1 - 0.6) + 0.6;
    whirSource.buffer = whirs[whirIndex];
    whirSource.playbackRate.value = whirPlaybackRate;
    whirSource.connect(whirGainNode);
    whirSource.start(0);

    whirSource.onended = () => {
        
        whirSource.disconnect(whirGainNode);
        const nextWhirDelay = Math.floor(Math.random() * 2001);
        setTimeout(chooseWhir, nextWhirDelay);
    };
}

same deal here: create a source node, select a random asset, playback rate (pitch), gain (volume), and connect it to the gain node (which stays constant in this process and only has it's value changed.) the key here is that when the source asset ends, instead of looping, we remove that node from the graph and add a new one (by calling chooseWhir again.) because there is an expected delay before the next whir, i'm ok with adding whatever latency is added by doing the random calculation, it's likely marginal.

联系我们 contact @ memedata.com