Merge pull request 'Fix wav export on firefox' (#2101) from tzwaan/fix-firefox-export into main

Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2101
This commit is contained in:
froos
2026-08-13 19:58:42 +02:00
3 changed files with 48 additions and 25 deletions
+3
View File
@@ -19,6 +19,9 @@ export const setAudioContext = (context) => {
// Existing nodes in the node pool contain references to the previous AudioContext,
// so all the nodes in the pool must be cleared when we set a new AudioContext.
clearNodePool();
if (audioContext && audioContext.state !== 'closed') {
audioContext.close();
}
audioContext = context;
return audioContext;
};
+3 -3
View File
@@ -6,8 +6,8 @@ import { clamp, nanFallback, midiToFreq, noteToMidi } from './util.mjs';
export const noises = ['pink', 'white', 'brown', 'crackle'];
export function gainNode(value) {
const node = getAudioContext().createGain();
export function gainNode(value, audioContext = getAudioContext()) {
const node = audioContext.createGain();
node.gain.value = value;
return node;
}
@@ -374,7 +374,7 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
// Certain browsers requires audio nodes to be connected in order for their onended events
// to fire, so we _mute it_ and then connect it to the destination
const zeroGain = gainNode(0);
const zeroGain = gainNode(0, audioContext);
zeroGain.connect(audioContext.destination);
constantNode.connect(zeroGain);
+42 -22
View File
@@ -57,7 +57,13 @@ export async function renderPatternAudio(
multiChannelOrbits,
});
return renderPatternAudioInChunks(audioContext, pattern, cps, begin, end, 1)
// Firefox currently doesn't support suspending an OfflineAudioContext,
// so no chunked rendering. Bad performance, but at least it works.
return (
audioContext.suspend === undefined
? renderPatternAudioWhole(audioContext, pattern, cps, begin, end)
: renderPatternAudioInChunks(audioContext, pattern, cps, begin, end, 1)
)
.then((renderedBuffer) => {
const wavBuffer = audioBufferToWav(renderedBuffer);
const blob = new Blob([wavBuffer], { type: 'audio/wav' });
@@ -78,6 +84,16 @@ export async function renderPatternAudio(
});
}
async function renderPatternAudioWhole(audioContext, pattern, cps, begin, end) {
logger(`[webaudio] preloading`);
await scheduleHapsChunk(pattern, cps, begin, begin, end);
logger('[webaudio] start rendering');
return audioContext.startRendering();
}
async function renderPatternAudioInChunks(audioContext, pattern, cps, begin, end, chunkSizeInCycles) {
let currentCycle = begin;
let renderPromise = null;
@@ -90,27 +106,7 @@ async function renderPatternAudioInChunks(audioContext, pattern, cps, begin, end
logger(`[webaudio] preloading cycles ${chunkStart} - ${chunkEnd}`);
// Calling superdough(...) in ascending onset time order is important
// for controls that depend on the audio graph state like `cut`
let haps = pattern
.queryArc(chunkStart, chunkEnd, { _cps: cps })
.sort((a, b) => a.whole.begin.valueOf() - b.whole.begin.valueOf());
for (const hap of haps) {
if (hap.hasOnset()) {
try {
await superdough(
hap2value(hap),
(hap.whole.begin.valueOf() - begin) / cps,
hap.duration / cps,
cps,
(hap.whole?.begin.valueOf() - begin) / cps,
);
} catch (err) {
errorLogger(err, 'webaudio');
}
}
}
await scheduleHapsChunk(pattern, cps, begin, chunkStart, chunkEnd);
logger(`[webaudio] rendering cycles ${chunkStart} - ${chunkEnd}`);
@@ -142,6 +138,30 @@ async function renderPatternAudioInChunks(audioContext, pattern, cps, begin, end
return renderPromise;
}
async function scheduleHapsChunk(pattern, cps, begin, chunkStart, chunkEnd) {
// Calling superdough(...) in ascending onset time order is important
// for controls that depend on the audio graph state like `cut`
let haps = pattern
.queryArc(chunkStart, chunkEnd, { _cps: cps })
.sort((a, b) => a.whole.begin.valueOf() - b.whole.begin.valueOf());
for (const hap of haps) {
if (hap.hasOnset()) {
try {
await superdough(
hap2value(hap),
(hap.whole.begin.valueOf() - begin) / cps,
hap.duration / cps,
cps,
(hap.whole?.begin.valueOf() - begin) / cps,
);
} catch (err) {
errorLogger(err, 'webaudio');
}
}
}
}
export function webaudioRepl(options = {}) {
const audioContext = options.audioContext ?? getAudioContext();
setAudioContext(audioContext);