From 344d9ca1a8692f4bef1fe63ba201edab9434c3e5 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 7 Apr 2025 23:43:29 -0400 Subject: [PATCH 1/5] working --- packages/superdough/superdough.mjs | 61 ++++++++++++++++++- .../components/panel/AudioDeviceSelector.jsx | 5 +- website/src/repl/useReplContext.jsx | 22 ++----- website/src/repl/util.mjs | 35 +---------- website/src/settings.mjs | 5 +- 5 files changed, 68 insertions(+), 60 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 8c471eb3..bb152dd9 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -15,6 +15,8 @@ import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; export const DEFAULT_MAX_POLYPHONY = 128; +export const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; + let maxPolyphony = DEFAULT_MAX_POLYPHONY; export function setMaxPolyphony(polyphony) { maxPolyphony = parseInt(polyphony) ?? DEFAULT_MAX_POLYPHONY; @@ -91,6 +93,38 @@ export function getSound(s) { 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; +}; + +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(); +}; + const defaultDefaultValues = { s: 'triangle', gain: 0.8, @@ -161,19 +195,40 @@ export function getAudioContextCurrentTime() { let workletsLoading; function loadWorklets() { if (!workletsLoading) { - workletsLoading = getAudioContext().audioWorklet.addModule(workletsUrl); + const contextPromise = getAudioContext(); + workletsLoading = contextPromise.audioWorklet.addModule(workletsUrl); } + return workletsLoading; } // this function should be called on first user interaction (to avoid console warning) export async function initAudio(options = {}) { - const { disableWorklets = false, maxPolyphony } = options; + const { disableWorklets = false, maxPolyphony, audioDeviceName } = options; setMaxPolyphony(maxPolyphony); if (typeof window === 'undefined') { 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) { logger('[superdough]: AudioWorklets disabled with disableWorklets'); return; diff --git a/website/src/repl/components/panel/AudioDeviceSelector.jsx b/website/src/repl/components/panel/AudioDeviceSelector.jsx index d1f13c22..288ffc0a 100644 --- a/website/src/repl/components/panel/AudioDeviceSelector.jsx +++ b/website/src/repl/components/panel/AudioDeviceSelector.jsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; -import { getAudioDevices, setAudioDevice } from '../../util.mjs'; + import { SelectInput } from './SelectInput'; +import { getAudioDevices } from '@strudel/webaudio'; const initdevices = new Map(); @@ -21,9 +22,7 @@ export function AudioDeviceSelector({ audioDeviceName, onChange, isDisabled }) { if (!devicesInitialized) { return; } - const deviceID = devices.get(deviceName); onChange(deviceName); - setAudioDevice(deviceID); }; const options = new Map(); Array.from(devices.keys()).forEach((deviceName) => { diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 801a0886..f0895aaa 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -14,7 +14,7 @@ import { resetLoadedSounds, initAudioOnFirstClick, } from '@strudel/webaudio'; -import { getAudioDevices, setAudioDevice, setVersionDefaultsFrom } from './util.mjs'; +import { setVersionDefaultsFrom } from './util.mjs'; import { StrudelMirror, defaultSettings } from '@strudel/codemirror'; import { clearHydra } from '@strudel/hydra'; import { useCallback, useEffect, useRef, useState } from 'react'; @@ -28,7 +28,7 @@ import { setViewingPatternData, } from '../user_pattern_utils.mjs'; import { superdirtOutput } from '@strudel/osc/superdirtoutput'; -import { audioEngineTargets, defaultAudioDeviceName } from '../settings.mjs'; +import { audioEngineTargets } from '../settings.mjs'; import { useStore } from '@nanostores/react'; import { prebake } from './prebake.mjs'; import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs'; @@ -36,11 +36,11 @@ import './Repl.css'; import { setInterval, clearInterval } from 'worker-timers'; import { getMetadata } from '../metadata_parser'; -const { latestCode, maxPolyphony } = settingsMap.get(); +const { latestCode, maxPolyphony, audioDeviceName } = settingsMap.get(); let modulesLoading, presets, drawContext, clearCanvas, audioReady; if (typeof window !== 'undefined') { - audioReady = initAudioOnFirstClick({ maxPolyphony }); + audioReady = initAudioOnFirstClick({ maxPolyphony, audioDeviceName }); modulesLoading = loadModules(); presets = prebake(); drawContext = getDrawContext(); @@ -159,20 +159,6 @@ export function useReplContext() { editorRef.current?.updateSettings(editorSettings); }, [_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 // diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index a8d18428..82f66cd4 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -1,6 +1,6 @@ import { evalScope, hash2code, logger } from '@strudel/core'; -import { settingPatterns, defaultAudioDeviceName } from '../settings.mjs'; -import { getAudioContext, initializeAudioOutput, setDefaultAudioContext, setVersionDefaults } from '@strudel/webaudio'; +import { settingPatterns } from '../settings.mjs'; +import { setVersionDefaults } from '@strudel/webaudio'; import { getMetadata } from '../metadata_parser'; import { isTauri } from '../tauri.mjs'; import './Repl.css'; @@ -159,37 +159,6 @@ export const isUdels = () => { 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) { try { diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 132d84bd..c50c31f6 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -2,8 +2,7 @@ import { persistentMap } from '@nanostores/persistent'; import { useStore } from '@nanostores/react'; import { register } from '@strudel/core'; import { isUdels } from './repl/util.mjs'; - -export const defaultAudioDeviceName = 'System Standard'; +import { DEFAULT_AUDIO_DEVICE_NAME } from '@strudel/webaudio'; export const audioEngineTargets = { webaudio: 'webaudio', @@ -36,7 +35,7 @@ export const defaultSettings = { isPanelOpen: true, togglePanelTrigger: 'click', //click | hover userPatterns: '{}', - audioDeviceName: defaultAudioDeviceName, + audioDeviceName: DEFAULT_AUDIO_DEVICE_NAME, audioEngineTarget: audioEngineTargets.webaudio, isButtonRowHidden: false, isCSSAnimationDisabled: false, From b1090a1dd8a0ccb0a6c4c2e153a644afd5601eed Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 7 Apr 2025 23:52:15 -0400 Subject: [PATCH 2/5] rm deadcode --- packages/superdough/superdough.mjs | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index bb152dd9..b94d5d31 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -105,26 +105,6 @@ export const getAudioDevices = async () => { 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(); -}; - const defaultDefaultValues = { s: 'triangle', gain: 0.8, From 9e233fe587ed720d56de84a3d4ee4f0d3c7ca9f0 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Mon, 7 Apr 2025 23:55:34 -0400 Subject: [PATCH 3/5] var name --- packages/superdough/superdough.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index b94d5d31..55ff2eab 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -175,8 +175,8 @@ export function getAudioContextCurrentTime() { let workletsLoading; function loadWorklets() { if (!workletsLoading) { - const contextPromise = getAudioContext(); - workletsLoading = contextPromise.audioWorklet.addModule(workletsUrl); + const audioCtx = getAudioContext(); + workletsLoading = audioCtx.audioWorklet.addModule(workletsUrl); } return workletsLoading; From a32bb58fb324e1520deeca9eb1fdfe5539fd0ac9 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 8 Apr 2025 00:01:34 -0400 Subject: [PATCH 4/5] rm dependency --- packages/superdough/superdough.mjs | 4 ++-- website/src/settings.mjs | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 55ff2eab..e936d38e 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -15,7 +15,7 @@ import { logger } from './logger.mjs'; import { loadBuffer } from './sampler.mjs'; export const DEFAULT_MAX_POLYPHONY = 128; -export const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; +const DEFAULT_AUDIO_DEVICE_NAME = 'System Standard'; let maxPolyphony = DEFAULT_MAX_POLYPHONY; export function setMaxPolyphony(polyphony) { @@ -184,7 +184,7 @@ function loadWorklets() { // this function should be called on first user interaction (to avoid console warning) export async function initAudio(options = {}) { - const { disableWorklets = false, maxPolyphony, audioDeviceName } = options; + const { disableWorklets = false, maxPolyphony, audioDeviceName = DEFAULT_AUDIO_DEVICE_NAME } = options; setMaxPolyphony(maxPolyphony); if (typeof window === 'undefined') { return; diff --git a/website/src/settings.mjs b/website/src/settings.mjs index c50c31f6..1b4ba33f 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -2,7 +2,6 @@ import { persistentMap } from '@nanostores/persistent'; import { useStore } from '@nanostores/react'; import { register } from '@strudel/core'; import { isUdels } from './repl/util.mjs'; -import { DEFAULT_AUDIO_DEVICE_NAME } from '@strudel/webaudio'; export const audioEngineTargets = { webaudio: 'webaudio', @@ -35,7 +34,6 @@ export const defaultSettings = { isPanelOpen: true, togglePanelTrigger: 'click', //click | hover userPatterns: '{}', - audioDeviceName: DEFAULT_AUDIO_DEVICE_NAME, audioEngineTarget: audioEngineTargets.webaudio, isButtonRowHidden: false, isCSSAnimationDisabled: false, From 2b69004eb442913072eacbcd22b0217c6571b4a0 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Tue, 8 Apr 2025 00:04:22 -0400 Subject: [PATCH 5/5] lint --- website/src/repl/util.mjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index 82f66cd4..f49dd568 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -1,6 +1,6 @@ import { evalScope, hash2code, logger } from '@strudel/core'; import { settingPatterns } from '../settings.mjs'; -import { setVersionDefaults } from '@strudel/webaudio'; +import { setVersionDefaults } from '@strudel/webaudio'; import { getMetadata } from '../metadata_parser'; import { isTauri } from '../tauri.mjs'; import './Repl.css'; @@ -159,7 +159,6 @@ export const isUdels = () => { return window.top?.location?.pathname.includes('udels'); }; - export function setVersionDefaultsFrom(code) { try { const metadata = getMetadata(code);