全国服务热线:4008-888-888

技术知识

html5音频作用实战演练示例

缘起

因为新项目必须,大家要在web端完成音频作用。1刚开始,寻找的计划方案有两个,1个是根据iframe,1个是html5的getUserMedia api。因为大家的音频作用不必须适配IE访问器,因此绝不迟疑的挑选了html5出示的getUserMedia去完成。基础思路是参照了官方的api文本文档和在网上搜索的1些计划方案做融合做出了合适新项目必须的计划方案。但因为大家务必确保这个音频作用可以另外在pad端、pc端都可以以开启,因此在其中也踩了1些坑。下列为全过程复原。

流程1

因为新的api是根据navigator.mediaDevices.getUserMedia,且回到1个promise。

而旧的api是navigator.getUserMedia,因而做了1个适配性。编码以下:

// 老的访问器将会压根沒有完成 mediaDevices,因此大家能够先设定1个空的目标
if (navigator.mediaDevices === undefined) {
    navigator.mediaDevices = {};
}

// 1些访问器一部分适用 mediaDevices。大家不可以立即给目标设定 getUserMedia
// 由于这样将会会遮盖已有的特性。这里大家只会在沒有getUserMedia特性的情况下加上它。
if (navigator.mediaDevices.getUserMedia === undefined) {
    let getUserMedia =
        navigator.getUserMedia ||
        navigator.webkitGetUserMedia ||
        navigator.mozGetUserMedia ||
        navigator.msGetUserMedia;
    navigator.mediaDevices.getUserMedia = function(constraints) {
        // 最先,假如有getUserMedia的话,就得到它

        // 1些访问器压根没完成它 - 那末就回到1个error到promise的reject来维持1个统1的插口
        if (!getUserMedia) {
            return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
        }

        // 不然,为老的navigator.getUserMedia方式包裹1个Promise
        return new Promise(function(resolve, reject) {
            getUserMedia.call(navigator, constraints, resolve, reject);
        });
    };

流程2

这是在网上存在的1个方式,封裝了1个HZRecorder。基础上引入了这个方式。启用HZRecorder.get便可以调起音频插口,这个方式传入1个callback涵数,new HZRecorder后实行callback涵数且传入1个实体线化后的HZRecorder目标。能够根据该目标的方式完成刚开始音频、中止、终止、播发等作用。

var HZRecorder = function (stream, config) {  
    config = config || {};  
    config.sampleBits = config.sampleBits || 8;      //取样多位 8, 16  
    config.sampleRate = config.sampleRate || (44100 / 6);   //取样率(1/6 44100)  

      
    //建立1个声频自然环境目标  
    audioContext = window.AudioContext || window.webkitAudioContext;  
    var context = new audioContext();  

    //将响声键入这个对像  
    var audioInput = context.createMediaStreamSource(stream);  
      
    //设定声音连接点  
    var volume = context.createGain();  
    audioInput.connect(volume);  

    //建立缓存文件,用来缓存文件响声  
    var bufferSize = 4096;  

    // 建立响声的缓存文件连接点,createScriptProcessor方式的  
    // 第2个和第3个主要参数指的是键入和輸出全是双声道。  
    var recorder = context.createScriptProcessor(bufferSize, 2, 2);  

    var audioData = {  
        size: 0          //音频文档长度  
        , buffer: []     //音频缓存文件  
        , inputSampleRate: context.sampleRate    //键入取样率  
        , inputSampleBits: 16       //键入取样多位 8, 16  
        , outputSampleRate: config.sampleRate    //輸出取样率  
        , oututSampleBits: config.sampleBits       //輸出取样多位 8, 16  
        , input: function (data) {  
            this.buffer.push(new Float32Array(data));  
            this.size += data.length;  
        }  
        , compress: function () { //合拼缩小  
            //合拼  
            var data = new Float32Array(this.size);  
            var offset = 0;  
            for (var i = 0; i < this.buffer.length; i++) {  
                data.set(this.buffer[i], offset);  
                offset += this.buffer[i].length;  
            }  
            //缩小  
            var compression = parseInt(this.inputSampleRate / this.outputSampleRate);  
            var length = data.length / compression;  
            var result = new Float32Array(length);  
            var index = 0, j = 0;  
            while (index < length) {  
                result[index] = data[j];  
                j += compression;  
                index++;  
            }  
            return result;  
        }  
        , encodeWAV: function () {  
            var sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);  
            var sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);  
            var bytes = this.compress();  
            var dataLength = bytes.length * (sampleBits / 8);  
            var buffer = new ArrayBuffer(44 + dataLength);  
            var data = new DataView(buffer);  

            var channelCount = 1;//单声道  
            var offset = 0;  

            var writeString = function (str) {  
                for (var i = 0; i < str.length; i++) {  
                    data.setUint8(offset + i, str.charCodeAt(i));  
                }  
            };  
              
            // 資源互换文档标志符   
            writeString('RIFF'); offset += 4;  
            // 下个详细地址刚开始到文档尾总字节数,即文档尺寸⑻   
            data.setUint32(offset, 36 + dataLength, true); offset += 4;  
            // WAV文档标示  
            writeString('WAVE'); offset += 4;  
            // 波形文件格式标示   
            writeString('fmt '); offset += 4;  
            // 过虑字节,1般为 0x10 = 16   
            data.setUint32(offset, 16, true); offset += 4;  
            // 文件格式种别 (PCM方式取样数据信息)   
            data.setUint16(offset, 1, true); offset += 2;  
            // 安全通道数   
            data.setUint16(offset, channelCount, true); offset += 2;  
            // 取样率,每秒样版数,表明每一个安全通道的播发速率   
            data.setUint32(offset, sampleRate, true); offset += 4;  
            // 波形数据信息传送率 (每秒均值字节数) 单声道×每秒数据信息位数×每样版数据信息位/8   
            data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true); offset += 4;  
            // 快数据信息调剂数 取样1次占有字节数 单声道×每样版的数据信息位数/8   
            data.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2;  
            // 每样版数据信息位数   
            data.setUint16(offset, sampleBits, true); offset += 2;  
            // 数据信息标志符   
            writeString('data'); offset += 4;  
            // 取样数据信息总数,即数据信息总尺寸⑷4   
            data.setUint32(offset, dataLength, true); offset += 4;  
            // 写入取样数据信息   
            if (sampleBits === 8) {  
                for (var i = 0; i < bytes.length; i++, offset++) {  
                    var s = Math.max(⑴, Math.min(1, bytes[i]));  
                    var val = s < 0 ? s * 0x8000 : s * 0x7FFF;  
                    val = parseInt(255 / (65535 / (val + 32768)));  
                    data.setInt8(offset, val, true);  
                }  
            } else {  
                for (var i = 0; i < bytes.length; i++, offset += 2) {  
                    var s = Math.max(⑴, Math.min(1, bytes[i]));  
                    data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);  
                }  
            }  

            return new Blob([data], { type: 'audio/wav' });  
        }  
    };  

    //刚开始音频  
    this.start = function () {  
        audioInput.connect(recorder);  
        recorder.connect(context.destination);  
    };  

    //终止  
    this.stop = function () {  
        recorder.disconnect();  
    };  
    
    // 完毕
    this.end = function() {
        context.close();
    };
    
    // 再次
    this.again = function() {
        recorder.connect(context.destination);
    };

    //获得声频文档  
    this.getBlob = function () {  
        this.stop();  
        return audioData.encodeWAV();  
    };  

    //回放  
    this.play = function (audio) {  
        audio.src = window.URL.createObjectURL(this.getBlob());  
    };  

    //提交  
    this.upload = function (url, callback) {  
        var fd = new FormData();  
        fd.append('audioData', this.getBlob());  
        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);  
    };  

    //声频收集  
    recorder.onaudioprocess = function (e) {  
        audioData.input(e.inputBuffer.getChannelData(0));  
        //record(e.inputBuffer.getChannelData(0));  
    };  

};  

