Merge branch 'main' into jade/gaincurve

This commit is contained in:
Jade (Rose) Rowland 2025-04-27 02:10:30 -04:00
commit 697f0a82e6
33 changed files with 591 additions and 332 deletions

View file

@ -11,7 +11,9 @@ export const flashField = StateField.define({
for (let e of tr.effects) {
if (e.is(setFlash)) {
if (e.value && tr.newDoc.length > 0) {
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
const mark = Decoration.mark({
attributes: { style: `background-color: rgba(255,255,255, .4); filter: invert(10%)` },
});
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
} else {
flash = Decoration.set([]);

View file

@ -5,16 +5,15 @@ import whitescreen, { settings as whitescreenSettings } from './themes/whitescre
import teletext, { settings as teletextSettings } from './themes/teletext.mjs';
import algoboy, { settings as algoboySettings } from './themes/algoboy.mjs';
import CutiePi, { settings as CutiePiSettings } from './themes/CutiePi.mjs';
import terminal, { settings as terminalSettings } from './themes/terminal.mjs';
import abcdef, { settings as abcdefSettings } from './themes/abcdef.mjs';
import sonicPink, { settings as sonicPinkSettings } from './themes/sonic-pink.mjs';
import redText, { settings as redTextSettings } from './themes/red-text.mjs';
import greenText, { settings as greenTextSettings } from './themes/green-text.mjs';
import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs';
import atomone, { settings as atomOneSettings } from './themes/atomone.mjs';
import aura, { settings as auraSettings } from './themes/aura.mjs';
import bespin, { settings as bespinSettings } from './themes/bespin.mjs';
import darcula, { settings as darculaSettings } from './themes/darcula.mjs';
import dracula, { settings as draculaSettings } from './themes/dracula.mjs';
import duotoneDark, { settings as duotoneDarkSettings } from './themes/duotoneDark.mjs';
import duotoneLight, { settings as duotoneLightSettings } from './themes/duotoneLight.mjs';
import eclipse, { settings as eclipseSettings } from './themes/eclipse.mjs';
import githubDark, { settings as githubDarkSettings } from './themes/githubDark.mjs';
import githubLight, { settings as githubLightSettings } from './themes/githubLight.mjs';
@ -32,52 +31,47 @@ import tokyoNightStorm, { settings as tokyoNightStormSettings } from './themes/t
import tokyoNightDay, { settings as tokyoNightDaySettings } from './themes/tokyoNightDay.mjs';
import vscodeDark, { settings as vscodeDarkSettings } from './themes/vscodeDark.mjs';
import vscodeLight, { settings as vscodeLightSettings } from './themes/vscodeLight.mjs';
// import xcodeDark, { settings as xcodeDarkSettings } from './themes/xcodeDark.mjs';
import xcodeLight, { settings as xcodeLightSettings } from './themes/xcodeLight.mjs';
import bbedit, { settings as bbeditSettings } from './themes/bbedit.mjs';
import noctisLilac, { settings as noctisLilacSettings } from './themes/noctisLilac.mjs';
import { setTheme } from '@strudel/draw';
export const themes = {
strudelTheme,
bluescreen,
blackscreen,
whitescreen,
teletext,
algoboy,
androidstudio,
atomone,
aura,
bbedit,
blackscreen,
bluescreen,
CutiePi,
darcula,
dracula,
// todo: optimize
// bespin,
//abcdef,
androidstudio,
duotoneDark,
eclipse,
githubDark,
CutiePi,
githubLight,
greenText,
gruvboxDark,
gruvboxLight,
sonicPink,
materialDark,
nord,
materialLight,
monokai,
noctisLilac,
nord,
redText,
solarizedDark,
solarizedLight,
sublime,
teletext,
tokyoNight,
tokyoNightDay,
tokyoNightStorm,
vscodeDark,
//xcodeDark,
// LIGHT
bbedit,
//duotoneLight,
eclipse,
githubLight,
gruvboxLight,
materialLight,
vscodeLight,
noctisLilac,
solarizedLight,
tokyoNightDay,
whitescreen,
xcodeLight,
};
@ -88,21 +82,19 @@ export const settings = {
whitescreen: whitescreenSettings,
teletext: teletextSettings,
algoboy: algoboySettings,
terminal: terminalSettings,
abcdef: abcdefSettings,
androidstudio: androidstudioSettings,
atomone: atomOneSettings,
aura: auraSettings,
bbedit: bbeditSettings,
bespin: bespinSettings,
darcula: darculaSettings,
dracula: draculaSettings,
duotoneLight: duotoneLightSettings,
duotoneDark: duotoneDarkSettings,
eclipse: eclipseSettings,
CutiePi: CutiePiSettings,
sonicPink: sonicPinkSettings,
githubLight: githubLightSettings,
githubDark: githubDarkSettings,
greenText: greenTextSettings,
gruvboxDark: gruvboxDarkSettings,
gruvboxLight: gruvboxLightSettings,
materialDark: materialDarkSettings,
@ -110,6 +102,7 @@ export const settings = {
noctisLilac: noctisLilacSettings,
nord: nordSettings,
monokai: monokaiSettings,
redText: redTextSettings,
solarizedLight: solarizedLightSettings,
solarizedDark: solarizedDarkSettings,
sublime: sublimeSettings,
@ -118,7 +111,6 @@ export const settings = {
vscodeDark: vscodeDarkSettings,
vscodeLight: vscodeLightSettings,
xcodeLight: xcodeLightSettings,
//xcodeDark: xcodeDarkSettings,
tokyoNightDay: tokyoNightDaySettings,
};

View file

@ -1,55 +0,0 @@
/**
* @name abcdef
* @author codemirror.net
* https://codemirror.net/5/theme/abcdef.css
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#0f0f0f',
lineBackground: '#0f0f0f99',
foreground: '#defdef',
caret: '#00FF00',
selection: '#515151',
selectionMatch: '#515151',
gutterBackground: '#555',
gutterForeground: '#FFFFFF',
lineHighlight: '#314151',
};
export default createTheme({
theme: 'dark',
settings: {
background: '#0f0f0f',
foreground: '#defdef',
caret: '#00FF00',
selection: '#515151',
selectionMatch: '#515151',
// gutterBackground: '#555',
gutterBackground: 'transparent',
/* gutterForeground: '#FFFFFF', */
gutterForeground: '#7a7b7c',
lineHighlight: '#0a6bcb3d',
},
styles: [
{ tag: t.labelName, color: 'inherit' },
{ tag: t.keyword, color: 'darkgoldenrod', fontWeight: 'bold' },
{ tag: t.atom, color: '#77F' },
{ tag: t.comment, color: '#7a7b7c', fontStyle: 'italic' },
{ tag: t.number, color: 'violet' },
{ tag: t.definition(t.variableName), color: '#fffabc' },
{ tag: t.variableName, color: '#abcdef' },
{ tag: t.function(t.variableName), color: '#fffabc' },
{ tag: t.typeName, color: '#FFDD44' },
{ tag: t.tagName, color: '#def' },
{ tag: t.string, color: '#2b4' },
{ tag: t.meta, color: '#C9F' },
// { tag: t.qualifier, color: '#FFF700' },
// { tag: t.builtin, color: '#30aabc' },
{ tag: t.bracket, color: '#8a8a8a' },
{ tag: t.attributeName, color: '#DDFF00' },
{ tag: t.heading, color: 'aquamarine', fontWeight: 'bold' },
{ tag: t.link, color: 'blueviolet', fontWeight: 'bold' },
],
});

View file

@ -54,6 +54,7 @@ export default createTheme({
tag: [t.keyword, t.tagName, t.arithmeticOperator],
color: palette[1],
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: palette[0] },
{ tag: [t.function(t.variableName), t.propertyName], color: palette[0] },
{ tag: t.atom, color: palette[1] },
],

View file

@ -38,12 +38,14 @@ export default createTheme({
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
color: 'hsl(207, 82%, 66%)',
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: 'hsl( 29, 54%, 61%)' },
{ tag: [t.tagName, t.heading], color: '#e06c75' },
{ tag: t.comment, color: '#54636D' },
{ tag: [t.variableName, t.propertyName, t.labelName], color: 'hsl(220, 14%, 71%)' },
{ tag: [t.attributeName, t.number], color: 'hsl( 29, 54%, 61%)' },
{ tag: t.className, color: 'hsl( 39, 67%, 69%)' },
{ tag: t.keyword, color: 'hsl(286, 60%, 67%)' },
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: '#98c379' },
],
});

View file

@ -1,39 +0,0 @@
// this is different from https://thememirror.net/bespin
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#28211c',
lineBackground: '#28211c99',
foreground: '#9d9b97',
caret: '#797977',
selection: '#36312e',
selectionMatch: '#4f382b',
gutterBackground: '#28211c',
gutterForeground: '#666666',
lineHighlight: 'rgba(255, 255, 255, 0.1)',
};
export default createTheme({
theme: 'dark',
settings: {
background: '#28211c',
foreground: '#9d9b97',
caret: '#797977',
selection: '#4f382b',
selectionMatch: '#4f382b',
gutterBackground: '#28211c',
gutterForeground: '#666666',
lineHighlight: '#ffffff1a',
},
styles: [
{ tag: [t.atom, t.number, t.link, t.bool], color: '#9b859d' },
{ tag: t.comment, color: '#937121' },
{ tag: [t.keyword, t.tagName], color: '#cf6a4c' },
{ tag: t.string, color: '#f9ee98' },
{ tag: t.bracket, color: '#9d9b97' },
{ tag: [t.variableName], color: '#5ea6ea' },
{ tag: t.definition(t.variableName), color: '#cf7d34' },
{ tag: [t.function(t.variableName), t.className], color: '#cf7d34' },
{ tag: [t.propertyName, t.attributeName], color: '#54be0d' },
],
});

