출처 바로가기
자바스크립트에서 마이크를 사용하려면 먼저 사용자로부터 마이크 사용 권한을 획득해야 합니다.
여기서 사용자가 허용 버튼을 눌러야 이후 녹음 작업이 진행이 됩니다.
이 부분은 navigator.mediaDevices.getUserMedia(constraints).then(...)
을 사용합니다.
if (navigator.mediaDevices) { const constraints = { audio: true } navigator.mediaDevices.getUserMedia(constraints) .then(stream => { ................. }) .catch(err => { console.log('The following error occurred: ' + err) }) }
마이크 소리를 실시간으로 들으려는 경우 오디오 컨텍스트를 사용하며, 마이크 소리를 녹음한 경우 저장하려는 경우 <span class="pl-k">new</span><span> </span><span class="pl-v">MediaRecorder</span><span class="pl-kos">(</span><span class="pl-s1">stream</span>
)
을 사용합니다.
미디어리코더에서 .start()
를 하면 녹음을 시작하고, .stop()
을 하면 녹음이 종료됩니다.
아래 코드는 녹음이 진행되는 동안 데이터를 배열에 저장하는 작업입니다.
mediaRecorder.ondataavailable = e => { chunks.push(e.data) }
이 배열을 블롭 데이터로 변환한 후 <audio>
의 src
에 적용하면 녹음 파일이 재생됩니다.
const audio = document.createElement('audio') ... audio.controls = true const blob = new Blob(chunks, { 'type': 'audio/ogg codecs=opus' }) ... const audioURL = URL.createObjectURL(blob) audio.src = audioURL
이것을 종합한 예제 코드는 아래와 같습니다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>마이크 테스트</title> | |
</head> | |
<body> | |
<input type=checkbox id="chk-hear-mic"><label for="chk-hear-mic">마이크 소리 듣기</label> | |
<button id="record">녹음</button> | |
<button id="stop">정지</button> | |
<div id="sound-clips"></div> | |
<script> | |
const record = document.getElementById("record") | |
const stop = document.getElementById("stop") | |
const soundClips = document.getElementById("sound-clips") | |
const chkHearMic = document.getElementById("chk-hear-mic") | |
const audioCtx = new(window.AudioContext || window.webkitAudioContext)() // 오디오 컨텍스트 정의 | |
const analyser = audioCtx.createAnalyser() | |
// const distortion = audioCtx.createWaveShaper() | |
// const gainNode = audioCtx.createGain() | |
// const biquadFilter = audioCtx.createBiquadFilter() | |
function makeSound(stream) { | |
const source = audioCtx.createMediaStreamSource(stream) | |
source.connect(analyser) | |
// analyser.connect(distortion) | |
// distortion.connect(biquadFilter) | |
// biquadFilter.connect(gainNode) | |
// gainNode.connect(audioCtx.destination) // connecting the different audio graph nodes together | |
analyser.connect(audioCtx.destination) | |
} | |
if (navigator.mediaDevices) { | |
console.log('getUserMedia supported.') | |
const constraints = { | |
audio: true | |
} | |
let chunks = [] | |
navigator.mediaDevices.getUserMedia(constraints) | |
.then(stream => { | |
const mediaRecorder = new MediaRecorder(stream) | |
chkHearMic.onchange = e => { | |
if(e.target.checked == true) { | |
audioCtx.resume() | |
makeSound(stream) | |
} else { | |
audioCtx.suspend() | |
} | |
} | |
record.onclick = () => { | |
mediaRecorder.start() | |
console.log(mediaRecorder.state) | |
console.log("recorder started") | |
record.style.background = "red" | |
record.style.color = "black" | |
} | |
stop.onclick = () => { | |
mediaRecorder.stop() | |
console.log(mediaRecorder.state) | |
console.log("recorder stopped") | |
record.style.background = "" | |
record.style.color = "" | |
} | |
mediaRecorder.onstop = e => { | |
console.log("data available after MediaRecorder.stop() called.") | |
const clipName = prompt("오디오 파일 제목을 입력하세요.", new Date()) | |
const clipContainer = document.createElement('article') | |
const clipLabel = document.createElement('p') | |
const audio = document.createElement('audio') | |
const deleteButton = document.createElement('button') | |
clipContainer.classList.add('clip') | |
audio.setAttribute('controls', '') | |
deleteButton.innerHTML = "삭제" | |
clipLabel.innerHTML = clipName | |
clipContainer.appendChild(audio) | |
clipContainer.appendChild(clipLabel) | |
clipContainer.appendChild(deleteButton) | |
soundClips.appendChild(clipContainer) | |
audio.controls = true | |
const blob = new Blob(chunks, { | |
'type': 'audio/ogg codecs=opus' | |
}) | |
chunks = [] | |
const audioURL = URL.createObjectURL(blob) | |
audio.src = audioURL | |
console.log("recorder stopped") | |
deleteButton.onclick = e => { | |
evtTgt = e.target | |
evtTgt.parentNode.parentNode.removeChild(evtTgt.parentNode) | |
} | |
} | |
mediaRecorder.ondataavailable = e => { | |
chunks.push(e.data) | |
} | |
}) | |
.catch(err => { | |
console.log('The following error occurred: ' + err) | |
}) | |
} | |
</script> | |
</body></html> |
https://gist.github.com/ayaysir/12c367a9f699f88ad59fbd47d794f745
9개의 댓글
cheyrl choi · 2020년 12월 9일 2:10 오전
안녕하세요 유익한 정보 감사합니다. 현재 한국어학습프로그램을 만들고 있는 학생입니다.
질문이 있어서 글 남깁니다.
제가 진행하고 있는 프로그램 상 PCM으로 저장을해야 하는데
혹시 위 코드에서 저장방법만 바꿀 수도 있을까요?
답변주신다면 정말 큰 도움이 될 것 같습니다ㅠ
감사합니다!
yoonbumtae (BGSMM) · 2020년 12월 9일 1:54 오후
안녕하세요.
찾아봤는데 위의 코드에서 ogg를 pcm 으로 바꿀 수 있는 방법은 없는 것 같습니다.
Ogg 배열을 pcm으로 바꾸는 함수를 추가하거나 외부 라이브러리를 사용해야 할 것 같은데 라이브러리 사용을 추천드립니다.
https://stackoverflow.com/questions/51687308/how-to-use-web-audio-api-to-get-raw-pcm-audio
링크 확인 부탁드립니다.
감사합니다.
cheryl choi · 2020년 12월 9일 6:35 오후
답변주셔서 정말 감사합니다.
안내해주신 링크로 코딩해보니 pcm으로 인코딩된 wav형식으로 저장이 되네요!
연결하려는 발음평가 api가 pcm형태만을 지원하고 있어서 변환하는 작업이 필요할듯합니다.
도움주셔서 감사합니다!
gunny · 2020년 12월 29일 8:54 오후
좋은 소스 정말 감사합니다 .
한가지 여쭤보고 싶은데 저장된 음성파일을 다운로드하려고하면 type이 audio/ogg로 되어있음에도 oga형식으로 다운로드가 되는데 ogg형식으로 다운로드하는방법이 없을까요 ?
yoonbumtae (BGSMM) · 2020년 12월 29일 9:16 오후
안녕하세요.
제가 위의 코드로 파일 저장 테스트를 해봤는데요
제 컴퓨터애서는(크롬 브라우저)는 ogg 확장자로 저장이 됩니다.
아마 브라우저나 OS 차이인 것 같습니다.
파일 포맷은 ogg audio 로 동일하니 (링크) 다운받은 파일 확장자를 바꾸셔도 재생에는 문제가 없을 것 같고
저장 확장자를 ogg 로 강제하는 방법은 제가 몰라서 더 알아보도록 하겠습니다.
감사합니다.
gunny · 2020년 12월 29일 9:05 오후
좋은 글 감사합니다.
good · 2021년 7월 19일 5:21 오후
최고입니다!감사합니다
rrj · 2021년 8월 4일 3:00 오후
녹음된 파일을 바로 컨트롤러에서 처리하고싶은데 데이터보낼때랑 받을때 어떻게 처리하면될까요?
yoonbumtae (BGSMM) · 2021년 8월 4일 7:24 오후
위 코드 예제에서
const blob
이 부분을formData
에 담아서 폼 전송이나 ajax로 백엔드 컨트롤러로 보내면 된다고 생각합니다. 백엔드 컨트롤러는 바이너리 타입을 받을수 있게 하고요