//抛出出现异常  
HZRecorder.throwError = function (message) {  
    throw new function () { this.toString = function () { return message; };};  
};  
//是不是适用音频  
HZRecorder.canRecording = (navigator.getUserMedia != null);  
//获得音频机  
HZRecorder.get = function (callback, config) {  
   if (callback) {
        navigator.mediaDevices
            .getUserMedia({ audio: true })
            .then(function(stream) {
                let rec = new HZRecorder(stream, config);
                callback(rec);
            })
            .catch(function(error) {
                HZRecorder.throwError('没法音频,请查验机器设备情况');
            });
    }
};  
window.HZRecorder = HZRecorder;

以上,早已能够考虑绝大多数的要求。可是大家要适配pad端。大家的pad有几个难题务必处理。

  • 音频文件格式务必是mp3才可以播发
  • window.URL.createObjectURL传入blob数据信息在pad端出错,转不上

下列为处理这两个难题的计划方案。

流程3

下列为我完成 音频文件格式为mp3 和 window.URL.createObjectURL传入blob数据信息在pad端出错 的计划方案。

1、改动HZRecorder里的audioData目标编码。并引进在网上1位高手的1个js文档lamejs.js

const lame = new lamejs();
let audioData = {
    samplesMono: null,
    maxSamples: 1152,
    mp3Encoder: new lame.Mp3Encoder(1, context.sampleRate || 44100, config.bitRate || 128),
    dataBuffer: [],
    size: 0, // 音频文档长度
    buffer: [], // 音频缓存文件
    inputSampleRate: context.sampleRate, // 键入取样率
    inputSampleBits: 16, // 键入取样多位 8, 16
    outputSampleRate: config.sampleRate, // 輸出取样率
    oututSampleBits: config.sampleBits, // 輸出取样多位 8, 16
    convertBuffer: function(arrayBuffer) {
        let data = new Float32Array(arrayBuffer);
        let out = new Int16Array(arrayBuffer.length);
        this.floatTo16BitPCM(data, out);
        return out;
    },
    floatTo16BitPCM: function(input, output) {
        for (let i = 0; i < input.length; i++) {
            let s = Math.max(⑴, Math.min(1, input[i]));
            output[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
        }
    },
    appendToBuffer: function(mp3Buf) {
        this.dataBuffer.push(new Int8Array(mp3Buf));
    },
    encode: function(arrayBuffer) {
        this.samplesMono = this.convertBuffer(arrayBuffer);
        let remaining = this.samplesMono.length;
        for (let i = 0; remaining >= 0; i += this.maxSamples) {
            let left = this.samplesMono.subarray(i, i + this.maxSamples);
            let mp3buf = this.mp3Encoder.encodeBuffer(left);
            this.appendToBuffer(mp3buf);
            remaining -= this.maxSamples;
        }
    },
    finish: function() {
        this.appendToBuffer(this.mp3Encoder.flush());
        return new Blob(this.dataBuffer, { type: 'audio/mp3' });
    },
    input: function(data) {
        this.buffer.push(new Float32Array(data));
        this.size += data.length;
    },
    compress: function() {
        // 合拼缩小
        // 合拼
        let 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;
        }
        // 缩小
        let compression = parseInt(this.inputSampleRate / this.outputSampleRate, 10);
        let length = data.length / compression;
        let result = new Float32Array(length);
        let index = 0;
        let j = 0;
        while (index < length) {
            result[index] = data[j];
            j += compression;
            index++;
        }
        return result;
    },
    encodeWAV: function() {
        let sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
        let sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
        let bytes = this.compress();
        let dataLength = bytes.length * (sampleBits / 8);
        let buffer = new ArrayBuffer(44 + dataLength);
        let data = new DataView(buffer);

        let channelCount = 1; // 单声道
        let offset = 0;

        let writeString = function(str) {
            for (let i = 0; i < str.length; i++) {
                data.setUint8(offset + i, str.charCodeAt(i));
            }
        };

        // 資源互换文档标志符
        writeString('RIFF');
        offset += 4;
        // 下个详细地址刚开始到文档尾总字节数,即文档尺寸⑻
        data.setUint32(offset, 36 + dataLength, true);
        offset += 4;
        // WAV文档标示
        writeString('WAVE');
        offset += 4;
        // 波形文件格式标示
        writeString('fmt ');
        offset += 4;
        // 过虑字节,1般为 0x10 = 16
        data.setUint32(offset, 16, true);
        offset += 4;
        // 文件格式种别 (PCM方式取样数据信息)
        data.setUint16(offset, 1, true);
        offset += 2;
        // 安全通道数
        data.setUint16(offset, channelCount, true);
        offset += 2;
        // 取样率,每秒样版数,表明每一个安全通道的播发速率
        data.setUint32(offset, sampleRate, true);
        offset += 4;
        // 波形数据信息传送率 (每秒均值字节数) 单声道×每秒数据信息位数×每样版数据信息位/8
        data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true);
        offset += 4;
        // 快数据信息调剂数 取样1次占有字节数 单声道×每样版的数据信息位数/8
        data.setUint16(offset, channelCount * (sampleBits / 8), true);
        offset += 2;
        // 每样版数据信息位数
        data.setUint16(offset, sampleBits, true);
        offset += 2;
        // 数据信息标志符
        writeString('data');
        offset += 4;
        // 取样数据信息总数,即数据信息总尺寸⑷4
        data.setUint32(offset, dataLength, true);
        offset += 4;
        // 写入取样数据信息
        if (sampleBits === 8) {
            for (let i = 0; i < bytes.length; i++, offset++) {
                const s = Math.max(⑴, Math.min(1, bytes[i]));
                let val = s < 0 ? s * 0x8000 : s * 0x7fff;
                val = parseInt(255 / (65535 / (val + 32768)), 10);
                data.setInt8(offset, val, true);
            }
        } else {
            for (let i = 0; i < bytes.length; i++, offset += 2) {
                const s = Math.max(⑴, Math.min(1, bytes[i]));
                data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
            }
        }

        return new Blob([data], { type: 'audio/wav' });
    }
};

