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

栏目: 服务器 · 发布时间: 7年前

内容简介:##简介最近由于工作需要,需要在react上用到一个录音的功能,录音主要包含开始录音,暂停录音,停止录音,并将频谱通过canvas绘制出来。起初开发时找了一个现成的包,但是这个第三方的包不支持暂停功能,也不支持音频转码,只能输出audio/webm格式,所以自己在周末决定重新写一个关于react录音的插件。 ##使用 目前这个包已经上传至npm,需要用的同学可以运行指令

##简介

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

演示地址

最近由于工作需要,需要在react上用到一个录音的功能,录音主要包含开始录音,暂停录音,停止录音,并将频谱通过canvas绘制出来。起初开发时找了一个现成的包,但是这个第三方的包不支持暂停功能,也不支持音频转码,只能输出audio/webm格式,所以自己在周末决定重新写一个关于react录音的插件。 ##使用 目前这个包已经上传至npm,需要用的同学可以运行指令

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

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

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

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

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

###react-audio-analyser index.js

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的更改来触发相关的钩子。

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

/**
 * @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种状态。

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

该装饰器包含三个关键的回调函数: 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份,然后有画布左边中点为圆点,开始根据数组的值为高来绘制音频曲线,即:

基于react的录音及音频曲线绘制的组件开发
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/…


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Hello World

Hello World

Hannah Fry / W. W. Norton Company / 2018-9 / GBP 17.99

A look inside the algorithms that are shaping our lives and the dilemmas they bring with them. If you were accused of a crime, who would you rather decide your sentence—a mathematically consistent ......一起来看看 《Hello World》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具