Merge pull request #1322 from daslyfe/jade/fixmultichannelaudio

FIX: Multichannel Audio
This commit is contained in:
Jade (Rose) Rowland 2025-04-08 00:08:57 -04:00 committed by GitHub
commit b64daf6284
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 46 additions and 61 deletions

View File

@ -15,6 +15,8 @@ import { logger } from './logger.mjs';
import { loadBuffer } from './sampler.mjs'; import { loadBuffer } from './sampler.mjs';
export const DEFAULT_MAX_POLYPHONY = 128; export const DEFAULT_MAX_POLYPHONY = 128;
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
let maxPolyphony = DEFAULT_MAX_POLYPHONY; let maxPolyphony = DEFAULT_MAX_POLYPHONY;
export function setMaxPolyphony(polyphony) { export function setMaxPolyphony(polyphony) {
maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY; maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY;
@ -91,6 +93,18 @@ export function getSound(s) {
return soundMap.get()[s.toLowerCase()]; 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 = { const defaultDefaultValues = {
s: 'triangle', s: 'triangle',
gain: 0.8, gain: 0.8,
@ -161,19 +175,40 @@ export function getAudioContextCurrentTime() {
let workletsLoading; let workletsLoading;
function loadWorklets() { function loadWorklets() {
if (!workletsLoading) { if (!workletsLoading) {
workletsLoading = getAudioContext().audioWorklet.addModule(workletsUrl); const audioCtx = getAudioContext();
workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl);
} }
return workletsLoading; return workletsLoading;
} }
// 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, maxPolyphony } = options; const { disableWorklets = false, maxPolyphony, audioDeviceName = DEFAULT_AUDIO_DEVICE_NAME } = options;
setMaxPolyphony(maxPolyphony); setMaxPolyphony(maxPolyphony);
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return; 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) { if (disableWorklets) {
logger('[superdough]: AudioWorklets disabled with disableWorklets'); logger('[superdough]: AudioWorklets disabled with disableWorklets');
return; return;

View File

@ -1,6 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { getAudioDevices, setAudioDevice } from '../../util.mjs';
import { SelectInput } from './SelectInput'; import { SelectInput } from './SelectInput';
import { getAudioDevices } from '@strudel/webaudio';
const initdevices = new Map(); const initdevices = new Map();
@ -21,9 +22,7 @@ export function AudioDeviceSelector({ audioDeviceName, onChange, isDisabled }) {
if (!devicesInitialized) { if (!devicesInitialized) {
return; return;
} }
const deviceID = devices.get(deviceName);
onChange(deviceName); onChange(deviceName);
setAudioDevice(deviceID);
}; };
const options = new Map(); const options = new Map();
Array.from(devices.keys()).forEach((deviceName) => { Array.from(devices.keys()).forEach((deviceName) => {

View File

@ -14,7 +14,7 @@ import {
resetLoadedSounds, resetLoadedSounds,
initAudioOnFirstClick, initAudioOnFirstClick,
} from '@strudel/webaudio'; } from '@strudel/webaudio';
import { getAudioDevices, setAudioDevice, setVersionDefaultsFrom } from './util.mjs'; import { setVersionDefaultsFrom } from './util.mjs';
import { StrudelMirror, defaultSettings } from '@strudel/codemirror'; import { StrudelMirror, defaultSettings } from '@strudel/codemirror';
import { clearHydra } from '@strudel/hydra'; import { clearHydra } from '@strudel/hydra';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
@ -28,7 +28,7 @@ import {
setViewingPatternData, setViewingPatternData,
} from '../user_pattern_utils.mjs'; } from '../user_pattern_utils.mjs';
import { superdirtOutput } from '@strudel/osc/superdirtoutput'; import { superdirtOutput } from '@strudel/osc/superdirtoutput';
import { audioEngineTargets, defaultAudioDeviceName } from '../settings.mjs'; import { audioEngineTargets } from '../settings.mjs';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { prebake } from './prebake.mjs'; import { prebake } from './prebake.mjs';
import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs'; import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs';
@ -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, maxPolyphony } = settingsMap.get(); const { latestCode, maxPolyphony, audioDeviceName } = settingsMap.get();
let modulesLoading, presets, drawContext, clearCanvas, audioReady; let modulesLoading, presets, drawContext, clearCanvas, audioReady;
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
audioReady = initAudioOnFirstClick({ maxPolyphony }); audioReady = initAudioOnFirstClick({ maxPolyphony, audioDeviceName });
modulesLoading = loadModules(); modulesLoading = loadModules();
presets = prebake(); presets = prebake();
drawContext = getDrawContext(); drawContext = getDrawContext();
@ -159,20 +159,6 @@ export function useReplContext() {
editorRef.current?.updateSettings(editorSettings); editorRef.current?.updateSettings(editorSettings);
}, [_settings]); }, [_settings]);
// on first load, set stored audio device if possible
useEffect(() => {
const { audioDeviceName } = _settings;
if (audioDeviceName !== defaultAudioDeviceName) {
getAudioDevices().then((devices) => {
const deviceID = devices.get(audioDeviceName);
if (deviceID == null) {
return;
}
setAudioDevice(deviceID);
});
}
}, []);
// //
// UI Actions // UI Actions
// //

View File

@ -1,6 +1,6 @@
import { evalScope, hash2code, logger } from '@strudel/core'; import { evalScope, hash2code, logger } from '@strudel/core';
import { settingPatterns, defaultAudioDeviceName } from '../settings.mjs'; import { settingPatterns } from '../settings.mjs';
import { getAudioContext, initializeAudioOutput, setDefaultAudioContext, setVersionDefaults } from '@strudel/webaudio'; import { setVersionDefaults } from '@strudel/webaudio';
import { getMetadata } from '../metadata_parser'; import { getMetadata } from '../metadata_parser';
import { isTauri } from '../tauri.mjs'; import { isTauri } from '../tauri.mjs';
import './Repl.css'; import './Repl.css';
@ -159,38 +159,6 @@ export const isUdels = () => {
return window.top?.location?.pathname.includes('udels'); return window.top?.location?.pathname.includes('udels');
}; };
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(defaultAudioDeviceName, '');
mediaDevices.forEach((device) => {
devicesMap.set(device.label, device.deviceId);
});
return devicesMap;
};
export const setAudioDevice = async (id) => {
let audioCtx = getAudioContext();
if (audioCtx.sinkId === id) {
return;
}
await audioCtx.suspend();
await audioCtx.close();
audioCtx = setDefaultAudioContext();
await audioCtx.resume();
const isValidID = (id ?? '').length > 0;
if (isValidID) {
try {
await audioCtx.setSinkId(id);
} catch {
logger('failed to set audio interface', 'warning');
}
}
initializeAudioOutput();
};
export function setVersionDefaultsFrom(code) { export function setVersionDefaultsFrom(code) {
try { try {
const metadata = getMetadata(code); const metadata = getMetadata(code);

View File

@ -3,8 +3,6 @@ import { useStore } from '@nanostores/react';
import { register } from '@strudel/core'; import { register } from '@strudel/core';
import { isUdels } from './repl/util.mjs'; import { isUdels } from './repl/util.mjs';
export const defaultAudioDeviceName = 'System Standard';
export const audioEngineTargets = { export const audioEngineTargets = {
webaudio: 'webaudio', webaudio: 'webaudio',
osc: 'osc', osc: 'osc',
@ -36,7 +34,6 @@ export const defaultSettings = {
isPanelOpen: true, isPanelOpen: true,
togglePanelTrigger: 'click', //click | hover togglePanelTrigger: 'click', //click | hover
userPatterns: '{}', userPatterns: '{}',
audioDeviceName: defaultAudioDeviceName,
audioEngineTarget: audioEngineTargets.webaudio, audioEngineTarget: audioEngineTargets.webaudio,
isButtonRowHidden: false, isButtonRowHidden: false,
isCSSAnimationDisabled: false, isCSSAnimationDisabled: false,