Skip to content

Commit

Permalink
Fix recording settings (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
ericyd authored Dec 8, 2022
1 parent f3fe3b4 commit 163563e
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 57 deletions.
3 changes: 2 additions & 1 deletion src/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 4 additions & 2 deletions src/AudioProvider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ import React, { createContext, useContext } from 'react'

type AudioAdapter = {
audioContext: AudioContext
stream: MediaStream
defaultDeviceId: string
devices: MediaDeviceInfo[]
}

const AudioRouter = createContext<AudioAdapter | null>(null)

type Props = {
stream: MediaStream
defaultDeviceId: string
audioContext: AudioContext
devices: MediaDeviceInfo[]
children: React.ReactNode
}

Expand Down
67 changes: 51 additions & 16 deletions src/Start/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<MediaStream>()
const [defaultDeviceId, setDefaultDeviceId] = useState<string | null>(null)
const [audioContext, setAudioContext] = useState<AudioContext>()
const [devices, setDevices] = useState<MediaDeviceInfo[] | null>(null)
const [latencySupported, setLatencySupported] = useState(true)

useEffect(() => {
Expand All @@ -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()
Expand All @@ -71,8 +94,20 @@ export const Start: React.FC = () => {
}
}

return stream && audioContext ? (
<App stream={stream} audioContext={audioContext} />
async function handleClick() {
await Promise.all([
grantDevicePermission(),
enumerateDevices(),
initializeAudioContext(),
])
}

return defaultDeviceId && audioContext && devices?.length ? (
<App
defaultDeviceId={defaultDeviceId}
audioContext={audioContext}
devices={devices}
/>
) : (
<>
<div className="flex flex-col items-center justify-center mx-auto">
Expand Down
73 changes: 43 additions & 30 deletions src/Track/controls/SelectInput.tsx
Original file line number Diff line number Diff line change
@@ -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<MediaDeviceInfo[]>([])
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<HTMLSelectElement> = 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 (
<select
className="rounded-full bg-white border border-zinc-400 px-2 max-w-[50%] text-xs"
onChange={handleChange}
value={selected}
>
{inputs.map((input) => (
<option key={JSON.stringify(input)} value={input.deviceId}>
{devices.map((device) => (
<option key={JSON.stringify(device)} value={device.deviceId}>
{/* Chrome appends a weird hex ID to some inputs */}
{input.label.replace(/\([a-z0-9]+:[a-z0-9]+\)/, '')}
{device.label.replace(/\([a-z0-9]+:[a-z0-9]+\)/, '')}
</option>
))}
</select>
Expand Down
15 changes: 7 additions & 8 deletions src/Track/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -84,9 +83,9 @@ export const Track: React.FC<Props> = ({
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<MediaStream | null>(null)

// title is mostly display-only, but also defines the file name when downloading files
const [title, setTitle] = useState(`Track ${id}`)
Expand Down Expand Up @@ -229,6 +228,9 @@ export const Track: React.FC<Props> = ({
* 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,
Expand Down Expand Up @@ -424,10 +426,7 @@ export const Track: React.FC<Props> = ({
{/* Remove */}
<div className="flex items-stretch content-center justify-between">
<RemoveTrack onRemove={onRemove} />
<SelectInput
setStream={setStream}
defaultDeviceId={defaultDeviceId}
/>
<SelectInput setStream={setStream} />
</div>
</div>

Expand Down

0 comments on commit 163563e

Please sign in to comment.