mirror of
https://codeberg.org/uzu/strudel
synced 2026-08-13 09:00:39 -04:00
Merge pull request 'Improve wav render export performance' (#2093) from tzwaan/wav-export-performance into main
Reviewed-on: https://codeberg.org/uzu/strudel/pulls/2093
This commit is contained in:
@@ -7,14 +7,18 @@ Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { clearNodePool } from './nodePools.mjs';
|
||||
|
||||
let audioContext;
|
||||
|
||||
export const setDefaultAudioContext = () => {
|
||||
audioContext = new AudioContext();
|
||||
return audioContext;
|
||||
return setAudioContext(new AudioContext());
|
||||
};
|
||||
|
||||
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();
|
||||
audioContext = context;
|
||||
return audioContext;
|
||||
};
|
||||
|
||||
@@ -5,11 +5,24 @@ Copyright (C) 2025 Strudel contributors - see <https://codeberg.org/uzu/strudel/
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { releaseAudioNode } from './helpers.mjs';
|
||||
|
||||
const nodePools = new Map();
|
||||
const POOL_KEY = Symbol('nodePoolKey');
|
||||
|
||||
export const isPoolable = (node) => !!node[POOL_KEY];
|
||||
|
||||
export const clearNodePool = () => {
|
||||
for (const pool of nodePools) {
|
||||
for (const node of pool) {
|
||||
if (node instanceof AudioNode) {
|
||||
releaseAudioNode(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
nodePools.clear();
|
||||
};
|
||||
|
||||
const getNodeTime = (node) => {
|
||||
return node.context?.currentTime ?? 0;
|
||||
};
|
||||
|
||||
@@ -56,32 +56,8 @@ export async function renderPatternAudio(
|
||||
maxPolyphony,
|
||||
multiChannelOrbits,
|
||||
});
|
||||
logger('[webaudio] preloading');
|
||||
|
||||
// Calling superdough(...) in ascending onset time order is important
|
||||
// for controls that depend on the audio graph state like `cut`
|
||||
let haps = pattern
|
||||
.queryArc(begin, end, { _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');
|
||||
}
|
||||
}
|
||||
}
|
||||
logger('[webaudio] start rendering');
|
||||
|
||||
return audioContext
|
||||
.startRendering()
|
||||
return renderPatternAudioInChunks(audioContext, pattern, cps, begin, end, 1)
|
||||
.then((renderedBuffer) => {
|
||||
const wavBuffer = audioBufferToWav(renderedBuffer);
|
||||
const blob = new Blob([wavBuffer], { type: 'audio/wav' });
|
||||
@@ -102,6 +78,70 @@ export async function renderPatternAudio(
|
||||
});
|
||||
}
|
||||
|
||||
async function renderPatternAudioInChunks(audioContext, pattern, cps, begin, end, chunkSizeInCycles) {
|
||||
let currentCycle = begin;
|
||||
let renderPromise = null;
|
||||
|
||||
logger('[webaudio] start rendering');
|
||||
|
||||
while (currentCycle <= end) {
|
||||
const chunkStart = currentCycle;
|
||||
const chunkEnd = Math.min(currentCycle + chunkSizeInCycles, 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger(`[webaudio] rendering cycles ${chunkStart} - ${chunkEnd}`);
|
||||
|
||||
currentCycle += chunkSizeInCycles;
|
||||
|
||||
// According to the MDN docs, suspends should be scheduled while
|
||||
// the audioContext is not currently running for better precision.
|
||||
// So we schedule the suspend first, and await after resuming.
|
||||
var suspendPromise;
|
||||
if (currentCycle < end) {
|
||||
// Make sure to suspend one cycle before the next currentCycle
|
||||
// so the next haps can be scheduled on time.
|
||||
suspendPromise = audioContext.suspend((currentCycle - begin - 1) / cps);
|
||||
}
|
||||
|
||||
if (renderPromise === null) {
|
||||
renderPromise = audioContext.startRendering();
|
||||
} else {
|
||||
await audioContext.resume();
|
||||
}
|
||||
|
||||
if (currentCycle < end) {
|
||||
await suspendPromise;
|
||||
}
|
||||
}
|
||||
|
||||
logger('[webaudio] finish rendering');
|
||||
|
||||
return renderPromise;
|
||||
}
|
||||
|
||||
export function webaudioRepl(options = {}) {
|
||||
const audioContext = options.audioContext ?? getAudioContext();
|
||||
setAudioContext(audioContext);
|
||||
|
||||
Reference in New Issue
Block a user