2、改动HZRecord的声频收集的启用方式。

// 声频收集
recorder.onaudioprocess = function(e) {
    audioData.encode(e.inputBuffer.getChannelData(0));
};

3、HZRecord的getBlob方式。

this.getBlob = function() {
    this.stop();
    return audioData.finish();
};

4、HZRecord的play方式。把blob转base64url。

this.play = function(func) {
    readBlobAsDataURL(this.getBlob(), func);
};

function readBlobAsDataURL(data, callback) {
    let fileReader = new FileReader();
    fileReader.onload = function(e) {
        callback(e.target.result);
    };
    fileReader.readAsDataURL(data);
}

至此,早已处理以上两个难题。

流程4

这里关键详细介绍如何做音频时的动效。大家的1个动效要求为:

依据传入的声音尺寸,做1个圆弧动态性拓展。

// 建立analyser连接点,获得声频時间和频率数据信息
const analyser = context.createAnalyser();
audioInput.connect(analyser);
const inputAnalyser = new Uint8Array(1);
const wrapEle = $this.refs['wrap'];
let ctx = wrapEle.getContext('2d');
const width = wrapEle.width;
const height = wrapEle.height;
const center = {
    x: width / 2,
    y: height / 2
};

function drawArc(ctx, color, x, y, radius, beginAngle, endAngle) {
    ctx.beginPath();
    ctx.lineWidth = 1;
    ctx.strokeStyle = color;
    ctx.arc(x, y, radius, (Math.PI * beginAngle) / 180, (Math.PI * endAngle) / 180);
    ctx.stroke();
}

