基于react的录音及音频曲线绘制的组件开发

##简介 css

这里写图片描述

演示地址react

最近因为工做须要,须要在react上用到一个录音的功能,录音主要包含开始录音,暂停录音,中止录音,并将频谱经过canvas绘制出来。起初开发时找了一个现成的包,可是这个第三方的包不支持暂停功能,也不支持音频转码,只能输出audio/webm格式,因此本身在周末决定从新写一个关于react录音的插件。 ##使用 目前这个包已经上传至npm,须要用的同窗能够运行指令git

npm install react-audio-analyser --save
复制代码

下载到本地,更多详细的使用方法请看这里。欢迎你们使用,也但愿多多提issue。有兴趣的同窗能够继续往下看,文章接下来会详细讲述一下录音的实现及开发过程。 ##项目简介(react-audio-analyser)github

项目自己主要在2个文件夹,component就是组件react-audio-analyser存放的位置。 ###component:web

  • audioConvertWav.js audio/webm转audio/wav
  • index.js 外层的index.js用于暴露组件,内层index为组件的容器(组建自己)
  • MediaRecorder.js 组件录音主要处理逻辑。
  • RenderCanvas.js 音频曲线绘制处理逻辑。
  • index.css 暂未启用 ###demo:
  • demo主要用于对组件的演示,主要包含控制按钮(开始,暂停,结束)的渲染,及逻辑处理。

###react-audio-analyser index.jsnpm

import React, {Component} from "react";
import MediaRecorder from "./MediaRecorder";
import RenderCanvas from "./RenderCanvas";
import "./index.css";

@MediaRecorder
@RenderCanvas
class AudioAnalyser extends Component {

    componentDidUpdate(prevProps) { // 检测传入status的变化
        if (this.props.status !== prevProps.status) {
            const event = {
                inactive: this.stopAudio,
                recording: this.startAudio,
                paused: this.pauseAudio
            }[this.props.status];
            event && event();
        }
    }

    render() {
        const {
            children, className, audioSrc
        } = this.props;
        return (
            <div className={className}>
                <div>
                    {this.renderCanvas()} // canvas 渲染
                </div>
                {children} // 控制按钮
                {
                    audioSrc &&
                    <div>
                        <audio controls src={audioSrc}/>
                    </div>
                }
            </div>
        );
    }
}

AudioAnalyser.defaultProps = {
    status: "", //组件状态
    audioSrc: "", //音频资源URL
    backgroundColor: "rgba(0, 0, 0, 1)", //背景色
    strokeColor: "#ffffff", //音频曲线颜色
    className: "audioContainer", //样式类
    audioBitsPerSecond: 128000, //音频码率
    audioType: "audio/webm", //输出格式
    width: 500, //canvas宽
    height: 100 //canvas高
};

export default AudioAnalyser;
复制代码

组件的大致思路是,在src/component/AudioAnalyser/index.js 中渲染音频canvas,以及经过插槽的方式去将控制按钮渲染进来,这样作的好处是,使用组件的人能够自主的控制按钮样式,也暴露了组件的样式类,供父级传入新的样式类来修改整个组件的样式。 所以关于组件的开始,暂停,中止等状态的触发,也是由具体使用组件时提供的按钮来改变状态,传入组件,组件自己经过对props的更改来触发相关的钩子。canvas

组件挂载了2个装饰器,分别是MediaRecorder,RenderCanvas这两个装饰器分别用于处理音频逻辑和渲染canvas曲线。装饰器自己继承了当前挂载的类,使得上下文被打通,更有利于属性方法的调用。 ###MediaRecorderapi

/**
 * @author j_bleach 2018/8/18
 * @describe 媒体记录(包含开始,暂停,中止等媒体流及回调操做)
 * @param Target 被装饰类(AudioAnalyser)
 */
import convertWav from "./audioConvertWav";

