diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 4ede65869..7cbb925d8 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -268,9 +268,9 @@ export class StrudelMirror { this.flash(); await this.repl.evaluate(this.code); } - async exportAudio(begin, end) { + async exportAudio(begin, end, sampleRate, downloadName = undefined) { await this.repl.evaluate(this.code, false) - await this.repl.exportAudio(begin, end); + await this.repl.exportAudio(begin, end, sampleRate, downloadName); this.repl.scheduler.stop(); } diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index b7ab7dde4..167f18828 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -112,14 +112,12 @@ export class Cyclist { this.setStarted(true); } - async exportAudio(begin, end) { + async exportAudio(begin, end, sampleRate, downloadName = undefined) { if (!this.pattern) { throw new Error('Scheduler: no pattern set! call .setPattern first.'); } logger('[cyclist] exporting'); - // this.clock.start(); - // this.setStarted(true); - await renderPatternAudio(this.pattern, this.cps, begin, end) + await renderPatternAudio(this.pattern, this.cps, begin, end, sampleRate, downloadName) } pause() { diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 47527c334..970c15a67 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -91,7 +91,7 @@ export function repl({ const stop = () => scheduler.stop(); const start = () => scheduler.start(); - const exportAudio = async (begin, end) => await scheduler.exportAudio(begin, end); + const exportAudio = async (begin, end, sampleRate, downloadName = undefined) => await scheduler.exportAudio(begin, end, sampleRate, downloadName); const pause = () => scheduler.pause(); const toggle = () => scheduler.toggle(); const setCps = (cps) => { diff --git a/packages/superdough/audioContext.mjs b/packages/superdough/audioContext.mjs index 30fbd9fd0..ea60722b1 100644 --- a/packages/superdough/audioContext.mjs +++ b/packages/superdough/audioContext.mjs @@ -1,7 +1,10 @@ +import { analysers, resetGlobalEffects } from "./superdough.mjs"; + let audioContext; export const setDefaultAudioContext = () => { audioContext = new AudioContext(); + resetGlobalEffects() return audioContext; }; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 323866217..ed5cd9649 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -8,7 +8,7 @@ import './feedbackdelay.mjs'; import './reverb.mjs'; import './vowel.mjs'; import { nanFallback, _mod, cycleToSeconds } from './util.mjs'; -import workletsUrl from './worklets.mjs?audioworklet'; +import workletsUrl from './worklets.mjs?url'; import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs'; import { map } from 'nanostores'; import { logger } from './logger.mjs'; @@ -332,7 +332,7 @@ export let analysers = {}, analysersData = {}; export function getAnalyserById(id, fftSize = 1024, smoothingTimeConstant = 0.5) { - if (!analysers[id]) { + if (!analysers[id] || analysers[id].audioContext != getAudioContext()) { // make sure this doesn't happen too often as it piles up garbage const analyserNode = getAudioContext().createAnalyser(); analyserNode.fftSize = fftSize; @@ -722,7 +722,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) } // analyser - if (analyze) { + if (analyze && !(ac instanceof OfflineAudioContext)) { const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5)); const analyserSend = effectSend(post, analyserNode, 1); audioNodes.push(analyserSend); diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index cbc5e791c..27718bd7d 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -27,10 +27,10 @@ export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => { return superdough(hap2value(hap), t, hapDuration, cps, hap.whole?.begin.valueOf()); }; -export async function renderPatternAudio(pattern, cps, begin, end) { +export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, downloadName = undefined) { const oldAudioCtx = getAudioContext() await oldAudioCtx.close() - let audioContext = new OfflineAudioContext(2, (end - begin) / cps * 48000, 48000); + let audioContext = new OfflineAudioContext(2, (end - begin) / cps * sampleRate, sampleRate); setAudioContext(audioContext) let context = getAudioContext() setSuperdoughAudioController(new SuperdoughAudioController(context)) @@ -63,25 +63,20 @@ export async function renderPatternAudio(pattern, cps, begin, end) { } }) - return context.startRendering().then(async (renderedBuffer) => { + return context.startRendering().then((renderedBuffer) => { console.log(renderedBuffer) const wavBuffer = audioBufferToWav(renderedBuffer); const blob = new Blob([wavBuffer], { type: 'audio/wav' }); - let downloadName = '' const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - // remove leading dash if they want an empty download name - if (downloadName == '') { - downloadName = `${new Date().toISOString()}.wav`; - } else { - downloadName = downloadName + `-${new Date().toISOString()}.wav`; - } + downloadName = downloadName ? `${downloadName}.wav` : `${new Date().toISOString()}.wav`; a.download = `${downloadName}`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); + }).finally(async () => { setAudioContext(null) setSuperdoughAudioController(null) await initAudio() diff --git a/website/src/repl/components/ExportModal.jsx b/website/src/repl/components/ExportModal.jsx index 545350bbe..4f474f381 100644 --- a/website/src/repl/components/ExportModal.jsx +++ b/website/src/repl/components/ExportModal.jsx @@ -1,25 +1,51 @@ import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon'; import cx from '@src/cx.mjs'; import NumberInput from './NumberInput'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Textbox } from './textbox/Textbox'; -import { setMultiChannelOrbits as setStrudelMultiChannelOrbits, setMaxPolyphony as setStrudelMaxPolyphony } from '@strudel/webaudio'; +import { setMultiChannelOrbits as setStrudelMultiChannelOrbits, setMaxPolyphony as setStrudelMaxPolyphony, getAudioContext } from '@strudel/webaudio'; +import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon'; + function Checkbox({ label, value, onChange, disabled = false }) { return ( -