From 0fbde61b2dd4bb2f2c3bdf2b451d05bfa73264e4 Mon Sep 17 00:00:00 2001 From: Nikita Date: Sun, 19 Oct 2025 14:35:23 +0300 Subject: [PATCH 01/40] WIP non-realtime exporting --- packages/codemirror/codemirror.mjs | 10 +- packages/core/cyclist.mjs | 13 ++ packages/core/repl.mjs | 31 ++-- packages/superdough/audioContext.mjs | 5 + packages/superdough/feedbackdelay.mjs | 2 +- packages/superdough/reverb.mjs | 4 +- packages/superdough/superdough.mjs | 12 +- packages/superdough/vowel.mjs | 2 +- packages/webaudio/webaudio.mjs | 151 +++++++++++++++++- website/src/repl/components/Header.jsx | 12 +- .../src/repl/components/panel/SettingsTab.jsx | 2 +- website/src/repl/useReplContext.jsx | 4 + 12 files changed, 217 insertions(+), 31 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 4dc23996f..645a8a190 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -42,9 +42,9 @@ const extensions = { isMultiCursorEnabled: (on) => on ? [ - EditorState.allowMultipleSelections.of(true), - EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), - ] + EditorState.allowMultipleSelections.of(true), + EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), + ] : [], }; const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()])); @@ -268,6 +268,10 @@ export class StrudelMirror { this.flash(); await this.repl.evaluate(this.code); } + async exportAudio(begin, end) { + // await this.repl.evaluate(this.code, false) + this.repl.exportAudio(begin, end); + } async stop() { this.repl.scheduler.stop(); } diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 59e410c53..b7ab7dde4 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -6,6 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import createClock from './zyklus.mjs'; import { errorLogger, logger } from './logger.mjs'; +import { loadBuffer, renderPatternAudio, setAudioContext, superdough } from '@strudel/webaudio'; export class Cyclist { constructor({ @@ -98,6 +99,7 @@ export class Cyclist { this.started = v; this.onToggle?.(v); } + async start() { await this.beforeStart?.(); this.num_ticks_since_cps_change = 0; @@ -109,6 +111,17 @@ export class Cyclist { this.clock.start(); this.setStarted(true); } + + async exportAudio(begin, end) { + 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) + } + pause() { logger('[cyclist] pause'); this.clock.pause(); diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 171697eb9..d0cb1e19f 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -91,6 +91,7 @@ export function repl({ const stop = () => scheduler.stop(); const start = () => scheduler.start(); + const exportAudio = (begin, end) => scheduler.exportAudio(begin, end); const pause = () => scheduler.pause(); const toggle = () => scheduler.toggle(); const setCps = (cps) => { @@ -257,23 +258,23 @@ export function repl({ } }; const setCode = (code) => updateState({ code }); - return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state }; + return { scheduler, evaluate, start, exportAudio, stop, pause, setCps, setPattern, setCode, toggle, state }; } export const getTrigger = ({ getTime, defaultOutput }) => - async (hap, deadline, duration, cps, t) => { - // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) - // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 - try { - if (!hap.context.onTrigger || !hap.context.dominantTrigger) { - await defaultOutput(hap, deadline, duration, cps, t); + async (hap, deadline, duration, cps, t) => { + // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) + // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 + try { + if (!hap.context.onTrigger || !hap.context.dominantTrigger) { + await defaultOutput(hap, deadline, duration, cps, t); + } + if (hap.context.onTrigger) { + // call signature of output / onTrigger is different... + await hap.context.onTrigger(hap, getTime(), cps, t); + } + } catch (err) { + errorLogger(err, 'getTrigger'); } - if (hap.context.onTrigger) { - // call signature of output / onTrigger is different... - await hap.context.onTrigger(hap, getTime(), cps, t); - } - } catch (err) { - errorLogger(err, 'getTrigger'); - } - }; + }; diff --git a/packages/superdough/audioContext.mjs b/packages/superdough/audioContext.mjs index 71e01d57d..30fbd9fd0 100644 --- a/packages/superdough/audioContext.mjs +++ b/packages/superdough/audioContext.mjs @@ -5,6 +5,11 @@ export const setDefaultAudioContext = () => { return audioContext; }; +export const setAudioContext = (context) => { + audioContext = context + return audioContext; +}; + export const getAudioContext = () => { if (!audioContext) { return setDefaultAudioContext(); diff --git a/packages/superdough/feedbackdelay.mjs b/packages/superdough/feedbackdelay.mjs index c182d6558..b8269bc02 100644 --- a/packages/superdough/feedbackdelay.mjs +++ b/packages/superdough/feedbackdelay.mjs @@ -25,7 +25,7 @@ if (typeof DelayNode !== 'undefined') { } } - AudioContext.prototype.createFeedbackDelay = function (wet, time, feedback) { + BaseAudioContext.prototype.createFeedbackDelay = function (wet, time, feedback) { return new FeedbackDelayNode(this, wet, time, feedback); }; } diff --git a/packages/superdough/reverb.mjs b/packages/superdough/reverb.mjs index 2960b597c..c8b6eb3e5 100644 --- a/packages/superdough/reverb.mjs +++ b/packages/superdough/reverb.mjs @@ -2,7 +2,7 @@ import reverbGen from './reverbGen.mjs'; import { clamp } from './util.mjs'; if (typeof AudioContext !== 'undefined') { - AudioContext.prototype.adjustLength = function (duration, buffer, speed = 1, offsetAmount = 0) { + BaseAudioContext.prototype.adjustLength = function (duration, buffer, speed = 1, offsetAmount = 0) { const sampleOffset = Math.floor(clamp(offsetAmount, 0, 1) * buffer.length); const newLength = buffer.sampleRate * duration; const newBuffer = this.createBuffer(buffer.numberOfChannels, buffer.length, buffer.sampleRate); @@ -23,7 +23,7 @@ if (typeof AudioContext !== 'undefined') { return newBuffer; }; - AudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir, irspeed, irbegin) { + BaseAudioContext.prototype.createReverb = function (duration, fade, lp, dim, ir, irspeed, irbegin) { const convolver = this.createConvolver(); convolver.generate = (d = 2, fade = 0.1, lp = 15000, dim = 1000, ir, irspeed, irbegin) => { convolver.duration = d; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index a37504b1d..80205a298 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -13,7 +13,7 @@ import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorkle import { map } from 'nanostores'; import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; -import { getAudioContext } from './audioContext.mjs'; +import { getAudioContext, setAudioContext } from './audioContext.mjs'; import { SuperdoughAudioController } from './superdoughoutput.mjs'; export const DEFAULT_MAX_POLYPHONY = 128; @@ -367,6 +367,7 @@ function mapChannelNumbers(channels) { } export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => { + controller = null; // new: t is always expected to be the absolute target onset time const ac = getAudioContext(); const audioController = getSuperdoughAudioController(); @@ -387,14 +388,13 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // duration is passed as value too.. value.duration = hapDuration; // calculate absolute time - - if (t < ac.currentTime) { + if (t < ac.currentTime && !(ac instanceof OfflineAudioContext)) { console.warn( `[superdough]: cannot schedule sounds in the past (target: ${t.toFixed(2)}, now: ${ac.currentTime.toFixed(2)})`, ); return; - } - // destructure + } // FIXME: fix + // destructure let { tremolo, tremolosync, @@ -736,4 +736,4 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) export const superdoughTrigger = (t, hap, ct, cps) => { superdough(hap, t - ct, hap.duration / cps, cps); -}; +}; \ No newline at end of file diff --git a/packages/superdough/vowel.mjs b/packages/superdough/vowel.mjs index 3f30aef1e..025677fc1 100644 --- a/packages/superdough/vowel.mjs +++ b/packages/superdough/vowel.mjs @@ -63,7 +63,7 @@ if (typeof GainNode !== 'undefined') { } } - AudioContext.prototype.createVowelFilter = function (letter) { + BaseAudioContext.prototype.createVowelFilter = function (letter) { return new VowelNode(this, letter); }; } diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 383e87f87..46bef88d6 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -5,7 +5,7 @@ This program is free software: you can redistribute it and/or modify it under th */ import * as strudel from '@strudel/core'; -import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet } from 'superdough'; +import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet, setAudioContext, getSampleBufferSource, loadBuffer, getSampleInfo, getSound } from 'superdough'; import './supradough.mjs'; import { workletUrl } from 'supradough'; @@ -26,6 +26,60 @@ 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) { + let audioContext = new OfflineAudioContext(2, (end - begin) / cps * 48000, 48000); + setAudioContext(audioContext) + logger('[webaudio] start rendering'); + console.log(audioContext) + + let haps = pattern.queryArc(begin, end, { _cps: cps }) + Promise.all(haps.map(async h => { + let s; + if (h.value.s) { + if (h.value.bank) { + s = `${h.value.bank}_${h.value.s}`; + } + else { + s = h.value.s + } + } + let bank = getSound(s).data.samples + if (bank) { + let { url: sampleUrl, label } = getSampleInfo(h.value, bank); + await loadBuffer(sampleUrl, audioContext, label) + } + })) + haps.forEach(async (hap) => { + if (hap.hasOnset()) { + hap.ensureObjectValue(); + await superdough(hap.value, hap.whole.begin.valueOf() / cps, hap.duration, cps, hap.whole?.begin.valueOf()); + } + }) + + let context = getAudioContext() + 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`; + } + a.download = `${downloadName}`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + setAudioContext(null) + }); +} + export function webaudioRepl(options = {}) { options = { getTime: () => getAudioContext().currentTime, @@ -38,3 +92,98 @@ export function webaudioRepl(options = {}) { Pattern.prototype.dough = function () { return this.onTrigger(doughTrigger, 1); }; + + +function audioBufferToWav(buffer, opt) { + opt = opt || {} + + var numChannels = buffer.numberOfChannels + var sampleRate = buffer.sampleRate + var format = opt.float32 ? 3 : 1 + var bitDepth = format === 3 ? 32 : 16 + + var result + if (numChannels === 2) { + result = interleave(buffer.getChannelData(0), buffer.getChannelData(1)) + } else { + result = buffer.getChannelData(0) + } + + return encodeWAV(result, format, sampleRate, numChannels, bitDepth) +} + +function encodeWAV(samples, format, sampleRate, numChannels, bitDepth) { + var bytesPerSample = bitDepth / 8 + var blockAlign = numChannels * bytesPerSample + + var buffer = new ArrayBuffer(44 + samples.length * bytesPerSample) + var view = new DataView(buffer) + + /* RIFF identifier */ + writeString(view, 0, 'RIFF') + /* RIFF chunk length */ + view.setUint32(4, 36 + samples.length * bytesPerSample, true) + /* RIFF type */ + writeString(view, 8, 'WAVE') + /* format chunk identifier */ + writeString(view, 12, 'fmt ') + /* format chunk length */ + view.setUint32(16, 16, true) + /* sample format (raw) */ + view.setUint16(20, format, true) + /* channel count */ + view.setUint16(22, numChannels, true) + /* sample rate */ + view.setUint32(24, sampleRate, true) + /* byte rate (sample rate * block align) */ + view.setUint32(28, sampleRate * blockAlign, true) + /* block align (channel count * bytes per sample) */ + view.setUint16(32, blockAlign, true) + /* bits per sample */ + view.setUint16(34, bitDepth, true) + /* data chunk identifier */ + writeString(view, 36, 'data') + /* data chunk length */ + view.setUint32(40, samples.length * bytesPerSample, true) + if (format === 1) { // Raw PCM + floatTo16BitPCM(view, 44, samples) + } else { + writeFloat32(view, 44, samples) + } + + return buffer +} + +function interleave(inputL, inputR) { + var length = inputL.length + inputR.length + var result = new Float32Array(length) + + var index = 0 + var inputIndex = 0 + + while (index < length) { + result[index++] = inputL[inputIndex] + result[index++] = inputR[inputIndex] + inputIndex++ + } + return result +} + +function writeFloat32(output, offset, input) { + for (var i = 0; i < input.length; i++, offset += 4) { + output.setFloat32(offset, input[i], true) + } +} + +function floatTo16BitPCM(output, offset, input) { + for (var i = 0; i < input.length; i++, offset += 2) { + var s = Math.max(-1, Math.min(1, input[i])) + output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true) + } +} + +function writeString(view, offset, string) { + for (var i = 0; i < string.length; i++) { + view.setUint8(offset + i, string.charCodeAt(i)) + } +} \ No newline at end of file diff --git a/website/src/repl/components/Header.jsx b/website/src/repl/components/Header.jsx index ca4e1ecf9..656aa7861 100644 --- a/website/src/repl/components/Header.jsx +++ b/website/src/repl/components/Header.jsx @@ -8,7 +8,7 @@ const { BASE_URL } = import.meta.env; const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; export function Header({ context, embedded = false }) { - const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShuffle, handleShare } = + const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShuffle, handleShare, handleExport } = context; const isEmbedded = typeof window !== 'undefined' && (embedded || window.location !== window.parent.location); const { isZen, isButtonRowHidden, isCSSAnimationDisabled, fontFamily } = useSettings(); @@ -93,6 +93,16 @@ export function Header({ context, embedded = false }) { > {!isEmbedded && update} + {/* !isEmbedded && ( + + +
+

Start cycle

+ { + let v = parseInt(e.target.value); + console.log(v) + v = isNaN(v) ? 0 : Math.max(1, v); + setStartCycle(v); + }} + onChange={v => { + v = parseInt(v) + setStartCycle(v); + }} + type="number" + placeholder="" + value={startCycle ?? ''} + /> +
+
+

End cycle

+ { + let v = parseInt(e.target.value); + v = isNaN(v) ? Math.max(startCycle + 1, parseInt(v)) : v; + setEndCycle(v); + }} + onChange={(v) => { + v = parseInt(v) + setEndCycle(v); + }} + type="number" + placeholder="" + value={endCycle ?? ''} + /> +
+
+

Maximum polyphony

+ { + let v = parseInt(e.target.value); + v = isNaN(v) ? Math.max(1, parseInt(v)) : v; + setMaxPolyphony(v); + }} + onChange={(v) => { + v = Math.max(1, parseInt(v)); + setMaxPolyphony(v); + }} + type="number" + placeholder="" + value={maxPolyphony ?? ''} + /> +
+
+ { + const val = cbEvent.target.checked; + setMultiChannelOrbits(val) + }} + value={multiChannelOrbits} + /> +
+ +
+ + ); +} diff --git a/website/src/repl/components/Header.jsx b/website/src/repl/components/Header.jsx index 91e942d39..d5d8b5f75 100644 --- a/website/src/repl/components/Header.jsx +++ b/website/src/repl/components/Header.jsx @@ -3,6 +3,7 @@ import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon'; import cx from '@src/cx.mjs'; import { useSettings, setIsZen } from '../../settings.mjs'; import '../Repl.css'; +import ExportModal from './ExportModal'; const { BASE_URL } = import.meta.env; const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; @@ -93,18 +94,7 @@ export function Header({ context, embedded = false }) { > {!isEmbedded && update} - + {/* !isEmbedded && ( - - -
-

Start cycle

