基於AudioContext
和mediaDevices
實現的原生js的錄音功能
// recorder.js,這個是在網上找的,具體地址不記得了,這個存在一個問題就是,他分段之后會把audioData清空,導致最后結束的時候,audioData是一個空值,如果需要把整段的錄音轉化成一個音頻文件,不考慮分片的話,可以把onaudioprocess里面的sendData注釋掉,沒錯,我就是這樣搞的,只需要一個完整的音頻,如果需要分段傳送,就把注釋打開,然后作出對應的處理
const Recorder = function (stream, callback) {
const sampleBits = 16; //輸出采樣數位 8, 16
const sampleRate = 8000; //輸出采樣率
const context = new AudioContext();
const audioInput = context.createMediaStreamSource(stream);
const recorder = context.createScriptProcessor(4096, 1, 1);
const audioData = {
size: 0, //錄音文件長度
buffer: [], //錄音緩存
inputSampleRate: 48000, //輸入采樣率
inputSampleBits: 16, //輸入采樣數位 8, 16
outputSampleRate: sampleRate, //輸出采樣數位
oututSampleBits: sampleBits, //輸出采樣率
clear: function () {
this.buffer = [];
this.size = 0;
},
input: function (data) {
this.buffer.push(new Float32Array(data));
this.size += data.length;
},
compress: function () { //合並壓縮
//合並
const data = new Float32Array(this.size);
let offset = 0;
for (let i = 0; i < this.buffer.length; i++) {
data.set(this.buffer[i], offset);
offset += this.buffer[i].length;
}
//壓縮
const compression = parseInt(this.inputSampleRate / this.outputSampleRate);
const length = data.length / compression;
const result = new Float32Array(length);
let index = 0,
j = 0;
while (index < length) {
result[index] = data[j];
j += compression;
index++;
}
return result;
},
encodePCM: function () { //這里不對采集到的數據進行其他格式處理,如有需要均交給服務器端處理。
const sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
const sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
const bytes = this.compress();
const dataLength = bytes.length * (sampleBits / 8);
const buffer = new ArrayBuffer(dataLength);
const data = new DataView(buffer);
let offset = 0;
for (let i = 0; i < bytes.length; i++, offset += 2) {
const s = Math.max(-1, Math.min(1, bytes[i]));
data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
return new Blob([data], { 'type': 'audio/pcm' });
}
};
const sendData = function () { //對以獲取的數據進行處理(分包)
const reader = new FileReader();
reader.onload = e => {
const outbuffer = e.target.result;
// callback && callback(outbuffer);
const arr = new Int8Array(outbuffer);
if (arr.length > 0) {
let tmparr = new Int8Array(1024);
let j = 0;
for (let i = 0; i < arr.byteLength; i++) {
tmparr[j++] = arr[i];
if (((i + 1) % 1024) == 0) {
callback && callback(tmparr);
if (arr.byteLength - i - 1 >= 1024) {
tmparr = new Int8Array(1024);
} else {
tmparr = new Int8Array(arr.byteLength - i - 1);
}
j = 0;
}
if ((i + 1 == arr.byteLength) && ((i + 1) % 1024) != 0) {
callback && callback(tmparr);
}
}
}
};
reader.readAsArrayBuffer(audioData.encodePCM());
audioData.clear();//每次發送完成則清理掉舊數據
};
this.start = function () {
audioInput.connect(recorder);
recorder.connect(context.destination);
}
this.stop = function () {
recorder.disconnect();
}
this.getBlob = function () {
return audioData.encodePCM();
}
this.clear = function () {
audioData.clear();
}
recorder.onaudioprocess = function (e) {
const inputBuffer = e.inputBuffer.getChannelData(0);
audioData.input(inputBuffer);
// sendData();
}
}
export default Recorder;
/**
* 錄音組件
*/
// RecordItem.js
import React, { Component } from 'react';
import { Icon } from 'antd';
import { Toast } from 'antd-mobile';
import Recorder from './Recorder';
import './RecordItem.less';
class RecordItem extends Component {
state = {
isRecording: false, // 是否正在錄音
}
timer = null; // 判斷長按的定時器
handleTouchStart = () => {
this.timer = setTimeout(() => {
this.recorder.start();
this.setState({
isRecording: true
});
}, 300);
}
handleTouchEnd = () => {
if (this.timer) {
clearTimeout(this.timer);
}
this.recorder.stop();
this.setState({
isRecording: false
}, () => {
const { onEnd } = this.props;
onEnd && onEnd(this.recorder.getBlob());
});
}
// 處理錄音的回調
handleMsg = (data) => {
const { onProgress } = this.props;
onProgress && onProgress(data);
}
componentDidMount() {
const constraints = { audio: true };
navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
this.recorder = new Recorder(stream, this.handleMsg);
}, err => {
switch (err.message || err.name) {
case 'PERMISSION_DENIED':
case 'PermissionDeniedError':
Toast.info('用戶拒絕提供信息。');
break;
case 'NOT_SUPPORTED_ERROR':
case 'NotSupportedError':
Toast.info('瀏覽器不支持硬件設備。');
break;
case 'MANDATORY_UNSATISFIED_ERROR':
case 'MandatoryUnsatisfiedError':
Toast.info('無法發現指定的硬件設備。');
break;
default:
Toast.info('無法打開麥克風。異常信息:' + (err.code || err.name));
break;
}
});
// this.recorder = new Recorder({
// callback: this.handleMsg
// });
}
render() {
const { isRecording } = this.state;
return (
<div
className={`RecordItem ${isRecording ? 'recording' : ''}`}
onTouchStart={this.handleTouchStart}
onTouchEnd={this.handleTouchEnd}
>
<Icon type="audio" />
</div>
);
}
}
export default RecordItem;
// RecordItem.less
.RecordItem {
color: #333333;
&.recording {
color: #10C0DC;
}
.anticon {
font-size: 1.5rem;
}
}