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.