From f660ffcfc522fe9be1a1d89ed1a14fc574e6cca0 Mon Sep 17 00:00:00 2001 From: Eric Dauenhauer Date: Wed, 7 Dec 2022 21:11:38 -0600 Subject: [PATCH] Fix recording settings --- src/App/index.tsx | 3 +- src/AudioProvider/index.tsx | 6 ++- src/Start/index.tsx | 67 ++++++++++++++++++++------- src/Track/controls/SelectInput.tsx | 73 ++++++++++++++++++------------ src/Track/index.tsx | 15 +++--- 5 files changed, 107 insertions(+), 57 deletions(-) diff --git a/src/App/index.tsx b/src/App/index.tsx index ddb0bb0..962407e 100644 --- a/src/App/index.tsx +++ b/src/App/index.tsx @@ -4,8 +4,9 @@ import { useKeybindings } from '../hooks/use-keybindings' import { KeyboardBindingsList } from './KeyboardBindingsList' type Props = { - stream: MediaStream + defaultDeviceId: string audioContext: AudioContext + devices: MediaDeviceInfo[] } function App(props: Props) { diff --git a/src/AudioProvider/index.tsx b/src/AudioProvider/index.tsx index 5d9f839..681ede0 100644 --- a/src/AudioProvider/index.tsx +++ b/src/AudioProvider/index.tsx @@ -7,14 +7,16 @@ import React, { createContext, useContext } from 'react' type AudioAdapter = { audioContext: AudioContext - stream: MediaStream + defaultDeviceId: string + devices: MediaDeviceInfo[] } const AudioRouter = createContext(null) type Props = { - stream: MediaStream + defaultDeviceId: string audioContext: AudioContext + devices: MediaDeviceInfo[] children: React.ReactNode } diff --git a/src/Start/index.tsx b/src/Start/index.tsx index 159ab70..3ff2b7c 100644 --- a/src/Start/index.tsx +++ b/src/Start/index.tsx @@ -17,11 +17,13 @@ */ import { useEffect, useState } from 'react' import App from '../App' +import { deviceIdFromStream } from '../util/device-id-from-stream' import { logger } from '../util/logger' export const Start: React.FC = () => { - const [stream, setStream] = useState() + const [defaultDeviceId, setDefaultDeviceId] = useState(null) const [audioContext, setAudioContext] = useState() + const [devices, setDevices] = useState(null) const [latencySupported, setLatencySupported] = useState(true) useEffect(() => { @@ -33,27 +35,48 @@ export const Start: React.FC = () => { } }, []) - async function handleClick() { + async function grantDevicePermission() { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + // see src/Track/controls/SelectInput.tsx for related note + audio: { + echoCancellation: false, + autoGainControl: false, + noiseSuppression: false, + suppressLocalAudioPlayback: false, + latency: 0, + }, + video: false, + }) + setDefaultDeviceId(deviceIdFromStream(stream) ?? null) + } catch (e) { + alert( + 'big, terrible error occurred and there is no coming back from that 😿' + ) + logger.error({ e, message: 'Error getting user media' }) + } + } + + async function enumerateDevices() { try { - setStream( - await navigator.mediaDevices.getUserMedia({ - audio: true, - // audio: { - // echoCancellation: true, - // autoGainControl: false, - // noiseSuppression: true, - // latency: 0, - // }, - video: false, - }) + const devices = await navigator.mediaDevices.enumerateDevices() + const audioDevices = devices.filter( + (device) => device.kind === 'audioinput' ) + if (audioDevices.length === 0) { + logger.error({ devices }) + throw new Error('No audio devices found') + } + setDevices(audioDevices) } catch (e) { alert( 'big, terrible error occurred and there is no coming back from that 😿' ) - logger.error(e, 'Error getting user media') + logger.error({ e, message: 'Error getting user devices' }) } + } + async function initializeAudioContext() { const workletUrl = new URL('../workers/recorder', import.meta.url) try { const audioContext = new AudioContext() @@ -71,8 +94,20 @@ export const Start: React.FC = () => { } } - return stream && audioContext ? ( - + async function handleClick() { + await Promise.all([ + grantDevicePermission(), + enumerateDevices(), + initializeAudioContext(), + ]) + } + + return defaultDeviceId && audioContext && devices?.length ? ( + ) : ( <>
diff --git a/src/Track/controls/SelectInput.tsx b/src/Track/controls/SelectInput.tsx index 11039f6..bdf7563 100644 --- a/src/Track/controls/SelectInput.tsx +++ b/src/Track/controls/SelectInput.tsx @@ -1,55 +1,68 @@ -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { logger } from '../../util/logger' import { deviceIdFromStream } from '../../util/device-id-from-stream' +import { useAudioContext } from '../../AudioProvider' type Props = { - defaultDeviceId: string setStream(stream: MediaStream): void } -export function SelectInput(props: Props) { - const [inputs, setInputs] = useState([]) - const [selected, setSelected] = useState(props.defaultDeviceId) +export function SelectInput({ setStream }: Props) { + const { devices, defaultDeviceId } = useAudioContext() + const [selected, setSelected] = useState(defaultDeviceId) - useEffect(() => { - async function getInputs() { - const devices = await navigator.mediaDevices.enumerateDevices() - const audioInputs = devices.filter( - (device) => device.kind === 'audioinput' - ) - logger.debug({ audioInputs }) - setInputs(audioInputs) - } + const setStreamByDeviceId = useCallback( + async (id: string) => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + audio: { + deviceId: id, - getInputs() - }, []) + // for some reason, + // having these defined makes a HUGE difference in the recording quality. + // Without these defined, the audio will pulse in and out, but with these defined, it sounds great + echoCancellation: false, + autoGainControl: false, + noiseSuppression: false, + suppressLocalAudioPlayback: false, + latency: 0, + }, + video: false, + }) + setStream(stream) + setSelected(deviceIdFromStream(stream) ?? '') + } catch (e) { + alert('oh no, you broke it 😿') + logger.error({ + e, + id, + message: 'Failed to create stream from selected device', + }) + } + }, + [setStream] + ) const handleChange: React.ChangeEventHandler = async ( event ) => { - try { - const stream = await navigator.mediaDevices.getUserMedia({ - audio: { deviceId: event.target.value }, - video: false, - }) - props.setStream(stream) - setSelected(deviceIdFromStream(stream) ?? '') - } catch (e) { - alert('oh no, you broke it 😿') - console.error(e) - } + return setStreamByDeviceId(event.target.value) } + useEffect(() => { + setStreamByDeviceId(defaultDeviceId) + }, [setStreamByDeviceId, defaultDeviceId]) + return ( diff --git a/src/Track/index.tsx b/src/Track/index.tsx index 39c58e4..2bdb64a 100644 --- a/src/Track/index.tsx +++ b/src/Track/index.tsx @@ -30,7 +30,6 @@ import { Mute } from './controls/Mute' import { RemoveTrack } from './controls/RemoveTrack' import { Waveform } from './Waveform' import { SelectInput } from './controls/SelectInput' -import { deviceIdFromStream } from '../util/device-id-from-stream' import type { ExportWavWorkerEvent, WavBlobControllerEvent, @@ -84,9 +83,9 @@ export const Track: React.FC = ({ selected, exportTarget, }) => { - const { audioContext, stream: defaultStream } = useAudioContext() - const [stream, setStream] = useState(defaultStream) - const defaultDeviceId = deviceIdFromStream(defaultStream) ?? '' + const { audioContext } = useAudioContext() + // stream is initialized in SelectInput + const [stream, setStream] = useState(null) // title is mostly display-only, but also defines the file name when downloading files const [title, setTitle] = useState(`Track ${id}`) @@ -229,6 +228,9 @@ export const Track: React.FC = ({ * Initialize the recorder worklet, and connect the audio graph for eventual playback. */ useEffect(() => { + if (!stream) { + return + } const mediaSource = audioContext.createMediaStreamSource(stream) const recordingProperties: RecordingProperties = { numberOfChannels: mediaSource.channelCount, @@ -424,10 +426,7 @@ export const Track: React.FC = ({ {/* Remove */}
- +