HTML5录音控件

最近的项目又需要用到录音,年前有过调研,再次翻出来使用,这里做一个记录。

HTML5提供了录音支持,因此可以方便使用HTML5来录音,来实现录音、语音识别等功能,语音开发必备。但是ES标准提供的API并不人性化,不方便使用,并且不提供保存为wav的功能,开发起来费劲啊!!

github寻找轮子,发现Recorder.js,基本上可以满足需求了,良好的封装,支持导出wav,但是存在:

  • wav采样率不可调整
  • recorder创建麻烦,需要自己初始化getUserMedia
  • 无实时数据回调,不方便绘制波形
  • 。。。

改造轮子

创建recorder工具方法

提供创建recorder工具函数,封装audio接口:

static createRecorder(callback,config){
        window.AudioContext = window.AudioContext || window.webkitAudioContext;
        window.URL = window.URL || window.webkitURL;
        navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
        
        if (navigator.getUserMedia) {
            navigator.getUserMedia(
                { audio: true } //只启用音频
                , function (stream) {
                    var audio_context = new AudioContext;
                    var input = audio_context.createMediaStreamSource(stream);
                    var rec = new Recorder(input, config);
                    callback(rec);
                }
                , function (error) {
                    switch (error.code || error.name) {
                        case 'PERMISSION_DENIED':
                        case 'PermissionDeniedError':
                            throwError('用户拒绝提供信息。');
                            break;
                        case 'NOT_SUPPORTED_ERROR':
                        case 'NotSupportedError':
                            throwError('浏览器不支持硬件设备。');
                            break;
                        case 'MANDATORY_UNSATISFIED_ERROR':
                        case 'MandatoryUnsatisfiedError':
                            throwError('无法发现指定的硬件设备。');
                            break;
                        default:
                            throwError('无法打开麦克风。异常信息:' + (error.code || error.name));
                            break;
                    }
                });
        } else {
            throwError('当前浏览器不支持录音功能。'); return;
        }
    }

采样率

H5录制的默认是44k的,文件大,不方便传输,因此需要进行重新采样,一般采用插值取点方法:

以下代码主要来自stackoverflow:

             /**
             * 转换采样率
             * @param data
             * @param newSampleRate 目标采样率
             * @param oldSampleRate 原始数据采样率
             * @returns {any[]|Array}
             */
            function interpolateArray(data, newSampleRate, oldSampleRate) {
                var fitCount = Math.round(data.length * (newSampleRate / oldSampleRate));
                var newData = new Array();
                var springFactor = new Number((data.length - 1) / (fitCount - 1));
                newData[0] = data[0]; // for new allocation
                for (var i = 1; i < fitCount - 1; i++) {
                    var tmp = i * springFactor;
                    var before = new Number(Math.floor(tmp)).toFixed();
                    var after = new Number(Math.ceil(tmp)).toFixed();
                    var atPoint = tmp - before;
                    newData[i] = this.linearInterpolate(data[before], data[after], atPoint);
                }
                newData[fitCount - 1] = data[data.length - 1]; // for new allocation
                return newData;
            }

            function linearInterpolate(before, after, atPoint) {
                return before + (after - before) * atPoint;
            }

修改导出wav函数exportWAV,增加采样率选项:

            /**
             * 导出wav
             * @param type
             * @param desiredSamplingRate 期望的采样率
             */
            function exportWAV(type,desiredSamplingRate) {
                // 默认为16k
                desiredSamplingRate = desiredSamplingRate || 16000;
                var buffers = [];
                for (var channel = 0; channel < numChannels; channel++) {
                    var buffer = mergeBuffers(recBuffers[channel], recLength);
                    // 需要转换采样率
                    if (desiredSamplingRate!=sampleRate) {
                        // 插值去点
                        buffer = interpolateArray(buffer, desiredSamplingRate, sampleRate);
                    }
                    buffers.push(buffer);
                }
                var interleaved = numChannels === 2 ? interleave(buffers[0], buffers[1]) : buffers[0];
                var dataview = encodeWAV(interleaved,desiredSamplingRate);
                var audioBlob = new Blob([dataview], { type: type });
                self.postMessage({ command: 'exportWAV', data: audioBlob });
            }

