本文作者:辰光

最近在coding上观摩了一个可视化音频播放的项目,进而在MDN上研究了Web Audio API,
觉得结合canvas绘图还是非常酷炫的,Web Audio API可以利用AnalyserNode来截取音频源数据,可以是频率,波形或是其他的数据,MDN上有相关的API文档

在Coding的项目的链接:
https://coding.net/u/luojia/p/Audio-Visualizations/git

个人觉得真是大神,能做得那么炫酷,特地把这个项目下下来自己研究一番。

今儿打算用html来读取音频文件,通过截取音频在canvas上绘图。

起步-Analyser

首先,需要一个Analyser来截取,而Analyser由AudioContext来创建:

MDN上给出的写法是:

var audioCtx= new(window.AudioContext || window.webkitAudioContext) ();
var analyser = audioCtx.createAnalyser();

考虑兼容性问题,可以这样写:

window.AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext;

这样就相当打了兼容补丁上去,能保证可以使用AudioContext了。
在这里,打算用AudioContext加requestAnimationFrame来实现。

接下来的思路

1.定义一个可视化音频的类,类存放文件,文件名,audioContext;
2.初始化这个类;
3.类上定义监听事件的方法,即监听文件,获取文件相关数据;
4.解码文件数据,解码后的数据进行canvas可视化;

var AudioCanvas = function() {
    this.file = null, //文件,获取文件封装在监听器里
        this.fileName = null, //文件名
        this.audioContext = null, //截取音频处理,通过initial初始化
};

以下的方法可以写到AudioCanvas.prototype中:

initial

requestAnimationFrame保持绘图循环,部分浏览器不支持此特性,故采用了与AudioContext相同的方法,完成初始化

function () {
    window.AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.msAudioContext;
    window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame;
    try {
        this.audioContext = new AudioContext();
    } catch (e) {
        console.log('!你的浏览器不支持AudioContext:');
        console.log(e);
    }
}

初始化后应该考虑的是如何获取到文件了,这里我们用一个监听方法实现:

addEventListener

function() {
        var that = this,
            fileInput = document.getElementById('uploadedFile'),
            dropContainer = document.getElementsByTagName("canvas")[0];
        //监听是否有文件被选中
        fileInput.onchange = function() {
            //通过判断文件长度,若为0,则没有读取到文件
            if (fileInput.files.length !== 0) {
                that.file = fileInput.files[0]; //获取文件
                that.fileName = that.file.name;  //获取文件名
                that.decodeFile();    //解码文件数据
            }
        };
}

获取文件后,对文件进行相关处理,解码数据:

decodeFile

function() {
    var that = this,
        file = this.file, //文件
        fRead = new FileReader(); //实例化一个FileReader用于读取文件
    //将文件传给fRead的readAsArrayBuffer方法,获取ArrayBuffer格式的数据
    fRead.readAsArrayBuffer(file);
    fRead.onload = function(e) { //文件读取完后调用此函数
        var fileResult = e.target.result; //这是读取成功得到的结果ArrayBuffer数据
        var audioContext = that.audioContext;
        audioContext.decodeAudioData(fileResult, function(buffer) {
            that.visual(audioContext, buffer); //visual将进行一些相关的连接,看后面的说明和代码
        }, function(e) { //如果失败
            console.log("文件解码失败!");
        });
    };
}

visual

MDN上给出的相关接口:

source = audioCtx.createMediaStreamSource(stream);
source.connect(analyser);
analyser.connect(distortion);

个人觉得看不太明白这里用Node是怎么来连接音源的,上网查阅了相关信息,
觉得MDN这段话是先创建一个源,源连接analyserNode节点,节点连接扬声器,最后再将
Buffer数据传递给源。

function(audioContext, buffer) {
    var audioBufferSouceNode = audioContext.createBufferSource(),//创建源
        analyser = audioContext.createAnalyser();//创建AnalyserNode节点
    //连接源和AnalyserNode节点,在中途可以截取音频
    audioBufferSouceNode.connect(analyser);
    //连接扬声器,输出音频
    analyser.connect(audioContext.destination);
    //将上一步解码得到的buffer数据赋值给源
    audioBufferSouceNode.buffer = buffer;
    //指代开始的时间
    audioBufferSouceNode.start(0);
    //音乐响起后,就捕捉到音频频率了,接下来要做的就是如何绘图,即用canvas来实现
    this._drawAudio(analyser);
}

