音频标签:Web Components + Web Audio = ♥

文章作者:Soledad Penadés,编辑:Angelina Fabbro。

上周,我们发布了 Brick 1.0,我们精心挑选的一套用于快速开发的 Web Components。使用组件可以轻松地将这些 UI 小部件与现有代码和框架集成。

而本周,我们为您带来的是 *音频标签*,这是一个构建 Web Components 的实验,这些组件代表 Web Audio 块,使我们能够构建一个完整的乐器并提供一个界面来演奏它。通过可重复使用的音频块,开发者可以在无需编写大量样板代码的情况下,体验 Web Audio。

让我们构建一个简单的合成器来演示不同的标签如何协同工作!

音频上下文

首先,我们需要一个音频上下文。如果您之前做过任何 Canvas 编程,这听起来会很熟悉。上下文类似于一个工具箱:它包含了您需要的函数(工具),也是所有操作发生的地方。所有其他音频标签都将放置在上下文中。

以下是如何在使用音频标签时呈现音频上下文



就是这样!

振荡器

虽然能够只通过键入一个标签声明来创建音频上下文很棒,但如果我们无法获得任何可听的输出,那它就没有那么令人兴奋。我们需要一些东西来生成声音,为此,我们将从简单的事情开始,并使用一个 *振荡器*。顾名思义,输出是一个在两个值之间振荡的信号:-1 和 1,生成周期性波形。我们将它放在音频上下文中,以便其输出通过上下文的输出自动路由到计算机的扬声器


    
    

Context with oscillator

(查看在线演示).

在现实世界中,振荡器 可以生成不同的信号形状。同样,在 Web Audio 世界中,我们也有类似的波形类型可以使用:正弦波、方波、锯齿波和三角波。由于 Web Components 是第一类 DOM 元素,因此我们可以使用属性指定所需的波形类型


您甚至可以通过打开控制台并键入以下内容来 *实时* 更改它

document.querySelector('audio-oscillator').type = 'square';

同样,您还可以通过设置 frequency 属性来更改振荡器运行的频率


混音器

运行一个振荡器仅仅是第一步。大多数可用的合成器都同时播放多个振荡器,以使声音更复杂、更细腻。我们需要某种方法并行播放两种或多种声音,同时将其组合成单个输出。

这通常被称为音频混音,因此我们需要一个 *混音器*


    
        
        
    

混音器将获取其每个子元素的输出,并将它们组合在一起形成其自己的输出,然后将其连接到上下文的输出。还要注意,由于我们正在处理 DOM 元素,所以当我们说“子元素”时,我们实际上指的是混音器的 DOM 子元素。

Mixer

(示例).

链(和示波器)

当您开始添加多个声音时,能够 *查看* 合成器中发生了什么非常有用。如果我们能够以某种方式在某个子元素的输出和另一个子元素的输入之间插入一个组件,并显示该点处的声波看起来是什么样子?

我们无法使用混音器做到这一点,因为它只是将所有输出组合在一起。我们需要一个新的抽象结构:*链*。音频链将连接其第一个子元素的输出到第二个子元素的输入,以及第二个子元素的输出到第三个子元素的输入,依此类推,直到我们到达最后一个子元素,并将它的输出连接到链的输出。

或者换句话说:**混音器并行连接事物,而链串行连接事物**。

让我们使用一个链将一个新元素——示波器——连接到振荡器的输出。示波器将只显示连接到其输入的内容,并且信号将通过其输出而不被修改。您可以将振荡器的波形类型更改为 square,并查看示波器如何相应地更改其显示。


    
        
        
    

Chain

(示例).

滤波器

合成器不仅限于同时运行多个振荡器。它们通常会在此原始生成的音频中添加后处理单元,从而赋予合成器自身独特的音色。

有许多类型的后处理效果,其中一些最流行的是 *滤波器*,它们大致通过突出显示某些频率或去除其他频率来工作。例如,我们可以将一个低通滤波器连接到振荡器的输出,这将只允许较低的频率通过。这会产生一种阻尼效果,就像我们戴上了耳罩一样,因为高频通过空气传播,我们用耳廓 *听到* 它们,而低频则倾向于通过地球和物体传播。因此,与其说我们听到它们,不如说我们用身体 *感觉到* 它们,无论你是否戴着东西遮住耳朵都没有关系。


    
        
        
    

Filter

(示例).

Web Audio 本身实现了 双二阶极点滤波器,并且与 audio-oscillator 标签一样,您可以通过设置其 type 属性来更改滤波器的行为。例如


您甚至可以插入多个示波器:一个在滤波器之前,另一个在滤波器之后,以查看滤波器对信号的影响


    
        
        
        
        
    

Filter with two oscilloscopes

(示例).

