- 初始化录音
import recordWorker from './transcode.worker'
const URL = window.URL || window.webkitURL
let workerString = recordWorker.toString()
workerString = workerString.substr(workerString.indexOf("{") + 1)
workerString = workerString.substr(0, workerString.lastIndexOf("}"))
const workerBlob = new Blob([workerString])
const workerURL = URL.createObjectURL(workerBlob)
const worker = new Worker(workerURL)
class IatRecorder {
constructor(config) {
this.status = "null"
this.audioData = []
this.buffer = []
worker.onmessage = (event) => {
this.audioData.push(...event.data)
console.log('处理后的数据长度', this.audioData.length)
}
}
setStatus (status) {
this.onWillStatusChange &&
this.status !== status &&
this.onWillStatusChange(this.status, status)
this.status = status
}
recorderInit () {
navigator.getUserMedia =
navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia
try {
this.audioContext = new (window.AudioContext ||
window.webkitAudioContext)()
console.log('audioContext sampleRate:', this.audioContext.sampleRate)
console.log('UA', navigator.userAgent)
if (!this.audioContext) {
alert("浏览器不支持webAudioApi相关接口,请前往Chrome,FireFox浏览器体验")
return
}
} catch (e) {
if (!this.audioContext) {
alert("浏览器不支持webAudioApi相关接口,请前往Chrome,FireFox浏览器体验")
return
}
}
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices
.getUserMedia({
audio: true,
video: false,
})
.then((stream) => {
getMediaSuccess(stream)
})
.catch((e) => {
getMediaFail(e)
})
} else if (navigator.getUserMedia) {
navigator.getUserMedia(
{
audio: true,
video: false,
},
(stream) => {
getMediaSuccess(stream)
},
function (e) {
getMediaFail(e)
}
)
} else {
if (
navigator.userAgent.toLowerCase().match(/chrome/) &&
location.origin.indexOf("https://") < 0
) {
alert(
"chrome下获取浏览器录音功能,因为安全性问题,需要在localhost或127.0.0.1或https下才能获取权限"
)
} else {
alert("无法获取浏览器录音功能,请升级浏览器或使用chrome")
}
this.audioContext && this.audioContext.close()
return
}
let getMediaSuccess = (stream) => {
this.scriptProcessor = this.audioContext.createScriptProcessor(
4096,
1,
1
)
this.scriptProcessor.onaudioprocess = (e) => {
this.buffer.push(...e.inputBuffer.getChannelData(0))
console.log('录音原始数据长度,类型为 Float32Array', e.inputBuffer.getChannelData(0).length)
worker.postMessage({
command: "transform",
buffer: e.inputBuffer.getChannelData(0),
is16K: this.audioContext.sampleRate,
})
}
this.mediaSource = this.audioContext.createMediaStreamSource(stream)
this.mediaSource.connect(this.scriptProcessor)
this.scriptProcessor.connect(this.audioContext.destination)
};
let getMediaFail = () => {
alert("请求麦克风失败")
this.audioContext && this.audioContext.close()
this.audioContext = undefined
}
}
recorderStart () {
this.setStatus("ing")
this.recorderStop()
this.recorderInit()
}
recorderStop (e) {
this.setStatus("end")
this.audioContext && this.audioContext.close()
this.audioContext = undefined
}
getBuffer () {
let output = this.to16BitPCM(this.buffer)
return output
}
to16BitPCM (input) {
var dataLength = input.length * (16 / 8)
var dataBuffer = new ArrayBuffer(dataLength)
var dataView = new DataView(dataBuffer)
var offset = 0
for (var i = 0; i < input.length; i++, offset += 2) {
var s = Math.max(-1, Math.min(1, input[i]))
dataView.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true)
}
return dataView
}
download () {
const blob = new Blob([this.getBuffer()])
if ("msSaveOrOpenBlob" in navigator) {
window.navigator.msSaveOrOpenBlob(blob, "test.pcm")
return
}
const elink = document.createElement("a")
elink.download = "test.pcm"
elink.style.display = "none"
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href)
document.body.removeChild(elink)
}
}
export default IatRecorder
- 利用 worker 处理数据
const recordWorker = function () {
let self = this;
this.onmessage = function (e) {
switch (e.data.command) {
case "transform":
transform.transaction(e.data, e.data.is16K);
break;
}
};
let transform = {
transaction(audioData, is16K) {
let output;
if (is16K === 16000) {
output = transform.to16BitPCM(audioData.buffer, is16K);
} else if (is16K > 16000) {
output = transform.to16kHz(audioData.buffer, is16K);
output = transform.to16BitPCM(output);
} else if (is16K < 16000) {
output = transform.lessTo16kHz(audioData.buffer, is16K);
output = transform.to16BitPCM(output);
}
output = Array.from(new Uint8Array(output.buffer));
self.postMessage(output);
},
to16kHz(audioData, originalSampleRate) {
if (originalSampleRate <= 16000) {
return audioData;
}
let data = new Float32Array(audioData);
let fitCount = Math.round(data.length * (16000 / originalSampleRate));
let newData = new Float32Array(fitCount);
let springFactor = (data.length - 1) / (fitCount - 1);
newData[0] = data[0];
for (let i = 1; i < fitCount - 1; i++) {
let tmp = i * springFactor;
let before = Math.floor(tmp).toFixed();
let after = Math.ceil(tmp).toFixed();
let atPoint = tmp - before;
newData[i] = data[before] + (data[after] - data[before]) * atPoint;
}
newData[fitCount - 1] = data[data.length - 1];
return newData;
},
lessTo16kHz(audioData, originalSampleRate) {
if (originalSampleRate >= 16000) {
return audioData;
}
const upsampleFactor = 16000 / originalSampleRate;
const newLength = Math.ceil(audioData.length * upsampleFactor);
const newData = new Float32Array(newLength);
for (let i = 0; i < newLength; i++) {
const originalIndex = Math.floor(i / upsampleFactor);
newData[i] = audioData[originalIndex];
}
return newData;
},
to16BitPCM(input) {
let dataLength = input.length * (16 / 8);
let dataBuffer = new ArrayBuffer(dataLength);
let dataView = new DataView(dataBuffer);
let offset = 0;
for (let i = 0; i < input.length; i++, offset += 2) {
let s = Math.max(-1, Math.min(1, input[i]));
dataView.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
}
return dataView;
},
to8BitPCM(input) {
var dataLength = input.length;
var dataBuffer = new ArrayBuffer(dataLength);
var dataView = new DataView(dataBuffer);
var offset = 0;
for (var i = 0; i < input.length; i++, offset++) {
var s = Math.max(-1, Math.min(1, input[i]));
var val = s < 0 ? s * 0x80 : s * 0x7F;
var intVal = Math.round(val);
dataView.setInt8(offset, intVal);
}
return dataView;
},
};
};
export default recordWorker;
- 调用示例
<script setup>
import { onMounted } from 'vue';
import recorder from './utils/recorder';
let audioRecorder = null;
const initializeRecorder = () => {
audioRecorder = new recorder();
console.log('Recorder initialized:', audioRecorder);
}
const startRecording = () => {
console.log('开始录音');
if (!audioRecorder) {
console.error('Recorder not initialized!');
return;
}
audioRecorder.recorderStart();
}
const stopRecording = () => {
console.log('结束录音');
if (!audioRecorder) {
console.error('Recorder not initialized!');
return;
}
audioRecorder.recorderStop();
}
const download = () => {
if (audioRecorder.status !== 'end') {
alert('请完成录音后点击下载')
return;
}
audioRecorder.download()
}
onMounted(() => {
initializeRecorder();
});
</script>
<template>
<div id="app">
<button @click="startRecording">开始录音</button>
<br/>
<br/>
<button @click="stopRecording">结束录音</button>
<br/>
<br/>
<button @click="download">下载录音文件 PCM</button>
</div>
</template>
<style scoped>
</style>