const MediaRecorderFn = Target => {
    const constraints = {audio: true};
    return class MediaRecorderClass extends Target {
        static audioChunk = [] // 音频信息存储对象
        static mediaRecorder = null // 媒体记录对象
        static audioCtx = new (window.AudioContext || window.webkitAudioContext)(); // 音频上下文

        constructor(props) {
            super(props);
            MediaRecorderClass.compatibility();
            this.analyser = MediaRecorderClass.audioCtx.createAnalyser();
        }

        /**
         * @author j_bleach 2018/08/02 17:06
         * @describe 浏览器navigator.mediaDevices兼容性处理
         */
        static compatibility() {
            const promisifiedOldGUM = (constraints) => {
                // First get ahold of getUserMedia, if present
                const getUserMedia =
                    navigator.getUserMedia ||
                    navigator.webkitGetUserMedia ||
                    navigator.mozGetUserMedia;

                // Some browsers just don't implement it - return a rejected promise with an error // to keep a consistent interface if (!getUserMedia) { return Promise.reject( new Error("getUserMedia is not implemented in this browser") ); } // Otherwise, wrap the call to the old navigator.getUserMedia with a Promise return new Promise(function (resolve, reject) { getUserMedia.call(navigator, constraints, resolve, reject); }); }; // Older browsers might not implement mediaDevices at all, so we set an empty object first if (navigator.mediaDevices === undefined) { navigator.mediaDevices = {}; } // Some browsers partially implement mediaDevices. We can't just assign an object
            // with getUserMedia as it would overwrite existing properties.
            // Here, we will just add the getUserMedia property if it's missing. if (navigator.mediaDevices.getUserMedia === undefined) { navigator.mediaDevices.getUserMedia = promisifiedOldGUM; } } /** * @author j_bleach 2018/8/19 * @describe 验证函数,若是存在即执行 * @param fn: function 被验证函数 * @param e: object 事件对象 event object */ static checkAndExecFn(fn, e) { typeof fn === "function" && fn(e) } /** * @author j_bleach 2018/8/19 * @describe 音频流转blob对象 * @param type: string 音频的mime-type * @param cb: function 录音中止回调 */ static audioStream2Blob(type, cb) { let wavBlob = null; const chunk = MediaRecorderClass.audioChunk; const audioWav = () => { let fr = new FileReader(); fr.readAsArrayBuffer(new Blob(chunk, {type})) let frOnload = (e) => { const buffer = e.target.result MediaRecorderClass.audioCtx.decodeAudioData(buffer).then(data => { wavBlob = new Blob([new DataView(convertWav(data))], { type: "audio/wav" }) MediaRecorderClass.checkAndExecFn(cb, wavBlob); }) } fr.onload = frOnload } switch (type) { case "audio/webm": MediaRecorderClass.checkAndExecFn(cb, new Blob(chunk, {type})); break; case "audio/wav": audioWav(); break; default: return void 0 } } /** * @author j_bleach 2018/8/18 * @describe 开始录音 */ startAudio = () => { const recorder = MediaRecorderClass.mediaRecorder; if (!recorder || (recorder && recorder.state === "inactive")) { navigator.mediaDevices.getUserMedia(constraints).then(stream => { this.recordAudio(stream); }).catch(err => { throw new Error("getUserMedia failed:", err); } ) return false } if (recorder && recorder.state === "paused") { MediaRecorderClass.resumeAudio(); } } /** * @author j_bleach 2018/8/19 * @describe 暂停录音 */ pauseAudio = () => { const recorder = MediaRecorderClass.mediaRecorder; if (recorder && recorder.state === "recording") { recorder.pause(); recorder.onpause = () => { MediaRecorderClass.checkAndExecFn(this.props.pauseCallback); } MediaRecorderClass.audioCtx.suspend(); } } /** * @author j_bleach 2018/8/18 * @describe 中止录音 */ stopAudio = () => { const {audioType} = this.props; const recorder = MediaRecorderClass.mediaRecorder; if (recorder && ["recording", "paused"].includes(recorder.state)) { recorder.stop(); recorder.onstop = () => { MediaRecorderClass.audioStream2Blob(audioType, this.props.stopCallback); MediaRecorderClass.audioChunk = []; // 结束后,清空音频存储 } MediaRecorderClass.audioCtx.suspend(); this.initCanvas(); } } /** * @author j_bleach 2018/8/18 * @describe mediaRecorder音频记录 * @param stream: binary data 音频流 */ recordAudio(stream) { const {audioBitsPerSecond, mimeType} = this.props; MediaRecorderClass.mediaRecorder = new MediaRecorder(stream, {audioBitsPerSecond, mimeType}); MediaRecorderClass.mediaRecorder.ondataavailable = (event) => { MediaRecorderClass.audioChunk.push(event.data); } MediaRecorderClass.audioCtx.resume(); MediaRecorderClass.mediaRecorder.start(); MediaRecorderClass.mediaRecorder.onstart = (e) => { MediaRecorderClass.checkAndExecFn(this.props.startCallback, e); } MediaRecorderClass.mediaRecorder.onresume = (e) => { MediaRecorderClass.checkAndExecFn(this.props.startCallback, e); } MediaRecorderClass.mediaRecorder.onerror = (e) => { MediaRecorderClass.checkAndExecFn(this.props.errorCallback, e); } const source = MediaRecorderClass.audioCtx.createMediaStreamSource(stream); source.connect(this.analyser); this.renderCurve(this.analyser); } /** * @author j_bleach 2018/8/19 * @describe 恢复录音 */ static resumeAudio() { MediaRecorderClass.audioCtx.resume(); MediaRecorderClass.mediaRecorder.resume(); } } } export default MediaRecorderFn; 复制代码

这个装饰器主要使用到了navigator.mediaDevices.getUserMediaMediaRecorder这两个api,navigator.mediaDevices.getUserMedia是用于调用硬件设备的api,在对麦克风摄像头进行操做时都须要用到这个。以前在作视频相关开发的时候,还用到了mediaDevices下的MediaDevices.ondevicechange和navigator.mediaDevices.enumerateDevices这两个方法分别用来检测输入硬件变化,以及硬件设备列表查询,此次音频没有用这两个方法,缘由是我观察到开发时大多设备都默认包含有音频输入,要求不像视频那么严格,因此本组件只作了navigator.mediaDevices的兼容处理,有想法的同窗能够把这两个方法也加上。数组

在对音频作记录时,主要应用到的一个api是MediaRecorder,这个api对浏览器有必定的要求,目前只支持谷歌以及火狐。 MediaRecorder 主要有4种回调,MediaRecorder.pause(),MediaRecorder.resume(),MediaRecorder.start(),MediaRecorder.stop(),分别对应于录音的4种状态。promise

该装饰器包含三个关键的回调函数:startAudio,pauseAudio,stopAudio。用于对各状态的处理,触发条件就是经过改变传入组件的status属性,本组件在开发过程当中没有对开始和恢复的回调进行区别,这多是一个遗漏的地方,须要的同窗只能在上层状态机改变时自行区分了。

###RenderCanvas

在MediaRecorder.js中,当开始录音后,会经过AudioContext将设备输入的音频流,建立为一个音频资源对象,而后将这个对象关联至AnalyserNode(一个用于音频可视化的分析对象)。即

const source = MediaRecorderClass.audioCtx.createMediaStreamSource(stream);
source.connect(this.analyser);
复制代码

在组件挂载时期,初始化一块黑色背景白色中线的画布。

configCanvas() {
            const {height, width, backgroundColor, strokeColor} = this.props;
            const canvas = RenderCanvasClass.canvasRef.current;
            RenderCanvasClass.canvasCtx = canvas.getContext("2d");
            RenderCanvasClass.canvasCtx.clearRect(0, 0, width, height);
            RenderCanvasClass.canvasCtx.fillStyle = backgroundColor;
            RenderCanvasClass.canvasCtx.fillRect(0, 0, width, height);
            RenderCanvasClass.canvasCtx.lineWidth = 2;
            RenderCanvasClass.canvasCtx.strokeStyle = strokeColor;
            RenderCanvasClass.canvasCtx.beginPath();
        }
复制代码

这个画布用于组件初始化显示,以及中止以后的恢复状态。 在开启录音后,首先建立一个可视化无符号8位的类型数组,数组长度为analyserNode的fftsize(fft:快速傅里叶变换)长度,默认为2048。而后经过analyserNode的getByteTimeDomainData这个api,将音频信息存储在刚刚建立的类型数组上。这样就能够获得一个带有音频信息,且长度为2048的类型数组,将canvas画布的宽度分割为2048份,而后有画布左边中点为圆点,开始根据数组的值为高来绘制音频曲线,即:

renderCurve = () => {
            const {height, width} = this.props;
            RenderCanvasClass.animationId = window.requestAnimationFrame(this.renderCurve); // 定时动画
            const bufferLength = this.analyser.fftSize; // 默认为2048
            const dataArray = new Uint8Array(bufferLength);
            this.analyser.getByteTimeDomainData(dataArray);// 将音频信息存储在长度为2048(默认)的类型数组(dataArray)
            this.configCanvas();
            const sliceWidth = Number(width) / bufferLength;
            let x = 0;
            for (let i = 0; i < bufferLength; i++) {
                const v = dataArray[i] / 128.0;
                const y = v * height / 2;
                RenderCanvasClass.canvasCtx[i === 0 ? "moveTo" : "lineTo"](x, y);
                x += sliceWidth;
            }
            RenderCanvasClass.canvasCtx.lineTo(width, height / 2);
            RenderCanvasClass.canvasCtx.stroke();
        }
复制代码

经过requestAnimationFrame这个api来实现动画效果,这是一个作动画渲染经常使用到的api,最近作地图路径导航也用到了这个渲染,他比setTimeout在渲染视图上有着更好的性能,须要注意的点和定时器同样,就是在结束选而后,一个要手动取消动画,即:

window.cancelAnimationFrame(RenderCanvasClass.animationId);
复制代码

至此,关于音频曲线的绘制就结束了,项目自己仍是有一些小的细节待改进,也有一些小的迭代会更新上去,好比新的音频格式,新的曲线展现等等,更多请关注git更新。

###项目地址 github.com/jiwenjiang/…

相关文章
相关标签/搜索