最后,是迷你合成器

我们现在拥有足够的组件来构建一个合成器了!我们希望两个振荡器一起演奏(一个比另一个高八度),并使用一个滤波器使声音不那么刺耳,更“自成一体”。因此,毫不犹豫,这是使用我们迄今为止介绍的组件来表示我们的最小合成器 <mini-synth> 的结构


    
        
        
    
    

为了进行比较,这或多或少是我们如何使用原始的 Web Audio API 对象和函数组装类似的设置

var mixerGain = context.createGain();

var osc1 = context.createOscillator();
var osc2 = context.createOscillator();
osc1.connect(mixerGain);
osc2.connect(mixerGain);

var filter = context.createBiquadFilter();
mixerGain.connect(filter);

// and the actual output is at *filter*

代码本身并不复杂,只是它没有声明式语法的良好视觉层次结构。语法中的视觉提示使理解元素之间的关系变得快速而简单。

我们仍然需要几行 JavaScript 代码才能使 <mini-synth> 组件像合成器一样工作:它必须同时启动和停止两个振荡器。我们可以利用 AudioTag 原型 有一些通用的基本方法,我们可以重载这些方法以在自定义组件中获得特定的行为。

在这种特定情况下,我们将重载 startstop 方法,以便在我们在合成器中调用这些方法时,分别使振荡器开始和停止播放。这样,我们就将合成器的内部抽象出来,同时仍然公开一个一致的接口。

start: function(when) {
    // We want to make sure we don't clip (i.e. go under -1 or over 1),
    // so we'll divide the gain by the number of oscillators in the synth
    var oscGain = this.oscillators.length > 0 ? 1.0 / this.oscillators.length : 1.0;
    this.oscillators.forEach(function(osc) {
        osc.gain = oscGain;
        osc.start(when);
    });
}

stop: function(when) {
    this.oscillators.forEach(function(osc) {
        osc.stop(when);
    });
}

实现 应该很容易理解。

您可能想知道 when 参数。它用于告诉浏览器何时实际开始操作,以便您可以以精确的时序安排未来的各种事件。它表示“在 when 毫秒时执行此代码”。在我们的例子中,我们只使用值 0,这意味着“立即执行”。我建议您在 Web Audio 规范 中了解更多关于 when 的信息。

我们还需要实现一个方法来实际告诉合成器要演奏哪个音符,或者换句话说,每个振荡器应该以什么频率运行。所以让我们实现 noteOn

noteOn: function(noteNumber) {
    this.oscillators.forEach(function(osc, index) {
        // Each oscillator should play in a higher octave
        // Each octave is composed of 12 notes
        var oscNoteNumber = noteNumber + 12 * index;
        // We're using a library to convert note numbers to frequencies
        var frequency = MIDIUtils.noteNumberToFrequency(oscNoteNumber);
        osc.frequency = frequency;
    });
}

您 *不需要* 使用 MIDIUtils,但如果您想在浏览器中与乐器一起 *即兴演奏*,而其他人使用的是更传统的 MIDI 乐器,那么它会派上用场。通过使用标准频率,您可以确保您的乐器都调到相同的音高,这 *很好*。

我们还需要一种触发合成器中音符的方法,那么还有什么比拥有一个屏幕键盘组件更好的方法呢?


将插入一个带有 2 个八度的键盘组件。一旦键盘获得焦点(通过单击它),您就可以敲击计算机键盘上的键,它将发出 noteon 事件。如果我们监听这些事件,我们就可以将它们发送到合成器。noteoff 事件也是如此

keyboard.addEventListener('noteon', function(e) {

  var noteIndex = e.detail.index;
  // 48 is the base note here = C-3
  minisynth.noteOn(parseInt(noteIndex, 10) + 48);
  minisynth.start();

}, false);

keyboard.addEventListener('noteoff', function(e) {

  minisynth.noteOff();

}, false);

所以现在是 演示 时间了!

Minisynth

现在我们有了合成器,可以说我们是摇滚明星了!但摇滚明星需要看起来 *很酷*。现实生活中的摇滚明星有他们自己的签名吉他以及定制的音箱。而我们有……CSS!我们可以使用 CSS 尽情发挥,所以只需点击演示中的 *成为摇滚明星* 按钮,然后观看合成器如何凭借 CSS 的魔力变成 *其他东西*。

Minisynth, rockstarified

幕后一瞥

到目前为止,我们只讨论了这些花哨的新音频标签,并假设它们在您的浏览器中 *神奇地* 可用,尽管很明显它们是非标准元素。我们还没有解释它们的来源。好吧,如果您已经读到这里,那么您理应了解王国中的秘密!

