Compare commits

...

2 Commits

Author SHA1 Message Date
Aria c3018a29a0 Update docstring and add an example 2026-01-11 19:10:02 -06:00
Aria 6bd1c877aa Working version of inputs as sound sources 2026-01-11 19:03:20 -06:00
3 changed files with 110 additions and 6 deletions
+12
View File
@@ -3076,3 +3076,15 @@ export const bmod = (config) => pure({}).bmod(config);
export const { transient } = registerControl(['transient', 'transsustain']);
export const { FXrelease, FXrel, FXr, fxr } = registerControl('FXrelease', 'FXrel', 'FXr', 'fxr');
/**
* Input audio device to be used as a sound source in a pattern
*
* @name input
* @param {string | number | Pattern} input Name or index of the input audio device to use
* @example
* // Please be careful about feedback!
* input("1 2").gain(0.5)
* .stretch(0.5).lfo({ s: 0.5 })
*/
export const { input } = registerControl('input');
+85 -6
View File
@@ -14,9 +14,11 @@ import {
createFilter,
effectSend,
gainNode,
getADSRValues,
getCompressor,
getDistortion,
getLfo,
getParamADSR,
getWorklet,
releaseAudioNode,
webAudioTimeout,
@@ -176,6 +178,19 @@ export const getAudioDevices = async () => {
return devicesMap;
};
let AUDIO_INPUTS;
export const getAudioInputs = async () => {
await navigator.mediaDevices.getUserMedia({ audio: true });
let mediaDevices = await navigator.mediaDevices.enumerateDevices();
mediaDevices = mediaDevices.filter((device) => device.kind === 'audioinput' && device.deviceId !== 'default');
const devicesMap = new Map();
devicesMap.set(DEFAULT_AUDIO_DEVICE_NAME, '');
mediaDevices.forEach((device) => {
devicesMap.set(device.label, device.deviceId);
});
return devicesMap;
};
let defaultDefaultValues = {
s: 'triangle',
gain: 0.8,
@@ -254,6 +269,7 @@ export function loadWorklets() {
return workletsLoading;
}
let AUDIO_OUTPUTS;
// this function should be called on first user interaction (to avoid console warning)
export async function initAudio(options = {}) {
const {
@@ -274,8 +290,8 @@ export async function initAudio(options = {}) {
if (audioDeviceName != null && audioDeviceName != DEFAULT_AUDIO_DEVICE_NAME) {
try {
const devices = await getAudioDevices();
const id = devices.get(audioDeviceName);
AUDIO_OUTPUTS = await getAudioDevices();
const id = AUDIO_OUTPUTS.get(audioDeviceName);
const isValidID = (id ?? '').length > 0;
if (audioCtx.sinkId !== id && isValidID) {
await audioCtx.setSinkId(id);
@@ -316,6 +332,51 @@ export async function initAudioOnFirstClick(options) {
return audioReady;
}
const CACHED_INPUTS = {};
export async function getInput(input) {
let sourceNode = CACHED_INPUTS[input];
if (sourceNode === undefined) {
const ac = getAudioContext();
if (!AUDIO_INPUTS) {
AUDIO_INPUTS = await getAudioInputs();
}
const available = Array.from(AUDIO_INPUTS.keys());
// Convert numerical inputs to their corresponding device name
const deviceName = typeof input === 'number' ? available[input] : input;
const inputId = AUDIO_INPUTS.get(deviceName);
if (inputId === undefined) {
throw new Error(`[superdough] input "${input}" not found. Available inputs: ${available.join(', ')}`);
}
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
deviceId: { exact: inputId },
channelCount: { ideal: 2 },
sampleRate: { ideal: ac.sampleRate },
echoCancellation: false,
noiseSuppression: false,
autoGainControl: false,
latency: { ideal: 0.01 },
},
});
sourceNode = ac.createMediaStreamSource(stream);
CACHED_INPUTS[input] = sourceNode;
}
return sourceNode;
}
async function getInputHandle(input, value, start, end) {
const inputNode = await getInput(input);
const envGain = gainNode(0);
inputNode.connect(envGain);
const [attack, decay, sustain, release] = getADSRValues(
[value.attack, value.decay, value.sustain, value.release],
'linear',
[0.001, 0.05, 0.6, 0.01],
);
getParamADSR(envGain.gain, attack, decay, sustain, release, 0, 1, start, end, 'linear');
return { inputNode, envGain };
}
let controller;
export function getSuperdoughAudioController() {
if (controller == null) {
@@ -408,9 +469,9 @@ function mapChannelNumbers(channels) {
}
class Chain {
constructor(head) {
this.audioNodes = [head];
this.tails = [head];
constructor() {
this.audioNodes = [];
this.tails = [];
}
connect(...nodes) {
nodes.forEach((node) => {
@@ -497,6 +558,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
fft = getDefaultValue('fft'), // fftSize 0 - 10
FX = [],
FXrelease,
input,
} = value;
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
@@ -539,11 +601,28 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
value.s = s;
}
const chain = new Chain(); // connection manager which tracks audio nodes for releasing
// get source AudioNode
let sourceNode;
if (source) {
sourceNode = source(t, value, hapDuration, cps);
nodes.main['source'] = [sourceNode];
} else if (input) {
const { inputNode, envGain } = await getInputHandle(input, value, t, end);
sourceNode = envGain;
webAudioTimeout(
ac,
() => {
chain.releaseNodes();
activeSoundSources.delete(chainID);
// We disconnect inputNode instead of releasing it
// because we want inputs to persist
inputNode.disconnect(envGain);
},
t,
endWithRelease,
);
} else if (getSound(s)) {
const { onTrigger } = getSound(s);
@@ -579,7 +658,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
return;
}
const chain = new Chain(sourceNode); // connection manager which tracks audio nodes for releasing
chain.connect(sourceNode);
FX = [...FX, value]; // run through the FX chain and then run through all FX outside of it as well
for (let [idx, fx] of Object.entries(FX)) {
const key = idx == FX.length - 1 ? 'main' : idx;
+13
View File
@@ -5484,6 +5484,19 @@ exports[`runs examples > example "inhabit" example index 1 1`] = `
]
`;
exports[`runs examples > example "input" example index 0 1`] = `
[
"[ 0/1 → 1/2 | input:1 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
"[ 1/2 → 1/1 | input:2 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
"[ 1/1 → 3/2 | input:1 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
"[ 3/2 → 2/1 | input:2 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
"[ 2/1 → 5/2 | input:1 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
"[ 5/2 → 3/1 | input:2 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
"[ 3/1 → 7/2 | input:1 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
"[ 7/2 → 4/1 | input:2 gain:0.5 stretch:0.5 lfo:{0:{control:stretch sync:0.5} __ids:{}} ]",
]
`;
exports[`runs examples > example "inside" example index 0 1`] = `
[
"[ 0/1 → 1/8 | note:D3 ]",