(function drawSpectrum() {
    analyser.getByteFrequencyData(inputAnalyser); // 获得频域数据信息
    ctx.clearRect(0, 0, width, height);
    // 画线条
    for (let i = 0; i < 1; i++) {
        let value = inputAnalyser[i] / 3; // <===获得数据信息
        let colors = [];
        if (value <= 16) {
            colors = ['#f5A631', '#f5A631', '#e4e4e4', '#e4e4e4', '#e4e4e4', '#e4e4e4'];
        } else if (value <= 32) {
            colors = ['#f5A631', '#f5A631', '#f5A631', '#f5A631', '#e4e4e4', '#e4e4e4'];
        } else {
            colors = ['#f5A631', '#f5A631', '#f5A631', '#f5A631', '#f5A631', '#f5A631'];
        }
        drawArc(ctx, colors[0], center.x, center.y, 52 + 16, ⑶0, 30);
        drawArc(ctx, colors[1], center.x, center.y, 52 + 16, 150, 210);
        drawArc(ctx, colors[2], center.x, center.y, 52 + 32, ⑵2.5, 22.5);
        drawArc(ctx, colors[3], center.x, center.y, 52 + 32, 157.5, 202.5);
        drawArc(ctx, colors[4], center.x, center.y, 52 + 48, ⑴3, 13);
        drawArc(ctx, colors[5], center.x, center.y, 52 + 48, 167, 193);
    }

    // 恳求下1帧
    requestAnimationFrame(drawSpectrum);
})();

缘尽

至此,1个详细的html5音频作用计划方案早已进行。有甚么必须填补,不符合理的地区的欢迎留言。

ps:lamejs可参照这个github

以上便是本文的所有內容,期待对大伙儿的学习培训有一定的协助,也期待大伙儿多多适用脚本制作之家。



在线客服

关闭

客户服务热线
4008-888-888


点击这里给我发消息 在线客服

点击这里给我发消息 在线客服