• 使用HTML5 API(AudioContext)实现可视化频谱效果


    如今的HTML5技术正让网页变得越来越强大,通过其Canvas标签AudioContext对象可以轻松实现之前在Flash或Native App中才能实现的频谱指示器的功能。

    Demo: Cyandev Works - HTML5 Audio Visualizing

    The AudioContext interface represents an audio-processing graph built from audio modules linked together, each represented by an AudioNode.

    根据MDN的文档,AudioContext是一个专门用于音频处理的接口,并且工作原理是将AudioContext创建出来的各种节点(AudioNode)相互连接,音频数据流经这些节点并作出相应处理。


    创建AudioContext对象

    由于浏览器兼容性问题,我们需要为不同浏览器配置AudioContext,在这里我们可以用下面这个表达式来统一对AudioContext的访问。



    var AudioContext = window.AudioContext || window.webkitAudioContext;
    
    var audioContext = new AudioContext(); //实例化AudioContext对象



    附. 浏览器兼容性

    浏览器ChromeFirefoxIEOperaSafari
    支持版本 10.0 25.0 不支持 15.0 6.0



    当然,如果浏览器不支持的话,我们也没有办法,用IE的人们我想也不需要这些效果。但最佳实践是使用的时候判断一下上面声明的变量是否为空,然后再做其他操作。



    解码音频文件

    读取到的音频文件是二进制类型,我们需要让AudioContext先对其解码,然后再进行后续操作。



    audioContext.decodeAudioData(binary, function(buffer) { ... });



    方法decodeAudioData被调用后,浏览器将开始解码音频文件,这需要一定时间,我们应该让用户知道浏览器正在解码,解码成功后会调用传进去的回调函数decodeAudioData还有第三个可选参数是在解码失败时调用的,我们这里就先不实现了。



    创建音频处理节点

    这是最关键的一步,我们需要两个音频节点:




      • AudioBufferSourceNode
      • AnalyserNode



    前者是用于播放解码出来的buffer的节点,而后者是用于分析音频频谱的节点,两个节点顺次连接就能完成我们的工作。


    创建AudioBufferSourceNode

    var audioBufferSourceNode;

    audioBufferSourceNode = audioContext.createBufferSource();


    创建AnalyserNode

    var analyser;
    
    analyser = audioContext.createAnalyser();
    analyser.fftSize = 256;

    上面的fftSize是用于确定FFT大小的属性,那FFT是什么高三的博主还不知道,其实也不需要知道,总之最后获取到的数组长度应该是fftSize值的一半,还应该保证它是以2为底的幂。


    连接节点


    audioBufferSourceNode.connect(analyser);
    analyser.connect(audioContext.destination);

    上面的audioContext.destination是音频要最终输出的目标,我们可以把它理解为声卡。所以所有节点中的最后一个节点应该再连接到audioContext.destination才能听到声音。



    播放音频

    所有工作就绪,在解码完毕时调用的回调函数中我们就可以开始播放了。

    audioBufferSourceNode.buffer = buffer; //回调函数传入的参数
    audioBufferSourceNode.start(0); //部分浏览器是noteOn()函数,用法相同



    参数代表播放起点,我们这里设置为0意味着从头播放。



    文件读取

    HTML5支持文件选择、读取的特性,我们利用这个特性可以实现不上传,即播放的功能。



    HTML标签

    在你的页面中找个位置插入:



    <input id="fileChooser" type="file" />



    Js逻辑


    var file;
    
    var fileChooser = document.getElementById('fileChooser');
    fileChooser.onchange = function() {
        if (fileChooser.files[0]) {
            file = fileChooser.files[0];
    
            // Do something with 'file'...
        }
    }



    使用FileReader异步读取文件



    var fileContent;
    
    var fileReader = new FileReader();
    fileReader.onload = function(e) {
        fileContent = e.target.result;
    
        // Do something with 'fileContent'...
    }
    fileReader.readAsArrayBuffer(file);



    其实这里的fileContent就是上面AudioContext要解码的那个binary,至此两部分的工作就可以连起来了。

    WARNING:


    Chrome或Firefox浏览器的跨域访问限制会使FileReader在本地失效,Chrome用户可在调试时添加命令行参数:

    chrome.exe --disable-web-security



    Canvas绘制频谱

    这一部分我不打算详细叙述,就提几个重点。



    AnalyserNode数据解析

    在绘制之前通过下面的方法获取到AnalyserNode分析的数据:

    var dataArray = new Uint8Array(analyser.frequencyBinCount);
    analyser.getByteFrequencyData(dataArray);

    数组中每个元素是从0到fftSize属性值的数值,这样我们通过一定比例就能控制能量条的高度等状态。


    requestAnimationFrame的使用

    要使动画动起来,我们需要不断重绘Canvas标签里的内容,这就需要requestAnimationFrame这个函数了,它可以帮你以60fps的帧率绘制动画。

    使用方法:

    var draw = function() {
        // ...
    
        window.requestAnimationFrame(draw);
    }
    
    window.requestAnimationFrame(draw);



    这段代码应该不难理解,就是一个类似递归的调用,但不是递归,有点像Android中的postInvalidate



    实例代码

    贴上我写的一段绘制代码:





    var render = function() {
        ctx = canvas.getContext("2d");
        ctx.strokeStyle = "#00d0ff";
        ctx.lineWidth = 2;
        ctx.clearRect(0, 0, canvas.width, canvas.height); //清理画布
    
        var dataArray = new Uint8Array(analyser.frequencyBinCount);
        analyser.getByteFrequencyData(dataArray);
        var step = Math.round(dataArray.length / 60); //采样步长
    
        for (var i = 0; i < 40; i++) {
            var energy = (dataArray[step * i] / 256.0) * 50;
            for (var j = 0; j < energy; j++) {
                ctx.beginPath();
                ctx.moveTo(20 * i + 2, 200 + 4 * j);
                ctx.lineTo(20 * (i + 1) - 2, 200 + 4 * j);
                ctx.stroke();
                ctx.beginPath();
                ctx.moveTo(20 * i + 2, 200 - 4 * j);
                ctx.lineTo(20 * (i + 1) - 2, 200 - 4 * j);
                ctx.stroke();
            }
            ctx.beginPath();
            ctx.moveTo(20 * i + 2, 200);
            ctx.lineTo(20 * (i + 1) - 2, 200);
            ctx.stroke();
        }
    
        window.requestAnimationFrame(render);
    }






    OK,大致就是这样,之后可以加一些css样式,完善一下业务逻辑,这里就不再阐释了。最后贴上整理好的全部代码:



    HTML 部分



    <html>
        <head>
            <title>HTML5 Audio Visualizing</title>
            <style type="text/css">
                body {
                    background-color: #222222
                }
    
                input {
                    color: #ffffff
                }
    
                #wrapper {
                    display: table;
                     100%;
                    height: 100%;
                }
    
                #wrapper-inner {
                    display: table-cell;
                    vertical-align: middle;
                    padding-left: 25%;
                    padding-right: 25%;
                }
    
                #tip {
                    color: #fff;
                    opacity: 0;
                    transition: opacity 1s;
                    -moz-transition: opacity 1s;
                    -webkit-transition: opacity 1s;
                    -o-transition: opacity 1s;
                }
    
                #tip.show {
                    opacity: 1
                }
            </style>
            <script type="text/javascript" src="./index.js"></script>
        </head>
    
        <body>
            <div id="wrapper">
                <div id="wrapper-inner">
                    <p id="tip">Decoding...</p>
                    <input id="fileChooser" type="file" />
                    <br>
                    <canvas id="visualizer" width="800" height="400">Your browser does not support Canvas tag.</canvas>
                </div>
            </div>
        </body>
    </html>



    Js部分



    var AudioContext = window.AudioContext || window.webkitAudioContext; //Cross browser variant.
    
    var canvas, ctx;
    var audioContext;
    var file;
    var fileContent;
    var audioBufferSourceNode;
    var analyser;
    
    var loadFile = function() {
        var fileReader = new FileReader();
        fileReader.onload = function(e) {
            fileContent = e.target.result;
            decodecFile();
        }
        fileReader.readAsArrayBuffer(file);
    }
    
    var decodecFile = function() {
        audioContext.decodeAudioData(fileContent, function(buffer) {
            start(buffer);
        });
    }
    
    var start = function(buffer) {
        if(audioBufferSourceNode) {
            audioBufferSourceNode.stop();
        }
    
        audioBufferSourceNode = audioContext.createBufferSource();
        audioBufferSourceNode.connect(analyser);
        analyser.connect(audioContext.destination);
        audioBufferSourceNode.buffer = buffer;
        audioBufferSourceNode.start(0);
        showTip(false);
        window.requestAnimationFrame(render);
    }
    
    var showTip = function(show) {
        var tip = document.getElementById('tip');
        if (show) {
            tip.className = "show";
        } else {
            tip.className = "";
        }
    }
    
    var render = function() {
        ctx = canvas.getContext("2d");
        ctx.strokeStyle = "#00d0ff";
        ctx.lineWidth = 2;
        ctx.clearRect(0, 0, canvas.width, canvas.height);
    
        var dataArray = new Uint8Array(analyser.frequencyBinCount);
        analyser.getByteFrequencyData(dataArray);
        var step = Math.round(dataArray.length / 60);
    
        for (var i = 0; i < 40; i++) {
            var energy = (dataArray[step * i] / 256.0) * 50;
            for (var j = 0; j < energy; j++) {
                ctx.beginPath();
                ctx.moveTo(20 * i + 2, 200 + 4 * j);
                ctx.lineTo(20 * (i + 1) - 2, 200 + 4 * j);
                ctx.stroke();
                ctx.beginPath();
                ctx.moveTo(20 * i + 2, 200 - 4 * j);
                ctx.lineTo(20 * (i + 1) - 2, 200 - 4 * j);
                ctx.stroke();
            }
            ctx.beginPath();
            ctx.moveTo(20 * i + 2, 200);
            ctx.lineTo(20 * (i + 1) - 2, 200);
            ctx.stroke();
        }
    
        window.requestAnimationFrame(render);
    }
    
    window.onload = function() {
        audioContext = new AudioContext();
        analyser = audioContext.createAnalyser();
        analyser.fftSize = 256;
    
        var fileChooser = document.getElementById('fileChooser');
        fileChooser.onchange = function() {
            if (fileChooser.files[0]) {
                file = fileChooser.files[0];
                showTip(true);
                loadFile();
            }
        }
    
        canvas = document.getElementById('visualizer');
    }
  • 相关阅读:
    React教程(一) React介绍与搭建
    微信公众号订阅号以及服务号通过网页授权获取用户openid方法
    AES,DES加密JS源文件及其使用方法
    R 分类进行数值处理
    C++ const 关键字总结
    软工lintcode作业
    [恶意软件分析]DroidBox的环境搭建与使用
    Android第三次作业
    Android第二次作业
    android 第一次作业
  • 原文地址:https://www.cnblogs.com/niwalala/p/5124293.html
Copyright © 2020-2023  润新知