View file

@ -0,0 +1,39 @@
/**
* @name Atom One
* Atom One dark syntax theme
*
* https://github.com/atom/one-dark-syntax
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
const hex = ['#000000', '#8ed675', '#56bd2a', '#54636D', '#171717'];
export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[2],
selection: hex[4],
selectionMatch: hex[0],
gutterBackground: hex[0],
gutterForeground: hex[3],
gutterBorder: 'transparent',
lineHighlight: hex[0],
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
color: hex[2],
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] },
{ tag: t.comment, color: hex[3] },
{ tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] },
{ tag: [t.attributeName, t.number], color: hex[1] },
{ tag: t.keyword, color: hex[2] },
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] },
],
});

39
packages/codemirror/themes/red-text.mjs vendored Normal file
View file

@ -0,0 +1,39 @@
/**
* @name Atom One
* Atom One dark syntax theme
*
* https://github.com/atom/one-dark-syntax
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
const hex = ['#000000', '#ff5356', '#bd312a', '#54636D', '#171717'];
export const settings = {
background: hex[0],
lineBackground: 'transparent',
foreground: hex[2],
selection: hex[4],
selectionMatch: hex[0],
gutterBackground: hex[0],
gutterForeground: hex[3],
gutterBorder: 'transparent',
lineHighlight: hex[0],
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
color: hex[2],
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] },
{ tag: t.comment, color: hex[3] },
{ tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] },
{ tag: [t.attributeName, t.number], color: hex[1] },
{ tag: t.keyword, color: hex[2] },
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] },
],
});

View file

@ -0,0 +1,39 @@
/**
* @name Atom One
* Atom One dark syntax theme
*
* https://github.com/atom/one-dark-syntax
*/
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
const hex = ['#1e1e1e', '#fbde2d', '#ff1493', '#4c83ff', '#ededed', '#cccccc', '#ffffff30', '#dc2f8c'];
export const settings = {
background: '#000000',
lineBackground: 'transparent',
foreground: hex[4],
selection: hex[6],
gutterBackground: hex[0],
gutterForeground: hex[5],
gutterBorder: 'transparent',
lineHighlight: hex[0],
};
export default createTheme({
theme: 'dark',
settings,
styles: [
{
tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
color: hex[4],
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[3] },
{ tag: t.comment, color: '#54636D' },
{ tag: [t.variableName, t.propertyName, t.labelName], color: hex[4] },
{ tag: [t.attributeName, t.number], color: hex[3] },
{ tag: t.keyword, color: hex[1] },
{ tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[2] },
],
});

