白板鼓 – WebRTC 和 Web Audio API 的奇妙应用

浏览器功能发展迅速,早已超越了仅仅“浏览”文档的范畴。最近,Web 浏览器终于通过 Web Audio API 获得了音频处理能力。它的强大功能足以构建专业的音乐应用程序。

不仅如此,当它与其他 API 结合使用时,也变得非常有趣。其中一个 API 是 getUserMedia(),它允许我们从本地 PC 的麦克风/摄像头设备捕获音频和/或视频。 白板鼓 (GitHub 上的代码) 就是一个音乐应用程序,也是使用 Web Audio API 和 getUserMedia() 可以实现什么的绝佳示例。

我在 10 月份的东京 Web 音乐黑客马拉松上展示了白板鼓。这是一场关于 Web Audio API 和 Web MIDI API 的非常激动人心的活动。许多乐器可以与浏览器协作,它还可以创建与现实世界的新界面。

我相信这暗示了基于 Web 的音乐应用程序的更多可能性,特别是将 Web Audio API 与其他 API 结合使用。让我们解释一下白板鼓的关键功能是如何工作的,并在过程中展示相关的代码片段。

概述

首先,让我向您展示黑客马拉松上的一张照片

以及一个简单的演示视频

如您所见,白板鼓根据白板上的矩阵图案演奏节奏。白板没有任何魔力;它只需要用网络摄像头对准即可。虽然我在演示中使用了磁铁,但如果愿意,您也可以用笔画出标记。每一行代表相应的乐器:铙钹、Hi-Hat、军鼓和底鼓,每一列代表时间步长。在此实现中,序列有 8 个步长。蓝色会正常激活网格,红色会以重音激活。

处理流程如下:

  1. 网络摄像头捕获白板图像
  2. 分析矩阵图案
  3. 将此图案馈送到鼓声音生成器以创建相应的音符图案

虽然它使用了新兴的浏览器技术,但每个过程本身并不复杂。下面描述了一些关键点。

使用 getUserMedia() 捕获图像

getUserMedia() 是一个用于从网络摄像头/麦克风设备捕获视频/音频的函数。它是 WebRTC 的一部分,并且是 Web 浏览器中一项相当新的功能。请注意,需要用户的许可才能从网络摄像头获取图像。如果我们只是在屏幕上显示网络摄像头图像,那将非常容易。但是,我们希望在 JavaScript 中访问图像的原始像素数据以进行更多处理,因此我们需要使用 canvascreateImageData() 函数。

因为稍后在此应用程序中需要逐像素处理,所以捕获的图像的分辨率降低到 400 x 200 像素;这意味着在节奏模式矩阵中,一个节奏网格是 50 x 50 像素。

注意:虽然大多数最新的笔记本电脑/笔记本电脑都嵌入了网络摄像头,但您将从外部摄像头获得白板鼓的最佳效果,因为摄像头需要精确地对准白板上的图片。此外,目前对来自多个可用设备/摄像头的输入的选择还没有标准化,并且无法在 JavaScript 中控制。在 Firefox 中,它可以在连接时的权限对话框中选择,并且可以在 Google Chrome 的设置屏幕的“内容设置”选项中进行设置。

获取网络摄像头视频

我们不想在屏幕上显示这些处理部分,所以首先我们隐藏 video

现在获取视频

video = document.getElementById("video");
navigator.getUserMedia=navigator.getUserMedia||navigator.webkitGetUserMedia||navigator.mozGetUserMedia;
navigator.getUserMedia({"video":true},
    function(stream) {
        video.src= window.URL.createObjectURL(stream);
        video.play();
    },
    function(err) {
        alert("Camera Error");
    });

捕获它并获取像素值

我们也隐藏 canvas

然后在 canvas 上捕获我们的视频数据

function Capture() {
    ctxcapture.drawImage(video,0,0,400,200);
    imgdatcapture=ctxcapture.getImageData(0,0,400,200);
}

网络摄像头中的视频将以定期间隔绘制到 canvas 上。

