Back Original

Creating the Aetheryte Radio

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.