From 52628920d98b972d25681fecb3050c5a9804a7c7 Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Thu, 7 Dec 2023 22:17:50 -0500 Subject: [PATCH 001/554] working --- packages/superdough/superdough.mjs | 19 +++-- .../src/repl/panel/AudioDeviceSelector.jsx | 70 +++++++++++++++++++ website/src/repl/panel/SelectInput.jsx | 17 +++++ website/src/repl/panel/SettingsTab.jsx | 8 +++ website/src/settings.mjs | 1 + 5 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 website/src/repl/panel/AudioDeviceSelector.jsx create mode 100644 website/src/repl/panel/SelectInput.jsx diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 3be97615..6c7d8cd3 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -31,8 +31,6 @@ let audioContext; export const getAudioContext = () => { if (!audioContext) { audioContext = new AudioContext(); - const maxChannelCount = audioContext.destination.maxChannelCount; - audioContext.destination.channelCount = maxChannelCount; } return audioContext; }; @@ -85,14 +83,22 @@ const maxfeedback = 0.98; let channelMerger, destinationGain; +export function initializeAudioOutput() { + const audioContext = getAudioContext(); + const maxChannelCount = audioContext.destination.maxChannelCount; + console.log(maxChannelCount); + audioContext.destination.channelCount = maxChannelCount; + channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount }); + destinationGain = new GainNode(audioContext); + channelMerger.connect(destinationGain); + destinationGain.connect(audioContext.destination); +} + // input: AudioNode, channels: ?Array 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); + initializeAudioOutput(); } //This upmix can be removed if correct channel counts are set throughout the app, // and then strudel could theoretically support surround sound audio files @@ -114,6 +120,7 @@ export const panic = () => { } destinationGain.gain.linearRampToValueAtTime(0, getAudioContext().currentTime + 0.01); destinationGain = null; + channelMerger == null; }; function getDelay(orbit, delaytime, delayfeedback, t) { diff --git a/website/src/repl/panel/AudioDeviceSelector.jsx b/website/src/repl/panel/AudioDeviceSelector.jsx new file mode 100644 index 00000000..27d50349 --- /dev/null +++ b/website/src/repl/panel/AudioDeviceSelector.jsx @@ -0,0 +1,70 @@ +import React, { useState, useEffect } from 'react'; +import { getAudioContext, initializeAudioOutput } from '@strudel.cycles/webaudio'; +import { SelectInput } from './SelectInput'; + +async function setAudioDevice(id) { + const audioCtx = getAudioContext(); + await audioCtx.setSinkId(id); + await initializeAudioOutput(); +} +export function AudioDeviceSelector({ audioDeviceName, onChange }) { + const [options, setOptions] = useState({}); + const [optionsInitialized, setOptionsInitialized] = useState(false); + + async function initializeOptions() { + await navigator.mediaDevices.getUserMedia({ audio: true }); + let devices = await navigator.mediaDevices.enumerateDevices(); + devices = devices.filter((device) => device.kind === 'audiooutput' && device.deviceId !== 'default'); + const optionsArray = []; + devices.forEach((device) => { + optionsArray.push([device.deviceId, device.label]); + }); + const options = Object.fromEntries(optionsArray); + setOptions(options); + setOptionsInitialized(true); + return options; + } + + useEffect(() => { + if (!audioDeviceName.length || optionsInitialized) { + return; + } + + (async () => { + const options = await initializeOptions(); + + const deviceID = Object.keys(options).find((id) => options[id] === audioDeviceName); + + if (deviceID == null) { + onChange(''); + return; + } + + await setAudioDevice(deviceID); + })(); + }, []); + + const onClick = () => { + if (optionsInitialized) { + return; + } + (async () => { + await initializeOptions(); + })(); + }; + const onDeviceChange = (deviceID) => { + (async () => { + const deviceName = options[deviceID]; + onChange(deviceName); + await setAudioDevice(deviceID); + })(); + }; + return ( + options[id] === audioDeviceName)} + onChange={onDeviceChange} + /> + ); +} diff --git a/website/src/repl/panel/SelectInput.jsx b/website/src/repl/panel/SelectInput.jsx new file mode 100644 index 00000000..c0a952b0 --- /dev/null +++ b/website/src/repl/panel/SelectInput.jsx @@ -0,0 +1,17 @@ +import React from 'react'; + +export function SelectInput({ value, options, onChange }) { + return ( + + ); +} diff --git a/website/src/repl/panel/SettingsTab.jsx b/website/src/repl/panel/SettingsTab.jsx index cee5b286..53a883fa 100644 --- a/website/src/repl/panel/SettingsTab.jsx +++ b/website/src/repl/panel/SettingsTab.jsx @@ -2,6 +2,7 @@ import React from 'react'; import { defaultSettings, settingsMap, useSettings } from '../../settings.mjs'; import { themes } from '../themes.mjs'; import { ButtonGroup } from './Forms.jsx'; +import { AudioDeviceSelector } from './AudioDeviceSelector.jsx'; function Checkbox({ label, value, onChange }) { return ( @@ -85,6 +86,7 @@ export function SettingsTab() { fontSize, fontFamily, panelPosition, + audioDeviceName, } = useSettings(); return ( @@ -107,6 +109,12 @@ export function SettingsTab() { */} + + settingsMap.setKey('audioDeviceName', audioDeviceName)} + /> + settingsMap.setKey('theme', theme)} /> diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 67d386cf..a5e69ca7 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -20,6 +20,7 @@ export const defaultSettings = { panelPosition: 'bottom', userPatterns: '{}', activePattern: '', + audioDeviceName: '', }; export const settingsMap = persistentMap('strudel-settings', defaultSettings); From 5281c61f99c3a428bb4b91ec4f3e7ca6e14409c8 Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Fri, 8 Dec 2023 01:01:06 -0500 Subject: [PATCH 002/554] fixing async stuff --- packages/superdough/superdough.mjs | 1 - .../src/repl/panel/AudioDeviceSelector.jsx | 85 +++++++++---------- website/src/repl/panel/SelectInput.jsx | 9 +- website/src/repl/panel/SettingsTab.jsx | 14 +-- 4 files changed, 51 insertions(+), 58 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 6c7d8cd3..b8edac1c 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -86,7 +86,6 @@ let channelMerger, destinationGain; export function initializeAudioOutput() { const audioContext = getAudioContext(); const maxChannelCount = audioContext.destination.maxChannelCount; - console.log(maxChannelCount); audioContext.destination.channelCount = maxChannelCount; channelMerger = new ChannelMergerNode(audioContext, { numberOfInputs: audioContext.destination.channelCount }); destinationGain = new GainNode(audioContext); diff --git a/website/src/repl/panel/AudioDeviceSelector.jsx b/website/src/repl/panel/AudioDeviceSelector.jsx index 27d50349..afdaff00 100644 --- a/website/src/repl/panel/AudioDeviceSelector.jsx +++ b/website/src/repl/panel/AudioDeviceSelector.jsx @@ -1,70 +1,61 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { getAudioContext, initializeAudioOutput } from '@strudel.cycles/webaudio'; import { SelectInput } from './SelectInput'; -async function setAudioDevice(id) { - const audioCtx = getAudioContext(); - await audioCtx.setSinkId(id); - await initializeAudioOutput(); -} +const initdevices = new Map(); export function AudioDeviceSelector({ audioDeviceName, onChange }) { - const [options, setOptions] = useState({}); - const [optionsInitialized, setOptionsInitialized] = useState(false); + const [devices, setDevices] = useState(initdevices); + const devicesInitialized = devices.size > 0; - async function initializeOptions() { + const setAudioDevice = useCallback(async (id) => { + const audioCtx = getAudioContext(); + await audioCtx.setSinkId(id); + initializeAudioOutput(); + }); + const initializedevices = useCallback(async () => { await navigator.mediaDevices.getUserMedia({ audio: true }); - let devices = await navigator.mediaDevices.enumerateDevices(); - devices = devices.filter((device) => device.kind === 'audiooutput' && device.deviceId !== 'default'); - const optionsArray = []; - devices.forEach((device) => { - optionsArray.push([device.deviceId, device.label]); + let mediaDevices = await navigator.mediaDevices.enumerateDevices(); + mediaDevices = mediaDevices.filter((device) => device.kind === 'audiooutput' && device.deviceId !== 'default'); + const devicesMap = new Map(); + mediaDevices.forEach((device) => { + devicesMap.set(device.label, device.deviceId); }); - const options = Object.fromEntries(optionsArray); - setOptions(options); - setOptionsInitialized(true); - return options; - } + setDevices(devicesMap); + return devicesMap; + }, []); useEffect(() => { - if (!audioDeviceName.length || optionsInitialized) { + if (!audioDeviceName.length || devicesInitialized) { return; } - - (async () => { - const options = await initializeOptions(); - - const deviceID = Object.keys(options).find((id) => options[id] === audioDeviceName); - + initializedevices().then((devices) => { + const deviceID = devices.get(audioDeviceName); if (deviceID == null) { onChange(''); return; } - - await setAudioDevice(deviceID); - })(); + setAudioDevice(deviceID); + }); }, []); const onClick = () => { - if (optionsInitialized) { + if (devicesInitialized) { return; } - (async () => { - await initializeOptions(); - })(); + initializedevices(); }; - const onDeviceChange = (deviceID) => { - (async () => { - const deviceName = options[deviceID]; - onChange(deviceName); - await setAudioDevice(deviceID); - })(); + const onDeviceChange = (deviceName) => { + if (!devicesInitialized) { + return; + } + const deviceID = devices.get(deviceName); + onChange(deviceName); + deviceID && setAudioDevice(deviceID); }; - return ( - options[id] === audioDeviceName)} - onChange={onDeviceChange} - /> - ); + const options = new Set(); + + Array.from(devices.keys()).forEach((deviceName) => { + options.add({ id: deviceName, label: deviceName }); + }); + return ; } diff --git a/website/src/repl/panel/SelectInput.jsx b/website/src/repl/panel/SelectInput.jsx index c0a952b0..53f958b8 100644 --- a/website/src/repl/panel/SelectInput.jsx +++ b/website/src/repl/panel/SelectInput.jsx @@ -1,14 +1,15 @@ import React from 'react'; - -export function SelectInput({ value, options, onChange }) { +// value: ID, options: Set<{id: ID, label: string}>, onChange: ID => null, onClick: event => void +export function SelectInput({ value, options, onChange, onClick }) { return ( onChange(e.target.value)} > - {Array.from(options).map(({ id, label }) => ( + + {Array.from(options.keys()).map((id) => ( ))} From ec44103d202da07b57656c232e42548860b5a5df Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Fri, 8 Dec 2023 12:17:01 -0500 Subject: [PATCH 004/554] comments --- packages/superdough/superdough.mjs | 2 +- website/src/repl/panel/AudioDeviceSelector.jsx | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index b8edac1c..d5c1af66 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -82,7 +82,7 @@ let delays = {}; const maxfeedback = 0.98; let channelMerger, destinationGain; - +//update the output channel configuration to match user's audio device export function initializeAudioOutput() { const audioContext = getAudioContext(); const maxChannelCount = audioContext.destination.maxChannelCount; diff --git a/website/src/repl/panel/AudioDeviceSelector.jsx b/website/src/repl/panel/AudioDeviceSelector.jsx index a90b3dcd..9704f0cd 100644 --- a/website/src/repl/panel/AudioDeviceSelector.jsx +++ b/website/src/repl/panel/AudioDeviceSelector.jsx @@ -3,6 +3,7 @@ import { getAudioContext, initializeAudioOutput } from '@strudel.cycles/webaudio import { SelectInput } from './SelectInput'; const initdevices = new Map(); +// Allows the user to select an audio interface for Strudel to play through export function AudioDeviceSelector({ audioDeviceName, onChange }) { const [devices, setDevices] = useState(initdevices); const devicesInitialized = devices.size > 0; @@ -24,6 +25,7 @@ export function AudioDeviceSelector({ audioDeviceName, onChange }) { return devicesMap; }, []); + // on first load, check if there is a cached audio device name in settings and initialize it useEffect(() => { if (!audioDeviceName.length || devicesInitialized) { return; From f45a3933de53f54a5e725a9b85cb0ab99e73362d Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Sun, 10 Dec 2023 23:08:48 -0500 Subject: [PATCH 005/554] update text --- website/src/repl/panel/AudioDeviceSelector.jsx | 2 +- website/src/repl/panel/SelectInput.jsx | 2 +- website/src/repl/panel/SettingsTab.jsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/website/src/repl/panel/AudioDeviceSelector.jsx b/website/src/repl/panel/AudioDeviceSelector.jsx index 9704f0cd..6673a5d8 100644 --- a/website/src/repl/panel/AudioDeviceSelector.jsx +++ b/website/src/repl/panel/AudioDeviceSelector.jsx @@ -62,7 +62,7 @@ export function AudioDeviceSelector({ audioDeviceName, onChange }) { diff --git a/website/src/repl/panel/SelectInput.jsx b/website/src/repl/panel/SelectInput.jsx index 6693d669..78269fad 100644 --- a/website/src/repl/panel/SelectInput.jsx +++ b/website/src/repl/panel/SelectInput.jsx @@ -9,7 +9,7 @@ export function SelectInput({ value, options, onChange, onClick, placeholder }) onChange={(e) => onChange(e.target.value)} > {Array.from(options.keys()).map((id) => (