图像分析

接下来,我们需要使用 getImageData() 获取 400 x 200 像素值。分析阶段以 8 x 4 矩阵节奏模式分析 400 x 200 图像数据,其中单个矩阵网格为 50 x 50 像素。所有必要的输入数据都存储在 imgdatcapture.data 数组中,格式为 RGBA,每个像素 4 个元素。

var pixarray = imgdatcapture.data;
var step;
for(var x = 0; x < 8; ++x) {
    var px = x * 50;
    if(invert)
        step=7-x;
    else
        step=x;
    for(var y = 0; y < 4; ++y) {
        var py = y * 50;
        var lum = 0;
        var red = 0;
        for(var dx = 0; dx < 50; ++dx) {
            for(var dy = 0; dy < 50; ++dy) {
                var offset = ((py + dy) * 400 + px + dx)*4;
                lum += pixarray[offset] * 3 + pixarray[offset+1] * 6 + pixarray[offset+2];
                red += (pixarray[offset]-pixarray[offset+2]);
            }
        }
        if(lum < lumthresh) {
            if(red > redthresh)
                rhythmpat[step][y]=2;
            else
                rhythmpat[step][y]=1;
        }
        else
            rhythmpat[step][y]=0;
    }
}

这是对网格循环的诚实的逐像素分析。在此实现中,分析针对亮度和红色进行。如果网格“暗”,则激活网格;如果它是红色的,则应强调它。

亮度计算使用简化的矩阵 - R * 3 + G * 6 + B - 这将获得十倍的值 - 含义 - 这意味着为每个像素获取 0 到 2550 范围内的值。而红色 R - B 是一个实验值,因为只需要决定是红色还是蓝色。结果存储在 rhythmpat 数组中,值为 0 表示无,1 表示蓝色或 2 表示红色。

通过 Web Audio API 生成声音

因为 Web Audio API 是一项非常前沿的技术,所以并非每个 Web 浏览器都支持它。目前,基于 Google Chrome/Safari/Webkit 的 Opera 和 Firefox(25 或更高版本)支持此 API。注意:Firefox 25 是 10 月底发布的最新版本。

对于其他 Web 浏览器,我开发了一个回退到 Flash 的 polyfill:WAAPISim,可在 GitHub 上获取。它为不支持的浏览器(例如 Internet Explorer)提供了 Web Audio API 的几乎所有功能。

Web Audio API 是一个大型规范,但在我们的例子中,声音生成部分只需要非常简单地使用 Web Audio API:为每种乐器加载一个声音并在正确的时间触发它们。首先我们创建一个音频上下文,并在过程中注意供应商前缀。当前使用的前缀是 webkit 或无前缀。

audioctx = new (window.AudioContext||window.webkitAudioContext)();

接下来,我们通过 XMLHttpRequest 将声音加载到缓冲区中。在这种情况下,每种乐器的不同声音(bd.wav / sd.wav / hh.wav / cy.wav)将加载到 buffers 数组中

var buffers = [];
var req = new XMLHttpRequest();
var loadidx = 0;
var files = [
    "samples/bd.wav",
    "samples/sd.wav",
    "samples/hh.wav",
    "samples/cy.wav"
];
function LoadBuffers() {
    req.open("GET", files[loadidx], true);
    req.responseType = "arraybuffer";
    req.onload = function() {
        if(req.response) {
            audioctx.decodeAudioData(req.response,function(b){
                buffers[loadidx]=b;
                if(++loadidx < files.length)
                    LoadBuffers();
            },function(){});
        }
    };
    req.send();
}

Web Audio API 通过路由节点图来生成声音。白板鼓使用一个简单的图形,可以通过 AudioBufferSourceNodeGainNode 访问。AudioBufferSourceNode 直接回放 AudioBuffer 并路由到目标(输出)(用于正常的*蓝色*声音),或通过 GainNode 路由到目标(用于重音的*红色*声音)。因为 AudioBufferSourceNode 只能使用一次,所以它将在每次触发时重新创建。

准备 GainNode 作为重音声音的输出点如下所示。

gain=audioctx.createGain();
    gain.gain.value=2;
    gain.connect(audioctx.destination);

触发函数如下所示

function Trigger(instrument,accent,when) {
    var src=audioctx.createBufferSource();
    src.buffer=buffers[instrument];
    if(accent)
        src.connect(gain);
    else
        src.connect(audioctx.destination);
    src.start(when);
}

剩下的就是讨论根据节奏模式播放时间的准确性。虽然使用 setInterval() 定时器不断创建触发器很简单,但不建议这样做。任何 CPU 负载都可能轻松地弄乱计时。

为了获得准确的计时,建议使用嵌入在 Web Audio API 中的时间管理系统。它计算上面 Trigger() 函数的 when 参数。

// console.log(nexttick-audioctx.currentTime);
while(nexttick - audioctx.currentTime < 0.3) {
    var p = rhythmpat[step];
    for(var i = 0; i < 4; ++i)
        Trigger(i, p[i], nexttick);
    if(++step >= 8)
        step = 0;
    nexttick += deltatick;
}

在白板鼓中,此代码控制功能的核心。nexttick 包含下一步的准确时间(以秒为单位),而 audioctx.currentTime 是准确的当前时间(同样,以秒为单位)。因此,此例程每 300 毫秒触发一次 - 意味着提前 300 毫秒查看(在 nextticktime - currenttime < 0.3 时提前触发)。注释掉的 console.log 将打印时间间隔。当此例程定期调用时,如果此值为负,则计时将折叠。

有关更多详细信息,以下是一篇有帮助的文档:两个时钟的故事 - 精确调度 Web Audio

关于 UI

特别是在 DAW 或 VST 插件等音乐制作软件中,UI 非常重要。Web 应用程序不必完全模拟它,但类似的东西是一个好主意。幸运的是,非常方便的 WebComponent 库 webaudio-controls 可用,允许我们仅使用一个 HTML 标签来定义旋钮或滑块。

注意:webaudio-controls 使用 Polymer.js,它有时存在稳定性问题,偶尔会导致意外行为,尤其是在将其与复杂的 API 结合使用时。

未来的工作

这已经是一个有趣的应用程序,但可以进一步改进。显然,摄像头位置调整是一个问题。如果它具有位置自动调整(使用某种标记?)和自适应颜色检测,则分析可以更加智能。声音生成也可以改进,包括更多乐器、更多步长和更多音效。

挑战一下怎么样?

白板鼓可在 http://www.g200kg.com/whiteboarddrum/ 获取,并且 代码在 GitHub 上

玩一玩它,看看你能创作出什么节奏!

关于 新谷田达也

软件开发人员,尤其是在音频方面。开发了许多 VST 插件和基于 Web 的音频/音乐应用程序。新谷田达也,又名 g200kg

更多新谷田达也的文章……

关于 罗伯特·奈曼 [荣誉编辑]

Mozilla Hacks 的技术布道师和编辑。发表关于 HTML5、JavaScript 和开放 Web 的演讲和博客。罗伯特是 HTML5 和开放 Web 的坚定支持者,自 1999 年以来一直从事 Web 前端开发工作 - 在瑞典和纽约市。他还定期在 http://robertnyman.com 发布博客,热爱旅行和结识新朋友。

更多罗伯特·奈曼 [荣誉编辑]的文章……


4 条评论

  1. Sten Hougaard

    很棒的工作!

    2013年11月14日 00:45

  2. Arin Sime

    很棒的演示,这太酷了。感谢您提供详尽的示例。肯定会收录到 RealTimeWeekly.com 的下一期中。

    2013年11月15日 09:09

  3. Pavel Demyanenko

    为这个演示点赞!

    2013年11月28日 01:51

  4. Abdul Qabiz

    太棒了!很高兴有这样的示例来演示这些新的 API(浏览器的功能),这些功能以前只能通过 Adobe Flash Player 等插件获得。

    2013年12月7日 06:23

本文的评论已关闭。