Skip to content
This repository was archived by the owner on Jun 19, 2025. It is now read-only.

Commit b64daf6

Browse files
authored
Merge pull request #1322 from daslyfe/jade/fixmultichannelaudio
FIX: Multichannel Audio
2 parents b92a282 + 2b69004 commit b64daf6

File tree

5 files changed

+46
-61
lines changed

5 files changed

+46
-61
lines changed

packages/superdough/superdough.mjs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { logger } from './logger.mjs';
1515
import { loadBuffer } from './sampler.mjs';
1616

1717
export const DEFAULT_MAX_POLYPHONY = 128;
18+
const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard';
19+
1820
let maxPolyphony = DEFAULT_MAX_POLYPHONY;
1921
export function setMaxPolyphony(polyphony) {
2022
maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY;
@@ -91,6 +93,18 @@ export function getSound(s) {
9193
return soundMap.get()[s.toLowerCase()];
9294
}
9395

96+
export const getAudioDevices = async () => {
97+
await navigator.mediaDevices.getUserMedia({ audio: true });
98+
let mediaDevices = await navigator.mediaDevices.enumerateDevices();
99+
mediaDevices = mediaDevices.filter((device) => device.kind === 'audiooutput' && device.deviceId !== 'default');
100+
const devicesMap = new Map();
101+
devicesMap.set(DEFAULT_AUDIO_DEVICE_NAME, '');
102+
mediaDevices.forEach((device) => {
103+
devicesMap.set(device.label, device.deviceId);
104+
});
105+
return devicesMap;
106+
};
107+
94108
const defaultDefaultValues = {
95109
s: 'triangle',
96110
gain: 0.8,
@@ -161,19 +175,40 @@ export function getAudioContextCurrentTime() {
161175
let workletsLoading;
162176
function loadWorklets() {
163177
if (!workletsLoading) {
164-
workletsLoading = getAudioContext().audioWorklet.addModule(workletsUrl);
178+
const audioCtx = getAudioContext();
179+
workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl);
165180
}
181+
166182
return workletsLoading;
167183
}
168184

169185
// this function should be called on first user interaction (to avoid console warning)
170186
export async function initAudio(options = {}) {
171-
const { disableWorklets = false, maxPolyphony } = options;
187+
const { disableWorklets = false, maxPolyphony, audioDeviceName = DEFAULT_AUDIO_DEVICE_NAME } = options;
172188
setMaxPolyphony(maxPolyphony);
173189
if (typeof window === 'undefined') {
174190
return;
175191
}
176-
await getAudioContext().resume();
192+
193+
const audioCtx = getAudioContext();
194+
195+
if (audioDeviceName != null && audioDeviceName != DEFAULT_AUDIO_DEVICE_NAME) {
196+
try {
197+
const devices = await getAudioDevices();
198+
const id = devices.get(audioDeviceName);
199+
const isValidID = (id ?? '').length > 0;
200+
if (audioCtx.sinkId !== id && isValidID) {
201+
await audioCtx.setSinkId(id);
202+
}
203+
logger(
204+
`[superdough] Audio Device set to ${audioDeviceName}, it might take a few seconds before audio plays on all output channels`,
205+
);
206+
} catch {
207+
logger('[superdough] failed to set audio interface', 'warning');
208+
}
209+
}
210+
211+
await audioCtx.resume();
177212
if (disableWorklets) {
178213
logger('[superdough]: AudioWorklets disabled with disableWorklets');
179214
return;

website/src/repl/components/panel/AudioDeviceSelector.jsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useState } from 'react';
2-
import { getAudioDevices, setAudioDevice } from '../../util.mjs';
2+
33
import { SelectInput } from './SelectInput';
4+
import { getAudioDevices } from '@strudel/webaudio';
45

56
const initdevices = new Map();
67

@@ -21,9 +22,7 @@ export function AudioDeviceSelector({ audioDeviceName, onChange, isDisabled }) {
2122
if (!devicesInitialized) {
2223
return;
2324
}
24-
const deviceID = devices.get(deviceName);
2525
onChange(deviceName);
26-
setAudioDevice(deviceID);
2726
};
2827
const options = new Map();
2928
Array.from(devices.keys()).forEach((deviceName) => {

website/src/repl/useReplContext.jsx

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
resetLoadedSounds,
1515
initAudioOnFirstClick,
1616
} from '@strudel/webaudio';
17-
import { getAudioDevices, setAudioDevice, setVersionDefaultsFrom } from './util.mjs';
17+
import { setVersionDefaultsFrom } from './util.mjs';
1818
import { StrudelMirror, defaultSettings } from '@strudel/codemirror';
1919
import { clearHydra } from '@strudel/hydra';
2020
import { useCallback, useEffect, useRef, useState } from 'react';
@@ -28,19 +28,19 @@ import {
2828
setViewingPatternData,
2929
} from '../user_pattern_utils.mjs';
3030
import { superdirtOutput } from '@strudel/osc/superdirtoutput';
31-
import { audioEngineTargets, defaultAudioDeviceName } from '../settings.mjs';
31+
import { audioEngineTargets } from '../settings.mjs';
3232
import { useStore } from '@nanostores/react';
3333
import { prebake } from './prebake.mjs';
3434
import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs';
3535
import './Repl.css';
3636
import { setInterval, clearInterval } from 'worker-timers';
3737
import { getMetadata } from '../metadata_parser';
3838

39-
const { latestCode, maxPolyphony } = settingsMap.get();
39+
const { latestCode, maxPolyphony, audioDeviceName } = settingsMap.get();
4040
let modulesLoading, presets, drawContext, clearCanvas, audioReady;
4141

4242
if (typeof window !== 'undefined') {
43-
audioReady = initAudioOnFirstClick({ maxPolyphony });
43+
audioReady = initAudioOnFirstClick({ maxPolyphony, audioDeviceName });
4444
modulesLoading = loadModules();
4545
presets = prebake();
4646
drawContext = getDrawContext();
@@ -159,20 +159,6 @@ export function useReplContext() {
159159
editorRef.current?.updateSettings(editorSettings);
160160
}, [_settings]);
161161

162-
// on first load, set stored audio device if possible
163-
useEffect(() => {
164-
const { audioDeviceName } = _settings;
165-
if (audioDeviceName !== defaultAudioDeviceName) {
166-
getAudioDevices().then((devices) => {
167-
const deviceID = devices.get(audioDeviceName);
168-
if (deviceID == null) {
169-
return;
170-
}
171-
setAudioDevice(deviceID);
172-
});
173-
}
174-
}, []);
175-
176162
//
177163
// UI Actions
178164
//

website/src/repl/util.mjs

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { evalScope, hash2code, logger } from '@strudel/core';
2-
import { settingPatterns, defaultAudioDeviceName } from '../settings.mjs';
3-
import { getAudioContext, initializeAudioOutput, setDefaultAudioContext, setVersionDefaults } from '@strudel/webaudio';
2+
import { settingPatterns } from '../settings.mjs';
3+
import { setVersionDefaults } from '@strudel/webaudio';
44
import { getMetadata } from '../metadata_parser';
55
import { isTauri } from '../tauri.mjs';
66
import './Repl.css';
@@ -159,38 +159,6 @@ export const isUdels = () => {
159159
return window.top?.location?.pathname.includes('udels');
160160
};
161161

162-
export const getAudioDevices = async () => {
163-
await navigator.mediaDevices.getUserMedia({ audio: true });
164-
let mediaDevices = await navigator.mediaDevices.enumerateDevices();
165-
mediaDevices = mediaDevices.filter((device) => device.kind === 'audiooutput' && device.deviceId !== 'default');
166-
const devicesMap = new Map();
167-
devicesMap.set(defaultAudioDeviceName, '');
168-
mediaDevices.forEach((device) => {
169-
devicesMap.set(device.label, device.deviceId);
170-
});
171-
return devicesMap;
172-
};
173-
174-
export const setAudioDevice = async (id) => {
175-
let audioCtx = getAudioContext();
176-
if (audioCtx.sinkId === id) {
177-
return;
178-
}
179-
await audioCtx.suspend();
180-
await audioCtx.close();
181-
audioCtx = setDefaultAudioContext();
182-
await audioCtx.resume();
183-
const isValidID = (id ?? '').length > 0;
184-
if (isValidID) {
185-
try {
186-
await audioCtx.setSinkId(id);
187-
} catch {
188-
logger('failed to set audio interface', 'warning');
189-
}
190-
}
191-
initializeAudioOutput();
192-
};
193-
194162
export function setVersionDefaultsFrom(code) {
195163
try {
196164
const metadata = getMetadata(code);

website/src/settings.mjs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import { useStore } from '@nanostores/react';
33
import { register } from '@strudel/core';
44
import { isUdels } from './repl/util.mjs';
55

6-
export const defaultAudioDeviceName = 'System Standard';
7-
86
export const audioEngineTargets = {
97
webaudio: 'webaudio',
108
osc: 'osc',
@@ -36,7 +34,6 @@ export const defaultSettings = {
3634
isPanelOpen: true,
3735
togglePanelTrigger: 'click', //click | hover
3836
userPatterns: '{}',
39-
audioDeviceName: defaultAudioDeviceName,
4037
audioEngineTarget: audioEngineTargets.webaudio,
4138
isButtonRowHidden: false,
4239
isCSSAnimationDisabled: false,

0 commit comments

Comments
 (0)