From 52628920d98b972d25681fecb3050c5a9804a7c7 Mon Sep 17 00:00:00 2001 From: Jade Rowland Date: Thu, 7 Dec 2023 22:17:50 -0500 Subject: [PATCH] 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);