如果您查看任何示例的源代码,您会注意到我们始终包含 AudioTags.bundle.js (第 18 行) 和 AudioTags.bundle.css (第 6 行) 文件。CSS 并不特别令人兴奋,真正的魔法发生在 JavaScript 中。此文件包含几个实用程序库,使我们能够在浏览器中定义自定义标签,然后是定义和使这些新标签在浏览器中可用的代码。

在实用程序方面,我们首先包含 AudioContext-MonkeyPatch,用于统一浏览器之间的 Web Audio API 差异,并使我们能够使用现代且一致的语法。如果您想了解有关编写可移植 Web Audio 代码的更多信息,可以查看 这篇文章

我们包含的第二个库是 X-Tag,更具体地说,是它的最 内部核心。X-Tag 是一个自定义元素 polyfill,而自定义元素是新兴的 Web Components 规范 的一部分,这意味着这些内容很快就会内置到浏览器中。X-Tag 是 Mozilla Brick 使用的同一个库。您可以学习如何使用 这篇文章 创建自己的自定义元素。

也就是说,如果您计划在同一个项目中使用 Brick 和 Audio Tags,可能会发生灾难,因为 Brick 和 Audio Tags 都在其分发包中包含了 X-Tag 的核心。这两个库的作者正在讨论如何处理这个问题的最佳方法,但我们还没有达成任何行动,因为 Audio Tags 是 *由 X-Tag 提供支持的* 库场景中的新手。无论如何,最可能的结果是我们将提供一个选项来构建 Brick 和 Audio Tags *而无需* 包含 X-Tag 核心。

此外,这里有一个关于相同内容的视频,可在 CascadiaJS 上观看,因此您可以观看某人当面构建它。这可能有助于您理解主题

音频标签的下一步是什么?

许多人一直在问我音频标签的 *下一步* 是什么。即将推出的功能是什么?我计划了什么?如何贡献?我们如何添加新标签?

说实话?我不知道!但这就是它的魅力所在。这只是一个起点,一个邀请人们思考、玩耍和讨论声明式音频组件概念的机会。当然,在 Audio Tags 的 README 文件中,有一个尚不工作的列表、一些随机的想法以及可能的功能。我可能会继续扩展它并填补空白——这是一个在不变得太混乱的情况下试验音频的好场所,也是对超越通常的“增强型封装 UI 小部件”概念的 Web Components 的良好测试。

有些人发现这个项目本身很有启发性;另一些人认为它对教授信号处理很有用,另一些人将其与加速度计数据混合以创建物理控制的合成器,还有一些人决定放弃它的音频方面,而只是为 WebRTC 目的构建自定义组件。如果您想这样做,您可以贡献您的力量!

关于 Soledad Penadés

Sole 在 Mozilla 的开发者工具团队工作,帮助人们在 Web 上创造惊人的东西,最好是实时创造。在 irc.mozilla.org 上的 #devtools 频道找到她

更多 Soledad Penadés 的文章……

关于 Angelina Fabbro

我是一位来自加拿大不列颠哥伦比亚省温哥华的开发者,在 Mozilla 担任技术布道师和 Firefox OS 的开发者倡导者。我喜欢 JavaScript、Web Components、Node.js、移动应用开发,以及这个我经常光顾的酷地方——万维网。哦,还有 Firefox OS。在我的业余时间,我上声乐课,玩万智牌,教人们编程,并与科学家合作,以促进程序员与科学家之间的互动。

更多 Angelina Fabbro 的文章……