绘制之前,需要了解几个接口:
MDN上的说明:

AnalyserNode.getFloatFrequencyData()和AnalyserNode.getByteFrequencyData()获取频率;
AnalyserNode.getByteTimeDomainData()和AnalyserNode.getFloatTimeDomainData()获取波形

实际上这些方法返回一个数组,但并不是一个普通的数组,
AnalyserNode.getFloatFrequencyData()AnalyserNode.getFloatTimeDomainData()产生32位float数组,可以用new Float32Array()来创建,
AnalyserNode.getByteFrequencyData()AnalyserNode.getByteTimeDomainData()产生8位无符号数组,可以用new Uint8Array()创建;
这些在MDN上都有相关的说明,不懂什么是Float32ArrayUint8Array的去翻翻MDN的文档就知道了。

AnalyserNode.frequencyBinCount, 它是FFT的一部分, 可以调用Uint8Array(),把frequencyBinCount作为它的参数 — 这代表我们将对这个尺寸的FFT收集多少数据点。
用法:

analyser.fftsize= 2048;
var bufferLength=analyser.frequencyBinCount;
var dataArray= new Uint8Array(bufferLength);

然后将dataArray传到上述讲的获取频率或是波形的方法中:

analyser.getByteTimeDomainData(dataArray);

drawAudio

function(analyser) {
    var canvas = document.getElementById('canvas'),
        ctx = canvas.getContext('2d'),
        cwidth = canvas.width,
        cheight = canvas.height,
        meterWidth = 10, //频谱条宽度
        capHeight = 2,
        capStyle = '#fff',
        meterNum = 800 / (10 + 2), //频谱条数量
        capYPositionArray = [], //将上一画面各帽头的位置保存到这个数组
        gradient = ctx.createLinearGradient(0, 0, 0, 400);
    gradient.addColorStop(1, '#f00');
    gradient.addColorStop(0.5, '#f00');
    gradient.addColorStop(0, '#f00');
    var drawRect = function() {
        var array = new Uint8Array(analyser.frequencyBinCount);
        analyser.getByteFrequencyData(array);
        var step = Math.round(array.length / meterNum);    //取部分样式
        ctx.clearRect(0, 0, cwidth, cheight);
        for (var i = 0; i < meterNum; i++) {
            var value = array[i * step]; //获取对应的频率值
            if (capYPositionArray.length < Math.round(meterNum)) {
                capYPositionArray.push(value); //初始化顶端的值,将第一次压入数组中
                //如果想看到具体的效果,可以把capStyle的值改为"#000",会有很明显的不同
            }
            ctx.fillStyle = capStyle;
            //开始绘制
            if (value < capYPositionArray[i]) { //如果当前值小于之前值
                ctx.fillRect(i * 12, cheight - (--capYPositionArray[i]), meterWidth, capHeight); //则使用前一次保存的值来绘制顶端
            } else {
                ctx.fillRect(i * 12, cheight - value, meterWidth, capHeight); //否则使用当前值直接绘制
                capYPositionArray[i] = value;
            }
            //开始绘制频谱条
            ctx.fillStyle = gradient;
            ctx.fillRect(i * 12, cheight - value + capHeight, meterWidth, cheight);
        }
        requestAnimationFrame(drawRect);  //绘图循环,持续到音乐播放结束,无法再获取音频数据
    };
    requestAnimationFrame(drawRect);
}

最后,在加载脚本的时候进行调用:

window.onload=function()
{
    var CA=new CanvasAudio();
    CA.initial();
    CA.addEventListner();
    CA.drawAudio();
};

效果

在这里只能截图了,具体怎么样可以自己去尝试一番~

除非注明,麦麦小家文章均为原创,转载请以链接形式标明本文地址。

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)

本文地址:https://blog.micblo.com/2015/06/25/%E7%94%A8HTML5%E5%AE%9E%E7%8E%B0%E9%9F%B3%E9%A2%91%E5%8F%AF%E8%A7%86%E5%8C%96/