mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-27 05:28:41 +00:00
Merge pull request #1317 from daslyfe/jade/voicemax
feat: add max polyphony feature for superdough
This commit is contained in:
commit
b92a28242e
@ -212,6 +212,7 @@ export function webAudioTimeout(audioContext, onComplete, startTime, stopTime) {
|
|||||||
constantNode.onended = () => {
|
constantNode.onended = () => {
|
||||||
onComplete();
|
onComplete();
|
||||||
};
|
};
|
||||||
|
return constantNode;
|
||||||
}
|
}
|
||||||
const mod = (freq, range = 1, type = 'sine') => {
|
const mod = (freq, range = 1, type = 'sine') => {
|
||||||
const ctx = getAudioContext();
|
const ctx = getAudioContext();
|
||||||
|
|||||||
@ -344,7 +344,9 @@ export async function onTriggerSample(t, value, onended, bank, resolveUrl) {
|
|||||||
};
|
};
|
||||||
let envEnd = holdEnd + release + 0.01;
|
let envEnd = holdEnd + release + 0.01;
|
||||||
bufferSource.stop(envEnd);
|
bufferSource.stop(envEnd);
|
||||||
const stop = (endTime, playWholeBuffer) => {};
|
const stop = (endTime) => {
|
||||||
|
bufferSource.stop(endTime);
|
||||||
|
};
|
||||||
const handle = { node: out, bufferSource, stop };
|
const handle = { node: out, bufferSource, stop };
|
||||||
|
|
||||||
// cut groups
|
// cut groups
|
||||||
|
|||||||
@ -14,6 +14,11 @@ import { map } from 'nanostores';
|
|||||||
import { logger } from './logger.mjs';
|
import { logger } from './logger.mjs';
|
||||||
import { loadBuffer } from './sampler.mjs';
|
import { loadBuffer } from './sampler.mjs';
|
||||||
|
|
||||||
|
export const DEFAULT_MAX_POLYPHONY = 128;
|
||||||
|
let maxPolyphony = DEFAULT_MAX_POLYPHONY;
|
||||||
|
export function setMaxPolyphony(polyphony) {
|
||||||
|
maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY;
|
||||||
|
}
|
||||||
export const soundMap = map();
|
export const soundMap = map();
|
||||||
|
|
||||||
export function registerSound(key, onTrigger, data = {}) {
|
export function registerSound(key, onTrigger, data = {}) {
|
||||||
@ -163,7 +168,8 @@ function loadWorklets() {
|
|||||||
|
|
||||||
// this function should be called on first user interaction (to avoid console warning)
|
// this function should be called on first user interaction (to avoid console warning)
|
||||||
export async function initAudio(options = {}) {
|
export async function initAudio(options = {}) {
|
||||||
const { disableWorklets = false } = options;
|
const { disableWorklets = false, maxPolyphony } = options;
|
||||||
|
setMaxPolyphony(maxPolyphony);
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -375,6 +381,8 @@ export function resetGlobalEffects() {
|
|||||||
analysersData = {};
|
analysersData = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let activeSoundSources = new Map();
|
||||||
|
|
||||||
export const superdough = async (value, t, hapDuration) => {
|
export const superdough = async (value, t, hapDuration) => {
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
|
t = typeof t === 'string' && t.startsWith('=') ? Number(t.slice(1)) : ac.currentTime + t;
|
||||||
@ -474,14 +482,24 @@ export const superdough = async (value, t, hapDuration) => {
|
|||||||
|
|
||||||
gain = nanFallback(gain, 1);
|
gain = nanFallback(gain, 1);
|
||||||
|
|
||||||
|
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
|
//music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior
|
||||||
channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
|
channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
|
||||||
|
|
||||||
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
|
gain *= velocity; // velocity currently only multiplies with gain. it might do other things in the future
|
||||||
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
|
let audioNodes = [];
|
||||||
const onended = () => {
|
|
||||||
toDisconnect.forEach((n) => n?.disconnect());
|
|
||||||
};
|
|
||||||
if (bank && s) {
|
if (bank && s) {
|
||||||
s = `${bank}_${s}`;
|
s = `${bank}_${s}`;
|
||||||
value.s = s;
|
value.s = s;
|
||||||
@ -493,10 +511,15 @@ export const superdough = async (value, t, hapDuration) => {
|
|||||||
sourceNode = source(t, value, hapDuration);
|
sourceNode = source(t, value, hapDuration);
|
||||||
} else if (getSound(s)) {
|
} else if (getSound(s)) {
|
||||||
const { onTrigger } = 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) {
|
if (soundHandle) {
|
||||||
sourceNode = soundHandle.node;
|
sourceNode = soundHandle.node;
|
||||||
soundHandle.stop(t + hapDuration);
|
activeSoundSources.set(chainID, soundHandle);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`sound ${s} not found! Is it loaded?`);
|
throw new Error(`sound ${s} not found! Is it loaded?`);
|
||||||
@ -626,6 +649,7 @@ export const superdough = async (value, t, hapDuration) => {
|
|||||||
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
if (delay > 0 && delaytime > 0 && delayfeedback > 0) {
|
||||||
const delyNode = getDelay(orbit, delaytime, delayfeedback, t);
|
const delyNode = getDelay(orbit, delaytime, delayfeedback, t);
|
||||||
delaySend = effectSend(post, delyNode, delay);
|
delaySend = effectSend(post, delyNode, delay);
|
||||||
|
audioNodes.push(delaySend);
|
||||||
}
|
}
|
||||||
// reverb
|
// reverb
|
||||||
let reverbSend;
|
let reverbSend;
|
||||||
@ -643,6 +667,7 @@ export const superdough = async (value, t, hapDuration) => {
|
|||||||
}
|
}
|
||||||
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR);
|
const reverbNode = getReverb(orbit, roomsize, roomfade, roomlp, roomdim, roomIR);
|
||||||
reverbSend = effectSend(post, reverbNode, room);
|
reverbSend = effectSend(post, reverbNode, room);
|
||||||
|
audioNodes.push(reverbSend);
|
||||||
}
|
}
|
||||||
|
|
||||||
// analyser
|
// analyser
|
||||||
@ -650,14 +675,12 @@ export const superdough = async (value, t, hapDuration) => {
|
|||||||
if (analyze) {
|
if (analyze) {
|
||||||
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
|
const analyserNode = getAnalyserById(analyze, 2 ** (fft + 5));
|
||||||
analyserSend = effectSend(post, analyserNode, 1);
|
analyserSend = effectSend(post, analyserNode, 1);
|
||||||
|
audioNodes.push(analyserSend);
|
||||||
}
|
}
|
||||||
|
|
||||||
// connect chain elements together
|
// connect chain elements together
|
||||||
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
chain.slice(1).reduce((last, current) => last.connect(current), chain[0]);
|
||||||
|
audioNodes = audioNodes.concat(chain);
|
||||||
// toDisconnect = all the node that should be disconnected in onended callback
|
|
||||||
// this is crucial for performance
|
|
||||||
toDisconnect = chain.concat([delaySend, reverbSend, analyserSend]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const superdoughTrigger = (t, hap, ct, cps) => {
|
export const superdoughTrigger = (t, hap, ct, cps) => {
|
||||||
|
|||||||
@ -25,6 +25,10 @@ const getFrequencyFromValue = (value) => {
|
|||||||
|
|
||||||
return Number(freq);
|
return Number(freq);
|
||||||
};
|
};
|
||||||
|
function destroyAudioWorkletNode(node) {
|
||||||
|
node.disconnect();
|
||||||
|
node.parameters.get('end')?.setValueAtTime(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
|
const waveforms = ['triangle', 'square', 'sawtooth', 'sine'];
|
||||||
const noises = ['pink', 'white', 'brown', 'crackle'];
|
const noises = ['pink', 'white', 'brown', 'crackle'];
|
||||||
@ -63,7 +67,9 @@ export function registerSynthSounds() {
|
|||||||
stop(envEnd);
|
stop(envEnd);
|
||||||
return {
|
return {
|
||||||
node,
|
node,
|
||||||
stop: (releaseTime) => {},
|
stop: (endTime) => {
|
||||||
|
stop(endTime);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{ type: 'synth', prebake: true },
|
{ type: 'synth', prebake: true },
|
||||||
@ -110,10 +116,12 @@ export function registerSynthSounds() {
|
|||||||
let envGain = gainNode(1);
|
let envGain = gainNode(1);
|
||||||
envGain = o.connect(envGain);
|
envGain = o.connect(envGain);
|
||||||
|
|
||||||
webAudioTimeout(
|
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
|
||||||
|
|
||||||
|
let timeoutNode = webAudioTimeout(
|
||||||
ac,
|
ac,
|
||||||
() => {
|
() => {
|
||||||
o.disconnect();
|
destroyAudioWorkletNode(o);
|
||||||
envGain.disconnect();
|
envGain.disconnect();
|
||||||
onended();
|
onended();
|
||||||
fm?.stop();
|
fm?.stop();
|
||||||
@ -123,11 +131,11 @@ export function registerSynthSounds() {
|
|||||||
end,
|
end,
|
||||||
);
|
);
|
||||||
|
|
||||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 0.3 * gainAdjustment, begin, holdend, 'linear');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node: envGain,
|
node: envGain,
|
||||||
stop: (time) => {},
|
stop: (time) => {
|
||||||
|
timeoutNode.stop(time);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{ prebake: true, type: 'synth' },
|
{ prebake: true, type: 'synth' },
|
||||||
@ -169,10 +177,12 @@ export function registerSynthSounds() {
|
|||||||
let envGain = gainNode(1);
|
let envGain = gainNode(1);
|
||||||
envGain = o.connect(envGain);
|
envGain = o.connect(envGain);
|
||||||
|
|
||||||
webAudioTimeout(
|
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
||||||
|
|
||||||
|
let timeoutNode = webAudioTimeout(
|
||||||
ac,
|
ac,
|
||||||
() => {
|
() => {
|
||||||
o.disconnect();
|
destroyAudioWorkletNode(o);
|
||||||
envGain.disconnect();
|
envGain.disconnect();
|
||||||
onended();
|
onended();
|
||||||
fm?.stop();
|
fm?.stop();
|
||||||
@ -182,11 +192,11 @@ export function registerSynthSounds() {
|
|||||||
end,
|
end,
|
||||||
);
|
);
|
||||||
|
|
||||||
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, begin, holdend, 'linear');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
node: envGain,
|
node: envGain,
|
||||||
stop: (time) => {},
|
stop: (time) => {
|
||||||
|
timeoutNode.stop(time);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{ prebake: true, type: 'synth' },
|
{ prebake: true, type: 'synth' },
|
||||||
@ -229,7 +239,9 @@ export function registerSynthSounds() {
|
|||||||
stop(envEnd);
|
stop(envEnd);
|
||||||
return {
|
return {
|
||||||
node,
|
node,
|
||||||
stop: (releaseTime) => {},
|
stop: (endTime) => {
|
||||||
|
stop(endTime);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{ type: 'synth', prebake: true },
|
{ type: 'synth', prebake: true },
|
||||||
|
|||||||
@ -710,6 +710,9 @@ class PulseOscillatorProcessor extends AudioWorkletProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
process(inputs, outputs, params) {
|
process(inputs, outputs, params) {
|
||||||
|
if (this.disconnected) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (currentTime <= params.begin[0]) {
|
if (currentTime <= params.begin[0]) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs';
|
import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs';
|
||||||
import { themes } from '@strudel/codemirror';
|
import { themes } from '@strudel/codemirror';
|
||||||
|
import { Textbox } from '../textbox/Textbox.jsx';
|
||||||
import { isUdels } from '../../util.mjs';
|
import { isUdels } from '../../util.mjs';
|
||||||
import { ButtonGroup } from './Forms.jsx';
|
import { ButtonGroup } from './Forms.jsx';
|
||||||
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
|
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
|
||||||
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
|
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
|
||||||
import { confirmDialog } from '../../util.mjs';
|
import { confirmDialog } from '../../util.mjs';
|
||||||
|
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony } from '@strudel/webaudio';
|
||||||
|
|
||||||
function Checkbox({ label, value, onChange, disabled = false }) {
|
function Checkbox({ label, value, onChange, disabled = false }) {
|
||||||
return (
|
return (
|
||||||
@ -53,7 +55,7 @@ function NumberSlider({ value, onChange, step = 1, ...rest }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormItem({ label, children }) {
|
function FormItem({ label, children, sublabel }) {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<label>{label}</label>
|
<label>{label}</label>
|
||||||
@ -105,6 +107,7 @@ export function SettingsTab({ started }) {
|
|||||||
audioDeviceName,
|
audioDeviceName,
|
||||||
audioEngineTarget,
|
audioEngineTarget,
|
||||||
togglePanelTrigger,
|
togglePanelTrigger,
|
||||||
|
maxPolyphony,
|
||||||
} = useSettings();
|
} = useSettings();
|
||||||
const shouldAlwaysSync = isUdels();
|
const shouldAlwaysSync = isUdels();
|
||||||
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
||||||
@ -139,6 +142,26 @@ export function SettingsTab({ started }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
|
<FormItem label="Maximum Polyphony">
|
||||||
|
<Textbox
|
||||||
|
min={1}
|
||||||
|
max={Infinity}
|
||||||
|
onBlur={(e) => {
|
||||||
|
let v = parseInt(e.target.value);
|
||||||
|
v = isNaN(v) ? DEFAULT_MAX_POLYPHONY : v;
|
||||||
|
setMaxPolyphony(v);
|
||||||
|
settingsMap.setKey('maxPolyphony', v);
|
||||||
|
}}
|
||||||
|
onChange={(v) => {
|
||||||
|
v = Math.max(1, parseInt(v));
|
||||||
|
settingsMap.setKey('maxPolyphony', isNaN(v) ? undefined : v);
|
||||||
|
}}
|
||||||
|
type="number"
|
||||||
|
placeholder=""
|
||||||
|
value={maxPolyphony ?? ''}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
<FormItem label="Theme">
|
<FormItem label="Theme">
|
||||||
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
|
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@ -36,11 +36,11 @@ import './Repl.css';
|
|||||||
import { setInterval, clearInterval } from 'worker-timers';
|
import { setInterval, clearInterval } from 'worker-timers';
|
||||||
import { getMetadata } from '../metadata_parser';
|
import { getMetadata } from '../metadata_parser';
|
||||||
|
|
||||||
const { latestCode } = settingsMap.get();
|
const { latestCode, maxPolyphony } = settingsMap.get();
|
||||||
let modulesLoading, presets, drawContext, clearCanvas, audioReady;
|
let modulesLoading, presets, drawContext, clearCanvas, audioReady;
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
audioReady = initAudioOnFirstClick();
|
audioReady = initAudioOnFirstClick({ maxPolyphony });
|
||||||
modulesLoading = loadModules();
|
modulesLoading = loadModules();
|
||||||
presets = prebake();
|
presets = prebake();
|
||||||
drawContext = getDrawContext();
|
drawContext = getDrawContext();
|
||||||
|
|||||||
@ -40,6 +40,7 @@ export const defaultSettings = {
|
|||||||
audioEngineTarget: audioEngineTargets.webaudio,
|
audioEngineTarget: audioEngineTargets.webaudio,
|
||||||
isButtonRowHidden: false,
|
isButtonRowHidden: false,
|
||||||
isCSSAnimationDisabled: false,
|
isCSSAnimationDisabled: false,
|
||||||
|
maxPolyphony: 128,
|
||||||
};
|
};
|
||||||
|
|
||||||
let search = null;
|
let search = null;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user