15 条评论

  1. Hugo

    HTML5 音频的性能如何?我的意思是,理论上是否可能创建一个像 Reason、Ableton、Cubase 等完整的音频制作软件?或者当运行过多的滤镜和乐器时,性能会很快下降?HTML5 中能否实现适合此类应用的低延迟音频?或者这对 JavaScript 来说要求太高了吗?

    2014年3月12日 15:05

  2. Soledad Penadés

    Hugo,原生 Web Audio 节点非常快(它们运行...原生代码)。如果你使用脚本处理器创建自定义节点,性能可能会开始下降。如果你将频繁的 UI 更新与音频处理混合,通常会变得非常糟糕。有些人通过使用 Emscripten/Closure 等工具从 C/C++ 或 Java 导出到 JS,构建了非常棒的复杂音频软件。但这取决于你想做到多疯狂。

    2014年3月12日 17:12

    1. Hugo

      好的,谢谢你的回复。

      2014年3月13日 12:32

  3. Mario

    非常有趣的文章!我也喜欢音乐创作和录音,我想我会尝试(即使我是一个新手)创建更多组件(例如延迟、混响)来创建一个在线 DAW...谢谢!

    2014年3月13日 01:20

    1. Soledad Penadés

      请尝试一下!混响有点复杂(从字面上看),因为你必须从外部源加载卷积响应文件,所以有点复杂,但应该相对容易。

      2014年3月14日 12:41

  4. Alex

    是否可以用 HTML5 制作 VoIP 客户端?或者将来可能会计划此功能?不需要 Flash、Java 或其他任何东西,只需要 HTML5 和 JavaScript。

    2014年3月13日 10:37

    1. Soledad Penadés

      看看 WebRTC:https://mdn.org.cn/en-US/docs/WebRTC

      2014年3月14日 12:40

  5. Zeno Rocha

    很棒的组合 Sole,感谢这篇文章 :)

    2014年3月14日 06:45

    1. Soledad Penadés

      谢谢 Zeno!非常感谢…尤其是来自你的 :)

      2014年3月14日 12:40

  6. Anders

    (哇,一篇关于 Web 技术的博客,没有预览,也没有对评论进行 HTML 转义。这太复古了)

    我认为你声明式语言的设计中有一些选择需要重新考虑。特别是“audio-chain”元素似乎构思不佳。使用序列定义结构是不好的做法,这也是 br 元素(与它们分隔的线按顺序放置)大多被 div 元素(封装这些线)取代的原因。

    也就是说,这个例子
    <audio-context>
    <audio-chain>
    <audio-oscillator frequency=”220″></audio-oscillator>
    <audio-oscilloscope></audio-oscilloscope>
    <audio-filter type=”lowpass”></audio-filter>
    <audio-oscilloscope></audio-oscilloscope>
    </audio-chain>
    </audio-context>
    可能应该这样写(请注意,“audio-context”元素是冗余的,因为只能有一个顶级元素)
    <audio-oscilloscope>
    <audio-filter type=”lowpass”>
    <audio-oscilloscope>
    <audio-oscillator frequency=”220″></audio-oscillator>
    </audio-oscilloscope>
    </audio-filter>
    </audio-oscilloscope>

    你可能还想考虑从 XAML 中借用附加属性的语法,以便允许使用多个命名输入。例如,如果示波器可以在 2d 模式下使用并接收两个输入,一个用于 x 分量,一个用于 y 分量,它可能看起来像这样
    <audio-oscilloscope>
    <audio-oscillator audio-oscilloscope.input=”x” frequency=”220″></audio-oscillator>
    <audio-oscillator audio-oscilloscope.input=”y” frequency=”374″></audio-oscillator>
    </audio-oscilloscope>

    希望将来会支持命名空间以减少所需的输入。

    2014年3月14日 09:11

    1. Soledad Penadés

      自定义组件规范就是规范。如果你编写自己的规范、解析器等,可以使用任何你想要的语法,但我只是遵循建议的标准 :-)

      音频上下文不是冗余的,因为...如何初始化孤立节点?你只是凭空神奇地创建它的父节点吗?我们如何处理同级节点,我们假设它们属于最后一个上下文,还是…?

      2014年3月14日 12:38

      1. Anders

        我不清楚音频标签是建议的标准。它只是 x-tags 用法的(有点牵强)示例。
        在给出的示例中,始终只有一个父节点(对应于一个输出)。当需要“孤立节点”时尚不清楚。上下文是实现细节,因此,如果底层 API 需要它,可以隐式创建它。

        2014年3月17日 11:07

        1. Soledad Penadés

          因为音频标签不是建议的标准。它是一组自定义组件,恰好封装了 Web Audio 功能。它不需要成为标准,就像 jQuery 尽管被广泛使用,但也不是标准。

          在示例中,始终只有一个父节点,但你可以拥有除音频上下文之外的其他类型的父节点。例如:OfflineAudioContext。

          2014年3月17日 12:48

  7. Pablo Cúbico

    嘿 Sole!

    你是否知道任何使用 Emscripten 移植的音频实验示例?我正在寻找一些信息来探索它!

    2014年3月18日 15:58

    1. Hugo

      +1 我也很想知道 asm.js 用于音频处理的性能。

      如果 WebAudio API 在性能方面得到正确的改进(例如,使用并行内核运行 AudioNode 等),并且如果我们在此基础上使用 asm.js 构建自定义节点,我认为我们可以获得接近原生的性能。

      我唯一担心的是 WebAudio API 会朝着易用性和视频游戏创建的方向发展,而不是朝着实时音乐创作/合成方向发展。

      Web 平台对于音乐软件、实时协作来说将非常棒,无需使用专门的 MIDI 驱动程序/库(只需使用 Web MIDI API),使用更多奇特的 API(使用 Xbox 或 Wii 控制器通过 GamePad API 控制你的合成器效果)等…
      在原生开发中,处理跨平台方面并具备这些功能简直是一场噩梦。

      2014年3月31日 14:52

本文评论已关闭。