Merge pull request #820 from daslyfe/multichannel_audio
Multichannel audio
This commit is contained in:
commit
362f0df117
4 changed files with 85 additions and 22 deletions
|
|
@ -381,6 +381,19 @@ const generic_params = [
|
||||||
*/
|
*/
|
||||||
['coarse'],
|
['coarse'],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows you to set the output channels on the interface
|
||||||
|
*
|
||||||
|
* @name channels
|
||||||
|
* @synonyms ch
|
||||||
|
*
|
||||||
|
* @param {number | Pattern} channels pattern the output channels
|
||||||
|
* @example
|
||||||
|
* note("e a d b g").channels("3:4")
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
['channels', 'ch'],
|
||||||
|
|
||||||
['phaserrate', 'phasr'], // superdirt only
|
['phaserrate', 'phasr'], // superdirt only
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -27,28 +27,16 @@ export function getSound(s) {
|
||||||
export const resetLoadedSounds = () => soundMap.set({});
|
export const resetLoadedSounds = () => soundMap.set({});
|
||||||
|
|
||||||
let audioContext;
|
let audioContext;
|
||||||
|
|
||||||
export const getAudioContext = () => {
|
export const getAudioContext = () => {
|
||||||
if (!audioContext) {
|
if (!audioContext) {
|
||||||
audioContext = new AudioContext();
|
audioContext = new AudioContext();
|
||||||
|
const maxChannelCount = audioContext.destination.maxChannelCount;
|
||||||
|
audioContext.destination.channelCount = maxChannelCount;
|
||||||
}
|
}
|
||||||
return audioContext;
|
return audioContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
let destination;
|
|
||||||
const getDestination = () => {
|
|
||||||
const ctx = getAudioContext();
|
|
||||||
if (!destination) {
|
|
||||||
destination = ctx.createGain();
|
|
||||||
destination.connect(ctx.destination);
|
|
||||||
}
|
|
||||||
return destination;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const panic = () => {
|
|
||||||
getDestination().gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01);
|
|
||||||
destination = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
let workletsLoading;
|
let workletsLoading;
|
||||||
|
|
||||||
function loadWorklets() {
|
function loadWorklets() {
|
||||||
|
|
@ -95,6 +83,39 @@ export async function initAudioOnFirstClick(options) {
|
||||||
let delays = {};
|
let delays = {};
|
||||||
const maxfeedback = 0.98;
|
const maxfeedback = 0.98;
|
||||||
|
|
||||||
|
let channelMerger, destinationGain;
|
||||||
|
|
||||||
|
// input: AudioNode, channels: ?Array<int>
|
||||||
|
export const connectToDestination = (input, channels = [0, 1]) => {
|
||||||
|
const ctx = getAudioContext();
|
||||||
|
if (channelMerger == null) {
|
||||||
|
channelMerger = new ChannelMergerNode(ctx, { numberOfInputs: ctx.destination.channelCount });
|
||||||
|
destinationGain = new GainNode(ctx);
|
||||||
|
channelMerger.connect(destinationGain);
|
||||||
|
destinationGain.connect(ctx.destination);
|
||||||
|
}
|
||||||
|
//This upmix can be removed if correct channel counts are set throughout the app,
|
||||||
|
// and then strudel could theoretically support surround sound audio files
|
||||||
|
const stereoMix = new StereoPannerNode(ctx);
|
||||||
|
input.connect(stereoMix);
|
||||||
|
|
||||||
|
const splitter = new ChannelSplitterNode(ctx, {
|
||||||
|
numberOfOutputs: stereoMix.channelCount,
|
||||||
|
});
|
||||||
|
stereoMix.connect(splitter);
|
||||||
|
channels.forEach((ch, i) => {
|
||||||
|
splitter.connect(channelMerger, i % stereoMix.channelCount, clamp(ch, 0, ctx.destination.channelCount - 1));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const panic = () => {
|
||||||
|
if (destinationGain == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
destinationGain.gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01);
|
||||||
|
destinationGain = null;
|
||||||
|
};
|
||||||
|
|
||||||
function getDelay(orbit, delaytime, delayfeedback, t) {
|
function getDelay(orbit, delaytime, delayfeedback, t) {
|
||||||
if (delayfeedback > maxfeedback) {
|
if (delayfeedback > maxfeedback) {
|
||||||
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
|
//logger(`delayfeedback was clamped to ${maxfeedback} to save your ears`);
|
||||||
|
|
@ -104,7 +125,7 @@ function getDelay(orbit, delaytime, delayfeedback, t) {
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
|
const dly = ac.createFeedbackDelay(1, delaytime, delayfeedback);
|
||||||
dly.start?.(t); // for some reason, this throws when audion extension is installed..
|
dly.start?.(t); // for some reason, this throws when audion extension is installed..
|
||||||
dly.connect(getDestination());
|
connectToDestination(dly, [0, 1]);
|
||||||
delays[orbit] = dly;
|
delays[orbit] = dly;
|
||||||
}
|
}
|
||||||
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t);
|
delays[orbit].delayTime.value !== delaytime && delays[orbit].delayTime.setValueAtTime(delaytime, t);
|
||||||
|
|
@ -163,7 +184,7 @@ function getReverb(orbit, duration, fade, lp, dim, ir) {
|
||||||
if (!reverbs[orbit]) {
|
if (!reverbs[orbit]) {
|
||||||
const ac = getAudioContext();
|
const ac = getAudioContext();
|
||||||
const reverb = ac.createReverb(duration, fade, lp, dim, ir);
|
const reverb = ac.createReverb(duration, fade, lp, dim, ir);
|
||||||
reverb.connect(getDestination());
|
connectToDestination(reverb, [0, 1]);
|
||||||
reverbs[orbit] = reverb;
|
reverbs[orbit] = reverb;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
|
@ -269,7 +290,7 @@ export const superdough = async (value, deadline, hapDuration) => {
|
||||||
bpsustain = 1,
|
bpsustain = 1,
|
||||||
bprelease = 0.01,
|
bprelease = 0.01,
|
||||||
bandq = 1,
|
bandq = 1,
|
||||||
|
channels = [1, 2],
|
||||||
//phaser
|
//phaser
|
||||||
phaser,
|
phaser,
|
||||||
phaserdepth = 0.75,
|
phaserdepth = 0.75,
|
||||||
|
|
@ -301,6 +322,10 @@ export const superdough = async (value, deadline, hapDuration) => {
|
||||||
compressorAttack,
|
compressorAttack,
|
||||||
compressorRelease,
|
compressorRelease,
|
||||||
} = value;
|
} = value;
|
||||||
|
|
||||||
|
//music programs/audio gear usually increments inputs/outputs from 1, so imitate that behavior
|
||||||
|
channels = (Array.isArray(channels) ? channels : [channels]).map((ch) => ch - 1);
|
||||||
|
|
||||||
gain *= velocity; // legacy fix for velocity
|
gain *= velocity; // legacy fix for velocity
|
||||||
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
|
let toDisconnect = []; // audio nodes that will be disconnected when the source has ended
|
||||||
const onended = () => {
|
const onended = () => {
|
||||||
|
|
@ -434,9 +459,9 @@ export const superdough = async (value, deadline, hapDuration) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// last gain
|
// last gain
|
||||||
const post = gainNode(postgain);
|
const post = new GainNode(ac, { gain: postgain });
|
||||||
chain.push(post);
|
chain.push(post);
|
||||||
post.connect(getDestination());
|
connectToDestination(post, channels);
|
||||||
|
|
||||||
// delay
|
// delay
|
||||||
let delaySend;
|
let delaySend;
|
||||||
|
|
|
||||||
|
|
@ -1038,6 +1038,31 @@ exports[`runs examples > example "ceil" example index 0 1`] = `
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`runs examples > example "channels" example index 0 1`] = `
|
||||||
|
[
|
||||||
|
"[ 0/1 → 1/5 | note:e channels:[3 4] ]",
|
||||||
|
"[ 1/5 → 2/5 | note:a channels:[3 4] ]",
|
||||||
|
"[ 2/5 → 3/5 | note:d channels:[3 4] ]",
|
||||||
|
"[ 3/5 → 4/5 | note:b channels:[3 4] ]",
|
||||||
|
"[ 4/5 → 1/1 | note:g channels:[3 4] ]",
|
||||||
|
"[ 1/1 → 6/5 | note:e channels:[3 4] ]",
|
||||||
|
"[ 6/5 → 7/5 | note:a channels:[3 4] ]",
|
||||||
|
"[ 7/5 → 8/5 | note:d channels:[3 4] ]",
|
||||||
|
"[ 8/5 → 9/5 | note:b channels:[3 4] ]",
|
||||||
|
"[ 9/5 → 2/1 | note:g channels:[3 4] ]",
|
||||||
|
"[ 2/1 → 11/5 | note:e channels:[3 4] ]",
|
||||||
|
"[ 11/5 → 12/5 | note:a channels:[3 4] ]",
|
||||||
|
"[ 12/5 → 13/5 | note:d channels:[3 4] ]",
|
||||||
|
"[ 13/5 → 14/5 | note:b channels:[3 4] ]",
|
||||||
|
"[ 14/5 → 3/1 | note:g channels:[3 4] ]",
|
||||||
|
"[ 3/1 → 16/5 | note:e channels:[3 4] ]",
|
||||||
|
"[ 16/5 → 17/5 | note:a channels:[3 4] ]",
|
||||||
|
"[ 17/5 → 18/5 | note:d channels:[3 4] ]",
|
||||||
|
"[ 18/5 → 19/5 | note:b channels:[3 4] ]",
|
||||||
|
"[ 19/5 → 4/1 | note:g channels:[3 4] ]",
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`runs examples > example "chooseCycles" example index 0 1`] = `
|
exports[`runs examples > example "chooseCycles" example index 0 1`] = `
|
||||||
[
|
[
|
||||||
"[ 0/1 → 1/4 | s:bd ]",
|
"[ 0/1 → 1/4 | s:bd ]",
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import React, { useMemo, useCallback, useLayoutEffect, useRef, useState } from '
|
||||||
import { Reference } from './Reference';
|
import { Reference } from './Reference';
|
||||||
import { themes } from './themes.mjs';
|
import { themes } from './themes.mjs';
|
||||||
import { useSettings, settingsMap, setActiveFooter, defaultSettings } from '../settings.mjs';
|
import { useSettings, settingsMap, setActiveFooter, defaultSettings } from '../settings.mjs';
|
||||||
import { getAudioContext, soundMap } from '@strudel.cycles/webaudio';
|
import { getAudioContext, soundMap, connectToDestination } from '@strudel.cycles/webaudio';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { FilesTab } from './FilesTab';
|
import { FilesTab } from './FilesTab';
|
||||||
|
|
||||||
|
|
@ -271,7 +271,7 @@ function SoundsTab() {
|
||||||
const onended = () => trigRef.current?.node?.disconnect();
|
const onended = () => trigRef.current?.node?.disconnect();
|
||||||
trigRef.current = Promise.resolve(onTrigger(time, params, onended));
|
trigRef.current = Promise.resolve(onTrigger(time, params, onended));
|
||||||
trigRef.current.then((ref) => {
|
trigRef.current.then((ref) => {
|
||||||
ref?.node.connect(ctx.destination);
|
connectToDestination(ref?.node);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue