通过Web Audio API可视化输出MP3音乐频率波形


发布者 ourjs  发布时间 1423213574801
关键字 JS学习  JavaScript 
Web Audio API(网络音频API)过去几年中已经改进很多,通过网页播放声音和音乐已经成为了可能。但这还不够,不同浏览器的行为方式还有不同。但至少已经实现了.

在这篇文章中,我们将通过DOM和Web Audio API创建一个可视化音频的例子。由于Firefox还不能正确处理CORS,Safari浏览器存在一些字节处理问题,这个演示只能在Chrome上使用。 注* 形状会波形而变化.



Audio 组件


首先我们需要创建一个audio组件,通过预加载(preloading)和流式(streaming)播放时时处理.

创建Audio上下文Context


AudioContext是Web Audio API的基石,我们将创建一个全局的AudioContext对象,然后用它"线性"处理字节流.


/* 创建一个 AudioContext */
var context;

/* 尝试初始化一个新的 AudioContext, 如果失败抛出 error */
try {
    /* 创建 AudioContext. */
    context = new AudioContext();
} catch(e) {
    throw new Error('The Web Audio API is unavailable');
}


通过XHR预加载MP3(AJAX)


通过XMLHttpRequest的代理,我们能从服务器获取数据时做一些时髦而有用的处理。在这种情况下,我们将.mp3音频文件转化为数组缓冲区ArrayBuffer,这使得它能更容易地与Web Audio API交互。


/*一个新的 XHR 对象 */
var xhr = new XMLHttpRequest();
/* 通过 GET 请连接到 .mp3 */
xhr.open('GET', '/path/to/audio.mp3', true);
/* 设置响应类型为字节流 arraybuffer */
xhr.responseType = 'arraybuffer';
xhr.onload = function() {
    /* arraybuffer 可以在 xhr.response 访问到 */
};
xhr.send();

在XHR的onload处理函数中,该文件的数组缓冲区将在 response 属性中,而不是通常的responseText。现在,我们有array buffer,我们可以继续将其作为音频的缓冲区源。首先,我们将需要使用decodeAudioData异步地转换ArrayBuffer到AudioBuffer。


/* demo的音频缓冲缓冲源 */
var sound;

xhr.onload = function() {
    sound = context.createBufferSource();

    context.decodeAudioData(xhr.response, function(buffer) {
        /* 将 buffer 传入解码 AudioBuffer. */
        sound.buffer = buffer;
        /*连接 AudioBufferSourceNode 到 AudioContext */
        sound.connect(context.destination);
    });
};


通过XHR预加载文件的方法对小文件很实用,但也许我们不希望用户要等到整个文件下载完才开始播放。这里我们将使用一个稍微不同点的方法,它能让我们使用HTMLMediaElement的流媒体功能。


通过HTML Media元素流式加载


我们可以使用<audio>元素流式加载音乐文件, 在JavaScript中调用createMediaElementSource方式, 直接操作HTMLMediaElement, 像play()和pause()的方法均可调用.


/* 声明我们的 MediaElementAudioSourceNode 变量 */
var sound,
    /* 新建一个 `<audio>` 元素. Chrome 支持通过 `new Audio()` 创建,
     * Firefox 需要通过 `createElement` 方法创建. */
    audio = new Audio();

/* 添加 `canplay` 事件侦听当文件可以被播放时. */
audio.addEventListener('canplay', function() {
    /* 现在这个文件可以 `canplay` 了, 从 `<audio>` 元素创建一个
     * MediaElementAudioSourceNode(媒体元素音频源结点) . */
    sound = context.createMediaElementSource(audio);
    /* 将 MediaElementAudioSourceNode 与 AudioContext 关联 */
    sound.connect(context.destination);
    /*通过我们可以 `play` `<audio>` 元素了 */
    audio.play();
});
audio.src = '/path/to/audio.mp3';

这个方法减少了大量的代码,而且对于我们的示例来说更加合适,现在让我们整理一下代码用promise模式来定义一个Sound的Class类.


/* Hoist some variables. */
var audio, context;

/* Try instantiating a new AudioContext, throw an error if it fails. */
try {
    /* Setup an AudioContext. */
    context = new AudioContext();
} catch(e) {
    throw new Error('The Web Audio API is unavailable');
}

/* Define a `Sound` Class */
var Sound = {
    /* Give the sound an element property initially undefined. */
    element: undefined,
    /* Define a class method of play which instantiates a new Media Element
     * Source each time the file plays, once the file has completed disconnect
     * and destroy the media element source. */
    play: function() {
        var sound = context.createMediaElementSource(this.element);
        this.element.onended = function() {
            sound.disconnect();
            sound = null;
        }
        sound.connect(context.destination);

        /* Call `play` on the MediaElement. */
        this.element.play();
    }
};

/* Create an async function which returns a promise of a playable audio element. */
function loadAudioElement(url) {
    return new Promise(function(resolve, reject) {
        var audio = new Audio();
        audio.addEventListener('canplay', function() {
            /* Resolve the promise, passing through the element. */
            resolve(audio);
        });
        /* Reject the promise on an error. */
        audio.addEventListener('error', reject);
        audio.src = url;
    });
}

/* Let's load our file. */
loadAudioElement('/path/to/audio.mp3').then(function(elem) {
    /* Instantiate the Sound class into our hoisted variable. */
    audio = Object.create(Sound);
    /* Set the element of `audio` to our MediaElement. */
    audio.element = elem;
    /* Immediately play the file. */
    audio.play();
}, function(elem) {
    /* Let's throw an the error from the MediaElement if it fails. */
    throw elem.error;
});


现在我们能播放音乐文件,我们将继续尝试来获取audio的频率数据.


处理Audio音频数据



在开始从audio context获取实时数据前,我们要连线两个独立的音频节点。这些节点可以从一开始定义时就进行连接。

/* 声明变量 */
var audio,
    context = new (window.AudioContext ||
                   window.webAudioContext ||
                   window.webkitAudioContext)(),
    /* 创建一个1024长度的缓冲区 `bufferSize` */
    processor = context.createScriptProcessor(1024),
    /*创建一个分析节点 analyser node */
    analyser = context.createAnalyser();

/* 将 processor 和 audio 连接 */
processor.connect(context.destination);
/* 将 processor 和 analyser 连接 */
analyser.connect(processor);

/* 定义一个 Uint8Array 字节流去接收分析后的数据 */
var data = new Uint8Array(analyser.frequencyBinCount);


现 在我们定义好了analyser节点和数据流,我们需要略微更改一下Sound类的定义,除了将音频源和audio context连接,我们还需要将其与analyser连接.我们同样需要添加一个audioprocess处理processor节点.当播放结束时再 移除.


play: function() { 
    var sound = context.createMediaElementSource(this.element);
    this.element.onended = function() {
        sound.disconnect();
        sound = null;
        /* 当文件结束时置空事件处理 */
        processor.onaudioprocess = function() {};
    }
    /* 连接到 analyser. */
    sound.connect(analyser);
    sound.connect(context.destination);

    processor.onaudioprocess = function() {
        /* 产生频率数据 */
        analyser.getByteTimeDomainData(data);
    };
    /* 调用 MediaElement 的 `play`方法. */
    this.element.play();
}

这也意味着我们的连接关系大致是这样的:


MediaElementSourceNode \=> AnalyserNode => ScriptProcessorNode /=> AudioContext
                        \_____________________________________/


为了获取频率数据, 我们只需要简单地将audioprocess处理函数改成这样:


analyser.getByteFrequencyData(data);


可视化组件


现在所有的关于audio的东西都已经解决了, 现在我们需要将波形可视化地输出来,在这个例子中我们将使用DOM节点和 requestAnimationFrame. 这也意味着我们将从输出中获取更多的功能. 在这个功能中,我们将借助CSS的一些属性如:transofrm和opacity.

初始步骤


我们先在文档中添加一些css和logo.


<div class="logo-container">
    <img class="logo" src="/path/to/image.svg"/>
</div>


.logo-container, .logo, .container, .clone {
    width: 300px;
    height: 300px;
    position: absolute;
    top: 0; bottom: 0;
    left: 0; right: 0;
    margin: auto;
}

.logo-container, .clone {
    background: black;
    border-radius: 200px;
}

.mask {
    overflow: hidden;
    will-change: transform;
    position: absolute;
    transform: none;
    top: 0; left: 0;
}


现在,最重要的一点,我们将会把图片切成很多列.这是通过JavaScript完成的.


/* 开始可视化组件, 让我们定义一些参数. */
var NUM_OF_SLICES = 300,
    /* `STEP` 步长值,
     * 影响我们将数据切成多少列 */
    STEP = Math.floor(data.length / NUM_OF_SLICES),
    /* 当 analyser 不再接收数据时, array中的所有值都值是 128. */
    NO_SIGNAL = 128;

/* 获取我们将切片的元素 */
var logo = document.querySelector('.logo-container');

/* 我们稍微会将切好的图片与数据交互 */
var slices = []
    rect = logo.getBoundingClientRect(),
    /* 感谢 Thankfully在 `TextRectangle`中给我们提供了宽度和高度属性 */
    width = rect.width,
    height = rect.height,
    widthPerSlice = width / NUM_OF_SLICES;

/* 为切好的列,创建一个容器 */
var container = document.createElement('div');
container.className = 'container';
container.style.width = width + 'px';
container.style.height = height + 'px';


创建 'slices' 切片



我们需要为每一列添加一个遮罩层,然后按x轴进行偏移.


/* Let's create our 'slices'. */
for (var i = 0; i < NUM_OF_SLICES; i++) {
    /* Calculate the `offset` for each individual 'slice'. */
    var offset = i * widthPerSlice;

    /* Create a mask `<div>` for this 'slice'. */
    var mask = document.createElement('div');
    mask.className = 'mask';
    mask.style.width = widthPerSlice + 'px';
    /* For the best performance, and to prevent artefacting when we
     * use `scale` we instead use a 2d `matrix` that is in the form:
     * matrix(scaleX, 0, 0, scaleY, translateX, translateY). We initially
     * translate by the `offset` on the x-axis. */
    mask.style.transform = 'matrix(1,0,0,1,' + offset + '0)';

    /* Clone the original element. */
    var clone = logo.cloneNode(true);
    clone.className = 'clone';
    clone.style.width = width + 'px';
    /* We won't be changing this transform so we don't need to use a matrix. */
    clone.style.transform = 'translate3d(' + -offset + 'px,0,0)';
    clone.style.height = mask.style.height = height + 'px';

    mask.appendChild(clone);
    container.appendChild(mask);

    /* We need to maintain the `offset` for when we
     * alter the transform in `requestAnimationFrame`. */
    slices.push({ offset: offset, elem: mask });
}

/* Replace the original element with our new container of 'slices'. */
document.body.replaceChild(container, logo);


定义我们的渲染函数



每当audioprocess处理函数接收到数据,我们就需要重新渲染,这时 requestAnimationFrame 就派上用场了.


/* Create our `render` function to be called every available frame. */
function render() {
    /* Request a `render` on the next available frame.
     * No need to polyfill because we are in Chrome. */
    requestAnimationFrame(render);

    /* Loop through our 'slices' and use the STEP(n) data from the
     * analysers data. */
    for (var i = 0, n = 0; i < NUM_OF_SLICES; i++, n+=STEP) {
        var slice = slices[i],
            elem = slice.elem,
            offset = slice.offset;

        /* Make sure the val is positive and divide it by `NO_SIGNAL`
         * to get a value suitable for use on the Y scale. */
        var val = Math.abs(data[n]) / NO_SIGNAL;
        /* Change the scaleY value of our 'slice', while keeping it's
         * original offset on the x-axis. */
        elem.style.transform = 'matrix(1,0,0,' + val + ',' + offset + ',0)';
        elem.style.opacity = val;
    }
}

/* Call the `render` function initially. */
render();


现在我们完成了所有的DOM构建, 完整的在线示例 , 完整的源码文件同样在此DEMO中.







回复 (5)
  • #
  • #1 zjzhome 1428586848472

    文章好极了。

    var audio = new Audio(); firefox已经支持了

  • #2 彭叫右 1457299643025

    修改了MP3地址就无法播放,用他的地址就可以正常播放。

  • #3 危另央 1459332859230

    说明你的mp3的格式有问题,可以重新编码一下。

  • #4 方宅工 1469008298737

    能否用一个特别大的mp3文件做测试?浏览器会crash掉。 mp3文件超过140M。

  • #5 叶丹主 1547187429539

    不错不错,很有学习借鉴意义的,给楼主点赞。这个分享挺棒的,辛苦了。 https://www.cnblogs.com/xiaoyan2017/p/10163043.html

微信扫码 立即评论