实时录音数据回调

为了方便绘制音量、波形图,需要获取到实时数据:

config新增一个回调函数onaudioprocess:

  config = {
        bufferLen: 4096,
        numChannels: 1, // 默认单声道
        mimeType: 'audio/wav',
        onaudioprocess:null
    };

修改录音数据处理函数:

        this.node.onaudioprocess = (e) => {
            if (!this.recording) return;
            var buffer = [];

            for (var channel = 0; channel < this.config.numChannels; channel++) {
                buffer.push(e.inputBuffer.getChannelData(channel));
            }

            // 发送给worker
            this.worker.postMessage({
                command: 'record',
                buffer: buffer
            });

            // 数据回调
            if(this.config.onaudioprocess){
                this.config.onaudioprocess(buffer[0]);
            }
        };

这样,在创建recorder时,配置onaudioprocess就可以获取到实时数据了

实时数据编码

编码计算耗时,需要放到worker执行:

接口函数新增encode,发送消息给worker,让worker执行:

    encode(cb,buffer,sampleRate) {
        cb = cb || this.config.callback;
        if (!cb) throw new Error('Callback not set');
        this.callbacks.encode.push(cb);
        this.worker.postMessage({ command: 'encode',buffer:buffer,sampleRate:sampleRate});
    }

worker里新增encode函数,处理encode请求,完成后执行回调

 self.onmessage = function (e) {
                switch (e.data.command) {

                    case 'encode':
                        encode(e.data.buffer,e.data.sampleRate);
                        break;

                }
            };
            
    encode(cb,buffer,sampleRate) {
        cb = cb || this.config.callback;
        if (!cb) throw new Error('Callback not set');
        this.callbacks.encode.push(cb);
        this.worker.postMessage({ command: 'encode',buffer:buffer,sampleRate:sampleRate});
    }

wav上传

增加一个上传函数:

     exportWAVAndUpload(url, callback) {
        var _url = url;
        exportWAV(function(blob){
            var fd = new FormData();
            fd.append("audioData", blob);
            var xhr = new XMLHttpRequest();
            if (callback) {
                xhr.upload.addEventListener("progress", function (e) {
                    callback('uploading', e);
                }, false);
                xhr.addEventListener("load", function (e) {
                    callback('ok', e);
                }, false);
                xhr.addEventListener("error", function (e) {
                    callback('error', e);
                }, false);
                xhr.addEventListener("abort", function (e) {
                    callback('cancel', e);
                }, false);
            }
            xhr.open("POST", url);
            xhr.send(fd);
        })     
    }

完整代码

=点击下载

发现新轮子

今天再次看这个项目,发现这个项目已经不维护了,

Note: This repository is not being actively maintained due to lack of time and interest. If you maintain or know of a good fork, please let me know so I can direct future visitors to it. In the meantime, if this library isn't working, you can find a list of popular forks here: http://forked.yannick.io/mattdiamond/recorderjs.

