Skip to content

Commit

Permalink
[NDD-309] 목소리 사운드 바 기능 추가 (4h/3h) (#161)
Browse files Browse the repository at this point in the history
* feat: microphone의 volume을 통제 할 수 있는 util 함수 구현

* feat: useMedia hook 에서 인자로 받아 VolumeStatus 컴포넌트 구현

1. createMicrophoneAudioController 적용을 통해서 해당 컴포넌트 "내부"에서만 mic 요소 연결
2. useMedia hook 을 통해서 해당 요소에 media 속성 연결
3. 2번을 수행하고 나니 불필요하게 useMedia를 통해서 두번 요청하는것이 아닌가...? 라는 생각이 들어서 media에 대한 전역상태를 선언하는것이 필요할것이란 생각이 들었음, 이번엔 적용하지 않지만 꼭 진행할 예정
4. UI 상 우리의 테마 컬러에 따라서 동적으로 div 태그의 높이를 조절하도록 구현 + (색)

* feat: Header 내부에서 volume에 대한 적용 완료

* chore: audioMonitor의 주석 추가
  • Loading branch information
adultlee authored Dec 6, 2023
1 parent f9d2215 commit 0fe75b7
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { css } from '@emotion/react';
import { theme } from '@styles/theme';

import { RecordStatus, IntervieweeName, RecordTimer } from './index';
import {
RecordStatus,
IntervieweeName,
RecordTimer,
VolumeStatus,
} from './index';

type InterviewHeaderProps = {
isRecording: boolean;
};
Expand All @@ -22,7 +28,15 @@ const InterviewHeader: React.FC<InterviewHeaderProps> = ({ isRecording }) => {
>
<RecordStatus isRecording={isRecording} />
<IntervieweeName />
<RecordTimer isRecording={isRecording} />
<div
css={css`
display: flex;
gap: 1.875rem;
`}
>
<RecordTimer isRecording={isRecording} />
<VolumeStatus />
</div>
</div>
);
};
Expand Down
69 changes: 69 additions & 0 deletions FE/src/components/interviewPage/InterviewHeader/VolumeStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import useMedia from '@hooks/useMedia';
import { createMicrophoneVolumeMonitor } from '@/utils/media';
import { useEffect, useState } from 'react';
import { css } from '@emotion/react';

const VolumeStatus: React.FC = () => {
const { media, startMedia, stopMedia } = useMedia();
const [audioVolume, setAudioVolume] = useState<number>(0);

useEffect(() => {
if (!media) {
void startMedia();
return;
}

const { startMonitoring, stopMonitoring } = createMicrophoneVolumeMonitor(
media,
setAudioVolume
);

startMonitoring();

return () => {
stopMonitoring();
stopMedia();
};
}, [media, startMedia, stopMedia]);

const getVolumeDivColor = () => {
const green = Math.floor(Math.random() * 256)
.toString(16)
.padStart(2, '0');

return `#00${green}FF`;
};

const colors = Array.from({ length: 6 }, getVolumeDivColor);

const getRandomHeight = () => {
return Math.max(10, audioVolume * (0.5 + Math.random() / 2));
};

return (
<div
css={css`
display: flex;
align-items: end;
justify-content: space-between;
width: 30px;
height: 30px;
`}
>
{colors.map((color, index) => (
<div
key={index}
css={css`
width: 4px;
background-color: ${color};
height: ${getRandomHeight()}%;
transition:
height 0.3s ease,
background-color 0.3s ease;
`}
/>
))}
</div>
);
};
export default VolumeStatus;
1 change: 1 addition & 0 deletions FE/src/components/interviewPage/InterviewHeader/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as IntervieweeName } from './IntervieweeName';
export { default as RecordStatus } from './RecordStatus';
export { default as RecordTimer } from './RecordTimer';
export { default as VolumeStatus } from './VolumeStatus';
55 changes: 55 additions & 0 deletions FE/src/utils/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,58 @@ export const getSupportedMimeTypes = () => {
];
return types.filter((type) => MediaRecorder.isTypeSupported(type));
};

// 마이크 입력 스트림의 볼륨을 모니터링하는 함수를 정의합니다.
export const createMicrophoneVolumeMonitor = (
stream: MediaStream, // 마이크 입력 스트림
volumeCallback: React.Dispatch<React.SetStateAction<number>> // 볼륨 업데이트 콜백
) => {
// 오디오 컨텍스트를 생성하고 입력 스트림을 오디오 컨텍스트에 연결합니다.
const audioContext = new AudioContext();
const sourceNode = audioContext.createMediaStreamSource(stream);
const analyser = audioContext.createAnalyser();

// FFT 사이즈를 설정합니다. 이 값은 PCM 데이터의 길이를 결정합니다.
analyser.fftSize = 2048;

// 소스 노드를 분석기에 연결하고 PCM 데이터를 저장할 배열을 생성합니다.
sourceNode.connect(analyser);
const pcmData = new Float32Array(analyser.fftSize);

let intervalId: NodeJS.Timer | number;

// 모니터링을 시작하는 함수를 정의합니다.
const startMonitoring = () => {
// 주기적으로 볼륨을 체크하기 위한 인터벌을 설정합니다. -> 마이크 입력에 따라 해당 함수가 호출되는것이 아닌, 주기적으로 현재 마이크 상태를 반환하는것
intervalId = setInterval(() => {
analyser.getFloatTimeDomainData(pcmData);

// PCM 데이터를 통해 제곱합을 계산합니다.
const sumSquares = pcmData.reduce((sum, value) => sum + value * value, 0);
const rms = Math.sqrt(sumSquares / pcmData.length);

// 볼륨을 증폭하기 위한 가중치를 설정합니다.
const weight = 7;
const amplifiedVolume = rms * weight;

// 볼륨을 0에서 100 사이로 정규화합니다.
const normalizedVolume = Math.min(Math.round(amplifiedVolume * 100), 100);

// 볼륨을 업데이트 하는 setState 함수를 실행시킵니다.
volumeCallback(normalizedVolume);
}, 150);
};

// 모니터링을 중지하는 함수를 정의합니다.
const stopMonitoring = () => {
// 인터벌 타이머가 설정되어 있으면 해제합니다.
if (intervalId) {
clearInterval(intervalId as number);
}
sourceNode.disconnect();
analyser.disconnect();
void audioContext.close();
};

return { startMonitoring, stopMonitoring };
};

0 comments on commit 0fe75b7

Please sign in to comment.