mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-24 22:55:38 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3018a29a0 | |||
| 6bd1c877aa |
@@ -3076,3 +3076,15 @@ export const bmod = (config) => pure({}).bmod(config);
|
|||||||
export const { transient } = registerControl(['transient', 'transsustain']);
|
export const { transient } = registerControl(['transient', 'transsustain']);
|
||||||
|
|
||||||
export const { FXrelease, FXrel, FXr, fxr } = registerControl('FXrelease', 'FXrel', 'FXr', 'fxr');
|
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');
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ import {
|
|||||||
createFilter,
|
createFilter,
|
||||||
effectSend,
|
effectSend,
|
||||||
gainNode,
|
gainNode,
|
||||||
|
getADSRValues,
|
||||||
getCompressor,
|
getCompressor,
|
||||||
getDistortion,
|
getDistortion,
|
||||||
getLfo,
|
getLfo,
|
||||||
|
getParamADSR,
|
||||||
getWorklet,
|
getWorklet,
|
||||||
releaseAudioNode,
|
releaseAudioNode,
|
||||||
webAudioTimeout,
|
webAudioTimeout,
|
||||||
@@ -176,6 +178,19 @@ export const getAudioDevices = async () => {
|
|||||||
return devicesMap;
|
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 = {
|
let defaultDefaultValues = {
|
||||||
s: 'triangle',
|
s: 'triangle',
|
||||||
gain: 0.8,
|
gain: 0.8,
|
||||||
@@ -254,6 +269,7 @@ export function loadWorklets() {
|
|||||||
return workletsLoading;
|
return workletsLoading;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let AUDIO_OUTPUTS;
|
||||||
// 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 {
|
const {
|
||||||
@@ -274,8 +290,8 @@ export async function initAudio(options = {}) {
|
|||||||
|
|
||||||
if (audioDeviceName != null && audioDeviceName != DEFAULT_AUDIO_DEVICE_NAME) {
|
if (audioDeviceName != null && audioDeviceName != DEFAULT_AUDIO_DEVICE_NAME) {
|
||||||
try {
|
try {
|
||||||
const devices = await getAudioDevices();
|
AUDIO_OUTPUTS = await getAudioDevices();
|
||||||
const id = devices.get(audioDeviceName);
|
const id = AUDIO_OUTPUTS.get(audioDeviceName);
|
||||||
const isValidID = (id ?? '').length > 0;
|
const isValidID = (id ?? '').length > 0;
|
||||||
if (audioCtx.sinkId !== id && isValidID) {
|
if (audioCtx.sinkId !== id && isValidID) {
|
||||||
await audioCtx.setSinkId(id);
|
await audioCtx.setSinkId(id);
|
||||||
@@ -316,6 +332,51 @@ export async function initAudioOnFirstClick(options) {
|
|||||||
return audioReady;
|
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;
|
let controller;
|
||||||
export function getSuperdoughAudioController() {
|
export function getSuperdoughAudioController() {
|
||||||
if (controller == null) {
|
if (controller == null) {
|
||||||
@@ -408,9 +469,9 @@ function mapChannelNumbers(channels) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Chain {
|
class Chain {
|
||||||
constructor(head) {
|
constructor() {
|
||||||
this.audioNodes = [head];
|
this.audioNodes = [];
|
||||||
this.tails = [head];
|
this.tails = [];
|
||||||
}
|
}
|
||||||
connect(...nodes) {
|
connect(...nodes) {
|
||||||
nodes.forEach((node) => {
|
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
|
fft = getDefaultValue('fft'), // fftSize 0 - 10
|
||||||
FX = [],
|
FX = [],
|
||||||
FXrelease,
|
FXrelease,
|
||||||
|
input,
|
||||||
} = value;
|
} = value;
|
||||||
|
|
||||||
delaytime = delaytime ?? cycleToSeconds(delaysync, cps);
|
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;
|
value.s = s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const chain = new Chain(); // connection manager which tracks audio nodes for releasing
|
||||||
|
|
||||||
// get source AudioNode
|
// get source AudioNode
|
||||||
let sourceNode;
|
let sourceNode;
|
||||||
if (source) {
|
if (source) {
|
||||||
sourceNode = source(t, value, hapDuration, cps);
|
sourceNode = source(t, value, hapDuration, cps);
|
||||||
nodes.main['source'] = [sourceNode];
|
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)) {
|
} else if (getSound(s)) {
|
||||||
const { onTrigger } = getSound(s);
|
const { onTrigger } = getSound(s);
|
||||||
|
|
||||||
@@ -579,7 +658,7 @@ export const superdough = async (value, t, hapDuration, cps = 0.5, cycle = 0.5)
|
|||||||
return;
|
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
|
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)) {
|
for (let [idx, fx] of Object.entries(FX)) {
|
||||||
const key = idx == FX.length - 1 ? 'main' : idx;
|
const key = idx == FX.length - 1 ? 'main' : idx;
|
||||||
|
|||||||
@@ -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`] = `
|
exports[`runs examples > example "inside" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/8 | note:D3 ]",
|
"[ 0/1 → 1/8 | note:D3 ]",
|
||||||
|
|||||||
Reference in New Issue
Block a user