| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149 |
- // AudioManager.js (오디오 처리)
- import {state} from "./state.js";
- import {WebSocketManager} from "./WebSocketManager.js";
- import {UIManager} from "./UIManager.js";
- export const AudioManager = {
- async initWAVMediaRecorder() {
- let audioChunks = [];
- let speaking = false;
- let silenceStart = 0;
- const SILENCE_THRESHOLD = 0.02; // 음성 감지 임계값
- const SILENCE_DURATION = 1000; // 묵음 유지 시간(ms)
-
- try {
- // 마이크 스트림 생성
- const userMedia = await navigator.mediaDevices.getUserMedia({audio: true});
- const audioContext = new AudioContext({sampleRate: 16000}); // 16kHz 샘플레이트
- const mediaStreamSource = audioContext.createMediaStreamSource(userMedia);
- const processor = audioContext.createScriptProcessor(4096, 1, 1);
- let isTransmitting = false;
-
- processor.onaudioprocess = (event) => {
- if (!state.isRecording || state.websocket.readyState !== WebSocket.OPEN) return;
-
- const inputData = event.inputBuffer.getChannelData(0); // Float32Array
- const rms = Math.sqrt(inputData.reduce((sum, sample) => sum + sample * sample, 0) / inputData.length);
- const pcmData = getPCMData(inputData); // Int16Array로 변환
- console.log('### rms', new Date().toLocaleTimeString(), rms);
- if (rms > SILENCE_THRESHOLD) {
- speaking = true;
- audioChunks.push(pcmData);
- isTransmitting = false; // 음성 다시 감지되면 전송 상태 해제
- silenceStart = 0;
- } else if (speaking && !isTransmitting) {
- if (silenceStart === 0) silenceStart = Date.now();
- if (Date.now() - silenceStart > SILENCE_DURATION) {
- isTransmitting = true; // 전송 중 표시
- const totalLength = audioChunks.reduce((sum, chunk) => sum + chunk.length, 0);
- const combinedData = new Int16Array(totalLength);
- let offset = 0;
- for (const chunk of audioChunks) {
- combinedData.set(chunk, offset);
- offset += chunk.length;
- }
- // 서버로 전송
- WebSocketManager.sendData(combinedData.buffer);
- console.log("묵음 감지! 서버로 전송");
- audioChunks = [];
- speaking = false;
- silenceStart = 0;
- } else {
- audioChunks.push(pcmData);
- }
- }
- };
- // 노드 연결
- mediaStreamSource.connect(processor);
- processor.connect(audioContext.destination);
- // 상태 저장
- state.isRecording = true;
- state.userMedia = userMedia;
- state.audioContext = audioContext;
- state.mediaStreamSource = mediaStreamSource;
- state.processor = processor;
- } catch (error) {
- console.error("Failed to initialize WAV Media Recorder:", error);
- await AudioManager.cleanup(); // 초기화 실패 시 자원 정리
- }
- },
- async cleanup() {
- state.isRecording = false;
- // 5초 간격 전송 타이머 해제
- if (state.sendInterval) {
- clearInterval(state.sendInterval);
- state.sendInterval = null;
- }
- // 1. ScriptProcessorNode 연결 해제
- if (state.processor) {
- state.processor.disconnect(); // 모든 연결 끊기
- state.processor.onaudioprocess = null; // 이벤트 핸들러 제거
- state.processor = null;
- }
- // MediaRecorder 정리
- if (state.mediaRecorder) {
- state.mediaRecorder.stop();
- state.mediaRecorder.ondataavailable = null; // 이벤트 핸들러 제거
- state.mediaRecorder.onstop = null;
- if (state.mediaRecorder.stream) {
- state.mediaRecorder.stream.getTracks().forEach(track => track.stop());
- }
- state.mediaRecorder = null;
- }
- // 2. MediaStreamAudioSourceNode 연결 해제 (필요 시)
- if (state.mediaStreamSource) {
- state.mediaStreamSource.disconnect();
- state.mediaStreamSource = null;
- }
- // 3. AudioContext 종료
- if (state.audioContext && state.audioContext.state !== "closed") {
- await state.audioContext.close();
- state.audioContext = null;
- }
- // 4. MediaStream 트랙 종료
- if (state.userMedia) {
- state.userMedia.getTracks().forEach(track => track.stop());
- state.userMedia = null;
- }
- // AudioWorklet 정리
- if (state.audioWorkletNode) {
- state.audioWorkletNode.port.postMessage('stop');
- state.audioWorkletNode.disconnect();
- state.audioWorkletNode = null;
- }
- // 기타 상태 초기화
- if (state.silenceTimeout) {
- clearTimeout(state.silenceTimeout);
- state.silenceTimeout = null;
- }
- state.audioBuffer = [];
- console.log('AudioManager fully destroyed');
- },
- };
- const getPCMData = (inputBuffer) => {
- const pcmData = new Int16Array(inputBuffer.length);
- // Float32를 Int16으로 변환 (클리핑 포함)
- for (let i = 0; i < inputBuffer.length; i++) {
- const sample = Math.max(-1, Math.min(1, inputBuffer[i])); // 클리핑
- pcmData[i] = sample * 32767; // -32768 ~ 32767 범위로 스케일링
- }
- // 원시 PCM 데이터를 ArrayBuffer로 변환
- return pcmData;
- }
|