fciannella's picture
Added the healthcare example
2f49513
raw
history blame
5.69 kB
/*
* SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useReducer, useRef } from "react";
import logger from "../utils/logger";
const USER_SPEECH_SAMPLE_RATE = 16_000;
export interface MicrophoneState {
micAccessState: MicAccessState;
error: Error | null;
isRecording: boolean;
}
enum MicrophoneActionType {
MIC_ACCESS_REQUESTED = "MIC_ACCESS_REQUESTED",
MIC_ACCESS_GRANTED = "MIC_ACCESS_GRANTED",
MIC_ACCESS_ERROR = "MIC_ACCESS_ERROR",
RECORDING_STARTED = "RECORDING_STARTED",
RECORDING_STOPPED = "RECORDING_STOPPED",
}
export enum MicAccessState {
INITIAL = "INITIAL",
LOADING = "LOADING",
GRANTED = "GRANTED",
ERROR = "ERROR",
}
interface MicrophoneStateActionMicAccessRequested {
type: MicrophoneActionType.MIC_ACCESS_REQUESTED;
}
interface MicrophoneStateActionMicAccessGranted {
type: MicrophoneActionType.MIC_ACCESS_GRANTED;
}
interface MicrophoneStateActionMicAccessError {
type: MicrophoneActionType.MIC_ACCESS_ERROR;
payload: Error;
}
interface MicrophoneStateActionRecordingStarted {
type: MicrophoneActionType.RECORDING_STARTED;
}
interface MicrophoneStateActionRecordingStopped {
type: MicrophoneActionType.RECORDING_STOPPED;
}
type MicrophoneStateAction =
| MicrophoneStateActionMicAccessRequested
| MicrophoneStateActionMicAccessGranted
| MicrophoneStateActionMicAccessError
| MicrophoneStateActionRecordingStarted
| MicrophoneStateActionRecordingStopped;
function reducer(
state: MicrophoneState,
action: MicrophoneStateAction
): MicrophoneState {
switch (action.type) {
case MicrophoneActionType.MIC_ACCESS_REQUESTED:
return { ...state, micAccessState: MicAccessState.LOADING };
case MicrophoneActionType.MIC_ACCESS_GRANTED:
return {
...state,
micAccessState: MicAccessState.GRANTED,
};
case MicrophoneActionType.MIC_ACCESS_ERROR:
return {
...state,
micAccessState: MicAccessState.ERROR,
error: action.payload,
};
case MicrophoneActionType.RECORDING_STARTED:
return {
...state,
isRecording: true,
};
case MicrophoneActionType.RECORDING_STOPPED:
return {
...state,
isRecording: false,
};
default:
return state;
}
}
const INITIAL_STATE: MicrophoneState = {
micAccessState: MicAccessState.INITIAL,
error: null,
isRecording: false,
};
export default function useMicrophone({
onAudioChunkAvailable,
onError,
}: {
onAudioChunkAvailable: (
buffer: ArrayBuffer,
sampleRate: number,
numChannels: number
) => void;
onError: (error: Error) => void;
}): {
microphoneState: MicrophoneState;
startRecording: () => Promise<void>;
stopRecording: () => void;
source: AudioNode | null;
} {
const [microphoneState, dispatch] = useReducer(reducer, INITIAL_STATE);
const audioSourceRef = useRef<AudioNode>(null);
const audioCtxRef = useRef<AudioContext>(null);
async function requestAccess() {
dispatch({
type: MicrophoneActionType.MIC_ACCESS_REQUESTED,
});
try {
if (!isSecureContext) {
throw new Error(
`Cannot enable microphone in insecure context. To fix this issue, add the URL "${window.location.origin}" URL to chrome://flags/#unsafely-treat-insecure-origin-as-secure`
);
}
const stream = await navigator.mediaDevices.getUserMedia({
audio: { deviceId: "default" },
});
if (!audioCtxRef.current) {
audioCtxRef.current = new AudioContext({
sampleRate: USER_SPEECH_SAMPLE_RATE,
});
}
audioSourceRef.current =
audioCtxRef.current.createMediaStreamSource(stream);
await audioCtxRef.current.audioWorklet.addModule(
"linear-pcm-processor.worklet.js"
);
const audioWorkletNode = new AudioWorkletNode(
audioCtxRef.current,
"linear-pcm-processor"
);
audioSourceRef.current.connect(audioWorkletNode);
audioWorkletNode.connect(audioCtxRef.current.destination);
audioWorkletNode.port.onmessage = (e: MessageEvent<Int16Array>) => {
onAudioChunkAvailable(
e.data.buffer,
audioCtxRef.current!.sampleRate,
1
);
};
dispatch({
type: MicrophoneActionType.MIC_ACCESS_GRANTED,
});
} catch (e) {
logger.error(e);
onError(e as Error);
dispatch({
type: MicrophoneActionType.MIC_ACCESS_ERROR,
payload: e as Error,
});
}
}
async function startRecording() {
if (microphoneState.micAccessState !== MicAccessState.GRANTED) {
await requestAccess();
}
logger.log("resuming...");
audioCtxRef.current?.resume();
dispatch({
type: MicrophoneActionType.RECORDING_STARTED,
});
}
function stopRecording() {
audioCtxRef.current?.suspend();
dispatch({
type: MicrophoneActionType.RECORDING_STOPPED,
});
}
return {
microphoneState,
startRecording,
stopRecording,
source: audioSourceRef.current ?? null,
};
}