View file

@ -5,14 +5,11 @@ export const settings = {
background: '#222',
lineBackground: '#22222299',
foreground: '#fff',
// foreground: '#75baff',
caret: '#ffcc00',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
// lineHighlight: '#8a91991a', // original
lineHighlight: '#00000050',
gutterBackground: 'transparent',
// gutterForeground: '#8a919966',
gutterForeground: '#8a919966',
};
@ -20,6 +17,7 @@ export default createTheme({
theme: 'dark',
settings,
styles: [
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: '#89ddff' },
{ tag: t.labelName, color: '#89ddff' },
{ tag: t.keyword, color: '#c792ea' },
{ tag: t.operator, color: '#89ddff' },

View file

@ -1,34 +0,0 @@
import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs';
export const settings = {
background: '#292A30',
lineBackground: '#292A3099',
foreground: '#CECFD0',
caret: '#fff',
selection: '#727377',
selectionMatch: '#727377',
lineHighlight: '#2F3239',
};
export default createTheme({
theme: 'dark',
settings: {
background: '#292A30',
foreground: '#CECFD0',
caret: '#fff',
selection: '#727377',
selectionMatch: '#727377',
lineHighlight: '#ffffff0f',
},
styles: [
{ tag: [t.comment, t.quote], color: '#7F8C98' },
{ tag: [t.keyword], color: '#FF7AB2', fontWeight: 'bold' },
{ tag: [t.string, t.meta], color: '#FF8170' },
{ tag: [t.typeName], color: '#DABAFF' },
{ tag: [t.definition(t.variableName)], color: '#6BDFFF' },
{ tag: [t.name], color: '#6BAA9F' },
{ tag: [t.variableName], color: '#ACF2E4' },
{ tag: [t.regexp, t.link], color: '#FF8170' },
],
});

View file

@ -1780,3 +1780,32 @@ export const as = register('as', (mapping, pat) => {
return v;
});
});
/**
* Allows you to scrub an audio file like a tape loop by passing values that represents the position in the audio file
* in the optional array syntax ex: "0.5:2", the second value controls the speed of playback
* @name scrub
* @memberof Pattern
* @returns Pattern
* @example
* samples('github:switchangel/pad')
* s("swpad:0").scrub("{0.1!2 .25@3 0.7!2 <0.8:1.5>}%8")
* @example
* samples('github:yaxu/clean-breaks/main');
* s("amen/4").fit().scrub("{0@3 0@2 4@3}%8".div(16))
*/
export const scrub = register(
'scrub',
(beginPat, pat) => {
return beginPat.outerBind((v) => {
if (!Array.isArray(v)) {
v = [v];
}
const [beginVal, speedMultiplier = 1] = v;
return pat.begin(beginVal).mul(speed(speedMultiplier)).clip(1);
});
},
false,
);