- { - let v = parseInt(e.target.value); - console.log(v) - v = isNaN(v) ? 0 : Math.max(0, v); - setStartCycle(v); - }} - onChange={v => { - v = parseInt(v) - setStartCycle(v); - }} - type="number" - placeholder="" - value={startCycle ?? ''} - /> -
-
-

End cycle

- { - let v = parseInt(e.target.value); - v = isNaN(v) ? Math.max(startCycle + 1, parseInt(v)) : v; - setEndCycle(v); - }} - onChange={(v) => { - v = parseInt(v) - setEndCycle(v); - }} - type="number" - placeholder="" - value={endCycle ?? ''} - /> -
-
-

Maximum polyphony

- { - let v = parseInt(e.target.value); - v = isNaN(v) ? Math.max(1, parseInt(v)) : v; - setMaxPolyphony(v); - }} - onChange={(v) => { - v = Math.max(1, parseInt(v)); - setMaxPolyphony(v); - }} - type="number" - placeholder="" - value={maxPolyphony ?? ''} - /> -
-
- { - const val = cbEvent.target.checked; - setMultiChannelOrbits(val) - }} - value={multiChannelOrbits} - /> -
- + }} + className={cx( + 'absolute text-foreground max-h-8 min-h-8 max-w-8 min-w-8 items-center p-1.5 right-2 top-2 z-50', + exporting && "opacity-50" + )} + aria-label="Close Modal" + > + + +
+ + { + setDownloadName(e.target.value) + }} + onChange={v => { + setDownloadName(v) + }} + disabled={exporting} + placeholder="Leave empty to use current date" + className={cx("placeholder:opacity-50", exporting && "opacity-50 border-opacity-50")} + value={downloadName ?? ''} + /> + +
+ + { + let v = parseInt(e.target.value); + v = isNaN(v) ? 0 : Math.max(0, v); + setStartCycle(v); + }} + onChange={v => { + v = parseInt(v) + setStartCycle(v); + }} + type="number" + placeholder="" + disabled={exporting} + className={cx(exporting && "opacity-50 border-opacity-50")} + value={startCycle ?? ''} + /> + + + { + let v = parseInt(e.target.value); + v = isNaN(v) ? Math.max(startCycle + 1, parseInt(v)) : v; + setEndCycle(v); + }} + onChange={(v) => { + v = parseInt(v) + setEndCycle(v); + }} + type="number" + placeholder="" + disabled={exporting} + className={cx(exporting && "opacity-50 border-opacity-50")} + value={endCycle ?? ''} + /> + +
+
+ + { + let v = parseInt(e.target.value); + v = isNaN(v) ? 1 : Math.max(1, v); + setSampleRate(v); + }} + onChange={v => { + v = parseInt(v) + setSampleRate(v); + }} + type="number" + placeholder="" + disabled={exporting} + className={cx(exporting && "opacity-50 border-opacity-50")} + value={sampleRate ?? ''} + /> + + + { + let v = parseInt(e.target.value); + v = isNaN(v) ? Math.max(1, parseInt(v)) : v; + setMaxPolyphony(v); + }} + onChange={(v) => { + v = Math.max(1, parseInt(v)); + setMaxPolyphony(v); + }} + type="number" + placeholder="" + disabled={exporting} + className={cx(exporting && "opacity-50 border-opacity-50")} + value={maxPolyphony ?? ''} + /> + +
+
+ { + const val = cbEvent.target.checked; + setMultiChannelOrbits(val) + }} + disabled={exporting} + value={multiChannelOrbits} + /> +
+ +
); diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index b9f1c8c8b..0e89fb68b 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -203,8 +203,8 @@ export function useReplContext() { const handleEvaluate = () => { editorRef.current.evaluate(); }; - const handleExport = async (begin, end) => { - await editorRef.current.exportAudio(begin, end); + const handleExport = async (begin, end, sampleRate, downloadName = undefined) => { + await editorRef.current.exportAudio(begin, end, sampleRate, downloadName); }; const handleShuffle = async () => { const patternData = await getRandomTune(); From 428a4b3dd742087b5fa50cc0c15ebd380c5d573b Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 20 Oct 2025 20:21:23 +0300 Subject: [PATCH 08/40] Revert workletsUrl to use ?audioworklet --- packages/superdough/superdough.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index ed5cd9649..2f9eca3ee 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?url'; +import workletsUrl from './worklets.mjs?audioworklet'; import { createFilter, gainNode, getCompressor, getDistortion, getLfo, getWorklet, effectSend } from './helpers.mjs'; import { map } from 'nanostores'; import { logger } from './logger.mjs'; From 4bdaf577c08cab55afdc4440ab737054578f8aa7 Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 20 Oct 2025 20:22:16 +0300 Subject: [PATCH 09/40] Format --- packages/codemirror/codemirror.mjs | 9 +- packages/core/cyclist.mjs | 2 +- packages/core/repl.mjs | 31 +- packages/superdough/audioContext.mjs | 6 +- packages/superdough/helpers.mjs | 2 +- packages/superdough/superdough.mjs | 12 +- packages/webaudio/webaudio.mjs | 198 +++++----- website/src/repl/components/ExportModal.jsx | 405 ++++++++++---------- website/src/repl/components/Header.jsx | 13 +- 9 files changed, 360 insertions(+), 318 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 7cbb925d8..84cbfa96a 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -42,9 +42,9 @@ const extensions = { isMultiCursorEnabled: (on) => on ? [ - EditorState.allowMultipleSelections.of(true), - EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), - ] + EditorState.allowMultipleSelections.of(true), + EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), + ] : [], }; const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()])); @@ -269,10 +269,9 @@ export class StrudelMirror { await this.repl.evaluate(this.code); } async exportAudio(begin, end, sampleRate, downloadName = undefined) { - await this.repl.evaluate(this.code, false) + await this.repl.evaluate(this.code, false); await this.repl.exportAudio(begin, end, sampleRate, downloadName); this.repl.scheduler.stop(); - } async stop() { this.repl.scheduler.stop(); diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 167f18828..185d5588b 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -117,7 +117,7 @@ export class Cyclist { throw new Error('Scheduler: no pattern set! call .setPattern first.'); } logger('[cyclist] exporting'); - await renderPatternAudio(this.pattern, this.cps, begin, end, sampleRate, downloadName) + await renderPatternAudio(this.pattern, this.cps, begin, end, sampleRate, downloadName); } pause() { diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 970c15a67..4a35b932e 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -91,7 +91,8 @@ export function repl({ const stop = () => scheduler.stop(); const start = () => scheduler.start(); - const exportAudio = async (begin, end, sampleRate, downloadName = undefined) => await scheduler.exportAudio(begin, end, sampleRate, downloadName); + 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) => { @@ -263,18 +264,18 @@ export function repl({ export const getTrigger = ({ getTime, defaultOutput }) => - async (hap, deadline, duration, cps, t) => { - // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) - // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 - try { - if (!hap.context.onTrigger || !hap.context.dominantTrigger) { - await defaultOutput(hap, deadline, duration, cps, t); - } - if (hap.context.onTrigger) { - // call signature of output / onTrigger is different... - await hap.context.onTrigger(hap, getTime(), cps, t); - } - } catch (err) { - errorLogger(err, 'getTrigger'); + async (hap, deadline, duration, cps, t) => { + // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) + // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 + try { + if (!hap.context.onTrigger || !hap.context.dominantTrigger) { + await defaultOutput(hap, deadline, duration, cps, t); } - }; + if (hap.context.onTrigger) { + // call signature of output / onTrigger is different... + await hap.context.onTrigger(hap, getTime(), cps, t); + } + } catch (err) { + errorLogger(err, 'getTrigger'); + } + }; diff --git a/packages/superdough/audioContext.mjs b/packages/superdough/audioContext.mjs index ea60722b1..9a5913d80 100644 --- a/packages/superdough/audioContext.mjs +++ b/packages/superdough/audioContext.mjs @@ -1,15 +1,15 @@ -import { analysers, resetGlobalEffects } from "./superdough.mjs"; +import { analysers, resetGlobalEffects } from './superdough.mjs'; let audioContext; export const setDefaultAudioContext = () => { audioContext = new AudioContext(); - resetGlobalEffects() + resetGlobalEffects(); return audioContext; }; export const setAudioContext = (context) => { - audioContext = context + audioContext = context; return audioContext; }; diff --git a/packages/superdough/helpers.mjs b/packages/superdough/helpers.mjs index cf65c390f..a471da9dd 100644 --- a/packages/superdough/helpers.mjs +++ b/packages/superdough/helpers.mjs @@ -348,7 +348,7 @@ export function applyFM(param, value, begin) { duration, } = value; let modulator; - let stop = () => { }; + let stop = () => {}; if (fmModulationIndex) { const ac = getAudioContext(); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 2f9eca3ee..118a152b1 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -212,7 +212,9 @@ export function loadWorklets() { if (!workletsLoading) { const audioCtx = getAudioContext(); const allWorkletURLs = externalWorklets.concat([workletsUrl]); - workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL))).then(() => workletsLoading = undefined); + workletsLoading = Promise.all(allWorkletURLs.map((workletURL) => audioCtx.audioWorklet.addModule(workletURL))).then( + () => (workletsLoading = undefined), + ); } return workletsLoading; @@ -250,7 +252,7 @@ export async function initAudio(options = {}) { logger('[superdough] failed to set audio interface', 'warning'); } } - if (!audioCtx instanceof OfflineAudioContext) { + if ((!audioCtx) instanceof OfflineAudioContext) { await audioCtx.resume(); } if (disableWorklets) { @@ -288,7 +290,7 @@ function getSuperdoughAudioController() { } export function setSuperdoughAudioController(newController) { - controller = newController + controller = newController; return controller; } @@ -401,7 +403,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) ); return; } // FIXME: fix - // destructure + // destructure let { tremolo, tremolosync, @@ -743,4 +745,4 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) export const superdoughTrigger = (t, hap, ct, cps) => { superdough(hap, t - ct, hap.duration / cps, cps); -}; \ No newline at end of file +}; diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 27718bd7d..43c011460 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -5,7 +5,23 @@ This program is free software: you can redistribute it and/or modify it under th */ import * as strudel from '@strudel/core'; -import { superdough, getAudioContext, setLogger, doughTrigger, registerWorklet, setAudioContext, getSampleBufferSource, loadBuffer, getSampleInfo, getSound, initAudio, setSuperdoughAudioController, loadWorklets, setMaxPolyphony, setMultiChannelOrbits } from 'superdough'; +import { + superdough, + getAudioContext, + setLogger, + doughTrigger, + registerWorklet, + setAudioContext, + getSampleBufferSource, + loadBuffer, + getSampleInfo, + getSound, + initAudio, + setSuperdoughAudioController, + loadWorklets, + setMaxPolyphony, + setMultiChannelOrbits, +} from 'superdough'; import './supradough.mjs'; import { workletUrl } from 'supradough'; import { SuperdoughAudioController } from 'superdough/superdoughoutput.mjs'; @@ -28,59 +44,63 @@ export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => { }; 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 * sampleRate, sampleRate); - setAudioContext(audioContext) - let context = getAudioContext() - setSuperdoughAudioController(new SuperdoughAudioController(context)) - setMaxPolyphony(1024) - setMultiChannelOrbits(true) - await loadWorklets() + const oldAudioCtx = getAudioContext(); + await oldAudioCtx.close(); + let audioContext = new OfflineAudioContext(2, ((end - begin) / cps) * sampleRate, sampleRate); + setAudioContext(audioContext); + let context = getAudioContext(); + setSuperdoughAudioController(new SuperdoughAudioController(context)); + setMaxPolyphony(1024); + setMultiChannelOrbits(true); + await loadWorklets(); logger('[webaudio] start rendering'); - let haps = pattern.queryArc(begin, end, { _cps: cps }) - Promise.all(haps.map(async h => { - let s; - if (h.value.s) { - if (h.value.bank) { - s = `${h.value.bank}_${h.value.s}`; + let haps = pattern.queryArc(begin, end, { _cps: cps }); + Promise.all( + haps.map(async (h) => { + let s; + if (h.value.s) { + if (h.value.bank) { + s = `${h.value.bank}_${h.value.s}`; + } else { + s = h.value.s; + } + let bank = getSound(s).data.samples; + if (bank) { + let { url: sampleUrl, label } = getSampleInfo(h.value, bank); + await loadBuffer(sampleUrl, context, label); + } } - else { - s = h.value.s - } - let bank = getSound(s).data.samples - if (bank) { - let { url: sampleUrl, label } = getSampleInfo(h.value, bank); - await loadBuffer(sampleUrl, context, label) - } - } - })) + }), + ); haps.forEach(async (hap) => { if (hap.hasOnset()) { hap.ensureObjectValue(); await superdough(hap.value, hap.whole.begin.valueOf() / cps, hap.duration, cps, hap.whole?.begin.valueOf()); } - }) - - return context.startRendering().then((renderedBuffer) => { - console.log(renderedBuffer) - const wavBuffer = audioBufferToWav(renderedBuffer); - const blob = new Blob([wavBuffer], { type: 'audio/wav' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - 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() }); + + return context + .startRendering() + .then((renderedBuffer) => { + console.log(renderedBuffer); + const wavBuffer = audioBufferToWav(renderedBuffer); + const blob = new Blob([wavBuffer], { type: 'audio/wav' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + 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(); + }); } export function webaudioRepl(options = {}) { @@ -96,97 +116,97 @@ Pattern.prototype.dough = function () { return this.onTrigger(doughTrigger, 1); }; - function audioBufferToWav(buffer, opt) { - opt = opt || {} + opt = opt || {}; - var numChannels = buffer.numberOfChannels - var sampleRate = buffer.sampleRate - var format = opt.float32 ? 3 : 1 - var bitDepth = format === 3 ? 32 : 16 + var numChannels = buffer.numberOfChannels; + var sampleRate = buffer.sampleRate; + var format = opt.float32 ? 3 : 1; + var bitDepth = format === 3 ? 32 : 16; - var result + var result; if (numChannels === 2) { - result = interleave(buffer.getChannelData(0), buffer.getChannelData(1)) + result = interleave(buffer.getChannelData(0), buffer.getChannelData(1)); } else { - result = buffer.getChannelData(0) + result = buffer.getChannelData(0); } - return encodeWAV(result, format, sampleRate, numChannels, bitDepth) + return encodeWAV(result, format, sampleRate, numChannels, bitDepth); } function encodeWAV(samples, format, sampleRate, numChannels, bitDepth) { - var bytesPerSample = bitDepth / 8 - var blockAlign = numChannels * bytesPerSample + var bytesPerSample = bitDepth / 8; + var blockAlign = numChannels * bytesPerSample; - var buffer = new ArrayBuffer(44 + samples.length * bytesPerSample) - var view = new DataView(buffer) + var buffer = new ArrayBuffer(44 + samples.length * bytesPerSample); + var view = new DataView(buffer); /* RIFF identifier */ - writeString(view, 0, 'RIFF') + writeString(view, 0, 'RIFF'); /* RIFF chunk length */ - view.setUint32(4, 36 + samples.length * bytesPerSample, true) + view.setUint32(4, 36 + samples.length * bytesPerSample, true); /* RIFF type */ - writeString(view, 8, 'WAVE') + writeString(view, 8, 'WAVE'); /* format chunk identifier */ - writeString(view, 12, 'fmt ') + writeString(view, 12, 'fmt '); /* format chunk length */ - view.setUint32(16, 16, true) + view.setUint32(16, 16, true); /* sample format (raw) */ - view.setUint16(20, format, true) + view.setUint16(20, format, true); /* channel count */ - view.setUint16(22, numChannels, true) + view.setUint16(22, numChannels, true); /* sample rate */ - view.setUint32(24, sampleRate, true) + view.setUint32(24, sampleRate, true); /* byte rate (sample rate * block align) */ - view.setUint32(28, sampleRate * blockAlign, true) + view.setUint32(28, sampleRate * blockAlign, true); /* block align (channel count * bytes per sample) */ - view.setUint16(32, blockAlign, true) + view.setUint16(32, blockAlign, true); /* bits per sample */ - view.setUint16(34, bitDepth, true) + view.setUint16(34, bitDepth, true); /* data chunk identifier */ - writeString(view, 36, 'data') + writeString(view, 36, 'data'); /* data chunk length */ - view.setUint32(40, samples.length * bytesPerSample, true) - if (format === 1) { // Raw PCM - floatTo16BitPCM(view, 44, samples) + view.setUint32(40, samples.length * bytesPerSample, true); + if (format === 1) { + // Raw PCM + floatTo16BitPCM(view, 44, samples); } else { - writeFloat32(view, 44, samples) + writeFloat32(view, 44, samples); } - return buffer + return buffer; } function interleave(inputL, inputR) { - var length = inputL.length + inputR.length - var result = new Float32Array(length) + var length = inputL.length + inputR.length; + var result = new Float32Array(length); - var index = 0 - var inputIndex = 0 + var index = 0; + var inputIndex = 0; while (index < length) { - result[index++] = inputL[inputIndex] - result[index++] = inputR[inputIndex] - inputIndex++ + result[index++] = inputL[inputIndex]; + result[index++] = inputR[inputIndex]; + inputIndex++; } - return result + return result; } function writeFloat32(output, offset, input) { for (var i = 0; i < input.length; i++, offset += 4) { - output.setFloat32(offset, input[i], true) + output.setFloat32(offset, input[i], true); } } function floatTo16BitPCM(output, offset, input) { for (var i = 0; i < input.length; i++, offset += 2) { - var s = Math.max(-1, Math.min(1, input[i])) - output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true) + var s = Math.max(-1, Math.min(1, input[i])); + output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true); } } function writeString(view, offset, string) { for (var i = 0; i < string.length; i++) { - view.setUint8(offset + i, string.charCodeAt(i)) + view.setUint8(offset + i, string.charCodeAt(i)); } -} \ No newline at end of file +} diff --git a/website/src/repl/components/ExportModal.jsx b/website/src/repl/components/ExportModal.jsx index 4f474f381..036f6f36a 100644 --- a/website/src/repl/components/ExportModal.jsx +++ b/website/src/repl/components/ExportModal.jsx @@ -3,215 +3,226 @@ import cx from '@src/cx.mjs'; import NumberInput from './NumberInput'; import { useEffect, useState } from 'react'; import { Textbox } from './textbox/Textbox'; -import { setMultiChannelOrbits as setStrudelMultiChannelOrbits, setMaxPolyphony as setStrudelMaxPolyphony, getAudioContext } 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 ( - - ); + return ( + + ); } function FormItem({ label, children, disabled }) { - return ( -
- - {children} -
- ); + return ( +
+ + {children} +
+ ); } export default function ExportModal(Props) { - const { started, isEmbedded, handleExport } = Props; + const { started, isEmbedded, handleExport } = Props; - const [downloadName, setDownloadName] = useState("") // TODO: make a form? - const [startCycle, setStartCycle] = useState(0) - const [endCycle, setEndCycle] = useState(1) - const [sampleRate, setSampleRate] = useState(48000) - const [multiChannelOrbits, setMultiChannelOrbits] = useState(true) - const [maxPolyphony, setMaxPolyphony] = useState(1024) - const [exporting, setExporting] = useState(false) - const [progress, setProgress] = useState(0) - const [length, setLength] = useState(1) + const [downloadName, setDownloadName] = useState(''); // TODO: make a form? + const [startCycle, setStartCycle] = useState(0); + const [endCycle, setEndCycle] = useState(1); + const [sampleRate, setSampleRate] = useState(48000); + const [multiChannelOrbits, setMultiChannelOrbits] = useState(true); + const [maxPolyphony, setMaxPolyphony] = useState(1024); + const [exporting, setExporting] = useState(false); + const [progress, setProgress] = useState(0); + const [length, setLength] = useState(1); - const refreshProgress = () => { - const audioContext = getAudioContext() - if (audioContext instanceof OfflineAudioContext) { - setProgress(audioContext.currentTime) - setLength(audioContext.length / sampleRate) - setTimeout(refreshProgress, 100); - } + const refreshProgress = () => { + const audioContext = getAudioContext(); + if (audioContext instanceof OfflineAudioContext) { + setProgress(audioContext.currentTime); + setLength(audioContext.length / sampleRate); + setTimeout(refreshProgress, 100); } + }; - return ( - <> - + + +
+ + { + setDownloadName(e.target.value); + }} + onChange={(v) => { + setDownloadName(v); + }} + disabled={exporting} + placeholder="Leave empty to use current date" + className={cx('placeholder:opacity-50', exporting && 'opacity-50 border-opacity-50')} + value={downloadName ?? ''} + /> + +
+ + { + let v = parseInt(e.target.value); + v = isNaN(v) ? 0 : Math.max(0, v); + setStartCycle(v); }} - title="export" - className={cx( - 'flex items-center space-x-1', - !isEmbedded ? 'p-2' : 'px-2', - started ? 'opacity-50' : 'hover:opacity-50', - )} - > - {!isEmbedded && export} - - - -
- - { - setDownloadName(e.target.value) - }} - onChange={v => { - setDownloadName(v) - }} - disabled={exporting} - placeholder="Leave empty to use current date" - className={cx("placeholder:opacity-50", exporting && "opacity-50 border-opacity-50")} - value={downloadName ?? ''} - /> - -
- - { - let v = parseInt(e.target.value); - v = isNaN(v) ? 0 : Math.max(0, v); - setStartCycle(v); - }} - onChange={v => { - v = parseInt(v) - setStartCycle(v); - }} - type="number" - placeholder="" - disabled={exporting} - className={cx(exporting && "opacity-50 border-opacity-50")} - value={startCycle ?? ''} - /> - - - { - let v = parseInt(e.target.value); - v = isNaN(v) ? Math.max(startCycle + 1, parseInt(v)) : v; - setEndCycle(v); - }} - onChange={(v) => { - v = parseInt(v) - setEndCycle(v); - }} - type="number" - placeholder="" - disabled={exporting} - className={cx(exporting && "opacity-50 border-opacity-50")} - value={endCycle ?? ''} - /> - -
-
- - { - let v = parseInt(e.target.value); - v = isNaN(v) ? 1 : Math.max(1, v); - setSampleRate(v); - }} - onChange={v => { - v = parseInt(v) - setSampleRate(v); - }} - type="number" - placeholder="" - disabled={exporting} - className={cx(exporting && "opacity-50 border-opacity-50")} - value={sampleRate ?? ''} - /> - - - { - let v = parseInt(e.target.value); - v = isNaN(v) ? Math.max(1, parseInt(v)) : v; - setMaxPolyphony(v); - }} - onChange={(v) => { - v = Math.max(1, parseInt(v)); - setMaxPolyphony(v); - }} - type="number" - placeholder="" - disabled={exporting} - className={cx(exporting && "opacity-50 border-opacity-50")} - value={maxPolyphony ?? ''} - /> - -
-
- { - const val = cbEvent.target.checked; - setMultiChannelOrbits(val) - }} - disabled={exporting} - value={multiChannelOrbits} - /> -
- -
-
- - ); + onChange={(v) => { + v = parseInt(v); + setStartCycle(v); + }} + type="number" + placeholder="" + disabled={exporting} + className={cx(exporting && 'opacity-50 border-opacity-50')} + value={startCycle ?? ''} + /> +
+ + { + let v = parseInt(e.target.value); + v = isNaN(v) ? Math.max(startCycle + 1, parseInt(v)) : v; + setEndCycle(v); + }} + onChange={(v) => { + v = parseInt(v); + setEndCycle(v); + }} + type="number" + placeholder="" + disabled={exporting} + className={cx(exporting && 'opacity-50 border-opacity-50')} + value={endCycle ?? ''} + /> + +
+
+ + { + let v = parseInt(e.target.value); + v = isNaN(v) ? 1 : Math.max(1, v); + setSampleRate(v); + }} + onChange={(v) => { + v = parseInt(v); + setSampleRate(v); + }} + type="number" + placeholder="" + disabled={exporting} + className={cx(exporting && 'opacity-50 border-opacity-50')} + value={sampleRate ?? ''} + /> + + + { + let v = parseInt(e.target.value); + v = isNaN(v) ? Math.max(1, parseInt(v)) : v; + setMaxPolyphony(v); + }} + onChange={(v) => { + v = Math.max(1, parseInt(v)); + setMaxPolyphony(v); + }} + type="number" + placeholder="" + disabled={exporting} + className={cx(exporting && 'opacity-50 border-opacity-50')} + value={maxPolyphony ?? ''} + /> + +
+
+ { + const val = cbEvent.target.checked; + setMultiChannelOrbits(val); + }} + disabled={exporting} + value={multiChannelOrbits} + /> +
+ +
+
+ + ); } diff --git a/website/src/repl/components/Header.jsx b/website/src/repl/components/Header.jsx index d5d8b5f75..dd9f684c1 100644 --- a/website/src/repl/components/Header.jsx +++ b/website/src/repl/components/Header.jsx @@ -9,8 +9,17 @@ const { BASE_URL } = import.meta.env; const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; export function Header({ context, embedded = false }) { - const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShuffle, handleShare, handleExport } = - context; + const { + started, + pending, + isDirty, + activeCode, + handleTogglePlay, + handleEvaluate, + handleShuffle, + handleShare, + handleExport, + } = context; const isEmbedded = typeof window !== 'undefined' && (embedded || window.location !== window.parent.location); const { isZen, isButtonRowHidden, isCSSAnimationDisabled, fontFamily } = useSettings(); From 120b46c4a516d0a5932b6162f83185761503f8dc Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 20 Oct 2025 20:24:42 +0300 Subject: [PATCH 10/40] Add webaudio as a cyclist dependency (do we want that?) --- packages/core/cyclist.mjs | 2 +- packages/core/package-lock.json | 331 ++++++++++++++++++++++++++++++++ packages/core/package.json | 1 + 3 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 packages/core/package-lock.json diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 185d5588b..119f1d511 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import createClock from './zyklus.mjs'; import { errorLogger, logger } from './logger.mjs'; -import { loadBuffer, renderPatternAudio, setAudioContext, superdough } from '@strudel/webaudio'; +import { renderPatternAudio } from '@strudel/webaudio'; export class Cyclist { constructor({ diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json new file mode 100644 index 000000000..dea7b53a7 --- /dev/null +++ b/packages/core/package-lock.json @@ -0,0 +1,331 @@ +{ + "name": "@strudel/core", + "version": "1.2.4", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@strudel/core", + "version": "1.2.4", + "license": "AGPL-3.0-or-later", + "dependencies": { + "@strudel/webaudio": "^1.2.5", + "fraction.js": "^5.2.1" + }, + "devDependencies": { + "vite": "^6.0.11", + "vitest": "^3.0.4" + } + }, + "../../node_modules/.pnpm/fraction.js@5.2.1/node_modules/fraction.js": { + "version": "5.2.1", + "license": "MIT", + "devDependencies": { + "crude-build": "^0.1.2", + "mocha": "*" + }, + "engines": { + "node": ">= 12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "../../node_modules/.pnpm/vite@6.0.11_@types+node@22.10.10_jiti@2.4.2_lightningcss@1.29.1_terser@5.37.0_yaml@2.7.0/node_modules/vite": { + "version": "6.0.11", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.24.2", + "postcss": "^8.4.49", + "rollup": "^4.23.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "devDependencies": { + "@ampproject/remapping": "^2.3.0", + "@babel/parser": "^7.26.3", + "@jridgewell/trace-mapping": "^0.3.25", + "@polka/compression": "^1.0.0-next.25", + "@rollup/plugin-alias": "^5.1.1", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-dynamic-import-vars": "2.1.4", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "16.0.0", + "@rollup/pluginutils": "^5.1.4", + "@types/escape-html": "^1.0.4", + "@types/pnpapi": "^0.0.5", + "artichokie": "^0.3.1", + "cac": "^6.7.14", + "chokidar": "^3.6.0", + "connect": "^3.7.0", + "convert-source-map": "^2.0.0", + "cors": "^2.8.5", + "cross-spawn": "^7.0.6", + "debug": "^4.4.0", + "dep-types": "link:./src/types", + "dotenv": "^16.4.7", + "dotenv-expand": "^12.0.1", + "es-module-lexer": "^1.6.0", + "escape-html": "^1.0.3", + "estree-walker": "^3.0.3", + "etag": "^1.8.1", + "http-proxy": "^1.18.1", + "launch-editor-middleware": "^2.9.1", + "lightningcss": "^1.28.2", + "magic-string": "^0.30.17", + "mlly": "^1.7.3", + "mrmime": "^2.0.0", + "nanoid": "^5.0.9", + "open": "^10.1.0", + "parse5": "^7.2.1", + "pathe": "^2.0.0", + "periscopic": "^4.0.2", + "picocolors": "^1.1.1", + "picomatch": "^4.0.2", + "postcss-import": "^16.1.0", + "postcss-load-config": "^6.0.1", + "postcss-modules": "^6.0.1", + "resolve.exports": "^2.0.3", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-esbuild": "^6.1.1", + "rollup-plugin-license": "^3.5.3", + "sass": "^1.83.1", + "sass-embedded": "^1.83.1", + "sirv": "^3.0.0", + "source-map-support": "^0.5.21", + "strip-literal": "^2.1.1", + "terser": "^5.37.0", + "tinyglobby": "^0.2.10", + "tsconfck": "^3.1.4", + "tslib": "^2.8.1", + "types": "link:./types", + "ufo": "^1.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "../../node_modules/.pnpm/vitest@3.0.4_@types+debug@4.1.12_@types+node@22.10.10_@vitest+ui@3.0.4_jiti@2.4.2_light_04b28d5d33383b617211459e5738d579/node_modules/vitest": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "3.0.4", + "@vitest/mocker": "3.0.4", + "@vitest/pretty-format": "^3.0.4", + "@vitest/runner": "3.0.4", + "@vitest/snapshot": "3.0.4", + "@vitest/spy": "3.0.4", + "@vitest/utils": "3.0.4", + "chai": "^5.1.2", + "debug": "^4.4.0", + "expect-type": "^1.1.0", + "magic-string": "^0.30.17", + "pathe": "^2.0.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.0.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "devDependencies": { + "@ampproject/remapping": "^2.3.0", + "@antfu/install-pkg": "^1.0.0", + "@edge-runtime/vm": "^5.0.0", + "@sinonjs/fake-timers": "14.0.0", + "@types/debug": "^4.1.12", + "@types/estree": "^1.0.6", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/jsdom": "^21.1.7", + "@types/micromatch": "^4.0.9", + "@types/node": "^22.10.9", + "@types/prompts": "^2.4.9", + "@types/sinonjs__fake-timers": "^8.1.5", + "acorn-walk": "^8.3.4", + "birpc": "0.2.19", + "cac": "^6.7.14", + "chai-subset": "^1.6.0", + "fast-glob": "3.3.3", + "find-up": "^6.3.0", + "flatted": "^3.3.2", + "get-tsconfig": "^4.10.0", + "happy-dom": "^16.7.2", + "jsdom": "^25.0.1", + "local-pkg": "^0.5.1", + "micromatch": "^4.0.8", + "pretty-format": "^29.7.0", + "prompts": "^2.4.2", + "strip-literal": "^2.1.1", + "ws": "^8.18.0" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.0.4", + "@vitest/ui": "3.0.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/@strudel/core": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@strudel/core/-/core-1.2.4.tgz", + "integrity": "sha512-sUm9/zjEEQuWfW3hNOCcfNbn8jq2G+Ye+mvpLUgcGyLfI5XPlPTn/PBIkv2hFjNs/basnMyrOUvg0Z85AA6+OA==", + "license": "AGPL-3.0-or-later", + "dependencies": { + "fraction.js": "^5.2.1" + } + }, + "node_modules/@strudel/draw": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@strudel/draw/-/draw-1.2.4.tgz", + "integrity": "sha512-9ui5+ZQQB+EEgwJQueGVMgpnPSHnFgRgivlAZbdjEoMdSm5vULWvKK3Psml8WKtOBNiuL+5Bci7a0INRTRusOA==", + "license": "AGPL-3.0-or-later", + "dependencies": { + "@strudel/core": "1.2.4" + } + }, + "node_modules/@strudel/webaudio": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@strudel/webaudio/-/webaudio-1.2.5.tgz", + "integrity": "sha512-ilcyBsuwkH6Ghz0egaqIJ5DZi0xFBZwVWvZb0vnfU7kVESroD2j6yptyFMysJFZhQ5pfRiZs2ZeF6yTsviUjng==", + "license": "AGPL-3.0-or-later", + "dependencies": { + "@strudel/core": "1.2.4", + "@strudel/draw": "1.2.4", + "superdough": "1.2.5" + } + }, + "node_modules/fraction.js": { + "resolved": "../../node_modules/.pnpm/fraction.js@5.2.1/node_modules/fraction.js", + "link": true + }, + "node_modules/nanostores": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-0.11.4.tgz", + "integrity": "sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/superdough": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/superdough/-/superdough-1.2.5.tgz", + "integrity": "sha512-QVkMGGZjWzaRxYKXoZYMfsRHVk5vwNiqAaDOseccSTgqPPwFRTY2fmHAm5OmwwS8XqFOenZjn+XU3LEaCZMdJw==", + "license": "AGPL-3.0-or-later", + "dependencies": { + "nanostores": "^0.11.3" + } + }, + "node_modules/vite": { + "resolved": "../../node_modules/.pnpm/vite@6.0.11_@types+node@22.10.10_jiti@2.4.2_lightningcss@1.29.1_terser@5.37.0_yaml@2.7.0/node_modules/vite", + "link": true + }, + "node_modules/vitest": { + "resolved": "../../node_modules/.pnpm/vitest@3.0.4_@types+debug@4.1.12_@types+node@22.10.10_@vitest+ui@3.0.4_jiti@2.4.2_light_04b28d5d33383b617211459e5738d579/node_modules/vitest", + "link": true + } + } +} diff --git a/packages/core/package.json b/packages/core/package.json index 7cf20cea7..daaa111de 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -31,6 +31,7 @@ }, "homepage": "https://strudel.cc", "dependencies": { + "@strudel/webaudio": "^1.2.5", "fraction.js": "^5.2.1" }, "gitHead": "0e26d4e741500f5bae35b023608f062a794905c2", From 8b3c0d2a4aa8e9d2c89622e63d2d614ff15f2ef0 Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 20 Oct 2025 20:27:31 +0300 Subject: [PATCH 11/40] Refresh deps --- packages/osc/server.js | 0 pnpm-lock.yaml | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) mode change 100644 => 100755 packages/osc/server.js diff --git a/packages/osc/server.js b/packages/osc/server.js old mode 100644 new mode 100755 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c225057f3..40d6eb57b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -225,6 +225,9 @@ importers: packages/core: dependencies: + '@strudel/webaudio': + specifier: ^1.2.5 + version: 1.2.5 fraction.js: specifier: ^5.2.1 version: 5.2.1 @@ -2468,6 +2471,15 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@strudel/core@1.2.4': + resolution: {integrity: sha512-sUm9/zjEEQuWfW3hNOCcfNbn8jq2G+Ye+mvpLUgcGyLfI5XPlPTn/PBIkv2hFjNs/basnMyrOUvg0Z85AA6+OA==} + + '@strudel/draw@1.2.4': + resolution: {integrity: sha512-9ui5+ZQQB+EEgwJQueGVMgpnPSHnFgRgivlAZbdjEoMdSm5vULWvKK3Psml8WKtOBNiuL+5Bci7a0INRTRusOA==} + + '@strudel/webaudio@1.2.5': + resolution: {integrity: sha512-ilcyBsuwkH6Ghz0egaqIJ5DZi0xFBZwVWvZb0vnfU7kVESroD2j6yptyFMysJFZhQ5pfRiZs2ZeF6yTsviUjng==} + '@supabase/auth-js@2.67.3': resolution: {integrity: sha512-NJDaW8yXs49xMvWVOkSIr8j46jf+tYHV0wHhrwOaLLMZSFO4g6kKAf+MfzQ2RaD06OCUkUHIzctLAxjTgEVpzw==} @@ -6989,6 +7001,9 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + superdough@1.2.5: + resolution: {integrity: sha512-QVkMGGZjWzaRxYKXoZYMfsRHVk5vwNiqAaDOseccSTgqPPwFRTY2fmHAm5OmwwS8XqFOenZjn+XU3LEaCZMdJw==} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -9829,6 +9844,20 @@ snapshots: '@sinclair/typebox@0.27.8': {} + '@strudel/core@1.2.4': + dependencies: + fraction.js: 5.2.1 + + '@strudel/draw@1.2.4': + dependencies: + '@strudel/core': 1.2.4 + + '@strudel/webaudio@1.2.5': + dependencies: + '@strudel/core': 1.2.4 + '@strudel/draw': 1.2.4 + superdough: 1.2.5 + '@supabase/auth-js@2.67.3': dependencies: '@supabase/node-fetch': 2.6.15 @@ -15431,6 +15460,10 @@ snapshots: pirates: 4.0.6 ts-interface-checker: 0.1.13 + superdough@1.2.5: + dependencies: + nanostores: 0.11.3 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 From 329289fc3589661473447f0f1d7563d858cdd04d Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 20 Oct 2025 20:33:04 +0300 Subject: [PATCH 12/40] Revert refreshing deps --- packages/core/cyclist.mjs | 2 +- packages/core/package-lock.json | 331 -------------------------------- packages/core/package.json | 1 - packages/osc/server.js | 0 pnpm-lock.yaml | 33 ---- 5 files changed, 1 insertion(+), 366 deletions(-) delete mode 100644 packages/core/package-lock.json mode change 100755 => 100644 packages/osc/server.js diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 119f1d511..185d5588b 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th import createClock from './zyklus.mjs'; import { errorLogger, logger } from './logger.mjs'; -import { renderPatternAudio } from '@strudel/webaudio'; +import { loadBuffer, renderPatternAudio, setAudioContext, superdough } from '@strudel/webaudio'; export class Cyclist { constructor({ diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json deleted file mode 100644 index dea7b53a7..000000000 --- a/packages/core/package-lock.json +++ /dev/null @@ -1,331 +0,0 @@ -{ - "name": "@strudel/core", - "version": "1.2.4", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "@strudel/core", - "version": "1.2.4", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@strudel/webaudio": "^1.2.5", - "fraction.js": "^5.2.1" - }, - "devDependencies": { - "vite": "^6.0.11", - "vitest": "^3.0.4" - } - }, - "../../node_modules/.pnpm/fraction.js@5.2.1/node_modules/fraction.js": { - "version": "5.2.1", - "license": "MIT", - "devDependencies": { - "crude-build": "^0.1.2", - "mocha": "*" - }, - "engines": { - "node": ">= 12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } - }, - "../../node_modules/.pnpm/vite@6.0.11_@types+node@22.10.10_jiti@2.4.2_lightningcss@1.29.1_terser@5.37.0_yaml@2.7.0/node_modules/vite": { - "version": "6.0.11", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.24.2", - "postcss": "^8.4.49", - "rollup": "^4.23.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "devDependencies": { - "@ampproject/remapping": "^2.3.0", - "@babel/parser": "^7.26.3", - "@jridgewell/trace-mapping": "^0.3.25", - "@polka/compression": "^1.0.0-next.25", - "@rollup/plugin-alias": "^5.1.1", - "@rollup/plugin-commonjs": "^28.0.2", - "@rollup/plugin-dynamic-import-vars": "2.1.4", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "16.0.0", - "@rollup/pluginutils": "^5.1.4", - "@types/escape-html": "^1.0.4", - "@types/pnpapi": "^0.0.5", - "artichokie": "^0.3.1", - "cac": "^6.7.14", - "chokidar": "^3.6.0", - "connect": "^3.7.0", - "convert-source-map": "^2.0.0", - "cors": "^2.8.5", - "cross-spawn": "^7.0.6", - "debug": "^4.4.0", - "dep-types": "link:./src/types", - "dotenv": "^16.4.7", - "dotenv-expand": "^12.0.1", - "es-module-lexer": "^1.6.0", - "escape-html": "^1.0.3", - "estree-walker": "^3.0.3", - "etag": "^1.8.1", - "http-proxy": "^1.18.1", - "launch-editor-middleware": "^2.9.1", - "lightningcss": "^1.28.2", - "magic-string": "^0.30.17", - "mlly": "^1.7.3", - "mrmime": "^2.0.0", - "nanoid": "^5.0.9", - "open": "^10.1.0", - "parse5": "^7.2.1", - "pathe": "^2.0.0", - "periscopic": "^4.0.2", - "picocolors": "^1.1.1", - "picomatch": "^4.0.2", - "postcss-import": "^16.1.0", - "postcss-load-config": "^6.0.1", - "postcss-modules": "^6.0.1", - "resolve.exports": "^2.0.3", - "rollup-plugin-dts": "^6.1.1", - "rollup-plugin-esbuild": "^6.1.1", - "rollup-plugin-license": "^3.5.3", - "sass": "^1.83.1", - "sass-embedded": "^1.83.1", - "sirv": "^3.0.0", - "source-map-support": "^0.5.21", - "strip-literal": "^2.1.1", - "terser": "^5.37.0", - "tinyglobby": "^0.2.10", - "tsconfck": "^3.1.4", - "tslib": "^2.8.1", - "types": "link:./types", - "ufo": "^1.5.4", - "ws": "^8.18.0" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "../../node_modules/.pnpm/vitest@3.0.4_@types+debug@4.1.12_@types+node@22.10.10_@vitest+ui@3.0.4_jiti@2.4.2_light_04b28d5d33383b617211459e5738d579/node_modules/vitest": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "3.0.4", - "@vitest/mocker": "3.0.4", - "@vitest/pretty-format": "^3.0.4", - "@vitest/runner": "3.0.4", - "@vitest/snapshot": "3.0.4", - "@vitest/spy": "3.0.4", - "@vitest/utils": "3.0.4", - "chai": "^5.1.2", - "debug": "^4.4.0", - "expect-type": "^1.1.0", - "magic-string": "^0.30.17", - "pathe": "^2.0.2", - "std-env": "^3.8.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinypool": "^1.0.2", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.0.4", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "devDependencies": { - "@ampproject/remapping": "^2.3.0", - "@antfu/install-pkg": "^1.0.0", - "@edge-runtime/vm": "^5.0.0", - "@sinonjs/fake-timers": "14.0.0", - "@types/debug": "^4.1.12", - "@types/estree": "^1.0.6", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/jsdom": "^21.1.7", - "@types/micromatch": "^4.0.9", - "@types/node": "^22.10.9", - "@types/prompts": "^2.4.9", - "@types/sinonjs__fake-timers": "^8.1.5", - "acorn-walk": "^8.3.4", - "birpc": "0.2.19", - "cac": "^6.7.14", - "chai-subset": "^1.6.0", - "fast-glob": "3.3.3", - "find-up": "^6.3.0", - "flatted": "^3.3.2", - "get-tsconfig": "^4.10.0", - "happy-dom": "^16.7.2", - "jsdom": "^25.0.1", - "local-pkg": "^0.5.1", - "micromatch": "^4.0.8", - "pretty-format": "^29.7.0", - "prompts": "^2.4.2", - "strip-literal": "^2.1.1", - "ws": "^8.18.0" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.0.4", - "@vitest/ui": "3.0.4", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/@strudel/core": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@strudel/core/-/core-1.2.4.tgz", - "integrity": "sha512-sUm9/zjEEQuWfW3hNOCcfNbn8jq2G+Ye+mvpLUgcGyLfI5XPlPTn/PBIkv2hFjNs/basnMyrOUvg0Z85AA6+OA==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "fraction.js": "^5.2.1" - } - }, - "node_modules/@strudel/draw": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@strudel/draw/-/draw-1.2.4.tgz", - "integrity": "sha512-9ui5+ZQQB+EEgwJQueGVMgpnPSHnFgRgivlAZbdjEoMdSm5vULWvKK3Psml8WKtOBNiuL+5Bci7a0INRTRusOA==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@strudel/core": "1.2.4" - } - }, - "node_modules/@strudel/webaudio": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@strudel/webaudio/-/webaudio-1.2.5.tgz", - "integrity": "sha512-ilcyBsuwkH6Ghz0egaqIJ5DZi0xFBZwVWvZb0vnfU7kVESroD2j6yptyFMysJFZhQ5pfRiZs2ZeF6yTsviUjng==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "@strudel/core": "1.2.4", - "@strudel/draw": "1.2.4", - "superdough": "1.2.5" - } - }, - "node_modules/fraction.js": { - "resolved": "../../node_modules/.pnpm/fraction.js@5.2.1/node_modules/fraction.js", - "link": true - }, - "node_modules/nanostores": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-0.11.4.tgz", - "integrity": "sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/superdough": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/superdough/-/superdough-1.2.5.tgz", - "integrity": "sha512-QVkMGGZjWzaRxYKXoZYMfsRHVk5vwNiqAaDOseccSTgqPPwFRTY2fmHAm5OmwwS8XqFOenZjn+XU3LEaCZMdJw==", - "license": "AGPL-3.0-or-later", - "dependencies": { - "nanostores": "^0.11.3" - } - }, - "node_modules/vite": { - "resolved": "../../node_modules/.pnpm/vite@6.0.11_@types+node@22.10.10_jiti@2.4.2_lightningcss@1.29.1_terser@5.37.0_yaml@2.7.0/node_modules/vite", - "link": true - }, - "node_modules/vitest": { - "resolved": "../../node_modules/.pnpm/vitest@3.0.4_@types+debug@4.1.12_@types+node@22.10.10_@vitest+ui@3.0.4_jiti@2.4.2_light_04b28d5d33383b617211459e5738d579/node_modules/vitest", - "link": true - } - } -} diff --git a/packages/core/package.json b/packages/core/package.json index daaa111de..7cf20cea7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -31,7 +31,6 @@ }, "homepage": "https://strudel.cc", "dependencies": { - "@strudel/webaudio": "^1.2.5", "fraction.js": "^5.2.1" }, "gitHead": "0e26d4e741500f5bae35b023608f062a794905c2", diff --git a/packages/osc/server.js b/packages/osc/server.js old mode 100755 new mode 100644 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40d6eb57b..c225057f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -225,9 +225,6 @@ importers: packages/core: dependencies: - '@strudel/webaudio': - specifier: ^1.2.5 - version: 1.2.5 fraction.js: specifier: ^5.2.1 version: 5.2.1 @@ -2471,15 +2468,6 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - '@strudel/core@1.2.4': - resolution: {integrity: sha512-sUm9/zjEEQuWfW3hNOCcfNbn8jq2G+Ye+mvpLUgcGyLfI5XPlPTn/PBIkv2hFjNs/basnMyrOUvg0Z85AA6+OA==} - - '@strudel/draw@1.2.4': - resolution: {integrity: sha512-9ui5+ZQQB+EEgwJQueGVMgpnPSHnFgRgivlAZbdjEoMdSm5vULWvKK3Psml8WKtOBNiuL+5Bci7a0INRTRusOA==} - - '@strudel/webaudio@1.2.5': - resolution: {integrity: sha512-ilcyBsuwkH6Ghz0egaqIJ5DZi0xFBZwVWvZb0vnfU7kVESroD2j6yptyFMysJFZhQ5pfRiZs2ZeF6yTsviUjng==} - '@supabase/auth-js@2.67.3': resolution: {integrity: sha512-NJDaW8yXs49xMvWVOkSIr8j46jf+tYHV0wHhrwOaLLMZSFO4g6kKAf+MfzQ2RaD06OCUkUHIzctLAxjTgEVpzw==} @@ -7001,9 +6989,6 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true - superdough@1.2.5: - resolution: {integrity: sha512-QVkMGGZjWzaRxYKXoZYMfsRHVk5vwNiqAaDOseccSTgqPPwFRTY2fmHAm5OmwwS8XqFOenZjn+XU3LEaCZMdJw==} - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -9844,20 +9829,6 @@ snapshots: '@sinclair/typebox@0.27.8': {} - '@strudel/core@1.2.4': - dependencies: - fraction.js: 5.2.1 - - '@strudel/draw@1.2.4': - dependencies: - '@strudel/core': 1.2.4 - - '@strudel/webaudio@1.2.5': - dependencies: - '@strudel/core': 1.2.4 - '@strudel/draw': 1.2.4 - superdough: 1.2.5 - '@supabase/auth-js@2.67.3': dependencies: '@supabase/node-fetch': 2.6.15 @@ -15460,10 +15431,6 @@ snapshots: pirates: 4.0.6 ts-interface-checker: 0.1.13 - superdough@1.2.5: - dependencies: - nanostores: 0.11.3 - supports-color@7.2.0: dependencies: has-flag: 4.0.0 From 7774364ac78b6be15d1fab04b95abf3f9be94ef9 Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 20 Oct 2025 20:36:19 +0300 Subject: [PATCH 13/40] Add webaudio as a workspace dependency --- packages/core/package.json | 1 + packages/osc/server.js | 0 pnpm-lock.yaml | 3 +++ 3 files changed, 4 insertions(+) mode change 100644 => 100755 packages/osc/server.js diff --git a/packages/core/package.json b/packages/core/package.json index 7cf20cea7..e771f0a73 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -31,6 +31,7 @@ }, "homepage": "https://strudel.cc", "dependencies": { + "@strudel/webaudio": "workspace:*", "fraction.js": "^5.2.1" }, "gitHead": "0e26d4e741500f5bae35b023608f062a794905c2", diff --git a/packages/osc/server.js b/packages/osc/server.js old mode 100644 new mode 100755 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c225057f3..217a63d8f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -225,6 +225,9 @@ importers: packages/core: dependencies: + '@strudel/webaudio': + specifier: workspace:* + version: link:../webaudio fraction.js: specifier: ^5.2.1 version: 5.2.1 From f5fc0ff9d19dfb0dc89017be4aa8dd66960864d6 Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 20 Oct 2025 20:58:01 +0300 Subject: [PATCH 14/40] Remove resetGlobalEffects from AudioContext --- packages/superdough/audioContext.mjs | 3 --- packages/webaudio/webaudio.mjs | 2 ++ 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/superdough/audioContext.mjs b/packages/superdough/audioContext.mjs index 9a5913d80..2c34f9daf 100644 --- a/packages/superdough/audioContext.mjs +++ b/packages/superdough/audioContext.mjs @@ -1,10 +1,7 @@ -import { analysers, resetGlobalEffects } from './superdough.mjs'; - let audioContext; export const setDefaultAudioContext = () => { audioContext = new AudioContext(); - resetGlobalEffects(); return audioContext; }; diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 43c011460..b1247e34b 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -21,6 +21,7 @@ import { loadWorklets, setMaxPolyphony, setMultiChannelOrbits, + resetGlobalEffects, } from 'superdough'; import './supradough.mjs'; import { workletUrl } from 'supradough'; @@ -99,6 +100,7 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d .finally(async () => { setAudioContext(null); setSuperdoughAudioController(null); + resetGlobalEffects() await initAudio(); }); } From 2a0635879cf4fdbc21c933fcaaa46ced4acb6a25 Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 20 Oct 2025 20:58:27 +0300 Subject: [PATCH 15/40] Format --- packages/webaudio/webaudio.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index b1247e34b..8141b5363 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -100,7 +100,7 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d .finally(async () => { setAudioContext(null); setSuperdoughAudioController(null); - resetGlobalEffects() + resetGlobalEffects(); await initAudio(); }); } From 0f652e8b5bc73609daa1f52a77cea9db6bc5fb47 Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 20 Oct 2025 21:24:27 +0300 Subject: [PATCH 16/40] Remove webaudio from core dependencies --- packages/codemirror/codemirror.mjs | 11 +++------- packages/core/cyclist.mjs | 10 --------- packages/core/package.json | 3 +-- packages/core/repl.mjs | 32 ++++++++++++++--------------- website/src/repl/useReplContext.jsx | 6 +++++- 5 files changed, 24 insertions(+), 38 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 84cbfa96a..a5a978bbb 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -42,9 +42,9 @@ const extensions = { isMultiCursorEnabled: (on) => on ? [ - EditorState.allowMultipleSelections.of(true), - EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), - ] + EditorState.allowMultipleSelections.of(true), + EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), + ] : [], }; const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()])); @@ -268,11 +268,6 @@ export class StrudelMirror { this.flash(); await this.repl.evaluate(this.code); } - async exportAudio(begin, end, sampleRate, downloadName = undefined) { - await this.repl.evaluate(this.code, false); - await this.repl.exportAudio(begin, end, sampleRate, downloadName); - this.repl.scheduler.stop(); - } async stop() { this.repl.scheduler.stop(); } diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index 185d5588b..fdd8cf1c7 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -6,7 +6,6 @@ This program is free software: you can redistribute it and/or modify it under th import createClock from './zyklus.mjs'; import { errorLogger, logger } from './logger.mjs'; -import { loadBuffer, renderPatternAudio, setAudioContext, superdough } from '@strudel/webaudio'; export class Cyclist { constructor({ @@ -111,15 +110,6 @@ export class Cyclist { this.clock.start(); this.setStarted(true); } - - async exportAudio(begin, end, sampleRate, downloadName = undefined) { - if (!this.pattern) { - throw new Error('Scheduler: no pattern set! call .setPattern first.'); - } - logger('[cyclist] exporting'); - await renderPatternAudio(this.pattern, this.cps, begin, end, sampleRate, downloadName); - } - pause() { logger('[cyclist] pause'); this.clock.pause(); diff --git a/packages/core/package.json b/packages/core/package.json index e771f0a73..50e02e4b2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -31,7 +31,6 @@ }, "homepage": "https://strudel.cc", "dependencies": { - "@strudel/webaudio": "workspace:*", "fraction.js": "^5.2.1" }, "gitHead": "0e26d4e741500f5bae35b023608f062a794905c2", @@ -39,4 +38,4 @@ "vite": "^6.0.11", "vitest": "^3.0.4" } -} +} \ No newline at end of file diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 4a35b932e..6cbc9e9ce 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -91,8 +91,6 @@ export function repl({ const stop = () => scheduler.stop(); const start = () => scheduler.start(); - 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) => { @@ -259,23 +257,23 @@ export function repl({ } }; const setCode = (code) => updateState({ code }); - return { scheduler, evaluate, start, exportAudio, stop, pause, setCps, setPattern, setCode, toggle, state }; + return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state }; } export const getTrigger = ({ getTime, defaultOutput }) => - async (hap, deadline, duration, cps, t) => { - // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) - // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 - try { - if (!hap.context.onTrigger || !hap.context.dominantTrigger) { - await defaultOutput(hap, deadline, duration, cps, t); + async (hap, deadline, duration, cps, t) => { + // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) + // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 + try { + if (!hap.context.onTrigger || !hap.context.dominantTrigger) { + await defaultOutput(hap, deadline, duration, cps, t); + } + if (hap.context.onTrigger) { + // call signature of output / onTrigger is different... + await hap.context.onTrigger(hap, getTime(), cps, t); + } + } catch (err) { + errorLogger(err, 'getTrigger'); } - if (hap.context.onTrigger) { - // call signature of output / onTrigger is different... - await hap.context.onTrigger(hap, getTime(), cps, t); - } - } catch (err) { - errorLogger(err, 'getTrigger'); - } - }; + }; diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 0e89fb68b..118277083 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -9,6 +9,7 @@ import { getDrawContext } from '@strudel/draw'; import { transpiler } from '@strudel/transpiler'; import { getAudioContextCurrentTime, + renderPatternAudio, webaudioOutput, resetGlobalEffects, resetLoadedSounds, @@ -203,8 +204,11 @@ export function useReplContext() { const handleEvaluate = () => { editorRef.current.evaluate(); }; + const handleExport = async (begin, end, sampleRate, downloadName = undefined) => { - await editorRef.current.exportAudio(begin, end, sampleRate, downloadName); + await editorRef.current.evaluate(); + await renderPatternAudio(editorRef.current.repl.state.pattern, editorRef.current.repl.scheduler.cps, begin, end, sampleRate, downloadName) + editorRef.current.repl.scheduler.stop(); }; const handleShuffle = async () => { const patternData = await getRandomTune(); From a2b76bde142dbca85a993fc9f18e7bf2071d8095 Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 20 Oct 2025 21:24:54 +0300 Subject: [PATCH 17/40] Format --- packages/codemirror/codemirror.mjs | 6 +++--- packages/core/repl.mjs | 28 ++++++++++++++-------------- website/src/repl/useReplContext.jsx | 9 ++++++++- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index a5a978bbb..4dc23996f 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -42,9 +42,9 @@ const extensions = { isMultiCursorEnabled: (on) => on ? [ - EditorState.allowMultipleSelections.of(true), - EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), - ] + EditorState.allowMultipleSelections.of(true), + EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), + ] : [], }; const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()])); diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index 6cbc9e9ce..171697eb9 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -262,18 +262,18 @@ export function repl({ export const getTrigger = ({ getTime, defaultOutput }) => - async (hap, deadline, duration, cps, t) => { - // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) - // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 - try { - if (!hap.context.onTrigger || !hap.context.dominantTrigger) { - await defaultOutput(hap, deadline, duration, cps, t); - } - if (hap.context.onTrigger) { - // call signature of output / onTrigger is different... - await hap.context.onTrigger(hap, getTime(), cps, t); - } - } catch (err) { - errorLogger(err, 'getTrigger'); + async (hap, deadline, duration, cps, t) => { + // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) + // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 + try { + if (!hap.context.onTrigger || !hap.context.dominantTrigger) { + await defaultOutput(hap, deadline, duration, cps, t); } - }; + if (hap.context.onTrigger) { + // call signature of output / onTrigger is different... + await hap.context.onTrigger(hap, getTime(), cps, t); + } + } catch (err) { + errorLogger(err, 'getTrigger'); + } + }; diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 118277083..aacd0872a 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -207,7 +207,14 @@ export function useReplContext() { const handleExport = async (begin, end, sampleRate, downloadName = undefined) => { await editorRef.current.evaluate(); - await renderPatternAudio(editorRef.current.repl.state.pattern, editorRef.current.repl.scheduler.cps, begin, end, sampleRate, downloadName) + await renderPatternAudio( + editorRef.current.repl.state.pattern, + editorRef.current.repl.scheduler.cps, + begin, + end, + sampleRate, + downloadName, + ); editorRef.current.repl.scheduler.stop(); }; const handleShuffle = async () => { From a8ea9a1b6334e0792b404c7bdefa850a8d41329e Mon Sep 17 00:00:00 2001 From: Nikita Date: Mon, 20 Oct 2025 21:26:40 +0300 Subject: [PATCH 18/40] Update lockfile --- pnpm-lock.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 217a63d8f..c225057f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -225,9 +225,6 @@ importers: packages/core: dependencies: - '@strudel/webaudio': - specifier: workspace:* - version: link:../webaudio fraction.js: specifier: ^5.2.1 version: 5.2.1 From bf702c3b2a12a76d1dbac81c2d5eb0d50f72acff Mon Sep 17 00:00:00 2001 From: Nikita Date: Tue, 21 Oct 2025 16:30:18 +0300 Subject: [PATCH 19/40] Reviewed changes --- packages/core/cyclist.mjs | 1 - packages/superdough/superdough.mjs | 5 ++--- packages/webaudio/webaudio.mjs | 14 ++++++-------- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/core/cyclist.mjs b/packages/core/cyclist.mjs index fdd8cf1c7..59e410c53 100644 --- a/packages/core/cyclist.mjs +++ b/packages/core/cyclist.mjs @@ -98,7 +98,6 @@ export class Cyclist { this.started = v; this.onToggle?.(v); } - async start() { await this.beforeStart?.(); this.num_ticks_since_cps_change = 0; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 118a152b1..c2bbba36a 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -282,7 +282,7 @@ export async function initAudioOnFirstClick(options) { } let controller; -function getSuperdoughAudioController() { +export function getSuperdoughAudioController() { if (controller == null) { controller = new SuperdoughAudioController(getAudioContext()); } @@ -376,7 +376,6 @@ function mapChannelNumbers(channels) { } export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) => { - // controller = null; // new: t is always expected to be the absolute target onset time const ac = getAudioContext(); const audioController = getSuperdoughAudioController(); @@ -402,7 +401,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) `[superdough]: cannot schedule sounds in the past (target: ${t.toFixed(2)}, now: ${ac.currentTime.toFixed(2)})`, ); return; - } // FIXME: fix + } // destructure let { tremolo, diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 8141b5363..46b5d8d08 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -45,12 +45,11 @@ export const webaudioOutput = (hap, _deadline, hapDuration, cps, t) => { }; 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) * sampleRate, sampleRate); + let audioContext = getAudioContext(); + await audioContext.close(); + audioContext = new OfflineAudioContext(2, ((end - begin) / cps) * sampleRate, sampleRate); setAudioContext(audioContext); - let context = getAudioContext(); - setSuperdoughAudioController(new SuperdoughAudioController(context)); + setSuperdoughAudioController(new SuperdoughAudioController(audioContext)); setMaxPolyphony(1024); setMultiChannelOrbits(true); await loadWorklets(); @@ -69,7 +68,7 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d let bank = getSound(s).data.samples; if (bank) { let { url: sampleUrl, label } = getSampleInfo(h.value, bank); - await loadBuffer(sampleUrl, context, label); + await loadBuffer(sampleUrl, audioContext, label); } } }), @@ -81,10 +80,9 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d } }); - return context + return audioContext .startRendering() .then((renderedBuffer) => { - console.log(renderedBuffer); const wavBuffer = audioBufferToWav(renderedBuffer); const blob = new Blob([wavBuffer], { type: 'audio/wav' }); const url = URL.createObjectURL(blob); From 498624f6fe24b57b83d5e1e028b0b23a9ad8d5b7 Mon Sep 17 00:00:00 2001 From: Nikita Date: Tue, 21 Oct 2025 16:35:33 +0300 Subject: [PATCH 20/40] Make server.js a normal file --- packages/osc/server.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 packages/osc/server.js diff --git a/packages/osc/server.js b/packages/osc/server.js old mode 100755 new mode 100644 From 98031676d6a13e5ee8ceb76a0e82cb539f92552c Mon Sep 17 00:00:00 2001 From: Nikita Date: Tue, 21 Oct 2025 16:44:42 +0300 Subject: [PATCH 21/40] Add an EOL character to package.json --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 50e02e4b2..7cf20cea7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -38,4 +38,4 @@ "vite": "^6.0.11", "vitest": "^3.0.4" } -} \ No newline at end of file +} From 92d3fe496d7d893497724cfb436ca6c7965561a5 Mon Sep 17 00:00:00 2001 From: Nikita Date: Tue, 21 Oct 2025 21:00:30 +0300 Subject: [PATCH 22/40] Bug fixes --- packages/codemirror/codemirror.mjs | 4 ++-- packages/webaudio/webaudio.mjs | 4 ++-- website/src/repl/useReplContext.jsx | 8 +++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 4dc23996f..a42529cc4 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -264,9 +264,9 @@ export class StrudelMirror { console.warn('first frame could not be painted'); } } - async evaluate() { + async evaluate(autoplay = true) { this.flash(); - await this.repl.evaluate(this.code); + await this.repl.evaluate(this.code, autoplay); } async stop() { this.repl.scheduler.stop(); diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 46b5d8d08..7a5ffac32 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -52,11 +52,11 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d setSuperdoughAudioController(new SuperdoughAudioController(audioContext)); setMaxPolyphony(1024); setMultiChannelOrbits(true); - await loadWorklets(); + await initAudio(); logger('[webaudio] start rendering'); let haps = pattern.queryArc(begin, end, { _cps: cps }); - Promise.all( + await Promise.all( haps.map(async (h) => { let s; if (h.value.s) { diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index aacd0872a..49d493157 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -206,7 +206,8 @@ export function useReplContext() { }; const handleExport = async (begin, end, sampleRate, downloadName = undefined) => { - await editorRef.current.evaluate(); + await editorRef.current.evaluate(true); + editorRef.current.repl.scheduler.stop(); await renderPatternAudio( editorRef.current.repl.state.pattern, editorRef.current.repl.scheduler.cps, @@ -214,8 +215,9 @@ export function useReplContext() { end, sampleRate, downloadName, - ); - editorRef.current.repl.scheduler.stop(); + ).finally(() => { + editorRef.current.repl.scheduler.stop(); + }); }; const handleShuffle = async () => { const patternData = await getRandomTune(); From daea207aff9403ae7db8365136caa7702f08337a Mon Sep 17 00:00:00 2001 From: Nikita Date: Tue, 21 Oct 2025 22:42:01 +0300 Subject: [PATCH 23/40] Better sample loading --- packages/codemirror/codemirror.mjs | 10 ++++----- packages/superdough/sampler.mjs | 2 +- packages/superdough/superdough.mjs | 2 +- packages/webaudio/webaudio.mjs | 32 ++++++++++++++++++++++------- website/src/repl/useReplContext.jsx | 2 +- 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index a42529cc4..3411728a0 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -42,9 +42,9 @@ const extensions = { isMultiCursorEnabled: (on) => on ? [ - EditorState.allowMultipleSelections.of(true), - EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), - ] + EditorState.allowMultipleSelections.of(true), + EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), + ] : [], }; const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()])); @@ -264,9 +264,9 @@ export class StrudelMirror { console.warn('first frame could not be painted'); } } - async evaluate(autoplay = true) { + async evaluate(autostart = true) { this.flash(); - await this.repl.evaluate(this.code, autoplay); + await this.repl.evaluate(this.code, autostart); } async stop() { this.repl.scheduler.stop(); diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 3232fa476..d996f3a26 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -289,7 +289,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl); // asny stuff above took too long? - if (ac.currentTime > t) { + if (ac.currentTime > t && !(ac instanceof OfflineAudioContext)) { logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight'); // console.warn('sample still loading:', s, n); return; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index c2bbba36a..e6972ab71 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -562,7 +562,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) return; } - if (ac.currentTime > t) { + if (ac.currentTime > t && !(ac instanceof OfflineAudioContext)) { logger('[webaudio] skip hap: still loading', ac.currentTime - t); return; } diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 7a5ffac32..fae2c46cf 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -22,11 +22,14 @@ import { setMaxPolyphony, setMultiChannelOrbits, resetGlobalEffects, + getDefaultValue, } from 'superdough'; import './supradough.mjs'; import { workletUrl } from 'supradough'; import { SuperdoughAudioController } from 'superdough/superdoughoutput.mjs'; - +import { loadModules } from '@src/repl/util.mjs'; +import { prebake } from '@src/repl/prebake.mjs'; +import { getFontBufferSource, getFontPitch } from 'node_modules/@strudel/soundfonts/fontloader.mjs'; registerWorklet(workletUrl); const { Pattern, logger, repl } = strudel; @@ -56,7 +59,9 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d logger('[webaudio] start rendering'); let haps = pattern.queryArc(begin, end, { _cps: cps }); - await Promise.all( + await Promise.all([ + loadModules(), + prebake(), haps.map(async (h) => { let s; if (h.value.s) { @@ -65,13 +70,26 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d } else { s = h.value.s; } - let bank = getSound(s).data.samples; - if (bank) { - let { url: sampleUrl, label } = getSampleInfo(h.value, bank); - await loadBuffer(sampleUrl, audioContext, label); + // let bank = getSound(s).data.samples; + let sample = getSound(s); + if (sample.data.type == "soundfont") { + sample.data.fonts.forEach(async (font) => { + await getFontBufferSource(font, h.value, audioContext) + }) } + if (sample.data.type == "sample") { + let url; + if (Array.isArray(sample)) { + url = sample.data.samples[i % sample.data.samples.length]; + } else if (typeof sample === 'object') { + console.log(sample.data.samples) + url = Object.values(sample.data.samples).flat()[getDefaultValue("i") % Object.values(sample.data.samples).length]; + } + await loadBuffer(url, audioContext, s, 0); + } // TODO: waveform } - }), + }) + ], ); haps.forEach(async (hap) => { if (hap.hasOnset()) { diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 49d493157..b775df659 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -206,7 +206,7 @@ export function useReplContext() { }; const handleExport = async (begin, end, sampleRate, downloadName = undefined) => { - await editorRef.current.evaluate(true); + await editorRef.current.evaluate(false); editorRef.current.repl.scheduler.stop(); await renderPatternAudio( editorRef.current.repl.state.pattern, From 5e27358fe86706d36775ac6134c3d7025fc780f6 Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 22 Oct 2025 16:19:58 +0300 Subject: [PATCH 24/40] Better preloading --- packages/webaudio/webaudio.mjs | 65 ++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index fae2c46cf..2bdb9a98a 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -23,6 +23,7 @@ import { setMultiChannelOrbits, resetGlobalEffects, getDefaultValue, + getSampleBuffer, } from 'superdough'; import './supradough.mjs'; import { workletUrl } from 'supradough'; @@ -62,45 +63,47 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d await Promise.all([ loadModules(), prebake(), - haps.map(async (h) => { - let s; - if (h.value.s) { - if (h.value.bank) { - s = `${h.value.bank}_${h.value.s}`; - } else { - s = h.value.s; - } - // let bank = getSound(s).data.samples; - let sample = getSound(s); - if (sample.data.type == "soundfont") { - sample.data.fonts.forEach(async (font) => { - await getFontBufferSource(font, h.value, audioContext) - }) - } - if (sample.data.type == "sample") { - let url; - if (Array.isArray(sample)) { - url = sample.data.samples[i % sample.data.samples.length]; - } else if (typeof sample === 'object') { - console.log(sample.data.samples) - url = Object.values(sample.data.samples).flat()[getDefaultValue("i") % Object.values(sample.data.samples).length]; - } - await loadBuffer(url, audioContext, s, 0); - } // TODO: waveform + ]); + + haps.forEach(async (h) => { + let s; + if (h.value.s) { + if (h.value.bank) { + s = `${h.value.bank}_${h.value.s}`; + } else { + s = h.value.s; } - }) - ], - ); - haps.forEach(async (hap) => { + // let bank = getSound(s).data.samples; + let sample = getSound(s); + if (sample.data.type == "soundfont") { + for (let index = 0; index < sample.data.fonts.length; index++) { + const font = array[index]; + await getFontBufferSource(font, h.value, audioContext) + } + } + if (sample.data.type == "sample") { + for (let index = 0; index < sample.data.samples.length; index++) { + const sample = array[index]; + await getSampleBuffer(h.value, bank, sample) + } + } // TODO: waveform + } + }) + + for (let index = 0; index < haps.length; index++) { + const hap = haps[index]; if (hap.hasOnset()) { - hap.ensureObjectValue(); + await hap.ensureObjectValue(); + console.log("dough") await superdough(hap.value, hap.whole.begin.valueOf() / cps, hap.duration, cps, hap.whole?.begin.valueOf()); } - }); + } + console.log("starting rendering") return audioContext .startRendering() .then((renderedBuffer) => { + console.log('downloading') const wavBuffer = audioBufferToWav(renderedBuffer); const blob = new Blob([wavBuffer], { type: 'audio/wav' }); const url = URL.createObjectURL(blob); From e7578be6c5c11756e20669337ca33f310c24a2ea Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 22 Oct 2025 17:16:05 +0300 Subject: [PATCH 25/40] Remove preloading code and instead await superdough --- packages/webaudio/webaudio.mjs | 42 ++++------------------------------ 1 file changed, 5 insertions(+), 37 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 2bdb9a98a..9764516c7 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -57,48 +57,16 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d setMaxPolyphony(1024); setMultiChannelOrbits(true); await initAudio(); - logger('[webaudio] start rendering'); + logger('[webaudio] preloading'); let haps = pattern.queryArc(begin, end, { _cps: cps }); - await Promise.all([ - loadModules(), - prebake(), - ]); - haps.forEach(async (h) => { - let s; - if (h.value.s) { - if (h.value.bank) { - s = `${h.value.bank}_${h.value.s}`; - } else { - s = h.value.s; - } - // let bank = getSound(s).data.samples; - let sample = getSound(s); - if (sample.data.type == "soundfont") { - for (let index = 0; index < sample.data.fonts.length; index++) { - const font = array[index]; - await getFontBufferSource(font, h.value, audioContext) - } - } - if (sample.data.type == "sample") { - for (let index = 0; index < sample.data.samples.length; index++) { - const sample = array[index]; - await getSampleBuffer(h.value, bank, sample) - } - } // TODO: waveform - } - }) - - for (let index = 0; index < haps.length; index++) { - const hap = haps[index]; + await Promise.all(haps.map(async (hap) => { if (hap.hasOnset()) { - await hap.ensureObjectValue(); - console.log("dough") - await superdough(hap.value, hap.whole.begin.valueOf() / cps, hap.duration, cps, hap.whole?.begin.valueOf()); + await superdough(hap2value(hap), hap.whole.begin.valueOf() / cps, hap.duration, cps, hap.whole?.begin.valueOf()); } - } - console.log("starting rendering") + })); + logger('[webaudio] start rendering'); return audioContext .startRendering() From 95700e11967b9be35c984f67e9a8510573fbe80f Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 22 Oct 2025 20:00:36 +0300 Subject: [PATCH 26/40] Fix wavetables breaking after playback --- packages/codemirror/codemirror.mjs | 6 +++--- packages/superdough/superdough.mjs | 2 ++ packages/superdough/wavetable.mjs | 5 +++++ packages/webaudio/webaudio.mjs | 20 ++++++++++++++------ 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 3411728a0..ab3a3c995 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -42,9 +42,9 @@ const extensions = { isMultiCursorEnabled: (on) => on ? [ - EditorState.allowMultipleSelections.of(true), - EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), - ] + EditorState.allowMultipleSelections.of(true), + EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey), + ] : [], }; const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()])); diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index e6972ab71..7fe60ae49 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -15,6 +15,7 @@ import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; import { getAudioContext, setAudioContext } from './audioContext.mjs'; import { SuperdoughAudioController } from './superdoughoutput.mjs'; +import { resetSeenKeys } from './wavetable.mjs'; export const DEFAULT_MAX_POLYPHONY = 128; const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; @@ -231,6 +232,7 @@ export async function initAudio(options = {}) { setMaxPolyphony(maxPolyphony); setMultiChannelOrbits(multiChannelOrbits); + resetSeenKeys(); if (typeof window === 'undefined') { return; } diff --git a/packages/superdough/wavetable.mjs b/packages/superdough/wavetable.mjs index 01d4eb838..902fb4edd 100644 --- a/packages/superdough/wavetable.mjs +++ b/packages/superdough/wavetable.mjs @@ -40,6 +40,11 @@ export const Warpmode = Object.freeze({ }); const seenKeys = new Set(); + +export function resetSeenKeys() { + seenKeys.clear(); +} + async function getPayload(url, label, frameLen = 2048) { const key = `${url},${frameLen}`; if (!seenKeys.has(key)) { diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 9764516c7..777a271aa 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -61,17 +61,25 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d let haps = pattern.queryArc(begin, end, { _cps: cps }); - await Promise.all(haps.map(async (hap) => { - if (hap.hasOnset()) { - await superdough(hap2value(hap), hap.whole.begin.valueOf() / cps, hap.duration, cps, hap.whole?.begin.valueOf()); - } - })); + await Promise.all( + haps.map(async (hap) => { + if (hap.hasOnset()) { + await superdough( + hap2value(hap), + hap.whole.begin.valueOf() / cps, + hap.duration, + cps, + hap.whole?.begin.valueOf(), + ); + } + }), + ); logger('[webaudio] start rendering'); return audioContext .startRendering() .then((renderedBuffer) => { - console.log('downloading') + console.log('downloading'); const wavBuffer = audioBufferToWav(renderedBuffer); const blob = new Blob([wavBuffer], { type: 'audio/wav' }); const url = URL.createObjectURL(blob); From 7b74a902a21ffcf3aa171f2b28ebe29d821d25a6 Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 22 Oct 2025 20:05:03 +0300 Subject: [PATCH 27/40] Remove unneeded imports --- packages/webaudio/webaudio.mjs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 777a271aa..c87f8ab05 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -12,25 +12,15 @@ import { doughTrigger, registerWorklet, setAudioContext, - getSampleBufferSource, - loadBuffer, - getSampleInfo, - getSound, initAudio, setSuperdoughAudioController, - loadWorklets, setMaxPolyphony, setMultiChannelOrbits, resetGlobalEffects, - getDefaultValue, - getSampleBuffer, } from 'superdough'; import './supradough.mjs'; import { workletUrl } from 'supradough'; import { SuperdoughAudioController } from 'superdough/superdoughoutput.mjs'; -import { loadModules } from '@src/repl/util.mjs'; -import { prebake } from '@src/repl/prebake.mjs'; -import { getFontBufferSource, getFontPitch } from 'node_modules/@strudel/soundfonts/fontloader.mjs'; registerWorklet(workletUrl); const { Pattern, logger, repl } = strudel; @@ -79,7 +69,6 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d return audioContext .startRendering() .then((renderedBuffer) => { - console.log('downloading'); const wavBuffer = audioBufferToWav(renderedBuffer); const blob = new Blob([wavBuffer], { type: 'audio/wav' }); const url = URL.createObjectURL(blob); From 5a70599aac6907fbcb32b4e2a3cb85f55d8410de Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 22 Oct 2025 22:48:04 +0300 Subject: [PATCH 28/40] Ignore superdough errors when exporting, instead skip sound. --- packages/webaudio/webaudio.mjs | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index c87f8ab05..816434889 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -54,13 +54,17 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d await Promise.all( haps.map(async (hap) => { if (hap.hasOnset()) { - await superdough( - hap2value(hap), - hap.whole.begin.valueOf() / cps, - hap.duration, - cps, - hap.whole?.begin.valueOf(), - ); + try { + await superdough( + hap2value(hap), + hap.whole.begin.valueOf() / cps, + hap.duration, + cps, + hap.whole?.begin.valueOf(), + ); + } catch (error) { + logger("[webaudio] An error had occured while calling superdough. Skipping hap.") + } } }), ); @@ -81,12 +85,12 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d document.body.removeChild(a); URL.revokeObjectURL(url); }) - .finally(async () => { - setAudioContext(null); - setSuperdoughAudioController(null); - resetGlobalEffects(); - await initAudio(); - }); + // .finally(async () => { + // setAudioContext(null); + // setSuperdoughAudioController(null); + // resetGlobalEffects(); + // await initAudio(); + // }); } export function webaudioRepl(options = {}) { From 16f885c010d9fc27f7afd7a49a3419476b767f86 Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 22 Oct 2025 23:09:30 +0300 Subject: [PATCH 29/40] Use errorLogger instead --- packages/webaudio/webaudio.mjs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 816434889..4ad778d17 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -17,6 +17,7 @@ import { setMaxPolyphony, setMultiChannelOrbits, resetGlobalEffects, + errorLogger, } from 'superdough'; import './supradough.mjs'; import { workletUrl } from 'supradough'; @@ -62,8 +63,8 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d cps, hap.whole?.begin.valueOf(), ); - } catch (error) { - logger("[webaudio] An error had occured while calling superdough. Skipping hap.") + } catch (err) { + errorLogger(err, 'webaudio') } } }), @@ -85,12 +86,12 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d document.body.removeChild(a); URL.revokeObjectURL(url); }) - // .finally(async () => { - // setAudioContext(null); - // setSuperdoughAudioController(null); - // resetGlobalEffects(); - // await initAudio(); - // }); + .finally(async () => { + setAudioContext(null); + setSuperdoughAudioController(null); + resetGlobalEffects(); + await initAudio(); + }); } export function webaudioRepl(options = {}) { From 6baca67cd3f7ab18970dfff68d8e72dbfccd77f5 Mon Sep 17 00:00:00 2001 From: Nikita Date: Thu, 23 Oct 2025 17:07:53 +0300 Subject: [PATCH 30/40] Fix maxPolyphony and multiChannelOrbits not applying to export --- packages/webaudio/webaudio.mjs | 8 ++++---- website/src/repl/components/ExportModal.jsx | 2 +- website/src/repl/useReplContext.jsx | 4 +++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 4ad778d17..3fc4df8bd 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -39,15 +39,15 @@ 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, sampleRate, downloadName = undefined) { +export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, maxPolyphony, multiChannelOrbits, downloadName = undefined) { let audioContext = getAudioContext(); await audioContext.close(); audioContext = new OfflineAudioContext(2, ((end - begin) / cps) * sampleRate, sampleRate); setAudioContext(audioContext); setSuperdoughAudioController(new SuperdoughAudioController(audioContext)); - setMaxPolyphony(1024); - setMultiChannelOrbits(true); await initAudio(); + setMaxPolyphony(maxPolyphony); + setMultiChannelOrbits(multiChannelOrbits); logger('[webaudio] preloading'); let haps = pattern.queryArc(begin, end, { _cps: cps }); @@ -64,7 +64,7 @@ export async function renderPatternAudio(pattern, cps, begin, end, sampleRate, d hap.whole?.begin.valueOf(), ); } catch (err) { - errorLogger(err, 'webaudio') + errorLogger(err, 'webaudio'); } } }), diff --git a/website/src/repl/components/ExportModal.jsx b/website/src/repl/components/ExportModal.jsx index 036f6f36a..990eb1132 100644 --- a/website/src/repl/components/ExportModal.jsx +++ b/website/src/repl/components/ExportModal.jsx @@ -204,7 +204,7 @@ export default function ExportModal(Props) { setStrudelMaxPolyphony(maxPolyphony); setStrudelMultiChannelOrbits(multiChannelOrbits); setTimeout(refreshProgress, 1000); - await handleExport(startCycle, endCycle, sampleRate, downloadName).finally(() => { + await handleExport(startCycle, endCycle, sampleRate, maxPolyphony, multiChannelOrbits, downloadName).finally(() => { setExporting(false); const modal = document.getElementById('exportModal'); setProgress(0); diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index b775df659..15b3fab10 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -205,7 +205,7 @@ export function useReplContext() { editorRef.current.evaluate(); }; - const handleExport = async (begin, end, sampleRate, downloadName = undefined) => { + const handleExport = async (begin, end, sampleRate, maxPolyphony, multiChannelOrbits, downloadName = undefined) => { await editorRef.current.evaluate(false); editorRef.current.repl.scheduler.stop(); await renderPatternAudio( @@ -214,6 +214,8 @@ export function useReplContext() { begin, end, sampleRate, + maxPolyphony, + multiChannelOrbits, downloadName, ).finally(() => { editorRef.current.repl.scheduler.stop(); From fe70a2df43c874639ff0fd778fe03e9261212de5 Mon Sep 17 00:00:00 2001 From: Nikita Date: Thu, 23 Oct 2025 17:08:11 +0300 Subject: [PATCH 31/40] Format --- packages/webaudio/webaudio.mjs | 11 ++++++++++- website/src/repl/components/ExportModal.jsx | 9 ++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 3fc4df8bd..0df487be2 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -39,7 +39,16 @@ 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, sampleRate, maxPolyphony, multiChannelOrbits, downloadName = undefined) { +export async function renderPatternAudio( + pattern, + cps, + begin, + end, + sampleRate, + maxPolyphony, + multiChannelOrbits, + downloadName = undefined, +) { let audioContext = getAudioContext(); await audioContext.close(); audioContext = new OfflineAudioContext(2, ((end - begin) / cps) * sampleRate, sampleRate); diff --git a/website/src/repl/components/ExportModal.jsx b/website/src/repl/components/ExportModal.jsx index 990eb1132..ad4ddad5d 100644 --- a/website/src/repl/components/ExportModal.jsx +++ b/website/src/repl/components/ExportModal.jsx @@ -204,7 +204,14 @@ export default function ExportModal(Props) { setStrudelMaxPolyphony(maxPolyphony); setStrudelMultiChannelOrbits(multiChannelOrbits); setTimeout(refreshProgress, 1000); - await handleExport(startCycle, endCycle, sampleRate, maxPolyphony, multiChannelOrbits, downloadName).finally(() => { + await handleExport( + startCycle, + endCycle, + sampleRate, + maxPolyphony, + multiChannelOrbits, + downloadName, + ).finally(() => { setExporting(false); const modal = document.getElementById('exportModal'); setProgress(0); From ca936aada775ec734c25769fa10af2a35f4da402 Mon Sep 17 00:00:00 2001 From: Nikita Date: Thu, 23 Oct 2025 21:46:13 +0300 Subject: [PATCH 32/40] Fix incorrect duration calculation --- packages/webaudio/webaudio.mjs | 2 +- website/src/repl/components/ExportModal.jsx | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 0df487be2..99e5be5ed 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -68,7 +68,7 @@ export async function renderPatternAudio( await superdough( hap2value(hap), hap.whole.begin.valueOf() / cps, - hap.duration, + hap.duration / cps, cps, hap.whole?.begin.valueOf(), ); diff --git a/website/src/repl/components/ExportModal.jsx b/website/src/repl/components/ExportModal.jsx index ad4ddad5d..9f32d6930 100644 --- a/website/src/repl/components/ExportModal.jsx +++ b/website/src/repl/components/ExportModal.jsx @@ -211,12 +211,13 @@ export default function ExportModal(Props) { maxPolyphony, multiChannelOrbits, downloadName, - ).finally(() => { - setExporting(false); + ).then(() => { const modal = document.getElementById('exportModal'); + modal.close(); + }).finally(() => { + setExporting(false); setProgress(0); setLength(1); - modal.close(); }); }} > From 9cbeaef88defc63150db0eb094ca3e7e2ad21275 Mon Sep 17 00:00:00 2001 From: Nikita Date: Thu, 23 Oct 2025 22:21:40 +0300 Subject: [PATCH 33/40] Format --- website/src/repl/components/ExportModal.jsx | 25 +++++++++------------ 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/website/src/repl/components/ExportModal.jsx b/website/src/repl/components/ExportModal.jsx index 9f32d6930..93624fa07 100644 --- a/website/src/repl/components/ExportModal.jsx +++ b/website/src/repl/components/ExportModal.jsx @@ -204,21 +204,16 @@ export default function ExportModal(Props) { setStrudelMaxPolyphony(maxPolyphony); setStrudelMultiChannelOrbits(multiChannelOrbits); setTimeout(refreshProgress, 1000); - await handleExport( - startCycle, - endCycle, - sampleRate, - maxPolyphony, - multiChannelOrbits, - downloadName, - ).then(() => { - const modal = document.getElementById('exportModal'); - modal.close(); - }).finally(() => { - setExporting(false); - setProgress(0); - setLength(1); - }); + await handleExport(startCycle, endCycle, sampleRate, maxPolyphony, multiChannelOrbits, downloadName) + .then(() => { + const modal = document.getElementById('exportModal'); + modal.close(); + }) + .finally(() => { + setExporting(false); + setProgress(0); + setLength(1); + }); }} > {exporting ? 'Exporting...' : 'Export to WAV'} From ff370cf86f5d5a2b7bc21b59f74fb1839cd3c755 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 24 Oct 2025 14:07:57 +0300 Subject: [PATCH 34/40] Reapply realtimeOptions after export --- packages/superdough/sampler.mjs | 2 +- packages/superdough/superdough.mjs | 8 ++++---- packages/webaudio/webaudio.mjs | 15 +++++++++++---- website/src/repl/components/ExportModal.jsx | 4 ---- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/superdough/sampler.mjs b/packages/superdough/sampler.mjs index 742be592f..84bac3582 100644 --- a/packages/superdough/sampler.mjs +++ b/packages/superdough/sampler.mjs @@ -287,7 +287,7 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) { const { bufferSource, sliceDuration, offset } = await getSampleBufferSource(value, bank, resolveUrl); // asny stuff above took too long? - if (ac.currentTime > t && !(ac instanceof OfflineAudioContext)) { + if (ac.currentTime > t) { logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight'); // console.warn('sample still loading:', s, n); return; diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index ae722b7ce..e7d62a8eb 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -20,13 +20,13 @@ import { resetSeenKeys } from './wavetable.mjs'; export const DEFAULT_MAX_POLYPHONY = 128; const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; -let maxPolyphony = DEFAULT_MAX_POLYPHONY; +export let maxPolyphony = DEFAULT_MAX_POLYPHONY; export function setMaxPolyphony(polyphony) { maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY; } -let multiChannelOrbits = false; +export let multiChannelOrbits = false; export function setMultiChannelOrbits(bool) { multiChannelOrbits = bool == true; } @@ -398,7 +398,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) // duration is passed as value too.. value.duration = hapDuration; // calculate absolute time - if (t < ac.currentTime && !(ac instanceof OfflineAudioContext)) { + if (t < ac.currentTime) { console.warn( `[superdough]: cannot schedule sounds in the past (target: ${t.toFixed(2)}, now: ${ac.currentTime.toFixed(2)})`, ); @@ -564,7 +564,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5) return; } - if (ac.currentTime > t && !(ac instanceof OfflineAudioContext)) { + if (ac.currentTime > t) { logger('[webaudio] skip hap: still loading', ac.currentTime - t); return; } diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 99e5be5ed..c9b5cbda3 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -18,6 +18,8 @@ import { setMultiChannelOrbits, resetGlobalEffects, errorLogger, + maxPolyphony as superdoughMaxPolyphony, + multiChannelOrbits as superdoughMultiChannelOrbits } from 'superdough'; import './supradough.mjs'; import { workletUrl } from 'supradough'; @@ -49,14 +51,19 @@ export async function renderPatternAudio( multiChannelOrbits, downloadName = undefined, ) { + let realtimeOptions = { + maxPolyphony: superdoughMaxPolyphony, + multiChannelOrbits: superdoughMultiChannelOrbits + } let audioContext = getAudioContext(); await audioContext.close(); audioContext = new OfflineAudioContext(2, ((end - begin) / cps) * sampleRate, sampleRate); setAudioContext(audioContext); setSuperdoughAudioController(new SuperdoughAudioController(audioContext)); - await initAudio(); - setMaxPolyphony(maxPolyphony); - setMultiChannelOrbits(multiChannelOrbits); + await initAudio({ + maxPolyphony, + multiChannelOrbits + }); logger('[webaudio] preloading'); let haps = pattern.queryArc(begin, end, { _cps: cps }); @@ -99,7 +106,7 @@ export async function renderPatternAudio( setAudioContext(null); setSuperdoughAudioController(null); resetGlobalEffects(); - await initAudio(); + await initAudio(realtimeOptions); }); } diff --git a/website/src/repl/components/ExportModal.jsx b/website/src/repl/components/ExportModal.jsx index 93624fa07..4bc8990ea 100644 --- a/website/src/repl/components/ExportModal.jsx +++ b/website/src/repl/components/ExportModal.jsx @@ -4,8 +4,6 @@ import NumberInput from './NumberInput'; import { useEffect, useState } from 'react'; import { Textbox } from './textbox/Textbox'; import { - setMultiChannelOrbits as setStrudelMultiChannelOrbits, - setMaxPolyphony as setStrudelMaxPolyphony, getAudioContext, } from '@strudel/webaudio'; import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon'; @@ -201,8 +199,6 @@ export default function ExportModal(Props) { disabled={exporting} onClick={async () => { setExporting(true); - setStrudelMaxPolyphony(maxPolyphony); - setStrudelMultiChannelOrbits(multiChannelOrbits); setTimeout(refreshProgress, 1000); await handleExport(startCycle, endCycle, sampleRate, maxPolyphony, multiChannelOrbits, downloadName) .then(() => { From fc74dcdb1a56e7f2122e87210be53e6a60d1ea74 Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 24 Oct 2025 19:21:34 +0300 Subject: [PATCH 35/40] More bug fixes --- packages/webaudio/webaudio.mjs | 43 +++++++++++------------------ website/src/repl/useReplContext.jsx | 10 ++++++- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index c9b5cbda3..7c8ff648e 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -14,12 +14,8 @@ import { setAudioContext, initAudio, setSuperdoughAudioController, - setMaxPolyphony, - setMultiChannelOrbits, resetGlobalEffects, - errorLogger, - maxPolyphony as superdoughMaxPolyphony, - multiChannelOrbits as superdoughMultiChannelOrbits + errorLogger } from 'superdough'; import './supradough.mjs'; import { workletUrl } from 'supradough'; @@ -51,10 +47,6 @@ export async function renderPatternAudio( multiChannelOrbits, downloadName = undefined, ) { - let realtimeOptions = { - maxPolyphony: superdoughMaxPolyphony, - multiChannelOrbits: superdoughMultiChannelOrbits - } let audioContext = getAudioContext(); await audioContext.close(); audioContext = new OfflineAudioContext(2, ((end - begin) / cps) * sampleRate, sampleRate); @@ -63,28 +55,26 @@ export async function renderPatternAudio( await initAudio({ maxPolyphony, multiChannelOrbits - }); + }) logger('[webaudio] preloading'); let haps = pattern.queryArc(begin, end, { _cps: cps }); - await Promise.all( - haps.map(async (hap) => { - if (hap.hasOnset()) { - try { - await superdough( - hap2value(hap), - hap.whole.begin.valueOf() / cps, - hap.duration / cps, - cps, - hap.whole?.begin.valueOf(), - ); - } catch (err) { - errorLogger(err, 'webaudio'); - } + 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 @@ -106,7 +96,6 @@ export async function renderPatternAudio( setAudioContext(null); setSuperdoughAudioController(null); resetGlobalEffects(); - await initAudio(realtimeOptions); }); } diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 15b3fab10..361abc1d3 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -15,6 +15,7 @@ import { resetLoadedSounds, initAudioOnFirstClick, resetDefaults, + initAudio, } from '@strudel/webaudio'; import { setVersionDefaultsFrom } from './util.mjs'; import { StrudelMirror, defaultSettings } from '@strudel/codemirror'; @@ -217,7 +218,14 @@ export function useReplContext() { maxPolyphony, multiChannelOrbits, downloadName, - ).finally(() => { + ).finally(async () => { + const { latestCode, maxPolyphony, audioDeviceName, multiChannelOrbits } = settingsMap.get() + await initAudio({ + latestCode, + maxPolyphony, + audioDeviceName, + multiChannelOrbits + }) editorRef.current.repl.scheduler.stop(); }); }; From 91ebdd5ea62ddeb8d9a346f73d5c3dd2a06aa98b Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 24 Oct 2025 20:55:39 +0300 Subject: [PATCH 36/40] Better progress bar UI --- website/src/repl/components/ExportModal.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/src/repl/components/ExportModal.jsx b/website/src/repl/components/ExportModal.jsx index 4bc8990ea..ad1fc5827 100644 --- a/website/src/repl/components/ExportModal.jsx +++ b/website/src/repl/components/ExportModal.jsx @@ -199,7 +199,7 @@ export default function ExportModal(Props) { disabled={exporting} onClick={async () => { setExporting(true); - setTimeout(refreshProgress, 1000); + setTimeout(refreshProgress, 2000); await handleExport(startCycle, endCycle, sampleRate, maxPolyphony, multiChannelOrbits, downloadName) .then(() => { const modal = document.getElementById('exportModal'); @@ -212,13 +212,13 @@ export default function ExportModal(Props) { }); }} > - {exporting ? 'Exporting...' : 'Export to WAV'}
+ {exporting ? 'Exporting...' : 'Export to WAV'}
From 8f9967c76dae6d053f8157f2e091d697c385a19c Mon Sep 17 00:00:00 2001 From: Nikita Date: Fri, 24 Oct 2025 20:55:56 +0300 Subject: [PATCH 37/40] Format --- packages/webaudio/webaudio.mjs | 6 +++--- website/src/repl/components/ExportModal.jsx | 6 ++---- website/src/repl/useReplContext.jsx | 6 +++--- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 7c8ff648e..9da5a411d 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -15,7 +15,7 @@ import { initAudio, setSuperdoughAudioController, resetGlobalEffects, - errorLogger + errorLogger, } from 'superdough'; import './supradough.mjs'; import { workletUrl } from 'supradough'; @@ -54,8 +54,8 @@ export async function renderPatternAudio( setSuperdoughAudioController(new SuperdoughAudioController(audioContext)); await initAudio({ maxPolyphony, - multiChannelOrbits - }) + multiChannelOrbits, + }); logger('[webaudio] preloading'); let haps = pattern.queryArc(begin, end, { _cps: cps }); diff --git a/website/src/repl/components/ExportModal.jsx b/website/src/repl/components/ExportModal.jsx index ad1fc5827..bac8c6b14 100644 --- a/website/src/repl/components/ExportModal.jsx +++ b/website/src/repl/components/ExportModal.jsx @@ -3,9 +3,7 @@ import cx from '@src/cx.mjs'; import NumberInput from './NumberInput'; import { useEffect, useState } from 'react'; import { Textbox } from './textbox/Textbox'; -import { - getAudioContext, -} from '@strudel/webaudio'; +import { getAudioContext } from '@strudel/webaudio'; import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon'; function Checkbox({ label, value, onChange, disabled = false }) { @@ -215,7 +213,7 @@ export default function ExportModal(Props) {
{exporting ? 'Exporting...' : 'Export to WAV'} diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 361abc1d3..59d86a4d6 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -219,13 +219,13 @@ export function useReplContext() { multiChannelOrbits, downloadName, ).finally(async () => { - const { latestCode, maxPolyphony, audioDeviceName, multiChannelOrbits } = settingsMap.get() + const { latestCode, maxPolyphony, audioDeviceName, multiChannelOrbits } = settingsMap.get(); await initAudio({ latestCode, maxPolyphony, audioDeviceName, - multiChannelOrbits - }) + multiChannelOrbits, + }); editorRef.current.repl.scheduler.stop(); }); }; From 7d890156f4c2a4a584c975e57fc8fa3e44d897df Mon Sep 17 00:00:00 2001 From: Nikita Date: Sat, 25 Oct 2025 18:53:24 +0300 Subject: [PATCH 38/40] Fix haps not being sorted by time --- packages/webaudio/webaudio.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 9da5a411d..8f18d80dc 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -58,7 +58,9 @@ export async function renderPatternAudio( }); logger('[webaudio] preloading'); - let haps = pattern.queryArc(begin, end, { _cps: cps }); + 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()) { From c5250d7794a2600d0aeca36885adf0e93e1d7be7 Mon Sep 17 00:00:00 2001 From: Nikita Date: Sat, 25 Oct 2025 18:59:12 +0300 Subject: [PATCH 39/40] Add info comment about ascending time order --- packages/webaudio/webaudio.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 8f18d80dc..36ee9d645 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -58,10 +58,11 @@ export async function renderPatternAudio( }); 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 { From 5bddb6f48d969427d977ad807f3e7c3f0f482703 Mon Sep 17 00:00:00 2001 From: Nikita Date: Sun, 30 Nov 2025 10:09:06 +0300 Subject: [PATCH 40/40] Move export UI to the panel --- website/src/repl/components/ExportModal.jsx | 225 ------------------ website/src/repl/components/Header.jsx | 15 +- .../src/repl/components/panel/ExportTab.jsx | 197 +++++++++++++++ website/src/repl/components/panel/Panel.jsx | 4 + 4 files changed, 203 insertions(+), 238 deletions(-) delete mode 100644 website/src/repl/components/ExportModal.jsx create mode 100644 website/src/repl/components/panel/ExportTab.jsx diff --git a/website/src/repl/components/ExportModal.jsx b/website/src/repl/components/ExportModal.jsx deleted file mode 100644 index bac8c6b14..000000000 --- a/website/src/repl/components/ExportModal.jsx +++ /dev/null @@ -1,225 +0,0 @@ -import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon'; -import cx from '@src/cx.mjs'; -import NumberInput from './NumberInput'; -import { useEffect, useState } from 'react'; -import { Textbox } from './textbox/Textbox'; -import { getAudioContext } from '@strudel/webaudio'; -import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon'; - -function Checkbox({ label, value, onChange, disabled = false }) { - return ( - - ); -} - -function FormItem({ label, children, disabled }) { - return ( -
- - {children} -
- ); -} - -export default function ExportModal(Props) { - const { started, isEmbedded, handleExport } = Props; - - const [downloadName, setDownloadName] = useState(''); // TODO: make a form? - const [startCycle, setStartCycle] = useState(0); - const [endCycle, setEndCycle] = useState(1); - const [sampleRate, setSampleRate] = useState(48000); - const [multiChannelOrbits, setMultiChannelOrbits] = useState(true); - const [maxPolyphony, setMaxPolyphony] = useState(1024); - const [exporting, setExporting] = useState(false); - const [progress, setProgress] = useState(0); - const [length, setLength] = useState(1); - - const refreshProgress = () => { - const audioContext = getAudioContext(); - if (audioContext instanceof OfflineAudioContext) { - setProgress(audioContext.currentTime); - setLength(audioContext.length / sampleRate); - setTimeout(refreshProgress, 100); - } - }; - - return ( - <> - - - -
- - { - setDownloadName(e.target.value); - }} - onChange={(v) => { - setDownloadName(v); - }} - disabled={exporting} - placeholder="Leave empty to use current date" - className={cx('placeholder:opacity-50', exporting && 'opacity-50 border-opacity-50')} - value={downloadName ?? ''} - /> - -
- - { - let v = parseInt(e.target.value); - v = isNaN(v) ? 0 : Math.max(0, v); - setStartCycle(v); - }} - onChange={(v) => { - v = parseInt(v); - setStartCycle(v); - }} - type="number" - placeholder="" - disabled={exporting} - className={cx(exporting && 'opacity-50 border-opacity-50')} - value={startCycle ?? ''} - /> - - - { - let v = parseInt(e.target.value); - v = isNaN(v) ? Math.max(startCycle + 1, parseInt(v)) : v; - setEndCycle(v); - }} - onChange={(v) => { - v = parseInt(v); - setEndCycle(v); - }} - type="number" - placeholder="" - disabled={exporting} - className={cx(exporting && 'opacity-50 border-opacity-50')} - value={endCycle ?? ''} - /> - -
-
- - { - let v = parseInt(e.target.value); - v = isNaN(v) ? 1 : Math.max(1, v); - setSampleRate(v); - }} - onChange={(v) => { - v = parseInt(v); - setSampleRate(v); - }} - type="number" - placeholder="" - disabled={exporting} - className={cx(exporting && 'opacity-50 border-opacity-50')} - value={sampleRate ?? ''} - /> - - - { - let v = parseInt(e.target.value); - v = isNaN(v) ? Math.max(1, parseInt(v)) : v; - setMaxPolyphony(v); - }} - onChange={(v) => { - v = Math.max(1, parseInt(v)); - setMaxPolyphony(v); - }} - type="number" - placeholder="" - disabled={exporting} - className={cx(exporting && 'opacity-50 border-opacity-50')} - value={maxPolyphony ?? ''} - /> - -
-
- { - const val = cbEvent.target.checked; - setMultiChannelOrbits(val); - }} - disabled={exporting} - value={multiChannelOrbits} - /> -
- -
-
- - ); -} diff --git a/website/src/repl/components/Header.jsx b/website/src/repl/components/Header.jsx index dd9f684c1..ca4e1ecf9 100644 --- a/website/src/repl/components/Header.jsx +++ b/website/src/repl/components/Header.jsx @@ -3,23 +3,13 @@ import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon'; import cx from '@src/cx.mjs'; import { useSettings, setIsZen } from '../../settings.mjs'; import '../Repl.css'; -import ExportModal from './ExportModal'; const { BASE_URL } = import.meta.env; const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; export function Header({ context, embedded = false }) { - const { - started, - pending, - isDirty, - activeCode, - handleTogglePlay, - handleEvaluate, - handleShuffle, - handleShare, - handleExport, - } = context; + const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShuffle, handleShare } = + context; const isEmbedded = typeof window !== 'undefined' && (embedded || window.location !== window.parent.location); const { isZen, isButtonRowHidden, isCSSAnimationDisabled, fontFamily } = useSettings(); @@ -103,7 +93,6 @@ export function Header({ context, embedded = false }) { > {!isEmbedded && update} - {/* !isEmbedded && ( +
+ + + ); +} diff --git a/website/src/repl/components/panel/Panel.jsx b/website/src/repl/components/panel/Panel.jsx index b26c2edef..cfe90ae30 100644 --- a/website/src/repl/components/panel/Panel.jsx +++ b/website/src/repl/components/panel/Panel.jsx @@ -9,6 +9,7 @@ import { useLogger } from '../useLogger'; import { WelcomeTab } from './WelcomeTab'; import { PatternsTab } from './PatternsTab'; import { ChevronLeftIcon, XMarkIcon } from '@heroicons/react/16/solid'; +import ExportTab from './ExportTab'; const TAURI = typeof window !== 'undefined' && window.__TAURI__; @@ -80,6 +81,7 @@ const tabNames = { patterns: 'patterns', sounds: 'sounds', reference: 'reference', + export: 'export', console: 'console', settings: 'settings', }; @@ -126,6 +128,8 @@ function PanelContent({ context, tab }) { return ; case tabNames.reference: return ; + case tabNames.export: + return ; case tabNames.settings: return ; case tabNames.files: