From 64b5d51e0658492119b9c07c5b1b8f5f74103610 Mon Sep 17 00:00:00 2001 From: Matt Nemitz Date: Thu, 30 Jan 2025 13:29:00 +0000 Subject: [PATCH] browser-audio-input: Add mute + unmute to PCM recorder (#113) --- examples/nextjs-flow/components/Controls.tsx | 75 +++++++++++-------- .../browser-audio-input-react/package.json | 2 +- .../src/use-pcm-audio-recorder.tsx | 32 ++++++++ packages/browser-audio-input/package.json | 2 +- .../browser-audio-input/src/pcm-recorder.ts | 44 +++++++++++ 5 files changed, 123 insertions(+), 32 deletions(-) diff --git a/examples/nextjs-flow/components/Controls.tsx b/examples/nextjs-flow/components/Controls.tsx index 977c970..3e9272f 100644 --- a/examples/nextjs-flow/components/Controls.tsx +++ b/examples/nextjs-flow/components/Controls.tsx @@ -1,9 +1,10 @@ 'use client'; import { useFlow } from '@speechmatics/flow-client-react'; -import { useCallback, useMemo, type FormEventHandler } from 'react'; +import { useCallback, type FormEventHandler } from 'react'; import { useFlowWithBrowserAudio } from '../hooks/useFlowWithBrowserAudio'; import { MicrophoneSelect, Select } from './MicrophoneSelect'; import Card from './Card'; +import { usePCMAudioRecorder } from '@speechmatics/browser-audio-input-react'; interface ButtonProps extends React.ButtonHTMLAttributes { children: React.ReactNode; @@ -19,13 +20,16 @@ export function Controls({ personas, }: { personas: Record }) { const { socketState, sessionId } = useFlow(); - const { startSession, stopSession } = useFlowWithBrowserAudio(); const handleSubmit = useCallback( async (e) => { e.preventDefault(); + if (socketState === 'open' && sessionId) { + return stopSession(); + } + const formData = new FormData(e.target as HTMLFormElement); const personaId = formData.get('personaId')?.toString(); @@ -36,35 +40,9 @@ export function Controls({ startSession({ personaId, deviceId }); }, - [startSession], + [startSession, stopSession, socketState, sessionId], ); - const startButton = useMemo(() => { - if (socketState === 'open' && sessionId) { - return ( - - ); - } - if ( - socketState === 'connecting' || - socketState === 'closing' || - (socketState === 'open' && !sessionId) - ) { - return ( - - ); - } - return ( - - ); - }, [socketState, stopSession, sessionId]); - return (
@@ -76,8 +54,45 @@ export function Controls({ ))} -
{startButton}
+
+ + +
); } + +function ActionButton() { + const { socketState, sessionId } = useFlow(); + + if ( + socketState === 'connecting' || + socketState === 'closing' || + (socketState === 'open' && !sessionId) + ) { + return ( + + ); + } + + const running = socketState === 'open' && sessionId; + return ( + + ); +} + +function MuteMicrophoneButton() { + const { isRecording, mute, unmute, isMuted } = usePCMAudioRecorder(); + if (!isRecording) return null; + + return ( + + ); +} diff --git a/packages/browser-audio-input-react/package.json b/packages/browser-audio-input-react/package.json index 6b83e8c..0c9e9a5 100644 --- a/packages/browser-audio-input-react/package.json +++ b/packages/browser-audio-input-react/package.json @@ -1,6 +1,6 @@ { "name": "@speechmatics/browser-audio-input-react", - "version": "1.0.3", + "version": "1.1.0", "description": "React hooks for managing audio inputs and permissions across browsers", "type": "module", "exports": ["./dist/index.js"], diff --git a/packages/browser-audio-input-react/src/use-pcm-audio-recorder.tsx b/packages/browser-audio-input-react/src/use-pcm-audio-recorder.tsx index 8b520ae..409d54c 100644 --- a/packages/browser-audio-input-react/src/use-pcm-audio-recorder.tsx +++ b/packages/browser-audio-input-react/src/use-pcm-audio-recorder.tsx @@ -14,10 +14,13 @@ import { export interface IPCMAudioRecorderContext { startRecording: PCMRecorder['startRecording']; stopRecording: PCMRecorder['stopRecording']; + mute: PCMRecorder['mute']; + unmute: PCMRecorder['unmute']; addEventListener: PCMRecorder['addEventListener']; removeEventListener: PCMRecorder['removeEventListener']; analyser: PCMRecorder['analyser']; isRecording: PCMRecorder['isRecording']; + isMuted: PCMRecorder['isMuted']; } const context = createContext(null); @@ -95,10 +98,36 @@ export function PCMAudioRecorderProvider({ () => recorder.isRecording, ); + const mute = useCallback( + () => recorder.mute(), + [recorder], + ); + + const unmute = useCallback( + () => recorder.unmute(), + [recorder], + ); + + const isMuted = useSyncExternalStore( + (onChange) => { + recorder.addEventListener('mute', onChange); + recorder.addEventListener('unmute', onChange); + return () => { + recorder.removeEventListener('mute', onChange); + recorder.removeEventListener('unmute', onChange); + }; + }, + () => recorder.isMuted, + () => recorder.isMuted, + ); + const value = useMemo( () => ({ startRecording, stopRecording, + mute, + unmute, + isMuted, addEventListener, removeEventListener, analyser, @@ -107,6 +136,9 @@ export function PCMAudioRecorderProvider({ [ startRecording, stopRecording, + mute, + unmute, + isMuted, addEventListener, removeEventListener, analyser, diff --git a/packages/browser-audio-input/package.json b/packages/browser-audio-input/package.json index a1410db..4158d44 100644 --- a/packages/browser-audio-input/package.json +++ b/packages/browser-audio-input/package.json @@ -1,6 +1,6 @@ { "name": "@speechmatics/browser-audio-input", - "version": "1.0.3", + "version": "1.1.0", "description": "Manage audio input devices and persmissions across browsers", "exports": { ".": "./dist/index.js", diff --git a/packages/browser-audio-input/src/pcm-recorder.ts b/packages/browser-audio-input/src/pcm-recorder.ts index 722e3ec..cbdc0db 100644 --- a/packages/browser-audio-input/src/pcm-recorder.ts +++ b/packages/browser-audio-input/src/pcm-recorder.ts @@ -3,6 +3,8 @@ import { TypedEventTarget } from 'typescript-event-target'; const RECORDING_STARTED = 'recordingStarted'; const RECORDING_STOPPED = 'recordingStopped'; const AUDIO = 'audio'; +const MUTE = 'mute'; +const UNMUTE = 'unmute'; export class InputAudioEvent extends Event { constructor(public readonly data: Float32Array) { @@ -10,10 +12,24 @@ export class InputAudioEvent extends Event { } } +export class MuteEvent extends Event { + constructor() { + super(MUTE); + } +} + +export class UnmuteEvent extends Event { + constructor() { + super(UNMUTE); + } +} + interface PCMRecorderEventMap { [RECORDING_STARTED]: Event; [RECORDING_STOPPED]: Event; [AUDIO]: InputAudioEvent; + [MUTE]: MuteEvent; + [UNMUTE]: UnmuteEvent; } export type StartRecordingOptions = { @@ -85,6 +101,34 @@ export class PCMRecorder extends TypedEventTarget { this.dispatchTypedEvent(RECORDING_STARTED, new Event(RECORDING_STARTED)); } + mute() { + if (!this.mediaStream) return; + + for (const track of this.mediaStream.getTracks()) { + console.log(track); + track.enabled = false; + } + + this.dispatchTypedEvent(MUTE, new MuteEvent()); + } + + unmute() { + if (!this.mediaStream) return; + + for (const track of this.mediaStream.getTracks()) { + track.enabled = true; + } + + this.dispatchTypedEvent(UNMUTE, new UnmuteEvent()); + } + + get isMuted() { + return ( + this.mediaStream?.getAudioTracks().some((track) => !track.enabled) ?? + false + ); + } + stopRecording() { if (this.mediaStream) { for (const track of this.mediaStream.getTracks()) {