View file

@ -2105,11 +2105,13 @@ export const linger = register(
/**
* Samples the pattern at a rate of n events per cycle. Useful for turning a continuous pattern into a discrete one.
* @name segment
* @synonyms seg
* @param {number} segments number of segments per cycle
* @example
* note(saw.range(40,52).segment(24))
*/
export const segment = register('segment', function (rate, pat) {
export const { segment, seg } = register(['segment', 'seg'], function (rate, pat) {
return pat.struct(pure(true)._fast(rate)).setSteps(rate);
});
@ -2485,16 +2487,24 @@ export const bypass = register(
);
/**
* Loops the pattern inside at `offset` for `cycles`.
* Loops the pattern inside an `offset` for `cycles`.
* If you think of the entire span of time in cycles as a ribbon, you can cut a single piece and loop it.
* @name ribbon
* @synonym rib
* @param {number} offset start point of loop in cycles
* @param {number} cycles loop length in cycles
* @example
* note("<c d e f>").ribbon(1, 2).fast(2)
* note("<c d e f>").ribbon(1, 2)
* @example
* // Looping a portion of randomness
* note(irand(8).segment(4).scale('C3 minor')).ribbon(1337, 2)
* n(irand(8).segment(4)).scale("c:pentatonic").ribbon(1337, 2)
* @example
* // rhythm generator
* s("bd!16?").ribbon(29,.5)
*/
export const ribbon = register('ribbon', (offset, cycles, pat) => pat.early(offset).restart(pure(1).slow(cycles)));
export const { ribbon, rib } = register(['ribbon', 'rib'], (offset, cycles, pat) =>
pat.early(offset).restart(pure(1).slow(cycles)),
);
export const hsla = register('hsla', (h, s, l, a, pat) => {
return pat.color(`hsla(${h}turn,${s * 100}%,${l * 100}%,${a})`);

View file

@ -526,6 +526,9 @@ export const degradeByWith = register(
* s("hh*8").degradeBy(0.2)
* @example
* s("[hh?0.2]*8")
* @example
* //beat generator
* s("bd").segment(16).degradeBy(.5).ribbon(16,1)
*/
export const degradeBy = register(
'degradeBy',

View file

@ -37,7 +37,7 @@ function connect() {
export function parseControlsFromHap(hap, cps) {
hap.ensureObjectValue();
const cycle = hap.wholeOrPart().begin.valueOf();
const delta = hap.duration.valueOf();
const delta = hap.duration.valueOf() / cps;
const controls = Object.assign({}, { cps, cycle, delta }, hap.value);
// make sure n and note are numbers
controls.n && (controls.n = parseNumeral(controls.n));

View file

@ -212,6 +212,7 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
constantNode.onended = () => {
onComplete();
};
return constantNode;
}
const mod = (freq, range = 1, type = 'sine') => {
const ctx = getAudioContext();

View file

@ -344,7 +344,9 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
};
let envEnd = holdEnd + release + 0.01;
bufferSource.stop(envEnd);
const stop = (endTime, playWholeBuffer) => {};
const stop = (endTime) => {
bufferSource.stop(endTime);
};
const handle = { node: out, bufferSource, stop };
// cut groups

View file

@ -14,13 +14,21 @@ import { map } from 'nanostores';
import { logger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs';
export const DEFAULT_MAX_POLYPHONY = 128;
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
let maxPolyphony = DEFAULT_MAX_POLYPHONY;
export function setMaxPolyphony(polyphony) {
maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY;
}
export const soundMap = map();
export function registerSound(key, onTrigger, data = {}) {
soundMap.setKey(key.toLowerCase(), { onTrigger, data });
key = key.toLowerCase().replace(/\s+/g, '_');
soundMap.setKey(key, { onTrigger, data });
}
let gainCurveFunc = (val) => Math.pow(val, 2);
let gainCurveFunc = (val) => Math.pow(val, 1);
export function applyGainCurve(val) {
return gainCurveFunc(val);
@ -95,9 +103,21 @@ export function getSound(s) {
return soundMap.get()[s.toLowerCase()];
}
export const getAudioDevices = async () => {
await navigator.mediaDevices.getUserMedia({ audio: true });
let mediaDevices = await navigator.mediaDevices.enumerateDevices();
mediaDevices = mediaDevices.filter((device) => device.kind === 'audiooutput' && device.deviceId !== 'default');
const devicesMap = new Map();
devicesMap.set(DEFAULT_AUDIO_DEVICE_NAME, '');
mediaDevices.forEach((device) => {
devicesMap.set(device.label, device.deviceId);
});
return devicesMap;
};
const defaultDefaultValues = {
s: 'triangle',
gain: 1,
gain: .8,
postgain: 1,
density: '.03',
ftype: '12db',
@ -165,18 +185,40 @@ export function getAudioContextCurrentTime() {
let workletsLoading;
function loadWorklets() {
if (!workletsLoading) {
workletsLoading = getAudioContext().audioWorklet.addModule(workletsUrl);
const audioCtx = getAudioContext();
workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl);
}
return workletsLoading;
}
// this function should be called on first user interaction (to avoid console warning)
export async function initAudio(options = {}) {
const { disableWorklets = false } = options;
const { disableWorklets = false, maxPolyphony, audioDeviceName = DEFAULT_AUDIO_DEVICE_NAME } = options;
setMaxPolyphony(maxPolyphony);
if (typeof window === 'undefined') {
return;
}
await getAudioContext().resume();
const audioCtx = getAudioContext();
if (audioDeviceName != null && audioDeviceName != DEFAULT_AUDIO_DEVICE_NAME) {
try {
const devices = await getAudioDevices();
const id = devices.get(audioDeviceName);
const isValidID = (id ?? '').length > 0;
if (audioCtx.sinkId !== id && isValidID) {
await audioCtx.setSinkId(id);
}
logger(
`[superdough] Audio Device set to ${audioDeviceName}, it might take a few seconds before audio plays on all output channels`,
);
} catch {
logger('[superdough] failed to set audio interface', 'warning');
}
}
await audioCtx.resume();
if (disableWorklets) {
logger('[superdough]: AudioWorklets disabled with disableWorklets');
return;
@ -384,6 +426,8 @@ export function resetGlobalEffects() {
analysersData = {};
}
let activeSoundSources = new Map();
export const superdough = async (value, t, hapDuration) => {
const ac = getAudioContext();
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
@ -487,14 +531,26 @@ export const superdough = async (value, t, hapDuration) => {
distortvol = applyGainCurve(distortvol);
delay = applyGainCurve(delay);
velocity = applyGainCurve(velocity);
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
const chainID = Math.round(Math.random() * 1000000);
// oldest audio nodes will be destroyed if maximum polyphony is exceeded
for (let i = 0; i <= activeSoundSources.size - maxPolyphony; i++) {
const ch = activeSoundSources.entries().next();
const source = ch.value[1];
const chainID = ch.value[0];
const endTime = t + 0.25;
source?.node?.gain?.linearRampToValueAtTime(0, endTime);
source?.stop?.(endTime);
activeSoundSources.delete(chainID);
}
//music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior
channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
let audioNodes = [];
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
const onended = () => {
toDisconnect.forEach((n) => n?.disconnect());
};
if (bank && s) {
s = `${bank}_${s}`;
value.s = s;
@ -506,10 +562,15 @@ export const superdough = async (value, t, hapDuration) => {
sourceNode = source(t, value, hapDuration);
} else if (getSound(s)) {
const { onTrigger } = getSound(s);
const soundHandle = await onTrigger(t, value, onended);
const onEnded = () => {
audioNodes.forEach((n) => n?.disconnect());
activeSoundSources.delete(chainID);
};
const soundHandle = await onTrigger(t, value, onEnded);
if (soundHandle) {
sourceNode = soundHandle.node;
soundHandle.stop(t + hapDuration);
activeSoundSources.set(chainID, soundHandle);
}
} else {
throw new Error(`sound ${s} not found! Is it loaded?`);
@ -640,6 +701,7 @@ export const superdough = async (value, t, hapDuration) => {
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
const delyNode = getDelay(orbit, delaytime, delayfeedback, t);
delaySend = effectSend(post, delyNode, delay);
audioNodes.push(delaySend);
}
// reverb
let reverbSend;
@ -657,6 +719,7 @@ export const superdough = async (value, t, hapDuration) => {
}
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR);
reverbSend = effectSend(post, reverbNode, room);
audioNodes.push(reverbSend);
}
// analyser
@ -664,14 +727,12 @@ export const superdough = async (value, t, hapDuration) => {
if (analyze) {
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
analyserSend = effectSend(post, analyserNode, 1);
audioNodes.push(analyserSend);
}
// connect chain elements together
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
// toDisconnect = all the node that should be disconnected in onended callback
// this is crucial for performance
toDisconnect = chain.concat([delaySend, reverbSend, analyserSend]);
audioNodes = audioNodes.concat(chain);
};
export const superdoughTrigger = (t, hap, ct, cps) => {

View file

@ -25,6 +25,10 @@ const getFrequencyFromValue = (value) => {
return Number(freq);
};
function destroyAudioWorkletNode(node) {
node.disconnect();
node.parameters.get('end')?.setValueAtTime(0, 0);
}
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
const noises = ['pink', 'white', 'brown', 'crackle'];
@ -63,7 +67,9 @@ export function registerSynthSounds() {
stop(envEnd);
return {
node,
stop: (releaseTime) => {},
stop: (endTime) => {
stop(endTime);
},
};
},
{ type: 'synth', prebake: true },
@ -110,10 +116,12 @@ export function registerSynthSounds() {
let envGain = gainNode(1);
envGain = o.connect(envGain);
webAudioTimeout(
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
let timeoutNode = webAudioTimeout(
ac,
() => {
o.disconnect();
destroyAudioWorkletNode(o);
envGain.disconnect();
onended();
fm?.stop();
@ -123,11 +131,11 @@ export function registerSynthSounds() {
end,
);
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
return {
node: envGain,
stop: (time) => {},
stop: (time) => {
timeoutNode.stop(time);
},
};
},
{ prebake: true, type: 'synth' },
@ -169,10 +177,12 @@ export function registerSynthSounds() {
let envGain = gainNode(1);
envGain = o.connect(envGain);
webAudioTimeout(
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
let timeoutNode = webAudioTimeout(
ac,
() => {
o.disconnect();
destroyAudioWorkletNode(o);
envGain.disconnect();
onended();
fm?.stop();
@ -182,11 +192,11 @@ export function registerSynthSounds() {
end,
);
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
return {
node: envGain,
stop: (time) => {},
stop: (time) => {
timeoutNode.stop(time);
},
};
},
{ prebake: true, type: 'synth' },
@ -229,7 +239,9 @@ export function registerSynthSounds() {
stop(envEnd);
return {
node,
stop: (releaseTime) => {},
stop: (endTime) => {
stop(endTime);
},
};
},
{ type: 'synth', prebake: true },

View file

@ -710,6 +710,9 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
}
process(inputs, outputs, params) {
if (this.disconnected) {
return false;
}
if (currentTime <= params.begin[0]) {
return true;
}