본문 바로가기
웹 오디오 프로그래밍

[Web Audio API #17] MediaRecorder를 이용한 녹음예제

by 영바이트 2021. 8. 13.

이전 포스팅에서 MediaRecorder 오디오 노드를 이용해서 마이크 입력 신호를 녹음하는 과정을 단계별로 살펴보았다. 이 과정들을 묶어서 전체 흐름을 살펴보는 것이 도움이 될 것 같다. 녹음을 위한 단계들을 요약하면 아래와 같다.

 

1. 녹음을 위한 소스source 스트림stream을 얻는다.

2. MediaRecorder 객체를 생성한다.

3. MediaRecorder의 dataavailable 이벤트 핸들러를 작성한다.

4. MediaRecorder의 stop 이벤트 핸들러를 작성한다.

5. MediaRecorder.start() 메서드를 호출해서 녹음을 시작한다.

6. MediaRecorder.start() 메서드를 호출해서 녹음을 종료한다.

 

먼저 녹음/종료 버튼을 포함하는 HTML 파트는 아래와 같다.

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Basic recording example</title>
</head>
<body>
    <div id="recording_control_div">
        <div>
            <button id="rec_stop_button" data-recording="false">녹음</button>
            <input type="checkbox" id="chk_hear_mic">
            <label for="chk_hear_mic">마이크 소리 듣기</label>
        </div>
    </div>
    <div id="sound_clips_div">
    </div>
</body>
<script> 
    // Web Audio API 로직
</script>
</html>

 

'녹음/종료' 버튼이 먼저 위치하고 그 옆에 '마이크 소리 듣기' 체크 박스를 만들었다. '마이크 소리 듣기'는 녹음 신호를 직접 듣고 확인해야할 필요가 있을 때 사용된다. 녹음 컨트롤 영역 아래에 'sound_clips_div'라는 이름으로 녹음 후에 녹음 내용을 들어볼 수 있도록 '재생/멈춤' 버튼 등이 생성될 자리를 만들어 놓았다.

 

아래는 웹 오디오 API 동작 로직이 기술되는 스크립트 파트이다. 코드의 길이가 길기 때문에 나누어서 설명하지만 아래 코드들은 모두 이어져 있다. 코드의 들여쓰기를 보면 계속 이어져 있는 코드를 단지 보기 좋도록 나누었음을 알 수 있다.

 

먼저 필요한 DOM 엘리먼트들을 가져오고 오디오 그래프 구성을 위한 오디오 노드들을 선언한다.

<script>
    window.onload = () => {
        const audioContext = new(window.AudioContext || window.webkitAudioContext)();

        const recStopButton = document.querySelector('#rec_stop_button');
        const chkHearMic = document.querySelector('#chk_hear_mic');

        //  recording: mic - stream - MediaRecorder
        let recordingStreamSource = null;
        let mediaRecorder = null;

        //  hear mic: mic - audio destination
        let chkHearMicMediaStreamSource = null;

        //  playing recorded audio: audio buffer source - destination
        let audioBufferSource = null;
		
        //  file name for uploading&saving
        let recordingFileName = null;

 

오디오 그래프는 아래와 같이 두 개의 갈래로 구성된다. 아래 그림에서 위쪽 가지는 마이크 입력을 이어폰 등으로 듣기 위한 가지이고, 아래쪽 가지는 마이크 입력이 녹음되는 흐름이다.

 

다음으로 서버에 파일을 업로드하기 위한 함수 postAudio를 구현하였다. postAudio 함수는 파일(blob)과 파일 이름을 인자로 받아서 이를 form에 포함시켜 서버로 전송하는 내용으로 구성되어 있다. 이전 포스팅에서도 살펴보았지만 보안 관계로 웹 브라우저 스크립트에서 생성한 파일을 클라이언트에 저장하지 못하도록 되어있다. 따라서 생성한 파일을 서버로 전송하여 저장한다. 

        const postAudio = (blob, fileName) => {
            //  post audio file
            formData = new FormData;
            formData.set('audio_data', blob, fileName);

            var headerData = new Headers();

            fetch('{{url_for("webaudioexp.postAudioData")}}', {
                method: 'POST',
                headers: headerData,
                body: formData,
                credentials: 'same-origin'
            }).then((response) => {
                return(response.json());
            }).then((resBody) => {
                return 0;
            }).catch((error) => {
                console.log('postAudio.error: ', error);
                return -1;
            });
        }   //  postAudio()

 

장치에 연결되어 있는 마이크 입력을 가져오고 이를 '마이크 소리 듣기', 그리고 녹음을 위해 MediaStream 오디오 노드와 연결한다.

        if(navigator.mediaDevices) {
            console.log('mediaDevices supported.');

            navigator.mediaDevices.getUserMedia({audio: true, video: false}).then((stream) => {
                console.log('mediaDevices.getUserMedia supported.');

                chkHearMic.addEventListener('click', (event) =>{
                    if(event.target.checked == true){
                        // 마이크 소리 듣기를 위한 오디오 그래프 생성
                        chkHearMicMediaStreamSource = audioContext.createMediaStreamSource(stream);
                        chkHearMicMediaStreamSource.connect(audioContext.destination);

                        if(audioContext.state == 'suspended'){
                            audioContext.resume();
                        }
                    }
                    else{
                        if(chkHearMicMediaStreamSource){
                            chkHearMicMediaStreamSource.disconnect();
                        }
                    }
                });

                // 마이크 입력 녹음을 위한 오디오 그래프 생성
                mediaRecorder = new MediaRecorder(stream);

 

MediaRecorder의 dataavailable 이벤트 핸들러를 아래와 같이 구현하였다. 이벤트가 발생할 때 마다 이벤트 내용이 되는 데이터를 배열에 저장하는 아주 간단한 내용으로 되어있다.

                var chunks = [];

                mediaRecorder.addEventListener('dataavailable', (event) => {
                    chunks.push(event.data);
                });

 

녹음이 종료되면(MediaRecorder.stop() 메서드가 호출되면) MediaRecorder의 stop 이벤트가 발생한다. MediaRecorder의 stop 이벤트 핸들러를 아래와 같이 구성하였다. 그 동안 쌓아왔던 데이터를 Binary object(Blob)로 만들고 이를 서버에 전송하도록(postAudio) 되어 있다. 그 다음으로 녹음한 오디오를 읽어서(audioURL) 들어볼 수 있도록 구현하였다.

                mediaRecorder.addEventListener('stop', (event) => {
                    var blob = new Blob(chunks, {'type': 'audio/ogg; codecs=opus'});

                    // 아래 5줄은 파일 이름 생성을 위한 부분이다.
                    var todayISOString = new Date().toISOString().substring(0, 10);
                    var min = 10000000;
                    var max = 99999999;
                    var rn = (Math.floor(Math.random()*(max - min + 1)) + min).toString();
                    var defaultClipName = "recording_" + rn + "_" + todayISOString;

                    recordingFileName = MediaRecorder.isTypeSupported('audio/ogg') ? (defaultClipName + '.ogg'): (defaultClipName + '.webm');

                    console.log('audioFileName: ', recordingFileName);

                    postAudio(blob, recordingFileName);
                    
                    var soundClipsDiv = document.querySelector('#sound_clips_div');

                    // 오디오가 녹음된 blob 객체의 URL을 생성한다.
                    var audioURL = URL.createObjectURL(blob);
                    
                    // audio 엘리먼트 생성
                    var audio = document.createElement('audio');
                    soundClipsDiv.appendChild(audio);

                    // 재생 시간 등 metadata를 load한다.
                    audio.setAttribute('preload', 'metadata');
                    // 재생/멈춤, 볼륨 조정등의 인터페이스를 생성한다.
                    audio.controls = true;
                    audio.src = audioURL;

                    // 녹음내용 재생을 위한 오디오 그래프를 생성한다.
                    audioBufferSource = audioContext.createMediaElementSource(audio);
                    audioBufferSource.connect(audioContext.destination);
                }); //  mediaRecorder.addEventListener('stop', ...)

 

웹 오디오 API 로직의 마지막 부분은 '녹음/종료' 버튼 이벤트이다. 녹음 대기 상태(recStopButton.dataset.recording == 'false')에서 버튼이 클릭되면 mediaRecorder.start() 메서드를 호출하여 녹음을 시작하고, 녹음 중일 때는(else) mediaRecorder.stop() 메서드를 호출하여 녹음을 종료하고 상태를 다시 녹음 가능 상태로 돌려놓는다.

                recStopButton.addEventListener('click', () => {

                    var audioControlsDiv = document.querySelector('#audio_controls_div');

                    if(audioControlsDiv){
                        audioControlsDiv.parentNode.removeChild(audioControlsDiv);
                    }

                    if(recStopButton.dataset.recording == 'false'){
                        recStopButton.dataset.recording = 'true';
                        recStopButton.innerHTML = '중지';

                        if(audioContext.state == 'suspended'){
                            audioContext.resume();
                        }

                        mediaRecorder.start();
                    }
                    else{
                        mediaRecorder.stop();

                        recStopButton.dataset.recording = 'false';
                        recStopButton.innerHTML = '녹음';
                    }
                });
            }).catch((error) => {
                console.log('[Error] navigator.mediaDevices.getUserMedia()', error);
            });
        }
    }
</script>

 

예제 코드의 페이지가 웹 브라우저에서 로딩되면 먼저 아래와 같이 마이크 사용권한을 요청하는 팝업이 생성된다.

 

녹음/종료 버튼을 클릭해서 녹음을 마치면 아래와 같이 녹음 내용을 들어볼 수 있는 오디오 컨트롤 UI가 나타나고 이를 이용해 녹음된 내용을 들어볼 수 있다.

 

분량 관계상 postAudio에 대응하는 서버 쪽 내용을 살펴보지 못한것이 아쉽다. 이어지는 포스팅에서는 서버 쪽 내용을 살펴보고 아울러 이번 포스팅에서 이야기하지 못했던 부분들도 좀 더 살펴보려한다.

 

댓글