作者推荐https://github.com/chris-rudmin/Recorderjs,提供更多的功能:

  • bitRate (optional) Specifies the target bitrate in bits/sec. The encoder selects an application-specific default when this is not specified.
  • bufferLength - (optional) The length of the buffer that the internal JavaScriptNode uses to capture the audio. Can be tweaked if experiencing performance issues. Defaults to 4096.
  • encoderApplication - (optional) Specifies the encoder application. Supported values are 2048 - Voice, 2049 - Full Band Audio, 2051 - Restricted Low Delay. Defaults to 2049.
  • encoderComplexity - (optional) Value between 0 and 10 which determines latency and processing for resampling. 0 is fastest with lowest complexity. 10 is slowest with highest complexity. The encoder selects a default when this is not specified.
  • encoderFrameSize (optional) Specifies the frame size in ms used for encoding. Defaults to 20.
  • encoderPath - (optional) Path to encoderWorker.min.js worker script. Defaults to encoderWorker.min.js
  • encoderSampleRate - (optional) Specifies the sample rate to encode at. Defaults to 48000. Supported values are 8000, 12000, 16000, 24000 or 48000.
  • leaveStreamOpen - (optional) Keep the stream around when trying to stop recording, so you can re-start without re-initStream. Defaults to false.
  • maxBuffersPerPage - (optional) Specifies the maximum number of buffers to use before generating an Ogg page. This can be used to lower the streaming latency. The lower the value the more overhead the ogg stream will incur. Defaults to 40.
  • monitorGain - (optional) Sets the gain of the monitoring output. Gain is an a-weighted value between 0 and 1. Defaults to 0
  • numberOfChannels - (optional) The number of channels to record. 1 = mono, 2 = stereo. Defaults to 1. Maximum 2 channels are supported.
  • originalSampleRateOverride - (optional) Override the ogg opus 'input sample rate' field. Google Speech API requires this field to be 16000.
  • resampleQuality - (optional) Value between 0 and 10 which determines latency and processing for resampling. 0 is fastest with lowest quality. 10 is slowest with highest quality. Defaults to 3.
  • streamPages - (optional) dataAvailable event will fire after each encoded page. Defaults to false.

推荐使用


作者:Jadepeng 出处:jqpeng的技术记事本--http://www.cnblogs.com/xiaoqi 您的支持是对博主最大的鼓励,感谢您的认真阅读。 本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏AI研习社

Github 项目推荐 | Twitter 开源数据分析工具 —— tinfoleak

Tinfoleak 是 OSINT (Open Source Intelligence) 和 SOCMINT (Social Media Intelligenc...

19930
来自专栏Python数据科学

这些世界杯球星你真的认识吗?不如通过Python来认识一下吧

回到我们的正题,对于世界杯的球星们,人们知道的一般都是C罗,梅西,德罗巴等巨星,而对一些其它球星却很少了解。对于这些球星,你认识的有多少呢?下面就跟我一起认识一...

11020
来自专栏Crossin的编程教室

【Python 第4课】输入

Hi~Crossin又来了。 可以用编程语言让计算机按你说的指令做事情之后,大家是不是有些跃跃欲试呢?别着急,先回顾一下我们之前几节课。我们到现在一共提到了三种...

35670
来自专栏哲学驱动设计

CQRS讨论

今天和同事一起讨论了CQRS(Command Query Responsibility Segregation),过程中,我产生了一些疑问,先记录在这里,以后有...

19170
来自专栏Android 开发学习

音频开发ijkplayer小结 android

29320
来自专栏NetCore

微信快速开发框架(五)-- 利用快速开发框架,快速搭建微信浏览博客园首页文章

这几天接连发布了《快速开发微信公众平台框架---简介》和《体验微信公众平台快速开发框架》几篇关于微信平台的文章,不过反响一般,可能需求不是很多吧。闲来无事,还是...

25890
来自专栏菩提树下的杨过

Web开发感悟:数据绑定是一种技术,更是一门艺术

1、前言 作为一个多年从事b/s开发的程序猿,曾先后使用过asp、asp.net做为主要服务端语言。不管是相对低级的asp也好,还是高级的asp.net也罢,都...

21850
来自专栏蓝天

养成良好的编程习惯

4.从源头避免版本不一致问题(当同一个文件在不同目录下出现拷贝时,容易出现其中某个未同步更新的问题)

12530
来自专栏不二小段

Python自动统计微博抽奖中奖男女比例(附代码)

今天来写一个自动计算微博抽奖男女比例的代码,很早就应该发这篇了,只是不想蹭热点(才不是拖更)。

18830
来自专栏FreeBuf

Bash漏洞再次演进:缓冲区溢出导致远程任意命令执行

近几天,“Shellshock”Bash漏洞的出现可谓是给安全界投放了一颗重型炸弹,越来越多的厂商和黑白帽子都纷纷加入到分析阵营当中,同时也接二连三爆出了更多针...

221100

扫码关注云+社区

领取腾讯云代金券