元素有一个属性来指定被引用媒体资源的媒体类型。该属性是来自 web 开发者的提示,并且使浏览器更容易确定它是否可以播放所引用的媒体资源。它会跳过那些它确定无法加载的文件,只测试那些它有机会加载的文件。
浏览器基于它所获得的关于它支持哪些编解码器的信息来做出“可能”的决定。这可以是直接在浏览器中实现的一组固定的编解码器,也可以是从 GStreamer、Media Foundation 或 QuickTime 等底层媒体框架中检索的编解码器列表。
当给定不带编解码器参数的 MIME 类型时,浏览器将返回“可能”,当给定带有它们支持的格式的编解码器参数的 MIME 类型时,浏览器将返回“可能”。否则,它们返回空字符串。
虽然所有的浏览器都在转向所谓的内容类型嗅探,即通过下载一小部分文件来检查其类型,但旧版本的浏览器仍然依赖于正确提供的文件的 MIME 类型。无论如何,为您的文件提供正确的 MIME 类型是最佳实践。因此,请确保您的 web 服务器为您的文件报告正确的 MIME 类型。通常,web 服务器会检查文件扩展名和 MIME 类型之间的映射(例如,对于 Apache,它是mime.types
文件)。在浏览器页面检查器中,检查 web 浏览器下载的视频文件的“内容类型”HTTP 头以进行确认。
即便如此,我们也不得不承认,我们无法控制哪块屏幕——从智能手机到你家中的平板电视——将被用来观看或收听你的内容。在这种情况下,您可能需要创建各种分辨率的视频文件,一旦浏览器判断出使用的是哪种屏幕,就只能加载其中一种。这就是@media
属性在当今响应式 Web 设计世界中发挥巨大作用的地方。
为了保护您的用户的理智,以适合您的目标设备的分辨率对您的视频进行编码-这将使您能够从同一视频元素中针对从 4K 屏幕大小到移动设备的任何内容。您不需要调整视频元素的宽度和高度。您提供由媒体查询调用的不同文件的副本。您不希望将一个巨大的高清视频文件传送到一个小的移动屏幕上,这将导致浏览器不得不下载比它能够显示的更多的数据,解码比它能够显示的更高的分辨率,然后不得不针对您的实际设备进行降采样。因此,即使是高质量的编码视频,在移动设备上的呈现也比适当大小的视频差。
我们通过快速浏览浏览器为音频和视频元素实现的用户界面来结束这次讨论。这些界面设计仍在不断变化——YouTube 大约每六个月推出一个新的播放器界面——在未来一段时间内,网络浏览器很可能会对其音频和视频播放器进行改进并添加功能。
HTML5 音频和视频元素的默认用户界面分为两种不同的类型:可见控件和隐藏在上下文菜单中的控件,通常通过右键单击元素来实现。播放器的设计取决于浏览器,这意味着每个播放器都是不同的。
Macintosh 键盘的等效键是 PC CTRL 键的 Command 和 PC Alt 键的 option、PC Home 键的功能键左箭头键和 PC End 键的功能键右箭头键。
上下文菜单为用户提供了常用操作的快捷方式。在 Mac 上,当用户右键单击或按住 Control 键单击视频或音频元素时,可以看到上下文菜单。音频和视频的大多数功能是相同的。
在 Safari 的例子中,如图 2-15 中的所示的上下文菜单,如果不是极简的话,也有点不同。有些菜单项允许您隐藏控件或选择仅视频全屏渲染,而不是单击控制器上的“进入全屏”按钮。
至此,我们已经介绍了如何用 HTML5 视频元素编写网页。在前一章中,我们回顾了如何对媒体文件进行编码,以使它们受到支持 HTML5 视频的浏览器的支持。现在,我们通过了解这些文件如何从“这里”(服务器)移动到“那里”(浏览器),来看看如何实际发布视频及其网页,从而结束这个循环。在这之后,我们有了所有的工具来制作带有视频和音频的 HTML5 网页。
记住,你必须创建至少一个 MPEG-4 版本的视频文件和一个 WebM(或 Ogg Theora)版本的视频文件,这样你才能在所有浏览器上使用。对于音频文件,你需要 MP3 和 Ogg Vorbis 版本。此外,如果您针对高分辨率和低分辨率的视频,您最好使用不同尺寸的不同视频文件版本,并使用媒体查询来区分它们。
您应该将 WebM 和 MP4 文件复制到 web 服务器上适合您的 web 应用/网页的目录中。web 服务器是一种软件,它可以使用 HTTP(超文本传输协议)并通过计算机网络传送 web 内容。有几种开源 web 服务器,最流行的是 Apache 和 Nginx。
通过 HTTP 提供 HTML5 视频是 web 浏览器支持 HTML5 视频元素的标准方式。在选择服务器软件时,请确保它支持 HTTP 1.1 字节范围请求。大多数常见的 web 服务器,包括 Apache 和 Nginx,都会支持它,但偶尔您仍然可以找到一个不支持或不能正常工作的服务器。
支持 HTTP 字节范围请求非常重要,因为这是浏览器从 web 服务器接收 HTML5 媒体资源的标准方式,这称为渐进式下载。字节范围请求的重要性是双重的:首先,它允许以小块的形式传输媒体文件,这使浏览器能够开始回放,而不必等待下载完整的文件。其次,也是更重要的一点,它允许从文件中的任何地方获取这些块,而不是等待接收到所有以前的数据。这尤其允许寻找视频的随机位置并从那里开始回放。
这是怎么回事?媒体文件解码是一件复杂的事情。媒体文件包含设置音频和视频解码管道所需的信息(参见第一章)。该信息通常存储在文件的开头。音频和视频数据以多路复用的方式提供(即,一个视频位,然后是相关的音频位,然后是下一个视频位,等等)。).为了分隔这些数据位,并获得回放时间的映射,媒体文件通常包含一个索引表。在 MP4 文件中,该索引位于文件的末尾。如果没有收到该文件,浏览器将无法开始解码和播放。因此,如果没有字节范围请求,我们就必须等待回放,直到下载完完整的文件。
在设置元数据并获得索引表后,浏览器通常只对整个媒体资源发出一次请求,并在数据到达时开始播放。对于很长的文件,当下载在当前播放位置之前很远,并且用户已经停止观看视频时,浏览器可能会暂停下载。允许浏览器暂停下载的条件是 web 服务器支持字节范围请求,因此当回放位置再次接近缓冲位置时,浏览器可以恢复下载。这将节省带宽的使用,特别是超过几分钟的视频。
当设置您的 web 服务器来托管 HTML5 音频和视频时,您不必做任何特殊的事情。对于执行有限内容嗅探的旧浏览器,您需要确保媒体资源使用正确的 MIME 类型。您的 web 服务器可能需要手动将以下一些媒体类型添加到mime.types
安装文件中:
要测试您的 web 服务器是否支持 HTTP 1.1 字节范围请求,您可以通过浏览器开发工具检查 HTTP 头,或者尝试从以下命令行下载视频文件,例如,使用 curl 等 URL 加载工具:
正如典型的 web 设计和开发的演变一样,这些年来事情已经发生了变化。最大的变化之一是转向响应式网页设计,承认我们无法控制用来查看内容的屏幕/视窗。这导致了使用 CSS 来控制视频的呈现,而不是内联样式,从而将内容及其呈现完全嵌入到 HTML 文档的主体中。
随着向响应式世界的转变和向 HTML5 的转变,CSS 中也有一个重要的变化正在进行中。一个新的标准——CSS level 3 或 CSS3——正在开发中,它提供了许多新的和改进的功能,包括改进的布局、形状、扩展的媒体查询以及使用语音合成呈现文本的改进。其中许多可以应用于 HTML5 视频,在这一节中,我们将简要看看具体的例子。
CSS3 引入了许多新的样式方法,比如框上圆角的 border-radius,使用图像作为边框的 border-image,下拉阴影的 border-shadow,以及指定尺寸计算与框的哪些部分相关的框大小。
,这样视频就在它自己的匿名块中了。您还可以在 video 元素上放置一个“display:block”规则,将它变成一个“block”元素,并将每个元素前后的句子放在它们自己的匿名块中。
如果我们让视频“显示:无”,它将从渲染中消失。相反,我们可以用“可见性:隐藏”使它不可见。这将保持嵌入式视频框在原来的位置,但使它完全透明。请注意,视频仍将预加载,因此您可能希望使用@preload="none"
来避免这种情况。
一种更常见的技术是将视频放在文本块中,并让文本围绕它流动。这是使用 float 属性完成的,可以设置为 left 或 right,如清单 2-30 中的代码所示。
不透明属性已经存在很长时间了,但是直到它成为官方的 CSS3 阵容时才成为跨浏览器的标准。不透明度定义为 0.0 到 1.0 之间的任何值,其中 0.0 表示元素完全透明,1.0 表示元素完全不透明。
对视频应用渐变是一个非常有趣的挑战。对于图像,人们可以将图像放入背景中,并在其上叠加一个渐变。我们想复制这个视频,但不能使用 CSS 将视频放在背景中。因此,我们必须在视频顶部渲染一个渐变<div>
。CSS3 规范指定了两个函数来创建渐变:线性渐变()和径向渐变()。
这里的关键是背景属性。该属性需要两个值:开始颜色和结束颜色。在这种情况下,我们在渐变底部从透明的白色移动到不透明的白色。不透明度的变化给出了渐变遮罩在视频上的效果。我们使用供应商前缀来确保渐变在所有浏览器中都有效。图 2-24 显示了最终结果。
这种技术越来越普遍。你到达一个站点,在页面的内容下面有一个全屏的视频在播放。尽管有许多相当复杂的技术来实现这一点,但这很容易做到。诀窍就在 CSS 中。可用的 CSS 属性之一是 z 位置。正数将带有视频的
首先,视频的物理尺寸很好地限制了它在桌面上的使用。即使这样,这种技术也最适合于循环播放较短(大约 10-30 秒)且文件大小不大的视频。例如,将视频的颜色深度降低到灰度或 8 位(256 色)将对最终的文件大小产生重大影响。此外,视频需要尽可能有效地压缩,同时仍然能够跨设备和屏幕缩放。根据经验,尽量将文件大小保持在 5 mb 以下,500k 是最理想的。
在本例中,使用 transform 属性简单地将视频向左旋转 30 度。我们使用了–WebKit 前缀使其在 Chrome、Safari 和 Opera 中工作,这些浏览器正在移除这个前缀。
在讨论这个规范时忽略 JavaScript 并不是没有争议的,因为 JavaScript 已经允许相同的效果。然而,一个关键的网络原则赢得了胜利。这个原则是我们在本章一直强调的:HTML 管理内容,CSS 处理显示,JavaScript 处理交互性。
我们只是简单介绍了 CSS 对视频元素的使用。尤其是 CSS3,它有更多可以应用于 CSS 盒子的特性。你可以开发的视觉效果几乎是无限的。在之前的版本中,我们展示了旋转的 3D 立方体,每一面都有视频。
这是一个很大的范围,但它是你需要知道的成功添加音频或视频到 HTML5 页面。在下一章中,我们将深入探讨如何通过使用 JavaScript API 来实现媒体交互。我们在那里见。
随着 HTML5 的兴起,使用 JavaScript 来扩展网页上各种元素的功能也相应增加了。事实上,越来越少看到 HTML5 页面在页眉或文档的其他地方不包含指向 JavaScript 文档或库的链接。在处理 HTML5 视频或音频时也是如此。
由于可以在 web 浏览器中关闭 JavaScript 支持,所以解释 HTML 和 CSS 提供了什么而无需进一步编写脚本是很重要的。然而,将 JavaScript 加入其中,会将这些 web 技术转变为应用开发的强大平台,我们将会看到媒体元素能做出什么贡献。
在 HTML5 和 CSS3 开发之前的几年里,JavaScript 被用来为 Web 带来许多新特性。在许多人分享共同需求的地方,JavaScript 库和框架如 jQuery、YUI、Dojo 或 MooTools 被创造出来。许多 web 开发人员使用这些框架来简化 web 内容的开发。使用这些库的经验反过来又推动了 HTML5 的几个新特性的引入。因此,我们现在看到 HTML5 中的许多早期框架的功能,以及新框架的发展,使得开发 HTML5 web 应用变得更加容易。
由于 JavaScript 在 web 浏览器中执行,因此它只使用用户机器的资源,而不必与 web 服务器交互来更改网页。这对于处理任何类型的用户输入都特别有用,并且使得网页对用户的响应更快,因为网络上的任何交换都不会降低网页的响应速度。因此,在不需要将用户信息保存在服务器上的情况下,使用 JavaScript 最为合适。例如,游戏可以以这样的方式编写,即游戏的逻辑在 web 浏览器中以 JavaScript 执行,并且只有用户获得的高分才需要与 web 服务器进行交互。这当然假设游戏所需的所有资源——图像、音频等。—已经取回。
JavaScript 通过 DOM 与 HTML 接口。DOM 是一个分层的对象结构,它包含了一个 web 页面的所有元素以及它们的属性值和访问函数。它表示 HTML 文档的层次结构,并允许 JavaScript 访问 HTML 对象。webIDL,Web 接口定义语言(www.w3.org/TR/WebIDL/
),已经被创建来允许对象暴露给 JavaScript 和 Web 浏览器实现的接口的规范。
原因很简单。HTML 仅仅是一种在页面上放置对象的标记语言。这些对象及其属性由浏览器保存,并通过编程接口公开。IDL 实际上是一种语言,用来描述浏览器持有的这些数据结构,并使 JavaScript 可以对它们进行操作。
为了简化对媒体元素的 JavaScript API 的解释,我们将分别查看从内容属性和纯 IDL 属性创建的 IDL 属性。这将有助于更好地理解哪些属性是从 HTML 传入 JavaScript 的,哪些属性是为了允许脚本控制和操作而创建的。
出于本章的目的,我们假设您对 JavaScript 有基本的了解,并且能够遵循 WebIDL 规范。与阅读许多面向对象编程语言中的类定义相比,阅读 WebIDL 相当简单。我们将解释 HTML5 媒体元素为 WebIDL 中的 JavaScript 提供的新引入的接口,并提供一些关于通过使用这些接口 JavaScript 可以实现什么的示例。我们从内容属性开始。
我们已经在第二章中熟悉了 HTML5 媒体元素的内容属性。它们都直接映射到媒体元素的 IDL 接口。HTML 规范将这种映射称为 IDL 属性中内容属性的“反映”。你可以在媒体元素 JavaScript 对象中看到来自第二章的内容属性的反映。
如果你仔细查看列表,第二章中出现的每个元素和属性都会出现。所有这些属性都可以在 JavaScript 中读取(也称为“get”)和设置。您可以在前面提到的代码块中看到内容属性值被转换成的 JavaScript 类型。
如果一个开源的、易于使用的、完全可定制的播放器是你正在寻找的,那么你可能想试试 Video JS。这个播放器是由 Zencoder 的人制作的,旨在吸引从初学者到代码战士的所有 web 技能水平。
有时,各种浏览器或第三方播放器使用的播放器不符合项目的设计目标。这可能是由于品牌,功能集,甚至只是个人喜好。在这种情况下,构建一个定制的视频播放器是唯一合理的选择。
这是 JavaScript API 的使用真正闪光的地方。它最重要的用例是“滚动你自己的”控件,其样式在所有浏览器上都是一样的。由于这是一个如此常见的用例,我们为您提供了一个如何做到这一点的示例。它包括 HTML 代码的框架、一些 CSS 和控制它所需的 JavaScript 调用。
我们的计划是构建如图 3-27 所示的播放器。你可能会发现我们选择的控件和它们的布局有点不寻常。这是因为我们决定构建一个针对盲人或视力受损用户的播放器,提供他们可能需要的主要按钮。为了更好地使用,这些按钮被故意保持为大的、彩色编码的,并且不在视频中。您可以使用空格键轻松地切换并激活它们。
请注意,在 Safari 中,默认情况下,“tab”是禁用的,不能作为跨页面元素的导航方式,您必须使用 option-tab 来导航。要打开“tab”导航,请打开您的首选项并选中“首选项 高级 按 tab 突出显示页面上的每个项目。”
播放器由几个界面元素组成。它有一个进度显示(视频下面的栏),后面显示播放的秒数和视频持续时间。下面是一组按钮。这些按钮允许视频开始播放(播放/暂停切换)、倒退 5 秒、停止播放(并重置为文件开始)、增加 10 个百分点的音量、降低 10 个百分点的音量以及静音/取消静音切换。视频右侧是音量显示,静音时显示为灰色,音量水平以条形高度的百分比显示。
可访问性和国际化是可用性的两个方面。第一种——无障碍——是为那些有某种形式的感官或身体障碍(如失明)的人设计的。第二种方式——国际化——吸引了那些不会说音频或视频文件所用语言的人。
自 20 世纪 90 年代中期以来,Web 已经开发了大量的功能来满足这些用户的额外需求。网站以多种语言呈现,屏幕阅读器或盲文设备为视力受损的用户提供了阅读网页内容的能力。视频的字幕,尤其是外语视频,或者对图像使用@alt
属性,已经变得几乎无处不在,对图像使用@alt
已经是很长时间以来的最佳实践。
在 HTML5 规范中引入音频和视频带来了新的可访问性挑战,需要扩展这一最佳实践。我们第一次发布需要让听力受损用户和/或不讲音频数据中所用语言的用户能够访问的音频内容。我们还首次发布了 HTML 图像内容,这些内容会随着时间的推移而变化,需要让视力受损的用户能够访问。
我们绝不能忘记的是“万维网”中的“世界”一词。不像媒体广播公司可以挑选他们的观众,我们的观众是由通晓多种语言的健全人和残疾人以及不同的文化和语言组成的。每个访问你的视频或音频内容的人都有和其他人一样的权利去访问它,你不能挑选和选择谁来查看你的内容。
满足这种需求的主要手段是开发所谓的替代内容技术(或替代内容),在这种技术中,向用户提供的内容以他们能够消费的格式给出了原始内容的替代表示。1995 年,随着 HTML 2 中的@alt
属性的引入,提供 alt 内容的实践被正式化,从那时起,它就成了规范的一个固定部分。
当发布媒体内容时,所有这些备选方案也应该发布,这样就不会遗漏任何受众。不要把它当成一件苦差事:alt 内容通常是有用的附加内容,例如,在视频的字幕或章节标记的情况下,它可以帮助任何用户跟踪正在讲的内容,并导航到视频文件中有用的位置。
在本章中,我们将讨论 HTML5 为满足媒体用户的可访问性和国际化需求而提供的特性。我们从需求分析开始这一章,概述了媒体内容的替代内容技术。然后我们介绍 HTML5 为满足需求而提供的特性。
在本书中,我们以“特征-示例-演示”的形式介绍了各种可用于向 HTML5 文档添加音频和视频资产的技术在此之前,我们将介绍 web 设计人员和开发人员在努力应对日益增长的可访问性和国际化需求时面临的许多问题。
越来越多的国家正在通过关于网络的无障碍法律。例如,在美国,1973 年《康复法》第 504 条是第一部旨在保护残疾人免受基于其残疾状况的歧视的民权立法。虽然互联网,更不用说个人电脑,还不存在,但该法律适用于任何接受联邦资金的雇主或组织,这包括政府机构,从 K-12 到大专的教育机构,以及任何其他联邦资助的项目。1998 年,当互联网蓬勃发展时,重新授权的康复法案第 508 条创建了具有约束力和可执行的标准,明确概述和指定了“可访问”的电子和信息技术产品的含义。结果是,在美国,为接受联邦资金的公司或组织开发的任何 web 项目都必须遵守 Section 508。如果你不熟悉第五百零八部分,在http://webaim.org/articles/laws/usa/
有一个很好的概述。
挑战 web 开发人员的下一个问题是围绕音频和视频的 alt 内容的用户需求的多样性,这是相当复杂的。如果你想了解更多关于媒体可访问性需求的知识,还有一个由 WAI 发布的 W3C 文档,由本书的作者之一合著:www.w3.org/WAI/PF/media-a11y-reqs/
。
当需要扩展描述时,混合音频描述需要使用完全独立的视频元素,因为它们在不同的时间线上工作。然而,创建这样一个混合了音频描述的新视频文件所涉及的制作工作是巨大的。因此,只有在没有其他方法来提供所描述的视频时,才应该使用混音。
对于听力有困难的用户,音轨的内容需要以音频的替代形式提供。字幕、文字记录和手语翻译传统上被用作替代方法。此外,对播放音频的改进也可以帮助并非完全失聪的重听人掌握音频的内容。
这种字幕总是以文本形式创作,但有时会直接添加到视频图像中。这种技术被称为烧录字幕,或“开放字幕”,因为它们总是活跃的,开放给每个人看。传统上,这种方法已经被用于在电视和电影院传送字幕,因为它不需要任何额外的技术来复制。然而,这种方法非常不灵活。在网络上,这种方法是不鼓励的,因为它很容易提供文字说明。只有没有烧录字幕的视频不可用的传统内容才能以这种方式发布。最好的情况是,带有烧录字幕的视频应该作为多轨道媒体文件中的单独轨道或媒体源中的单独流提供,这样用户就可以在带有字幕的视频轨道和不带字幕的视频轨道之间进行选择。
视听资源的音频轨道的全文抄本是使重听用户,事实上是任何人都能获得这些内容的另一种手段。阅读(或交叉阅读)音频或视频资源的副本可能比完整阅读更有效。一个特别好的例子是一个名为 Metavid 的网站,它有美国参议院会议的全部记录,并且是完全可搜索的。
顺便说一句,后一种类型的交互式文字稿在与屏幕阅读器结合使用时,作为导航辅助工具,对视力受损的用户也很有用。然而,当搜索交互式抄本时,有必要将视听内容静音,因为否则它将与来自屏幕阅读器的声音竞争,使两者都难以理解。
对于重听用户,尤其是聋人用户,手语通常是他们说的最熟练的语言,其次是他们所居住国家的书面语。他们通常用手语更快更全面地交流,这很像普通话和类似的亚洲语言,通常通过单一的语义实体符号进行交流。字母也有手语,但是用手语表达字母非常慢,而且只在特殊情况下使用。手语的使用是重听用户之间最快也是最具表现力的交流方式。
这不是针对听力受损者的替代内容,而是提高音频内容可用性的更普遍适用的功能。人们普遍认为语音是音轨中最重要的部分,因为它传达了最多的信息。在现代多声道内容中,语音有时作为独立于声音环境的单独声道提供。一个很好的例子是卡拉 ok 音乐内容,但是清晰的音频内容也可以容易地提供给专业开发的视频内容,例如电影、动画或电视连续剧。
许多用户在理解混合音轨中的语音时存在问题。但是,当在单独的音轨中提供语音时,可以允许独立于其余音轨来增加语音音轨的音量,从而呈现“更清晰的音频”——即,更容易理解的语音。
如果聋哑用户自己消费视听内容,提供包含屏幕和音频上发生的事情的描述的抄本是有意义的。它基本上是一个文本视频描述和一个音频抄本的组合。因此,这在技术上的实现最好是作为一个组合的抄本。有趣的是,盲文设备非常擅长导航超文本,因此一些用导航标记增强的转录形式也很有用。
在一个共享的观看环境中,其中聋哑用户与一个看得见和/或听得见的人一起消费内容,文本和音频描述的组合需要与视频回放同步提供。典型的盲文阅读速度是每分钟 60 个单词。相比之下,成年人的平均阅读速度约为每分钟 250 至 300 个单词,甚至通常的说话速度为每分钟 130 至 200 个单词,你会意识到,对于一个聋哑人来说,很难跟上任何正常的视听演示。可能需要一个概要版本,它仍然可以同步提供,就像同步提供文本描述一样,并且可以交给盲文设备。因此,这一点的技术实现要么是作为交互式抄本,要么是通过概括的文本描述。
一些用户喜欢减慢回放速度,以帮助他们感知和理解视听内容;对于其他人来说,正常播放速度太慢。特别是视力受损的用户已经学会以惊人的速度消化音频。对于这样的用户来说,能够减慢或加快视频或音频资源的回放速度是非常有帮助的。这种速度变化需要保持音频的音高以保持其可用性。
对那些有学习障碍的人非常有帮助的一个特征是提供解释的能力。例如,每当使用一个不常见的单词时,弹出该术语的解释(例如,通过到维基百科或字典的链接)会非常有帮助。这有点类似于增强字幕的目的,并且可以通过允许超链接和/或覆盖以相同的方式提供。
有了学习材料,我们还可以在时间同步性上提供内容的语法标记。这通常用于语言学研究,但也可以帮助有学习障碍的人更好地理解内容。语法标记可以被增加到标题或副标题上,以提供给定上下文中单词的语法角色的转录。或者,语法角色可以仅作为时间段的标记来提供,依靠音频来提供实际的单词。
在学习类别下,我们还可以包含歌词或卡拉 ok 的用例。像字幕一样,这些为用户提供了一个时间同步的朗读(或演唱)文本显示。在这里,他们帮助用户学习和理解歌词。与字幕类似,它们在技术上可以通过烧录、带内多轨或外部轨道来实现。
视频轨道通常对外国用户来说只是一个小挑战。大多数场景文本并不重要,不需要翻译或者可以从上下文中理解。然而,有时屏幕上有解释位置的文本,如标题,翻译会很有用。建议在字幕中包含这样的文字。
抄本是一种方法,旨在提供在视听资源中找到的音轨的全文抄本——交互式或纯文本。这是一种让重听用户和任何人都可以访问视听内容的好方法。阅读(或交叉阅读)音频或视频资源的副本可能比完整阅读更有效。一个提供视频内容文字记录(见图 4-1 )的网站是ted.com
。
图 4-2 中显示的文字记录既有口头文本的记录,也有视频中发生的事情的记录。这是有意义的,因为抄本独立于视频,因此它必须包含视频中发生的一切。它还代表了文本描述和抄本,使其适用于被翻译成盲文的聋哑用户。
在前面的例子中,脚本以一个单独的 HTML 文档的形式呈现,该文档在自己的窗口中打开。在许多方面,这是一种提供抄本的静态方法。互动抄本以一种完全不同的方式提供体验。它们不仅提供了口语文本和视频中发生的事情的转录,而且它们还随着视频的时间移动,不需要单独的窗口。
此处设计的元素适用于视力和听力受损的用户。当您单击视频上的播放按钮时,视频会正常播放,并且作为交互式脚本一部分的字幕文本会显示在右侧的滚动显示中,突出显示当前提示。如果启用了 screenreader,脚本中标记有@aria-live
属性的标记将被复制到 screenreader 中,以便在适当的时候读出。点击一段文字,视频就会移动到播放的那个位置。
元素:字幕、标题和文本描述
现在你已经知道如何将脚本包含在你的视频项目中,让我们将注意力转向标题、副标题和描述,它们通常是与你的网页分开创作的。因此,HTML5 引入了特殊的标记和 API(应用编程接口)来自动将这些外部文件与视频的时间轴同步。
在这一节中,我们主要关注元素及其 API。它们已经被引入到 HTML 中,并允许您将基于时间的文本文件与媒体资源相关联。这个文本文件——通常是一个 WebVTT 或 .vtt
文件——可以以多种方式使用,包括添加媒体内容的字幕、标题和文本描述。
注意 值得一提的是,浏览器在< track >元素中可能还支持其他文件格式。比如 IE10 既支持 WebVTT,又支持 TTMLT5(定时文本标记语言)。字幕行业经常使用 TTML 在创作系统之间交换字幕,见www.w3.org/TR/ttml1/
。我们不会在这里更详细地讨论 TTML,因为它只在 IE 中受支持,其他浏览器已经明确表示他们对实现对它的支持不感兴趣。
WebVTT 是一个新标准,所有实现<track>
元素的浏览器都支持它。WebVTT 提供了一种简单的、可扩展的、人类可读的格式来构建文本轨道。
在下一节中,我们将深入探讨 WebVTT 格式的特性细节。然而,要使用元素,您需要使用一个基本的.vtt
文件。如果您不熟悉这种格式,基本的理解是有帮助的。WebVTT 文件是 UTF-8 文本文件,仅由“WEBVTT”文件标识符和一系列所谓的提示 组成,包含开始和结束时间以及一些提示文本。提示之间需要用空行隔开。例如,一个简单的 WebVTT 文件应该是
WEBVTT
00:00:15.000 --> 00:00:17.951
At the left we can see...
00:00:18.166 --> 00:00:20.083
At the right we can see the...
第一行——web vtt——必须全部用大写字母,浏览器用它来检查它是否真的是一个.vtt
文件。IE10 实际上要求这个头文件是“WEBVTT 文件”,因为其他浏览器忽略了额外的文本,所以你最好总是用这个标识符来创作文件。
提示中的时间标记提供了提示的持续时间,表示为小时:分钟:秒 mms ,提示文本是屏幕上显示的文本。在这种情况下,从视频播放时间线的 15 秒到 17.951 秒,将会看到左侧的“??”字样……“??”。任何创建纯文本文件的文字处理器或编辑器都可以用来创建一个.vtt
文件。
注 WebVTT 是以前叫 WebSRT 的现代版。对于那些在 SRT 已经有字幕项目的人来说,你会发现 VTT 采取了非常相似的方式。在https://atelier.u-sub.net/srt2vtt/
有一个简单的转换器。
随着.vtt
文件的创建,它需要被绑定到元素。该元素放在
或元素中,并引用外部时间同步的文本资源——一个
.vtt
文件——与
或元素的时间线对齐。在元素中,标题和字幕呈现在视频视窗的顶部。因为
元素没有视窗,所以作为
元素的子元素的元素不会被渲染,只对脚本可用。
??
??
注意 IE10 要求.vtt
文件服务的 mime 类型为“text/vtt”;否则它会忽略它们。因此,确保您的 Web 服务器有这样的配置(例如,对于 Apache,您需要将它添加到mime.types
文件中)。在浏览器页面检查器中,您可以检查 Web 浏览器为一个.vtt
文件下载的“内容类型”HTTP 头,以确认您的服务器提供了正确的 mime 类型。
让我们看看元素的内容属性。
@src
自然,这个属性引用了一个外部文本轨道文件。清单 4-3 是一个引用 WebVTT 文件的 track 元素的简单代码示例。
清单 4-3 。带有. vtt 文件的<轨道>标记示例
<video controls poster="img/ElephantDreams.png">
<source src="video/ElephantDreams.mp4" type="video/mp4">
<source src="video/ElephantDreams.webm" type="video/webm">
<track src="tracks/ElephantDreams_en.vtt">
</video>
@src
属性仅创建对外部文本轨道文件的引用。它不会激活它,但它允许浏览器向用户提供一个引用曲目的列表。这通常通过视频控件中的菜单显示。
注意 如果你和我们一起工作,在网络服务器上而不是本地运行<赛道>例子是很重要的。从文件 URL 加载的文档在基于 blink 的浏览器中有特殊的安全限制,以阻止您可能保存到桌面的恶意脚本做坏事。对于 Chrome,你也可以用这样的命令行标志来运行它,以避免这个问题:chrome --disable-web-security
。
图 4-4 显示了 Safari(左)和 Google Chrome(右)中清单 4-3 的结果显示。
图 4-4 。Safari 和 Google Chrome 中带有 track 子元素的视频元素
Safari ,如图 4-4 左侧所示,在视频控件上的语音气泡后面有一个菜单。你可以通过点击语音气泡来激活菜单。我们定义的曲目在菜单中被列为“未知”。通过点击激活该轨道,你可以观看渲染字幕。
谷歌 Chrome ,在右边,显示了一个“CC”按钮,通过它你可以激活标题和字幕。如果你点击那个按钮并观看视频,你将能够看到从呈现在视频顶部的.vtt
文件加载的字幕。
Opera 看起来和谷歌 Chrome 一模一样。在 Firefox 中,还没有字幕激活按钮。接下来我们将解释如何通过标记激活字幕。或者,您也可以通过 JavaScript 来完成,我们也将在本章的后面讨论。
Internet Explorer (见图 4-5 )是 Safari 和 Chrome 的结合。它包括 CC 按钮,当点击它时,会显示曲目的名称。点击名字,字幕就呈现出来了。
图 4-5 。Internet Explorer 中带有跟踪子元素的视频元素
注意 你可能已经注意到 Safari 和 Internet Explorer 为文本轨道提供了最有用的视觉激活和选择机制:从视频控件激活的菜单。所有的浏览器都打算实现这个特性,但是并不是所有的浏览器都达到了那个状态。谷歌 Chrome 和 Opera 目前只显示一个“CC”按钮,它可以激活最合适的字幕轨道(例如,如果你的浏览器语言设置为英语,则为英语)。
@默认
下一个属性——@default
——允许网页作者选择一个文本轨道,并在默认情况下将其标记为激活。这是一个布尔属性,意味着默认值与布尔真值相同。在这里,清单 4-4 提供了一个例子:
清单 4-4 。带有. vtt 文件的<跟踪>标记示例,使用@default 激活
<video controls poster="img/ElephantDreams.png">
<source src="video/ElephantDreams.mp4" type="video/mp4">
<source src="video/ElephantDreams.webm" type="video/webm">
<track src="tracks/ElephantDreams_en.vtt" default>
</video>
你可以在图 4-6 中看到左边 Opera 中的“CC”按钮和右边 Safari 中的菜单选择是如何自动开启的。谷歌 Chrome 和 Opera 一样,自动开启“抄送”按钮。
图 4-6 。Opera(左)和 Safari(右)中带有@default 激活轨道子元素的视频元素
现在,您也可以在 Firefox 中回放视频,并看到字幕显示,这与上一个示例中的结果不同。通过使用@default
属性,包含在.vtt
文件中的如图图 4-7 所示的字幕被激活。请注意,如果用户将光标放在视频底部,字幕将被视频控件隐藏。
如图 4-7 所示,Internet Explorer 不仅会激活字幕,还会显示正在播放的默认曲目。它目前被称为“无标题”,所以我们需要给它一个合适的名字。
图 4-7 。Firefox 和 Internet Explorer 中带有@default 激活轨道子元素的视频元素
@标签
我们刚刚了解到,在曲目选择菜单中,没有命名的曲目会被随机标记为“未知”或“未命名”。我们可以通过提供一个显式的@label
属性来解决这个问题。
清单 4-5 提供了一个例子。
清单 4-5 。带有. vtt 文件和@标签的<轨道>标记示例
<video controls poster="img/ElephantDreams.png">
<source src="video/ElephantDreams.mp4" type="video/mp4">
<source src="video/ElephantDreams.webm" type="video/webm">
<track src="tracks/ElephantDreams_en.vtt" default label="English">
</video>
图 4-8 显示了标签在 Safari 中的呈现方式。“英语”比“未知”明显得多,也容易理解得多。
图 4-8 。在 Safari 中使用@label 命名 track 子元素的视频元素(左)和 Internet Explorer(右)
@srclang
现在音轨已经有了标签,用户可以知道这是一首英文音轨。这是不应该对浏览器隐藏的重要信息。如果我们让浏览器知道我们正在处理一个英语音轨,那么当用户开始观看带有他们喜欢的语言字幕的视频时,浏览器可以决定自动激活这个音轨:“英语。”浏览器从浏览器或操作系统的设置中检索这样的用户偏好。为了使浏览器能够根据用户的喜好选择正确的曲目,我们有了@srclang
属性,它被赋予了一个根据 BCP47 的 IETF(互联网工程任务组)语言代码,以区分不同的曲目。
注意 浏览器还没有扩展他们的浏览器偏好来包括关于激活文本轨道的偏好设置。然而,一些浏览器使用平台设置来处理这个问题,特别是 Safari。
还要注意,在@srclang
中提供关于音轨资源语言的信息还有其他有效的用途(例如,Google 索引或自动翻译)。
清单 4-6 展示了一个如何使用@srclang
的例子。
清单 4-6 。带有. vtt 文件和@标签的<轨道>标记示例
<video controls poster="img/ElephantDreams.png">
<source src="video/ElephantDreams.mp4" type="video/mp4">
<source src="video/ElephantDreams.webm" type="video/webm">
<track src="tracks/ElephantDreams_en.vtt" srclang="en">
</video>
清单 4-6 中的一个重要方面是它不包含@label
或@default
属性。唯一的属性是@srclang
。当在 Safari 中渲染时,如图 4-9 所示,该音轨在菜单中仍标记为“英语”。该图还显示了标题的本地 OSX 辅助功能首选项设置。在这种情况下,字幕在 Safari 中显示为大文本,此选项还会打开“自动(推荐)”轨道选择,进而激活英文轨道。
图 4-9 。具有@srclang 属性的视频元素和平台上的默认激活
@善良
@kind
属性 指定您正在处理的文本轨道的类型和可用的@kind
属性值。这些值是:
字幕 : 对白的转录或翻译,适用于声音可用但听不懂的情况(如用户听不懂媒体资源的配乐语言)。这样的轨道适合于国际化的目的。
字幕 : 对话、声音效果、相关音乐提示和其他相关音频信息的转录或翻译,适用于当音轨不可用时(例如,对话被静音、被环境噪声淹没或因为用户是聋子)。这样的音轨适合听力不好的用户。
描述: 媒体资源的视频组件的文本描述,当可视组件模糊、不可用或不可用时(例如,用户正在与应用交互,因为用户是盲人),这对于音频合成是有用的。合成为音频。这种轨道适合视力受损的用户。
章节: 章节标题用于导航媒体资源。此类曲目在浏览器界面中显示为交互式(潜在嵌套)列表。
元数据 : 用于 JavaScript 的音轨。浏览器不会渲染这些轨道。
如果没有指定@kind
属性,该值默认为“字幕”,这是我们在前面的例子中经历过的。
如果被激活,标记为字幕 或字幕 的轨道将在视频视窗中呈现。在任一时间点只能激活一个字幕或副标题轨道。这也意味着这些轨道中只有一个应该使用@default
属性创作——否则浏览器不知道默认激活哪个。
标记为描述 的曲目,如果被激活,将会把它们的提示合成为音频——可能通过屏幕阅读器 API。由于屏幕阅读器也是盲文设备的中介,这足以使视力受损的用户可以访问描述。任何时候只能有一个描述 轨道处于活动状态。
注意 在撰写本文时,没有浏览器支持这样的描述“渲染”。然而,有两个 Chrome 扩展来呈现描述:一个使用 Chrome 的文本到语音 API ( https://chrome.google.com/webstore/detail/html5-audio-description-v/jafenodgdcelmajjnbcchlfjomlkaifp
),另一个使用屏幕阅读器(如果安装了的话)😦https://chrome.google.com/webstore/detail/html5-audio-description-v/mipjggdmdaagfmpnomakdcgchdcgfbdg
)。
标记为章节 的曲目用于导航目的。预计该功能将通过媒体控件时间线上的菜单或其他形式的导航标记在浏览器中实现。到目前为止,还没有浏览器本身支持章节渲染。
最后,标记为元数据 的轨道将不会被可视化渲染,而只会暴露给 JavaScript。web 开发人员可以用这些元数据做任何事情,这些元数据可以由 web 页面脚本可以解码的任何文本组成。这包括 JSON、XML 或任何其他特殊用途的标记,以及提供用于导航的视频缩略图或带有超链接的字幕(如广告中使用的那些)的图像 URL。
清单 4-7 是包含这些轨道类型的代码示例。
清单 4-7 。带有每种类型轨道的<轨道>标记示例
<video controls poster="img/ElephantDreams.png">
<source src="video/ElephantDreams.mp4" type="video/mp4">
<source src="video/ElephantDreams.webm" type="video/webm">
<track src="tracks/ElephantDreams_zh.vtt" srclang="zh" kind="subtitles">
<track src="tracks/ElephantDreams_jp.vtt" srclang="ja" kind="captions">
<track src="tracks/ElephantDreams_en.vtt"
srclang="en" kind="metadata" label="Metadata">
<track src="tracks/ElephantDreams_chapters_en.vtt"
srclang="en" kind="chapters" label="Chapters">
<track src="tracks/ElephantDreams_audesc_en.vtt"
srclang="en" kind="descriptions" label="Descriptions">
</video>
该示例包含以下内容:
中文字幕:srclang="zh" kind="subtitles"
日语字幕:srclang="ja" kind="captions"
英文元数据:srclang="en" kind="metadata" label="Metadata"
英文章节:srclang="en" kind="chapters" label="Chapters"
英文描述:srclang="en" kind="descriptions" label="Descriptions"
当在 Safari 中查看时(参见图 4-10 ),所有的轨迹都是暴露的。选择任何章节、描述或元数据轨道都不会导致任何渲染。令人惊讶的是,Safari 甚至将它们列在了菜单中。
图 4-10 。Safari 中的视频元素,带有多个不同@种类的轨道
选择日语字幕轨道后,我们可以看到(图 4-11)UTF-8 编码的字符正确地呈现在视频视窗的顶部。
图 4-11 。带有日文字幕轨道的视频元素被激活
尽管浏览器在实现控制文本轨道的按钮和菜单方面有点落后,但第三方玩家已经开始利用<轨道>元素及其字幕和副标题的呈现。
例如,我们在第三章的中探索的 JWPlayer ,支持包含在 WebVTT 文件中的“元数据”轨道中的标题、章节和缩略图。如图 4-12 中的所示,它让它们没有了多余的装饰。你可以看到章节在时间轴上用小标记呈现,当你悬停在它们上面时,你会得到该章的标题。您还可以看到,当您悬停在 JWPlayer 时间轴上时,当提供缩略图时,它们会弹出。
图 4-12 。JWPlayer 通过 WebVTT 呈现标题、章节轨道和预览缩略图
注意 用于 JWPlayer 的 WebVTT 的例子在http://support.jwplayer.com/customer/portal/articles/1407438-adding-closed-captions
、http://support.jwplayer.com/customer/portal/articles/1407454-adding-chapter-markers
和http://support.jwplayer.com/customer/portal/articles/1407439-adding-preview-thumbnails
。
用于缩略图时间轴的 WebVTT 标记如下所示在清单 4-8 中:
清单 4-8 。带有缩略图的“元数据”类轨道的示例 WebVTT 文件
WEBVTT
00:00:00.000 --> 00:00:30.000
/path/thumb1.png
00:01:00.000 --> 00:01:30.000
/path/thumb2.png
缩略图是使用以下命令行 ffmpeg 命令创建的,每 30 秒创建一个:
$ ffmpeg -i video.mp4 -f image2 -vf fps=fps=1/30 thumb%d.png
带内文本轨道
WebVTT 文件不一定要通过元素进行外部链接。它们也可以直接嵌入到视频文件中。这些被称为 带内曲目 。由于mp4
和webm
格式是容器格式,WebVTT 文件可以直接添加到容器中,通常通过多路复用到文件中作为数据轨道。这是一项相对较新的技术,浏览器刚刚开始添加带内支持。要了解有关这种新兴技术的更多信息,我们建议您从以下网站开始:
WebM has a specification for storing WebVTT in-band:
http://wiki.webmproject.org/webm-metadata/temporal-metadata/webvtt-in-webm
MPEG-4 has a specification for embedding WebVTT in-band:
www.w3.org/community/texttracks/2013/09/11/carriage-of-webvtt-and-ttml-in-mp4-files/
MPEG DASH can deal with WebVTT:
http://concolato.wp.mines-telecom.fr/category/general/mpeg/dash/
苹果的 HLS 也可以:http://tools.ietf.org/html/draft-pantos-http-live-streaming-09
目前,还没有可视化编辑器可以将 WebVTT 音轨嵌入到媒体文件中。然而,有一些命令行方法可以将这些音轨添加到一个mp4
或webm
文件中。
可以用 MP4Box ( http://concolato.wp.mines-telecom.fr/2013/07/28/webvtt-mp4-files-dash-and-gpac/
)在 MPEG-4 中创作 WebVTT,用 ffmpeg 在 WebM 中创作 WebVTT。
以下是如何使用 mp4box 创建带有 WebVTT 轨道的mp4
文件的示例:
$ mp4box -add Monty_subs_en.vtt:FMT=VTT:lang=en Monty_subtitles.mp4
该命令将monty_subs_en.vtt
字幕轨道添加到Monty_subtitles.mp4
。
以下是如何使用 ffmpeg 创建带有 WebVTT 轨道的webm
文件的示例:
$ ffmpeg -i Monty.mp4 -i Monty_subs_en.vtt -metadata:s:s:0 kind="captions" \
-scodec copy Monty_subtitles.webm
它告诉 ffmpeg 使用Monty.mp4
作为输入媒体文件,告诉它使用Monty_subs_en.vtt
作为 WebVTT 字幕复制到 WebM 文件中的输入文件,并给字幕轨道一种“字幕”
尽管这是一项相对较新的技术,HTML5 已经使带内文本轨道在 Web 浏览器中与在中定义的外部轨道一样公开。这意味着不管文本轨道的来源如何,都可以使用相同的 JavaScript API。
注意 作为一名 Web 开发人员,您可以选择将您的 WebVTT 文件作为独立文件发布,或者使用 WebVTT 带内的视频文件。在撰写本文时,浏览器支持并不一致。考虑到这一点,我们建议使用外部文本轨道文件— .vtt
文件—而不是带内轨道,直到浏览器一致实现带内文本轨道。
JavaScript API :网络开发者的灵活性
正如我们在第三章中指出的,JavaScript 可以用来扩展网页各种元素的功能。在这种情况下,JavaScript 可用于操纵媒体源中使用的文本轨道,无论该文本轨道是在媒体的带内还是外部。这为希望制作可访问的视频或音频内容的 web 开发人员和设计人员提供了许多创造性的可能性。在本节中,我们将回顾 JavaScript API,因为它与外部文本轨道相关。
我们从元素开始。
跟踪元件
track 元素的 IDL(接口定义语言)接口如下所示:
interface HTMLTrackElement : HTMLElement {
attribute DOMString kind;
attribute DOMString src;
attribute DOMString srclang;
attribute DOMString label;
attribute boolean default;
const unsigned short NONE = 0;
const unsigned short LOADING = 1;
const unsigned short LOADED = 2;
const unsigned short ERROR = 3;
readonly attribute unsigned short readyState;
readonly attribute TextTrack track;
};
这个 IDL 是表示一个元素的对象。当列出外部文本轨道时,它可用。IDL 属性kind
、src
、srclang
、label
和default
包含与前面介绍的相同名称的内容属性的值。与 audio 和 video 元素一样,其余的 DOM 属性反映了 track 元素的当前状态。
@readyState
@readyState
IDL 是一个只读属性,表示跟踪元素的当前就绪状态。可用状态如下:
NONE(0)
:表示没有获得文本轨迹的提示。
LOADING(1)
:表示文本音轨正在加载,目前为止没有遇到致命错误。解析器仍然可以将进一步的线索添加到音轨中。
LOADED(2)
:表示文本轨道已加载,没有致命错误。
ERROR(3)
:表示文本轨道已启用,但当用户代理尝试获取文本轨道时,由于某种原因失败(例如,无法解析 URL、网络错误和未知的文本轨道格式)。一些或所有提示可能会丢失,并且将不会被获得。
当获得轨道时,文本轨道的就绪状态动态地改变。
作为一名 JavaScript 开发人员,确保所有预期要加载的文本轨道都实际加载了并且没有导致ERROR
是很有用的。如果您正在显示您自己的可用字幕轨道菜单,这一点尤其重要,因为您可能只想显示轨道以供选择,如果它们实际上可以加载的话。
@曲目
如前所述,带内文本轨道和外部参考文本轨道创建的对象是相同的。它们是 TextTrack 对象的实例。该属性链接到相应的<轨道>元素的 TextTrack 对象。
注意 在接下来的例子中,我们将使用蒙蒂·蒙特格美里的名为“极客数字媒体入门”的视频摘录(根据知识共享署名非商业性类似共享许可证发布;http://xiph.org/video/vid1.shtml
见)。我们感谢 Xiph.org 基金会提供了这段视频——完整的视频和系列中的其他视频都非常值得一看。
为了更好地了解 track 元素的 IDL 中的属性是如何协同工作的,清单 4-9 显示了加载时一个 Track 元素的所有 IDL 属性的值,然后在回放开始后显示
readyState`。
清单 4-9 。<轨道>元素的 IDL 属性
<video poster="img/Monty.jpg" controls width="50%">
<source src="video/Monty.mp4" type="video/mp4">
<source src="video/Monty.webm" type="video/webm">
<track label="English" src="tracks/Monty_subs_en.vtt" kind="subtitles"
srclang="en" default>
</video>
<h3>Attribute values:</h3>
<p id="values"></p>
<script>
var video = document.getElementsByTagName(’video’)[0];
var track = document.getElementsByTagName(’track’)[0];
var values = document.getElementById(’values’);
values.innerHTML += "Kind: " + track.kind + "<br/>";
values.innerHTML += "Src: " + track.src + "<br/>";
values.innerHTML += "Srclang: " + track.srclang + "<br/>";
values.innerHTML += "Label: " + track.label + "<br/>";
values.innerHTML += "Default: " + track.default + "<br/>";
values.innerHTML += "ReadyState: " + track.readyState + "<br/>";
values.innerHTML += "Track: " + track.track + "<br/>";
function loaded() {
values.innerHTML += "ReadyState: " + track.readyState + "<br/>";
}
video.addEventListener("loadedmetadata", loaded, false);
</script>
图 4-13 显示了 Firefox 的结果。
图 4-13 。默认激活的<轨道>元素的 IDL 属性
所有关于元素属性值的信息,包括kind
、src
、srclang
、label
和defaul
t。你也可以看到readyState
起初是LOADING(1)
,当视频开始播放时,它变成了LOADED(2)
。
在讨论 track 属性的内容之前,让我们简单地列出可能在元素上触发的事件。
装载
当浏览器成功加载@src
属性中引用的资源时,在HTMLTrackElement
处触发一个onload
事件—readyState
随后也变为LOADED(2)
。
不良事件
当@src
属性中引用的资源加载失败时,在HTMLTrackElement
处触发onerror
事件。然后readyState
也变成了ERROR(3)
。
oncuechange
当该轨道中的提示变为活动或停止活动时,在HTMLTrackElement
触发一个oncuechange
事件。
清单 4-10 是捕捉这些事件的代码块的一个很好的例子。
清单 4-10 。捕捉<轨道>元素上的事件
<video poster="img/Monty.jpg" controls width="50%" autoplay>
<source src="video/Monty.mp4" type="video/mp4">
<source src="video/Monty.webm" type="video/webm">
<track label="Australian" src="tracks/Monty_subs_au.vtt" kind="subtitles"
srclang="en-au" default>
<track label="English" src="tracks/Monty_subs_en.vtt" kind="subtitles"
srclang="en">
</video>
<h3>Events:</h3>
<p id="values"></p>
<script>
var video = document.getElementsByTagName(’video’)[0];
var tracks = document.getElementsByTagName(’track’);
var values = document.getElementById(’values’);
function trackloaded(evt) {
values.innerHTML += "Track loaded: " + evt.target.label
+ " track<br/>";
}
function trackerror(evt) {
values.innerHTML += "Track error: " + evt.target.label + " track<br/>";
tracks[1].track.mode = "showing";
}
function cuechange(evt) {
values.innerHTML += "Cue change: " + evt.target.label + " track<br/>";
video.pause();
}
for (var i=0; i < tracks.length; i++) {
tracks[i].onload = trackloaded;
tracks[i].onerror = trackerror;
tracks[i].oncuechange = cuechange;
}
</script>
我们特意定义并由@default
激活了第一个文本轨道,它的@src
资源Monty_subs_au.vtt
并不存在。结果是触发了提及澳大利亚轨道的第一个错误事件。在错误事件回调中,我们激活了第二个轨道——Monty_subs_en.vtt
——这反过来又激活了加载回调。然后,当视频播放到达第一个提示时,cuechange
事件被激活并暂停视频。
在 Google Chrome 中运行这个程序会得到如图 4-14 所示的结果。
图 4-14 。在<轨道>元素上捕捉事件
注意 这些事件在浏览器中的实现存在 bug。例如,Firefox 似乎不会引发load
和cuechange
事件,Safari 也不会引发cuechange
事件。
既然我们理解了可以在HTMLTrackElement
触发的事件,我们可以将注意力转向@track
属性的内容,它是一个TextTrack
对象。
TextTrack 对象
为与媒体元素相关联的每个文本轨道创建一个TextTrack
对象。不管是否存在以下情况,都会创建此对象
它通过元素来自外部文件,
它来自媒体资源的带内文本轨道;或者
它完全是用 JavaScript 通过HTMLMediaElement
的addTextTrack()
方法创建的,我们将在本章后面讲到。
因此,TextTrack
对象的属性值来源于HTMLTrackElement
的属性值、带内值(参见http://dev.w3.org/html5/html-sourcing-inband-tracks/
)或addTextTrack()
方法的参数。
注意 源自元素的TextTrack
对象链接自 HTMLTrackElement
对象和媒体元素的TextTrackList
,后者是元素的子元素。带内轨道和脚本创建的轨道仅存在于媒体元素的 TextTrackList
中。
TextTrack
对象的 IDL 如下所示:
enum TextTrackMode { "disabled", "hidden", "showing" };
enum TextTrackKind { "subtitles", "captions", "descriptions", "chapters", "metadata" };
interface TextTrack : EventTarget {
readonly attribute TextTrackKind kind;
readonly attribute DOMString label;
readonly attribute DOMString language;
readonly attribute DOMString id;
readonly attribute DOMString inBandMetadataTrackDispatchType;
attribute TextTrackMode mode;
readonly attribute TextTrackCueList? cues;
readonly attribute TextTrackCueList? activeCues;
void addCue(TextTrackCue cue);
void removeCue(TextTrackCue cue);
attribute EventHandler oncuechange;
};
前四个属性如下:
@kind
属性被TextTrackKind
对象限制为合法值,我们在前面的元素中已经了解到了。
@label
属性包含由元素的 @label
属性、带内轨道的字段或HTMLMediaElement
的addTextTrack()
方法的标签参数提供的标签字符串。
@language
属性包含由元素的 @srclang
属性、带内轨道的字段或HTMLMediaElement
的addTextTrack()
方法的语言参数提供的语言字符串。
@id
属性包含从<轨道>元素的@id
属性(每个元素都有这样的属性)或从带内轨道的标识符字段提供的标识符字符串。
IDL 中的其余属性需要更多的解释。
@磁带中继资料 track dispatch type
这是从媒体资源中提取的字符串,专门用于@kind
“元数据”的文本轨道这个字符串解释了提示中数据的确切格式,因此可以设置足够的 JavaScript 函数来解析和显示这些数据。
例如,具有特定内容格式的文本轨道可以包含用于广告定位的元数据、游戏节目期间的琐事游戏数据、体育游戏期间的游戏状态或烹饪节目期间的食谱信息。因此,专用脚本模块可以使用该属性的值绑定到解析这样的轨道。
如何识别数据格式在http://dev.w3.org/html5/html-sourcing-inband-tracks/
中规定。由于该属性对于特定类型的应用来说是非常特定的,并且对可访问性的影响可以忽略不计,因此对该属性的进一步讨论超出了本书的范围。
@模式
根据TextTrackMode
类型的定义,TextTrack
对象可以有三种不同的模式。
禁用 : 表示文本轨道未激活。在这种情况下,浏览器已经识别出了一个<轨道>元素的存在,但是它还没有下载外部轨道文件或者解析它。没有激活的提示,也没有触发事件。<轨道>-默认情况下未激活的已定义文本轨道最初以此状态结束。
隐藏 : 表示文本轨迹的线索已经或者应该被获取,但是没有被显示。浏览器维护着一个列表,其中列出了哪些提示是活动的,并相应地触发事件。带内文本轨道和 JavaScript 创建的文本轨道最初都处于这种状态。
显示 : 表示文本轨迹的提示已经或应该被获取,如果是呈现的@kind
则正在显示。浏览器维护着一个列表,其中列出了哪些提示是活动的,哪些事件相应地被触发。-用 @default
属性激活的已定义文本轨道最初以此状态结束。
@cues
一旦TextTrack
被激活(即模式被隐藏或显示),这就是已加载的TextTrackCues
列表。为了连续加载媒体文件,该列表可以随着媒体资源连续解析带内文本轨道而连续更新。
@activeCues
这是当前活动的TextTrack
上的 T extTrackCues
列表。活动提示是那些在当前回放位置之前开始并在回放位置之后结束的提示。
在我们继续讨论TextTrack
对象使用的方法和事件之前,清单 4-11 给了我们一个机会来检查<track>
元素的 IDL 属性。
清单 4-11 。< track >元素的@track 属性的 IDL 属性
<video poster="img/Monty.jpg" controls width="50%">
<source src="video/Monty.mp4" type="video/mp4">
<source src="video/Monty.webm" type="video/webm">
<track id="track1" label="English" src="tracks/Monty_subs_en.vtt"
kind="subtitles" srclang="en" default>
</video>
<h3>TextTrack object:</h3>
<p id="values"><b>Before loading:</b><br/></p>
<script>
var video = document.getElementsByTagName(’video’)[0];
var track = document.getElementsByTagName(’track’)[0];
var values = document.getElementById(’values’);
values.innerHTML += JSON.stringify(track.track, undefined, 4) + "<br/>";
values.innerHTML += "track.cues length: " + track.track.cues.length
+ "<br/>";
function loaded() {
values.innerHTML += "<b>After loading:</b><br/>";
values.innerHTML += "track.cues[0]: " + track.track.cues[0] + "<br/>";
values.innerHTML += "track.cues length: " + track.track.cues.length;
}
video.addEventListener("loadeddata", loaded, false);
</script>
图 4-15 显示了元素加载前@track
属性中TextTrack
对象的值,以及在 Google Chrome 中加载后的提示数。您会看到 @cues
的长度是 0(因为“cues”在加载前为空,而在加载后为 51)。
图 4-15 。Opera 中显示的< track >元素的@track 属性的 IDL 属性值
Firefox 实际上并没有那么早创建TextTrack
对象,这意味着@track
属性在加载之前仍然是一个空对象,但是它始终报告提示的数量。
add()t 0]
该方法将一个TextTrackCue
对象添加到文本轨道的提示列表中。这意味着对象被添加到@cues
,并且如果媒体元素的当前时间在该提示的时间间隔内,还被添加到@activeCues
。注意,如果给定的提示已经在另一个提示的文本轨道列表中,那么在它被添加到这个提示之前,它被从那个提示的文本轨道列表中移除。
removeCue( )
该方法从文本轨道的提示列表中删除一个TextTrackCue
对象。
onCueChange 事件
当轨道中的一个或多个提示变为活动或停止活动时,引发cueChange
事件。
清单 4-12 提供了一个在元素的TextTrack
上应用addCue()
和removeCue()
方法并捕获结果 cuechange
事件的 JavaScript 示例。
清单 4-12 。< track >元素的@track 属性的方法和事件
var video = document.getElementsByTagName(’video’)[0];
var track = document.getElementsByTagName(’track’)[0];
var values = document.getElementById(’values’);
function loaded() {
var cue = new VTTCue(0.00, 5.00, "This is a script created cue.");
values.innerHTML += "Number of cues: " + track.track.cues.length
+ "<br/>";
values.innerHTML += "<b>After adding cue:</b><br/>"
track.track.addCue(cue);
values.innerHTML += "Number of cues: " + track.track.cues.length
+ "<br/>";
}
video.addEventListener("loadedmetadata", loaded, false);
function playing() {
values.innerHTML += "<b>After play start:</b><br/>"
values.innerHTML += "Number of cues: " + track.track.cues.length
+ "<br/>";
values.innerHTML += "First cue: "
+ JSON.stringify(track.track.cues[0].text) + "<br/>";
function cuechanged() {
track.track.removeCue(track.track.cues[1]);
values.innerHTML += "<b>After removing cue:</b><br/>"
values.innerHTML += "Number of cues: " + track.track.cues.length
+ "<br/>";
video.pause();
}
track.track.addEventListener("cuechange", cuechanged, false);
}
video.addEventListener("play", playing, false);
加载视频后,我们创建一个 vtt cue—new VTTCue
—它是一种TextTrackCue
。我们从 51 个线索开始,到 52 个结束。开始回放后,我们加载了所有的 52 个线索,然后注册了一个cuechange
事件,在这个事件中,线索 1 被删除,返回到 51 个线索。图 4-16 显示了谷歌浏览器中的结果。还要注意,第一个提示是 52 个提示列表中的脚本创建的提示。
图 4-16 。TextTrack 对象的方法和事件
注意 这个例子在 Firefox 中不能正常工作,因为 Firefox 还不支持TextTrack
对象上的oncuechange
事件。
TextTrackCue
TextTrack
IDL 的@cues
和@activeCues
属性中的提示具有以下格式:
interface TextTrackCue : EventTarget {
readonly attribute TextTrack? track;
attribute DOMString id;
attribute double startTime;
attribute double endTime;
attribute boolean pauseOnExit;
attribute EventHandler onenter;
attribute EventHandler onexit;
};
这些是球杆的基本属性。特定的提示格式如VTTCue
可以进一步扩展这些属性。这里快速回顾一下TextTrackCue
的属性。
@曲目
这是该提示所属的TextTrack
对象,如果有的话,否则为null
。
@id
这是提示的识别字符串。
@ start time、@endTime
这些是提示的开始和结束时间。它们与媒体元素的回放时间相关,并定义提示的活动时间范围。
@ pause exit
@pauseOnExit
标志是一个布尔值,它指示当到达提示的活动时间范围的末尾时是否暂停媒体资源的回放。例如,它可以用于在到达提示的末尾时暂停视频,以便引入广告。
onenter 和 onexit 事件
当提示变为活动状态时引发enter
事件,当提示停止活动时引发exit
事件。
文本跟踪列表
TextTrack
IDL 的@cues
和@activeCues
属性是以下格式的TextTrackCueList
对象:
interface TextTrackCueList {
readonly attribute unsigned long length;
getter TextTrackCue (unsigned long index);
TextTrackCue? getCueById(DOMString id);
};
@length
返回列表的长度。
getter 使得通过索引(例如,cues[i]
)访问提示列表元素成为可能。
getCueById()
函数允许通过提供 id 字符串来检索一个TextTrackCue
。
清单 4-13 展示了如何浏览一个音轨的提示列表并访问提示属性。
清单 4-13 。访问文本轨道的所有提示的属性
<video poster="img/Monty.jpg" controls width="50%">
<source src="video/Monty.mp4" type="video/mp4">
<source src="video/Monty.webm" type="video/webm">
<track id="track1" label="English" src="tracks/Monty_subs_en.vtt"
kind="subtitles" srclang="en" default>
</video>
<h3>TextTrack object:</h3>
<table>
<thead>
<tr>
<td>Cue Number</td>
<td>ID</td>
<td>StartTime</td>
<td>EndTime</td>
<td>Text</td>
</tr>
</thead>
<tbody id="values">
</tbody>
</table>
<script>
var video = document.getElementsByTagName(’video’)[0];
var track = document.getElementsByTagName(’track’)[0];
var values = document.getElementById(’values’);
var content;
function loaded() {
for (var i=0; i < track.track.cues.length; i++) {
content = "<tr>";
content += "<td>" + i + "</td>";
content += "<td>" + track.track.cues[i].id + "</td>";
content += "<td>" + track.track.cues[i].startTime + "</td>";
content += "<td>" + track.track.cues[i].endTime + "</td>";
content += "<td>" + track.track.cues[i].text + "</td></tr>";
values.innerHTML += content;
}
}
video.addEventListener("loadedmetadata", loaded, false);
</script>
当你在浏览器中测试时,如图 4-17 所示,你会发现这种技术有助于快速自省提示,以确保顺序、时间和文本拼写是正确的。
图 4-17 。列出文本轨道的所有线索
媒体元素
我们已经看到了如何访问元素、它们的提示列表以及每个提示的内容。现在我们将远离元素,回到媒体元素的文本轨道列表。这也包括带内文本轨道和脚本创建的轨道。
TextTrackList
首先我们需要理解TextTrackList
对象:
interface TextTrackList : EventTarget {
readonly attribute unsigned long length;
getter TextTrack (unsigned long index);
TextTrack? getTrackById(DOMString id);
attribute EventHandler onchange;
attribute EventHandler onaddtrack;
attribute EventHandler onremovetrack;
};
类似于TextTrackCueList
对象,TextTrackList
是一系列TextTrack
对象。
列表的长度在@length
属性中给出。
可以通过索引(例如track[i]
)来访问各个轨道。
他的getTrackById()
方法 允许通过提供一个TextTrack
的 id 字符串来检索它。
此外,每当列表中的一个或多个曲目被启用或禁用时,将引发“change”事件,每当曲目被添加到曲目列表时,将引发addtrack
事件,每当曲目被移除时,将引发removetrack
事件。
为了访问与音频或视频元素相关联的所有文本轨道,使用以下属性和方法扩展了MediaElement
的 IDL:
interface HTMLMediaElement : HTMLElement {
...
readonly attribute TextTrackList textTracks;
TextTrack addTextTrack(TextTrackKind kind, optional DOMString label = "",
optional DOMString language = "");
};
@textTracks
媒体元素的@textTracks
属性是一个TextTrackLis
t 对象,它包含媒体元素可用的文本轨道列表。
addTextTrack( )
这个用于媒体元素的新方法 addTextTrack ( 种类 , 标签 , 语言 ) 用于纯粹从 JavaScript 中为具有给定种类、标签和语言属性设置的媒体元素创建新的文本轨道。新轨道,如果有效,立即在LOADED(2) @readyState
和“隐藏的”@mode
中,带有一个空的@cues TextTrackCueList
。
我们之前提到过,@textTracks
包含了与媒体元素相关联的所有轨道,不管它们是由元素创建的,还是从带内文本轨道公开的,或者是由 JavaScript 通过 addTextTrack()
方法创建的。磁道实际上总是按以下顺序访问:
<track>
创建了TextTrack
个对象,按照它们在 DOM 中的顺序。
addTextTrack()
创建了TextTrack
个对象,按照它们被添加的顺序,最旧的放在最前面。
带内文本轨道,按照媒体资源中给定的顺序。
在清单 4-14 中,我们有一个通过<轨道>元素创建的字幕轨道和一个脚本创建的章节轨道。
清单 4-14 。列出并访问视频元素的所有文本轨道
<video poster="img/Monty.jpg" controls width="50%">
<source src="video/Monty.mp4" type="video/mp4">
<source src="video/Monty.webm" type="video/webm">
<track id="track1" label="English" src="tracks/Monty_subs_en.vtt"
kind="subtitles" srclang="en" default>
</video>
<h3>TextTrack object:</h3>
<p id="values"></p>
<script>
var video = document.getElementsByTagName(’video’)[0];
var values = document.getElementById(’values’);
var new_track = video.addTextTrack("chapters", "English Chapters", "en");
var cue;
cue = new VTTCue(0.00, 7.298, "Opening Credits");
new_track.addCue(cue);
cue = new VTTCue(7.298, 204.142, "Introduction");
new_track.addCue(cue);
function loaded() {
values.innerHTML += "Number of text tracks: "
+ video.textTracks.length + "</br>";
for (var i=0; i < video.textTracks.length; i++) {
values.innerHTML += "<b>Track[" + i + "]:</b></br>";
values.innerHTML += "Number of cues: "
+ video.textTracks[i].cues.length + "<br/>";
values.innerHTML += "First cue: "
+ video.textTracks[i].cues[0].text + "<br/>";
}
}
video.addEventListener("loadedmetadata", loaded, false);
</script>
视频加载完成后,我们显示文本轨道的数量,以及每个轨道的提示数量和第一个提示中的文本。图 4-18 显示了结果。
图 4-18 。列出一个视频元素的所有文本轨道
这就结束了对 JavaScript 对象及其 API 的描述,它们允许我们处理一般的文本轨迹并检索相关信息或对特定事件做出反应。
WebVTT:创作字幕、标题、文本描述和章节
尽管我们在本章的前面快速浏览了 WebVTT 以及如何使用它,但我们将在本章的这一节深入探讨这个主题。这将包括格式化提示和字幕,并在视频上定位它们。
正如我们指出的,WebVTT 是一种专门定义的文件格式,允许作者独立于网页创建文本轨道提示,并在单独的文件中分发它们。网页作者通常不创建视频内容;因此,要求字幕作为网页的一部分是没有意义的。
我们还看到,一个简单的 WebVTT 文件是一个文本文件,由一个 WEBVTT 字符串签名和一个由空行分隔的提示列表组成。以下是一个示例文件:
WEBVTT
1 this is an identifier
00:00:08.124 --> 00:00:10.742
Workstations and high end personal computers have been able to
transcription-line
00:00:10.742 --> 00:00:14.749
manipulate digital audio pretty easily for about fifteen years now.
3
00:00:14.749 --> 00:00:17.470
It’s only been about five years that a decent workstation’s been able
如您所见,每个提示都以 string 开始,这是可选的标识符。下一行包含提示显示的开始和结束时间,以 hh:min:sec.mms 的形式表示,并用“->”字符串分隔。请注意,每个小时和分钟部分必须由两位数字组成,例如 01 代表一小时或一分钟。第二段必须由两位数和三位小数组成。
接下来的一行或多行文本是实际的提示内容,也就是要呈现的文本。
这个文件将被元素引用,它只不过是一个简单的文本文件,文件名的扩展名为.vtt
。当提示被显示并且是“字幕”或“标题”时,它们出现在视频视窗底部中间的一个黑框中。很自然,这提出了一个明显的问题:这些线索能被“激活”吗?”答案是:“是的。“让我们来看看处理这些线索的一些方法。
球杆造型
一旦 WebVTT 格式的提示可用于网页,就可以使用 CSS 对其进行样式化。使用前面的例子,您可以使用::cue
伪选择器来:
使用
video::cue(#\31\ this\ is\ an\ identifier) { color: green; }
设定第一个球杆的样式
使用
video::cue(#transcription\-line) { color: red; }
设定第二个球杆的样式
使用
video::cue(#\33) { color: blue; }
设定第三个球杆的样式
使用
video::cue { background-color: lime; }
设置所有提示的样式
注意 CSS 比 WebVTT 在选择器字符串中允许更少的自由度,所以你需要对你的标识符中的一些字符进行转义来实现这个功能。更多信息见https://mathiasbynens.be/notes/css-escapes
。
可应用于 cue 中文本并与::cue
一起使用的 CSS 属性包括:
【颜色】
“不透明”
“能见度”
“文本装饰”
"文本-阴影"
对应于“背景”简写的属性
对应于“大纲”简写的属性
对应于"字体"速记的属性,包括
使用::cue()
,您可以另外设置与过渡和动画特性相关的所有属性的样式。
提示标记
提示属于“元数据”类型,可以包含提示内容中的任何内容。这包括 JSON (JavaScript 对象表示法)、XML 或数据 URL。
其他种类的提示包含受限制的提示文本。提示文本包含 UTF 8 中的纯文本以及一组有限的标记。&符号(&
)和小于号(<
)必须作为字符进行转义,因为它们代表转义字符序列或标记的开始。使用以下转义实体,就像在 HTML 中一样:& (&)
、< (<)
、> (>)
、&lrm
;(左右标),&rlm
;(左右标记),和
(不破空格)。左右和左右标记是非打印字符,允许作为国际化和双向文本的一部分更改文本的方向。这在用希伯来语或阿拉伯语等从右向左显示单词的语言标记脚本时,或者在标记混合语言文本时非常重要。
接下来,我们将列出当前定义的标签,并给出简单的例子,说明一旦这样的提示被包含并显示在网页上,如何用 CSS 从 HTML 页面处理它们。这些标签是:
Class span <c>
: to mark up a section of text for styling, for example,
<c.myClass>Apply styling to this text</c>
这将允许使用如下 CSS 选择器:
::cue(.myClass) { font-size: 2em; }
您可以在所有标签上使用.myClass
类属性。
Italics span <i>
: to mark up a section of italicized text, for example,
<i>Apply italics to this text</i>
这也允许使用 CSS 选择器,如下所示:
::cue(i) { color: green; }
Bold span <b>
: to mark up a section of bold text, for example,
<b>Apply bold to this text</b>
这也允许使用 CSS 选择器,如下所示:
::cue(b) { color: red; }
Underline span <u>
: to mark up a section of underlined text, for example,
<u>Apply underlines to this text</u>
这也允许使用 CSS 选择器,如下所示:
::cue(u) { color: blue; }
Ruby span <ruby>
: to mark up a section of ruby annotations.
拼音注释是与基本文本并排显示的简短文本,主要用于东亚排版中作为发音指南或包含其他注释。以下是一个标记示例:
<ruby>`</rt></ruby>`
`这也允许使用 CSS 选择器,如下所示:
::cue(ruby) { font-weight: bold; }
::cue(rt) { font-weight: normal; }
*
Voice span `: to mark up a section of text with a voice and speaker annotation, for example,
```html
<v Fred>How are you?</v>
```
一旦提示包含在 HTML 页面中,这也允许使用如下 CSS 选择器:
```html
::cue(v[voice="Fred"]) { font-style: italic; }
```
* `Language span <lang>`: to mark up a section of text in a specific language, for example,
```html
<lang de>Wie geht es Dir?</lang>
```
一旦提示包含在 HTML 页面中,这也允许使用如下 CSS 选择器:
```html
::cue(lang[lang="de"]) { font-style: oblique; }
::cue(:lang(ru)) { color: lime; }
```
* `Timestamps <hh:mm:ss.mss>`: to mark up a section of text with timestamps.
时间戳的美妙之处在于,它们给你机会在精确的时间点上设计提示,而不是接受我们在本章中使用的黑色背景上的白色文本。下面的代码块显示了时间戳的使用示例:
```html
<00:01:00.000><c>Wie </c><00:01:00.200><c>geht </c><00:01:00.400><c>es </c><00:01:00.600><c>Dir? </c><00:01:00.800>
```
在这个例子中,单词“Wie”、“geht”、“es”和“Dir”将在指示的时间出现在屏幕上。
这也允许在 HTML 页面中包含提示后使用 CSS 选择器,如下所示:
```html
::cue(:past) { color: lime; }
::cue(:future) { color: gray; }
```
例如,您可以使用时间戳来标记卡拉 ok 提示或添加字幕。什么是“绘画”标题?绘画式字幕是“画”在屏幕上的单个单词。它们以组成标题单词的单个单词的形式出现,从左到右出现,通常是一字不差的。`
![Image](https://gitee.com/OpenDocCN/vkdoc-html-css-zh/raw/master/docs/begin-h5-media/img/image00312.jpeg) **注意**除非另行通知,否则您需要使用< c >标签将文本括在时间戳之间,以使 CSS 选择器生效(直到
www.w3.org/Bugs/Public/show_bug.cgi?id=16875`被解析)。
下面是一个关于标签和 CSS 用法的有趣演示,应用于 Tay Zonday 的一个音乐视频。这个名为“巧克力雨”的视频几年前在 YouTube 上疯传,现在获得了知识共享许可。
我们从清单 4-15a 中的 WebVTT 标记开始。
清单 4-15a 。“巧克力雨”的 WebVTT 文件
WEBVTT
1 first cue
00:00:10.000 --> 00:00:21.710
<v Tay Zonday>Chocolate Rain</v>
2
00:00:12.210 --> 00:00:21.710
<b>Some </b><i>stay </i><u>dry </u>and others feel the pain
3
00:00:15.920 --> 00:00:21.170
<c.brown>Chocolate </c><u>Rain</u>
4
00:00:18.000 --> 00:00:21.170
<00:00:18.250><c>A </c><00:00:18.500><c>baby </c><00:00:19.000><c>born </c><00:00:19.250><c>will </c><00:00:19.500><c>die </c><00:00:19.750><c>before </c><00:00:20.500><c>the </c><00:00:20.750><c>sin</c>
并将适当的 CSS 标记应用于清单 4-15b 中的 HTML。
清单 4-15b 。这些提示在 HTML 页面中被设计为“巧克力雨”
<style>
video::cue {color: lime;}
video::cue(#\31\ first\ cue) {background-color: blue;}
video::cue(v[voice="Tay Zonday"]) {color: red !important;}
video::cue(:past) {color: lime;}
video::cue(:future) {color: gray;}
video::cue(c.brown) {color:brown; background-color: white;}
</style>
<video poster="img/chocolate_rain.png" controls>
<source src="video/chocolate_rain.mp4" type="video/mp4">
<source src="video/chocolate_rain.webm" type="video/webm">
<track id="track1" label="English" src="tracks/chocolate_rain.vtt"
kind="subtitles" srclang="en" default>
</video>
如您所见,提示是在 VTT 文档中创建的,并使用内嵌 CSS 和来自提示标记的各种样式在 HTML 中进行样式化。样式可以很容易地包含在外部 CSS 样式表中。结果如图 4-19 所示,在不同的浏览器之间并不完全一致,这是你在进行这种项目时需要注意的。
图 4-19 。在 Google Chrome(左)和 Safari(右)中渲染“巧克力雨”示例
注意 谷歌 Chrome 和 Safari 目前拥有最好的样式支持。Firefox 不支持::cue
伪选择器,Internet Explorer 已经无法解析标记提示文本。
提示设置
既然您已经知道了如何设置提示内容的样式,那么让我们来处理您可能会有的另一个问题:“它们必须总是在屏幕的底部吗?”简单的回答是:“不”。你可以选择把它们放在哪里,这就是“WebVTT:创作字幕、标题、文本描述和章节”一节的最后一部分的主题
这是由于 WebVTT 引入了“提示设置”而实现的。这些指令添加在同一行中提示的结束时间规范之后,由冒号(:)分隔的名称-值对组成。
我们从垂直线索开始。
垂直提示
一些语言垂直而不是水平地呈现它们的脚本。许多亚洲语言尤其如此。比如蒙古语,就是竖着写,右边加行。大多数其他竖排书写都是在左侧添加行,例如繁体中文、日文和韩文。
垂直提示的 WebVTT 提示设置如下:
vertical:rl
vertical:lr
第一个提示设置指定垂直文本从右向左生长,第二个提示设置指定文本从左向右生长。
清单 4-16 显示了一个日文文本的例子和图 4-20Safari 中的一个渲染。请注意< ruby >标记还不被支持。
清单 4-16 。带有垂直文本提示的 WebVTT 文件
如你所见,在图 4-20 中,增加了垂直提示。有一个小问题。Chrome 和 Opera 目前混淆了rl
和lr
,Firefox 和 Internet Explorer 还不支持垂直渲染。只有 Safari 能做到这一点。
图 4-20 。在 Safari 中渲染垂直文本提示
线定位
默认情况下,提示行呈现在视频视口的底部中央。然而,有时 WebVTT 作者会希望将文本移动到另一个位置,例如,当烧录文本在该位置的视频上显示时,或者当大部分动作在该位置时,如足球比赛中的情况。在这些情况下,您可以决定将提示放置在视频视窗的顶部或视窗顶部和底部之间的任何其他位置。
视口的顶部是垂直提示rl
的右侧和lr
的左侧,视口左右之间的间距的计算方式与水平文本非常相似。
行定位的典型 WebVTT 提示设置如下所示:
line:0
line:-1
第一个版本指定视频视口顶部的第一行,即从该处继续向下的任何连续数字(例如,4 是视口顶部的第五行)。第二行指定视口底部的第一行,从那里开始递减计数(例如,-5 是从底部算起的第五行)。
您也可以从视频视口的顶部指定百分比定位。
line:10%
如果我们假设视频的高度为 720 像素,则字幕将出现在视频视窗顶部下方 72 像素处。
正如您所看到的,行提示设置允许您以三种不同的方式定位提示顶部和底部:从顶部计数行、从底部计数行和从顶部百分比定位。
球杆校准
通过对齐设置,提示框中的文本可以左对齐、中对齐或右对齐。
align:left
align:middle
align:right
align:start
align:end
“开始”和“结束”设置适用于应与文本的开始/结束对齐的情况,与文本的方向是从左到右还是从右到左无关。
文本定位
有时,WebVTT 作者会希望将提示框从中间位置移开。例如,选择的位置覆盖说话者的脸。在这种情况下,提示应该移动到扬声器的左侧、右侧或下方。
文本定位的 WebVTT 提示设置为:
position:60%
这会将水平提示与距离视频视窗左边缘 60%的部分对齐。
注意 小心文本定位,因为提示的最终位置取决于文本的对齐方式。例如,如果提示居中对齐,文本位置的原点将是文本块的中心。对于右对齐文本,它将是块的右边缘,依此类推。如果得到不一致的结果,首先要查看提示对齐属性。
球杆尺寸
能够在视窗中改变提示位置是一个很好的特性,但是也存在标题可能太宽的风险。这就是size
属性——总是用百分比表示——有用的地方。例如,将一个提示放在扬声器下方的左侧将要求您也限制提示的宽度,如下所示。
position:10% align:left size:40%
为了更好地理解所有这些提示设置的效果,清单 4-17 显示了一个使用线条、对齐、位置和大小设置来适应变化的提示位置和宽度的提示示例。
清单 4-17 。带有提示的 WebVTT 文件,带有提示设置
WEBVTT
1a
00:00:08.124 --> 00:00:10.742 line:0 position:10% align:left
Workstations
1b
00:00:08.124 --> 00:00:10.742 line:50% position:50%
and high end personal computers
1c
00:00:08.124 --> 00:00:10.742 align:right size:10% position:100%
have been able to
第一个提示 1a 呈现在视频视窗的第一行,并以 10%的偏移量左对齐。第二个提示—1b—正好呈现在中间。第三个提示—1c—是视口宽度的 10%,在右边缘右对齐呈现。
图 4-21 显示了 Chrome(左)和 Safari(右)中的结果。
图 4-21 。在 Chrome 和 Safari 中使用提示设置渲染提示
Chrome、Opera 和 Firefox 基本上以相同的方式呈现提示。Safari 的定位有着有些不同的解读。IE 不支持任何提示设置。
其他 WebVTT 功能
至此,我们已经概述了最重要的 WebVTT 特性。在结束之前,我们还想提几个其他的问题。
评论 :你可以在一个 WebVTT 文件中创作评论——基本上,它们是一个没有标识符或时间线的提示,文本块以NOTE
开始
区域(Regions):这是一个正在讨论中的特性,允许更详细的定位,允许在提示上提供背景颜色,并允许滚动文本(滚动字幕)。目前还不清楚浏览器是否会实现这部分规范。
嵌套线索 :轨迹@kind=
’chapters’
允许定义嵌套线索(即完全包含在其他线索中的线索)。这对于轨道来说很有用,它区分了章、节、小节等等,其中每个较低的层次都完全包含在较高的层次中。因此,章节轨道可以用于不同分辨率的导航,尽管很难想象如何在浏览器中呈现。
到目前为止,我们已经关注了一个视频,并向您展示了如何添加脚本、字幕、标题、章节和文本描述。正如你所发现的,它们都是使视频和音频能够被不同的观众所接受的关键因素。尽管如此,我们都在电视上看到过这样的视频,在新闻发布会上,有人在一旁用手语翻译对聋人说的话。因此,在某些情况下,视频流也需要使用“签名者”的单独视频这是您需要创建具有多个同步音频和视频轨道的视频的地方。
多个音频和视频轨道:音频描述和手语视频
我们已经谈了很多关于如何发布视频的文本替代,包括抄本、标题、字幕和文本描述。然而,视力受损的视频观众习惯于使用视频中的音频描述,许多聋人用户发现阅读/观看手语比文本更容易。同样,国际用户已经习惯了配音音轨,如之前评论的清晰音轨。这给我们提出了一个相当有趣的挑战,一个视频不只有一个视频和一个音频轨道,而是有多个视频和音频轨道。
这一挑战可以通过两种方式来应对。首先是准备彼此同步的单独的音频和视频文件。第二种方法是产生一个单一的多路复用视频文件,我们从中检索与特定用户相关的音轨。HTML5 提供了这两种选择。前者通过MediaController
API 支持,后者通过多轨媒体文件支持。
多轨道媒体
当引用一个在元素中包含多个音频和视频轨道的视频时,浏览器只显示一个视频轨道并呈现所有启用的音频轨道。为了访问与
或元素相关联的所有音频和视频轨道,MediaElement
的 IDL 扩展了以下属性:
interface HTMLMediaElement : HTMLElement {
...
readonly attribute AudioTrackList audioTracks;
readonly attribute VideoTrackList videoTracks;
};
@音轨
媒体元素的@audioTracks
属性是一个AudioTrackList
对象,它包含媒体元素可用的音轨列表及其激活状态。
@视频跟踪
媒体元素的@videoTracks
属性是一个VideoTrackList
对象,它包含媒体元素可用的视频轨道列表及其激活状态。
音频和视频轨道
其中包含的AudioTrackList
对象和AudioTrack
对象定义如下:
interface AudioTrackList : EventTarget {
readonly attribute unsigned long length;
getter AudioTrack (unsigned long index);
AudioTrack? getTrackById(DOMString id);
attribute EventHandler onchange;
attribute EventHandler onaddtrack;
attribute EventHandler onremovetrack;
};
interface AudioTrack {
readonly attribute DOMString id;
readonly attribute DOMString kind;
readonly attribute DOMString label;
readonly attribute DOMString language;
attribute boolean enabled;
};
VideoTrackList
对象和它的VideoTrack
对象非常相似:
interface VideoTrackList : EventTarget {
readonly attribute unsigned long length;
getter VideoTrack (unsigned long index);
VideoTrack? getTrackById(DOMString id);
readonly attribute long selectedIndex;
attribute EventHandler onchange;
attribute EventHandler onaddtrack;
attribute EventHandler onremovetrack;
};
interface VideoTrack {
readonly attribute DOMString id;
readonly attribute DOMString kind;
readonly attribute DOMString label;
readonly attribute DOMString language;
attribute boolean selected;
};
一个AudioTrackList
是一列AudioTrack
对象。列表的长度由@length
属性提供。单个轨道可以通过它们的索引号(例如track[i]
)来访问,并且getTrackById()
方法允许通过提供其 id 字符串来检索AudioTrack
。此外,每当列表中的一个或多个曲目被启用或禁用时,就会引发一个change
事件;每当曲目被添加到曲目列表时,就会引发一个addtrack
事件;每当曲目被移除时,就会引发一个removetrack
事件。
VideoTrackList
是相同的,只适用于VideoTrack
对象。它有一个额外的属性:@selectedIndex
,指定当在元素中使用时,列表中的哪个轨迹被选中并被渲染。
AudioTrack
和VideoTrack
对象都包含以下属性:
@id
:可选的标识符字符串,
@kind
:轨道的可选类别,
@label
:可选的人类可读的字符串,带有音轨内容的简要描述,
@language
:根据 BCP47 的可选 IETF 语言代码,指定轨道中使用的语言,可以是手语代码。
AudioTrack
对象还有一个用于打开或关闭音轨的@enabled
属性。顺便提一下,这在包含AudioTrack
的列表中触发了一个onchange
事件。
VideoTrack
对象还有一个@selected
属性,通过该属性可以打开视频轨道。当视频轨道打开时,它会自动关闭VideoTrackList
中的任何其他视频轨道,并在该列表中触发一个onchange
事件。
为音轨定义了以下@kind
值:
"main"
:主音轨,
"alternative"
:主音轨的替代版本(例如,干净的音频版本),
"descriptions"
:主视频轨道的音频描述,
"main-desc"
:混合有音频描述的主音轨,
"translation"
:主音轨的配音版,
"commentary"
:主视频和音频轨道上的导演评论。
以下@kind
值是为视频轨道定义的:
"main"
:主视频轨道,
"alternative"
:主视频轨道的替代版本(例如,不同的角度),
"captions"
:带有烧录字幕的主视频轨道,
"subtitles"
:带有预烧字幕的主视频轨道,
"sign"
:主音轨的手语翻译,
"commentary"
:主视频和音频轨道上的导演评论。
创建多轨道媒体文件
您可以使用 MP4Box ( http://concolato.wp.mines-telecom.fr/2013/07/28/webvtt-mp4-files-dash-and-gpac/
)来创作多轨道 MPEG-4 文件。下面是一个例子:
$ MP4Box -new ElephantDreams.mux.mp4 -add ElephantDreams.mp4 \
-add ElephantDreams.sasl.mp4 -add ElephantDreams.audesc.mp3
该命令将ElephantDrams.sasl.mp4
和ElephantDreams.audesc.mp3
文件添加到ElephantDrams.mp4
文件中,从而创建 SASL(南非手语)和音频描述音轨。
为了检查它是否工作正常,您可以使用
$ MP4Box -info ElephantDreams.mux.mp4
。。。确认 mux 文件有四个磁道。
对于 WebM 文件,你可以使用mkvmerge
(参见 GUI 应用的http://mkvtoolnix.en.softonic.com/
)。下面是一个例子:
$ mkvmerge -w -o ElephantDreams.mux.webm ElephantDreams.webm \
ElephantDreams.sasl.webm ElephantDreams.audesc.ogg
该命令将ElephantDreams.sasl.webm
手语文件和ElephantDreams.audesc.ogg
音频描述文件添加到ElephantDreams.webm
中。
为了检查它是否工作正常,您可以使用
$ mkvinfo ElephantDreams.mux.webm
这确认了 mux 文件具有四个轨道。
你可以在 VLC 播放这些文件——它会显示两个视频轨道并同步它们。不幸的是,VLC 一次只允许一个音频轨道活跃,所以你只能听主音频轨道或音频描述轨道。
现在让我们把它们放在一个 HTML 文件的例子中,如清单 4-18 所示。
清单 4-18 。多轨视频文件的检查
<video poster="img/ElephantDreams.png" controls width="50%">
<source src="video/ElephantDreams.mux.webm" type="video/webm">
<source src="video/ElephantDreams.mux.mp4" type="video/mp4">
</video>
<h3>Attribute values:</h3>
<p id="values"></p>
<script>
var video = document.getElementsByTagName("video")[0];
var values = document.getElementById(’values’);
function start() {
if (video.videoTracks) {
values.innerHTML += "videoTracks.length: "
+ video.videoTracks.length + "<br/>";
values.innerHTML += "audioTracks.length: "
+ video.audioTracks.length;
} else {
values.innerHTML += "Browser does not support multitrack audio and video.";
}
video.pause();
}
video.addEventListener("play", start, false);
video.play();
</script>
我们正试图提取清单 4-19 中的@videoTracks
和@audioTracks
属性的内容,这样我们也许能够操纵哪个音频或视频轨道是活动的。然而,图 4-22 显示我们并不幸运——Safari 只显示 0 个视频和音频轨道。
图 4-22 。在 Safari 中渲染@videoTracks 和@ audio tracks
不幸的是,其他浏览器更差,甚至不支持该属性。现在不建议你尝试在 HTML5 中使用多轨媒体资源。浏览器大多认为多轨道资源不是处理多个同步音频和视频轨道的好方法,因为它会导致必须传输音频和视频轨道的成本,其中只有一小部分被呈现给用户。
今天的首选方法是使用新的媒体源扩展来提供多轨媒体资源。使用 MediaSource 扩展,在媒体播放开始时会传输一个清单文件,该文件描述了哪些曲目可用于资源。然后,只有来自那些被用户实际激活的轨道的数据将被传输。媒体源扩展超出了本书的范围。
HTML5 规范提供了另一种使不同媒体文件相互同步的方法,我们将在接下来探讨这一点。
媒体控制器:同步独立的媒体元素
MediaController
是一个协调多个媒体元素回放的对象,例如将手语视频与主视频同步。每个媒体元素都可以附加到一个MediaController
上,或者从属于一个MediaController
。当这种情况发生时,MediaController
修改从属于它的每个媒体元素的回放速率和音量,并确保当它控制的任何媒体停止时,其他媒体也同时停止。需要记住的另一点是,当使用MediaController
时,循环被禁用。
默认情况下,媒体元素没有MediaController
。因此,必须使用@mediagroup
属性或者通过显式设置MediaElement
的 IDL 的控制器属性来声明性地创建MediaController
:
interface HTMLMediaElement : HTMLElement {
...
attribute DOMString mediaGroup;
attribute MediaController? controller;
};
@ media group〔??〕
mediaGroup
IDL 属性反映了@mediagroup
内容属性的值。@mediagroup
属性包含一个字符串值。我们可以随机选择字符串的名称——它必须在我们试图同步的媒体元素之间相同。所有具有相同字符串值的@mediagroup
属性的媒体元素都从属于同一个MediaController
。
清单 4-19 展示了这一切是如何工作的一个例子。
清单 4-19 。将主视频和手语视频捆绑在一起
<video poster="img/ElephantDreams.png" controls width="50%" mediagroup="sync">
<source src="video/ElephantDreams.webm" type="video/webm">
<source src="video/ElephantDreams.mp4" type="video/mp4">
</video>
<video poster="img/ElephantDreams.sasl.png" width="35%" mediagroup="sync">
<source src="video/ElephantDreams.sasl.webm" type="video/webm">
<source src="video/ElephantDreams.sasl.mp4" type="video/mp4">
</video>
<h3>Attribute values:</h3>
<p id="values"></p>
<script>
var video1 = document.getElementsByTagName("video")[0];
var video2 = document.getElementsByTagName("video")[1];
var values = document.getElementById(’values’);
function start() {
setTimeout(function() {
video1.controller.pause();
values.innerHTML += "Video1: duration=" + video1.duration + "<br/>";
values.innerHTML += "Video2: duration=" + video2.duration + "<br/>";
values.innerHTML += "MediaGroup: " + video1.mediaGroup + "<br/>";
values.innerHTML += "MediaController: duration="
+ video1.controller.duration + "<br/>";
values.innerHTML += "MediaController: paused="
+ video1.controller.muted + "<br/>";
values.innerHTML += "MediaController: currentTime="
+ video1.controller.currentTime;
}, 10000);
}
video1.addEventListener("play", start, false);
video1.controller.play();
</script>
我们使用@mediagroup=
"sync"
同步两个视频元素——一个与 ElephantDreams 同步,另一个与同一视频的 SASL 签名者同步。在 JavaScript 中,我们让视频播放 8 秒钟,然后显示视频持续时间与控制器持续时间的比较值。你会注意到在图 4-23 的渲染中,控制器的持续时间是其从属媒体元素的最大值。我们还打印控制器的paused
、muted
和currentTime
IDL 属性值。
图 4-23 。Safari 中从属媒体元素的呈现
请注意,Safari 是目前唯一支持@mediagroup
属性和MediaController
的浏览器。
@控制器
MediaController
对象包含以下属性:
enum MediaControllerPlaybackState { "waiting", "playing", "ended" }; [Constructor] interface MediaController : EventTarget {
readonly attribute unsigned short readyState;
readonly attribute TimeRanges buffered;
readonly attribute TimeRanges seekable;
readonly attribute unrestricted double duration;
attribute double currentTime;
readonly attribute boolean paused;
readonly attribute MediaControllerPlaybackState playbackState;
readonly attribute TimeRanges played;
void pause();
void unpause();
void play();
attribute double defaultPlaybackRate;
attribute double playbackRate;
attribute double volume;
attribute boolean muted;
};
MediaContoller
的状态和属性表示其从属媒体元素的累积状态。readyState
和playbackState
是所有从属媒体元素的最低值。buffered
、seekable
和played TimeRanges
是集合,表示从属媒体元素上相同的各自属性的交集。Duration
是所有从属媒体元素的最大持续时间。CurrentTime
、paused
、defaultPlaybackRate
、playbackRate
、volume
和mute
d 被施加到所有的MediaController’s
从属媒体元素上,以保持它们全部同步。
A MediaController
还触发以下事件,这些事件有点类似于在MediaElement
上发现的事件:
Emptied
:当所有从属媒体元素结束或者不再有任何从属媒体元素时引发。
loadedmetadata
:当所有从属媒体元素至少达到HAVE_METADATA readyState
时引发。
loadeddata
:当所有从属媒体元素至少达到HAVE_CURRENT_DATA readyState
时引发。
canplay
:当所有从属媒体元素至少达到HAVE_FUTURE_DATA readyStat
e 时引发
canplaythrough
:当所有从属媒体元素至少达到HAVE_ENOUGH_DATA readyState
时引发。
playing
:所有从属媒体元素新播放时引发。
ended
:所有从属媒体元素新结束时引发。
waiting
:当至少有一个从属媒体元素正在新等待时引发。
durationchange
:当任何从属媒体元素的持续时间改变时引发。
timeupdate
:当MediaController
的currentTime
改变时引发。
play
:当MediaController
的暂停状态改变时引发。
pause
:当所有媒体元素移动到暂停时引发。
ratechange
:新换MediaController
的defaultPlaybackRate
或playbackRate
时触发。
volumechange
:当MediaController
的音量或静音属性新改变时引发。
清单 4-20 展示了一个脚本创建的MediaController
的例子。
清单 4-20 。使用 MediaController 将主视频和音频描述捆绑在一起
<video poster="img/ElephantDreams.png" controls width="50%">
<source src="video/ElephantDreams.webm" type="video/webm">
<source src="video/ElephantDreams.mp4" type="video/mp4">
</video>
<h3>Attribute values:</h3>
<p id="values"></p>
<script>
var values = document.getElementById(’values’);
var video = document.getElementsByTagName("video")[0];
video.volume = 0.1;
var audio = new Audio();
if (audio.canPlayType(’audio/mp3’) == "maybe" ||
audio.canPlayType(’audio/mp3’) == "probably") {
audio.src = "video/ElephantDreams.audesc.mp3";
} else {
audio.src = "video/ElephantDreams.audesc.ogg";
}
audio.volume = 1.0;
var controller = new MediaController();
video.controller = controller;
audio.controller = controller;
controller.play();
controller.addEventListener("timeupdate", function() {
if (controller.currentTime > 30) {
values.innerHTML += "MediaController: volume=" + controller.volume;
values.innerHTML += "MediaController: audio.volume=" + audio.volume;
values.innerHTML += "MediaController: video.volume=" + video.volume;
values.innerHTML += "MediaController: currentTime="
+ controller.currentTime;
values.innerHTML += "MediaController: audio.currentTime="
+ audio.currentTime;
values.innerHTML += "MediaController: video.currentTime="
+ video.currentTime;
controller.pause();
}
}, false);
</script>
MediaController
将一段音频描述与一段主视频同步,播放 30 秒左右,然后显示一些 IDL 属性值。
请特别注意,我们是如何决定在将音频和视频对象捆绑在一起之前设置它们的音量的。通过这样做,我们适应了资源的不同记录容量。如果音量是通过MediaController
设置的,MediaController
会将其音量施加到所有从属元素上。
图 4-24 显示了结果。
图 4-24 。Safari 中的 MediaController 对象及其从属元素
请注意所有从属音频和视频媒体元素在回放位置上的差异。
由于 Safari 是目前唯一支持@mediaGroup
和MediaController
的浏览器,你将不得不使用 JavaScript 来获得与其他浏览器相同的功能。这样做时要小心,因为这不仅仅是同时开始播放两个视频以保持它们同步的问题。它们将以不同的速率解码,并最终逐渐分离。频繁地重新同步他们的时间表是必要的。
导航:访问内容
如前所述,仅仅提供 alt 内容不足以满足所有的可访问性需求。
为视障用户创建视频的一个主要挑战是如何使浏览视频变得容易。浏览器中的媒体控件包含一个时间线导航条,可视用户使用它来点击并直接跳转到时间偏移。这避免了观看和等待某个感兴趣的片段出现。问题是视力有障碍的用户看不到导航栏。
在第二章中,我们讨论了默认播放器界面的功能,以及浏览器如何让它们可以通过键盘访问。Opera 的CTRL-left/right
箭头导航缩短了视频时长的 1/10,Firefox 的左/右箭头导航以 10 秒为增量,这些功能为视力受损的用户提供了更轻松的导航方式。
然而,缺少的是语义导航,即直接跳转到感兴趣的点的能力,例如,在长格式的媒体文件中。大多数内容都是结构化的。这本书——只要看看组成这本书结构的章节——就是结构化内容的一个例子。类似地,长格式媒体文件也有一个结构。例如,DVD 或蓝光光盘上的电影带有允许直接访问有意义的时间偏移的章节。事实上,在http://chapterdb.org/
有一个专门关于这个主题的网站。
在本章的前面,我们已经看到了元素如何暴露kind=
"chapters"
的文本轨迹,以及我们如何创作 WebVTT 文件来提供这些章节。我们如何利用章节和时间偏移来进行语义导航,让视力受损的用户也可以使用?
清单 4-21 提供了一个使用媒体片段 URIs 来导航通过 WebVTT 文件提供的章节的例子。
清单 4-21 。使用媒体片段 URIs 导航章节
<video poster="img/ElephantDreams.png" controls width="50%">
<source src="video/ElephantDreams.webm" type="video/webm">
<source src="video/ElephantDreams.mp4" type="video/mp4">
<track src="tracks/ElephantDreams_chapters_en.vtt" srclang="en"
kind="chapters" default>
</video>
<h3>Navigate through the following chapters:</h3>
<ul id="chapters">
</ul>
<script>
var video = document.getElementsByTagName("video")[0];
var source;
var chapters = document.getElementById(’chapters’);
function showChapters() {
source = video.currentSrc;
var cues = video.textTracks[0].cues;
for (var i=0; i<cues.length; i++) {
var li = document.createElement("li");
var link = document.createElement("a");
link.href = "#t=" + cues[i].startTime + "," + cues[i].endTime;
var cue = cues[i].getCueAsHTML();
cue.textContent = parseInt(cues[i].startTime) + " sec : "
+ cue.textContent;
link.appendChild(cue);
li.appendChild(link);
chapters.appendChild(li);
}
video.removeEventListener("loadeddata", showChapters, false);
}
video.addEventListener("loadeddata", showChapters, false);
function updateFragment() {
video.src = source + window.location.hash;
video.load();
video.play();
}
window.addEventListener("hashchange", updateFragment, false);
</script>
视频加载后,我们运行showChapters()
函数 来遍历.vtt
文件中的章节提示列表,并将它们添加到视频下方的<ul>
列表中。列表使用提示的开始和结束时间来构建媒体片段:#t=[starttime],[endtime
。这些媒体片段作为每个章节的 URL 提供:link.href = "#t=" + cues[i].startTime + "," + cues[i].endTime;
。
当链接被激活时,网页的 URL 哈希发生变化,并激活updateFragment()
函数,其中我们将 URL 更改为视频元素,以包含媒体片段:video.src = source + window.location.hash;
。然后,我们重新加载视频并播放它,这将激活对视频 URL 的更改,从而导航视频。
导航到“Emo 创建”章节后,结果可在图 4-25 中看到。
图 4-25 。使用谷歌浏览器中的媒体片段 URIs 导航章节
由于锚元素
摘要
这是一个相当长的章节,我们敢打赌,你从来没有考虑到这样一个事实,即让残疾人或只是不会说你的语言的人使用媒体有这么多好处。
本章最重要的教训是,可访问性不是一个简单的话题。这仅仅是因为存在如此多不同的可访问性需求。满足他们的最简单的方法是提供文本抄本。问题是,抄本提供了最差的用户体验,应该只作为后备机制。
媒体最重要的可访问性和国际化需求如下:
为听力受损的用户提供字幕或手语视频,
针对视力受损用户的音频或文本描述,以及
为国际用户提供字幕或配音音轨。
您已经看到了如何使用 WebVTT 创作文本轨道,以满足所有基于文本的可访问性需求。您还看到了如何使用多轨道媒体或MediaController
来处理手语视频、音频描述或配音音轨。
随着 HTML5 的成熟和浏览器制造商的不断进步,本章介绍的许多有限功能将变得司空见惯。
说到司空见惯,智能手机和设备在不到五年的时间里迅速从新奇变成了司空见惯。由于 HTML5 画布,HTML5 视频已经发展成为一种交互式和创造性的媒体。这是下一章的主题。我们在那里见。``
五、HTML5 视频和画布
到目前为止,本书中的视频一直被视为某种静态媒体。正如您所发现的,视频只不过是以特定速率呈现在屏幕上的一系列图像,用户与视频的唯一交互是点击控件和/或阅读脚本或字幕。除此之外,对于用户来说,除了坐下来欣赏节目之外,真的没有什么可以做的了。通过一点 JavaScript 和 HTML5 画布的使用,你可以让这个被动的媒体变得互动,更重要的是,把它变成一个创造性的媒体。这一切都始于一个非常重要的概念:成像。
当在屏幕上绘制图像时,HTML5 可以呈现两种图像类型:SVG(可缩放矢量图形)或光栅位图图形。简单地说,SVG 图像由点、线和空间填充组成。它们通常由代码驱动,由于它们的性质,它们是独立于设备的,这意味着它们可以在屏幕上调整大小和重新定位,而不会损失分辨率。
另一方面,光栅图形是基于像素的。它们本质上与屏幕中的像素相连。随着 HTML5 视频和 canvas 元素的出现,屏幕就像它的名字所暗示的那样:一个空白的画布,你可以在这里画任何东西,从直线到复杂的图形。
SVG 环境是处理基于矢量的形状的声明性图形环境,而 HTML canvas 提供了围绕像素或位图的基于脚本的图形环境。与 SVG 相比,在 canvas 中操作数据实体更快,因为直接访问单个像素更容易。另一方面,SVG 提供了一个 DOM(文档对象模型),并且有一个 canvas 没有的事件模型。这应该告诉您,需要交互式图形的应用通常会选择 SVG,而进行大量图像处理的应用通常会选择 canvas。两者中可用的变换和效果是相似的,可以用两者实现相同的视觉效果,但是需要不同的编程工作和潜在的不同性能。
当比较 SVG 和 canvas 的性能时,通常绘制大量对象最终会降低 SVG 的速度,因为 SVG 必须维护对对象的所有引用,而对于 canvas 来说,只需要照亮更多的像素。所以,当你有很多对象要画,而你继续访问单个对象并不重要,只是在画完像素后,你应该使用画布。
相比之下,画布绘制区域的大小对
的速度有着巨大的影响,因为它必须绘制更多的像素。所以,当你有一个很大的区域要覆盖少量的对象时,你应该使用 SVG。
请注意,canvas 和 SVG 之间的选择并不完全排斥。通过使用名为toDataURL()
的函数将画布转换成图像,可以将画布放入 SVG 图像中。例如,在为 SVG 图像绘制漂亮且重复的背景时,可以使用这种方法。在画布中绘制背景并通过toDataURL()
函数将其包含到 SVG 图像中可能更有效:这解释了为什么本章重点是画布。
像 SVG 一样,画布本质上是一种面向视觉的媒体——它与音频没有任何关系。当然,您可以通过简单地将
元素作为页面的一部分,将背景音乐与令人惊叹的图形显示结合起来。在 9elements ( http://9elements.com/io/?p=153
)可以找到音频和画布如何结合的惊人例子。该项目通过在音乐背景上使用彩色和动画圆圈,是一个令人惊叹的 Twitter 聊天可视化。
如果你已经有了 JavaScript 的经验,canvas 应该不会太难理解。它几乎就像一个具有绘图功能的 JavaScript 库。它特别支持以下功能类别:
画布处理 :创建绘图区域,2D 上下文,保存并恢复状态。
画基本形状 :矩形、路径、直线、圆弧、贝塞尔曲线、二次曲线。
绘图文本 :绘图填充文本、描边文本、测量文本。
使用图像 :创建、绘制、缩放和切片图像。
应用样式:颜色、填充样式、笔画样式、透明度、线条样式、渐变、阴影和图案。
应用变换 :平移、旋转、缩放和变换矩阵。
合成 :裁剪和重叠绘制合成。
应用动画 :通过关联时间间隔和超时,随时间执行绘图功能。
首先,让我们在画布上处理视频。
画布中的视频
理解如何在画布中处理视频的第一步是从元素中提取像素数据,并将其“绘制”在画布元素上。就像任何伟大的艺术家面对空白的画布一样,我们需要在画布上绘制图像。
drawImage( )
drawImage()
函数接受一个视频元素以及一个图像或画布元素。清单 5-1 展示了如何在视频中直接使用它。您可以在http://html5videoguide.net
跟随示例。
清单 5-1 。将视频像素数据引入画布
<video controls autoplay height="240" width="360" >
<source src="video/HelloWorld.mp4" type="video/mp4">
<source src="video/HelloWorld.webm" type="video/webm">
</video>
<canvas width="400" height="300" style="border: 1px solid black;">
</canvas>
<script>
var video, canvas, context;
video = document.getElementsByTagName("video")[0];
canvas = document.getElementsByTagName("canvas")[0];
context = canvas.getContext("2d");
video.addEventListener("timeupdate", paintFrame, false);
function paintFrame() {
context.drawImage(video, 0, 0, 160, 120);
}
</script>
HTML 标记很简单。它只包含我们正在绘制视频数据的元素和
元素。
JavaScript 相当简单。addEventListener
是关键。每次视频的currentTime
更新——timeupdate
——paintFrame
函数使用与getContext("2d")
对象关联的drawImage()
方法将捕获的像素绘制到画布上。如图 5-1 所示,这些像素被绘制在<画布>元素(0,0)的左上角,并填充一个 160 × 120 的空间。所有浏览器都支持此功能。
图 5-1 。每次发生 timeupdate 事件时,将视频绘制到画布中
您会注意到视频播放的帧速率高于画布。这是因为timeupdate
事件不会在视频的每一帧都触发。它每隔几帧就触发一次,大约每隔 100-250 毫秒。目前没有任何功能可以让您可靠地抓取每一帧。然而,我们可以使用requestAnimationFrame()
函数创建一个每次屏幕刷新时都会更新的绘画循环。在典型的浏览器中,这大约是每秒 60 次,鉴于大多数现代视频大约是每秒 30 帧,它应该获得大多数帧,如果不是所有帧的话。
在下一个例子中,我们使用play
事件在用户开始回放时启动绘画循环,并一直运行到视频暂停或结束。另一种选择是使用canplay
或loadeddata
事件独立于用户交互来启动显示。
此外,让我们让下一个例子更有趣一点。既然我们现在知道了如何捕捉视频帧并将其绘制到画布上,让我们开始处理这些数据。在清单 5-2 中,每次画布重绘时,我们在 x 轴和 y 轴上移动 10 个像素。
清单 5-2 。使用 requestAnimationFrame 将不同偏移量的视频帧绘制到画布中
<video controls autoplay height="240" width="360" >
<source src="video/HelloWorld.mp4" type="video/mp4">
<source src="video/HelloWorld.webm" type="video/webm">
</video>
<canvas width="400" height="300" style="border: 1px solid black;">
</canvas>
<script>
var video, canvas, context;
video = document.getElementsByTagName("video")[0];
canvas = document.getElementsByTagName("canvas")[0];
context = canvas.getContext("2d");
video.addEventListener("play", paintFrame, false);
var x = 0, xpos = 10;
var y = 0, ypos = 10;
function paintFrame() {
context.drawImage(video, x, y, 160, 120);
if (x > 240) xpos = -10;
if (x < 0) xpos = 10;
x = x + xpos;
if (y > 180) ypos = -10;
if (y < 0) ypos = 10;
y = y + ypos;
if (video.paused || video.ended) {
return;
}
requestAnimationFrame(paintFrame);
}
</script>
如图 5-2 中的所示,结果可能相当有趣。视频本身似乎是画布上的画笔,它在画布上移动,将视频帧绘制到似乎是随机的位置。实际上,如果你仔细观察paintFrame()
函数,情况并非如此。每个图像的尺寸设置为 160 × 120,视频的运动由xpos
和ypos
值决定。每一个连续的帧都与前一个帧向左和向右偏移 10 个像素,直到它到达画布的边缘,这时偏移被取消。
图 5-2 。使用 Chrome 中的 requestAnimationFrame 函数将视频绘制到画布中
本例绘制的帧率等于requestAnimationFrame()
函数的帧率,通常为 60Hz。这意味着我们现在更新画布的频率甚至比更新视频帧的频率还要高。
由于requestAnimationFrame()
方法还是相当新的,在旧的浏览器(尤其是 IE10 和更低版本)中,你需要使用setTimeout()
而不是requestAnimationFrame()
在给定的时间间隔后从视频中重复抓取一帧。
因为setTimeout()
函数在给定的毫秒数后调用一个函数,并且我们通常以每秒 24 (PAL)或 30 (NTSC)帧的速度运行视频,所以 41 毫秒或 33 毫秒的超时会更合适。为了安全起见,你可能想使用与requestAnimationFrame()
相同的帧率,这相当于你典型的 60Hz 的屏幕刷新率。因此,将超时设置为 1000/60 = 16 毫秒,以达到类似于图 5-2 的效果。对于您的应用,您可能希望进一步降低频率,使您的 web 页面更少占用 CPU(中央处理器)资源。
当您开始尝试使用setTimeout()
函数时,您会注意到它允许我们以比原始视频和requestAnimationFrame()
允许的更高的帧速率将视频帧“渲染”到画布中。让我们以清单 5-2 中的例子为例,用setTimeout()
和一个 0 超时重写它,这样你就能明白我们的意思了(参见清单 5-3 )。
清单 5-3 。使用 setTimeout 将不同偏移量的视频帧绘制到画布中
<video controls autoplay height="240" width="360" >
<source src="video/HelloWorld.mp4" type="video/mp4">
<source src="video/HelloWorld.webm" type="video/webm">
</video>
<canvas width="400" height="300" style="border: 1px solid black;">
</canvas>
<script>
var video, canvas, context;
video = document.getElementsByTagName("video")[0];
canvas = document.getElementsByTagName("canvas")[0];
context = canvas.getContext("2d");
video.addEventListener("play", paintFrame, false);
var x = 0, xpos = 10;
var y = 0, ypos = 10;
var count = 0;
function paintFrame() {
count++;
context.drawImage(video, x, y, 160, 120);
if (x > 240) xpos = -10;
if (x < 0) xpos = 10;
x = x + xpos;
if (y > 180) ypos = -10;
if (y < 0) ypos = 10;
y = y + ypos;
if (video.paused || video.ended) {
alert(count);
return;
}
setTimeout(function () {
paintFrame();
}, 0);
}
</script>
结果,如图 5-3 所示,起初可能会令人惊讶。我们看到比使用requestAnimationFrame()
方法更多的视频帧被渲染到画布中。当你进一步思考这个问题时,你会意识到我们所做的只是尽可能快地从视频中抓取一帧到画布中,而不去担心它是否是一个新的视频帧。视觉效果是,我们在画布中获得了比在视频中更高的帧速率。事实上,在我们的一台机器上的谷歌 Chrome 中,我们在画布上实现了 210 fps。请注意,您的屏幕不会以该帧速率进行渲染,但通常仍会以 60 fps 左右的速率进行渲染,但每次渲染时,画布都会放置三到四个新帧,因此它看起来比前一个版本快得多。
图 5-3 。使用 Chrome 中的 setTimeout 事件将视频绘制到画布中
如果你在各种各样的现代浏览器中尝试过这种方法,你可能会注意到,在完整的 6 秒钟的剪辑回放过程中,每种浏览器都设法绘制了不同数量的视频帧。这是因为他们的 JavaScript 引擎速度不一。他们甚至可能会在继续绘制更多帧之前在中间停留一会儿。这是因为如果有其他更高优先级的工作要做,浏览器有能力延迟一个setTimeout()
调用。requestAnimationFrame()
函数不会遇到这个问题,它保证了一个等距的递归渲染调用,从而避免了播放抖动。
注意 虽然我们已经演示了一个例子,但不要忘记这是代码,代码的巧妙之处在于能够使用它。例如,像改变xpos
和ypos
值这样简单的事情会产生与图中所示完全不同的结果。
扩展的 drawImage( )
到目前为止,我们已经使用了drawImage()
函数将从视频中提取的像素绘制到画布上。这幅图还包括画布为我们做的缩放,以将像素放入给定的宽度和高度尺寸中。还有一个版本的drawImage()
允许你从原始视频中提取一个矩形区域,并将其绘制到画布中的一个区域上。这种方法的一个例子是平铺,视频被分成多个矩形,并在矩形之间留有间隙。清单 5-4 展示了一个简单的实现。我们只展示了新的paintFrame()
函数,因为代码的其余部分与清单 5-2 中的相同。我们还选择了requestAnimationFrame()
版本的绘画,因为我们真的不需要以比视频更高的帧率进行绘画。
清单 5-4 。将视频平铺到画布中的简单实现
function paintFrame() {
in_w = 720; in_h = 480;
w = 360; h = 240;
// create 4x4 tiling
tiles = 4;
gap = 5;
for (x = 0; x < tiles; x++) {
for (y = 0; y < tiles; y++) {
context.drawImage(video, x*in_w/tiles, y*in_h/tiles,
in_w/tiles, in_h/tiles,
x*(w/tiles+gap), y*(h/tiles+gap),
w/tiles, h/tiles);
}
}
if (video.paused || video.ended) {
return;
}
requestAnimationFrame(paintFrame);
}
带有许多参数的drawImage()
函数允许从原始视频中的任何偏移中提取矩形区域,并将该像素数据绘制到画布中的任何缩放矩形区域中。图 5-4 显示了该功能的工作原理。如您所见,视频的特定区域取自源,并被绘制到画布中的特定区域。源和目的地的特定区域在drawimage()
参数中设置。
图 5-4 。使用 drawImage()将源视频中的矩形区域提取到画布中的缩放矩形区域中
参数如下:drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)
(见图 5-4 )。在清单 5-4 中,参数用于将视频细分为图块,图块的大小由in_h/tiles
使用in_w/tiles
设置,其中in_w
和in_h
是所用视频文件的固有宽度和高度(即video.videoWidth
和video.videoHeight
)。然后用w/tiles
乘以h/tiles
缩放这些图块,其中w
和h
是画布中视频图像的缩放宽度和高度。然后,每个图块以 5 像素的间距放置在画布上。
注意 重要的是你要明白,视频资源的固有宽度和高度用于从视频中提取区域,而不是视频元素中潜在的缩放视频。如果忽略这一点,您可能会计算缩放视频的宽度和高度,并提取错误的区域。还要注意,可以通过将提取的区域放入不同尺寸的目标矩形中来缩放它。
图 5-5 显示了运行清单 5-4 的结果。正如你所看到的,视频在一个 4 × 4 的网格上被分成一系列的小块,每个小块之间相隔 5 个像素。所有浏览器都显示相同的行为。
图 5-5 。在 Chrome 中将视频平铺到画布中,视频在左边,画布在右边
这种实现并不完全是最佳实践,因为我们对每个图块调用一次drawImage()
函数。如果您将变量tiles
设置为值32
,一些浏览器会跟不上画布渲染,画布中的帧速率会停滞不前。这是因为在setTimeout
函数期间,每次调用drawImage()
获取视频元素时,都会从视频中检索像素数据。结果是一个超负荷的浏览器。
有三种方法可以克服这一点。它们都依赖于通过画布将视频图像放入中间存储区域,并从那里重新绘制图像。在第一种方法中,你将抓取帧并重画它们,在第二种方法中,你将抓取帧并重画像素,在最后一种方法中,你将使用第二块画布进行像素操作。
抓帧
这种方法包括将视频像素绘制到画布中,然后用getImageData()
从画布中提取像素数据,再用putImageData()
将其写出。由于putImageData()
有参数再次只画出图片的一部分,你应该可以复制和上面一样的效果。下面是函数的签名:putImageData(imagedata, dx, dy [, dirtyx, dirtyy, dirtyw, dirtyh ])
。
不幸的是,这些参数与drawImage()
函数的参数不同。“脏”矩形从图像数据中定义要绘制的矩形(默认情况下是完整的图像)。则 dx 和 dy 允许将该矩形从其在 x 和 y 轴上的位置移动得更远。图像不会发生缩放。
你可以在清单 5-5 中看到代码——同样,只提供了paintFrame()
函数,因为其余部分与清单 5-2 相同。
清单 5-5 。使用 putImageData()在画布中重新实现视频平铺
function paintFrame() {
in_w = 720; in_h = 480;
w = 360; h = 240;
context.drawImage(video, 0, 0, in_w, in_h, 0, 0, w, h);
frame = context.getImageData(0, 0, w, h);
context.clearRect(0, 0, w, h);
// create 4x4 tiling
tiles = 4;
gap = 5;
for (x = 0; x < tiles; x++) {
for (y = 0; y < tiles; y++) {
context.putImageData(frame,
x*gap, y*gap,
x*w/tiles, y*h/tiles,
w/tiles, h/tiles);
}
}
if (video.paused || video.ended) {
return;
}
requestAnimationFrame(paintFrame);
}
在这个版本中,putImageData()
函数使用参数来指定绘图偏移量,包括视频帧的间隙和剪切矩形的大小。该帧已经通过getImageData()
作为调整大小的图像被接收。注意,用drawImage()
绘制的帧需要在用putImageData()
重新绘制之前清除,因为我们不会在 5 px 的间隙上进行绘制。图 5-6 显示了运行清单 5-5 的结果。
图 5-6 。试图使用putImageData()
将视频平铺到画布中
注意 注意,您必须从 web 服务器上运行这个示例,而不是从本地计算机上的文件中运行。原因是getImageData()
不能跨站点工作,安全检查将确保它只能在同一个 http 域上工作。这排除了本地文件访问。
像素绘画
第二种方法是手动执行剪切。由于我们已经通过getImageData()
获得了像素数据,我们可以自己创建每个图块,并使用仅带有偏移属性的putImageData()
来放置图块。清单 5-6 显示了这种情况下paintFrame()
函数的实现。
清单 5-6 。用 createImageData 在画布中重新实现视频平铺
function paintFrame() {
w = 360; h = 240;
context.drawImage(video, 0, 0, w, h);
frame = context.getImageData(0, 0, w, h);
context.clearRect(0, 0, w, h);
// create 15x15 tiling
tiles = 15;
gap = 2;
nw = w/tiles;
nh = h/tiles;
// Loop over the tiles
for (tx = 0; tx < tiles; tx++) {
for (ty = 0; ty < tiles; ty++) {
output = context.createImageData(nw, nh);
// Loop over each pixel of output file
for (x = 0; x < nw; x++) {
for (y = 0; y < nh; y++) {
// index in output image
i = x + nw*y;
// index in frame image
j = x + w*y + tx*nw + w*nh*ty;
// copy all the colours
for (c = 0; c < 4; c++) {
output.data[4*i+c] = frame.data[4*j+c];
}
}
}
// Draw the ImageData object.
context.putImageData(output, tx*(nw+gap), ty*(nh+gap));
}
}
if (video.paused || video.ended) {
return;
}
requestAnimationFrame(paintFrame);
}
首先,我们遍历每个图块,并调用createImageData()
来创建图块图像。为了用像素数据填充图块,我们循环遍历图块图像的像素,并从视频帧图像的相关像素进行填充。然后我们使用putImageData()
放置瓷砖。图 5-7 显示了 15 × 15 格子瓷砖的结果。
图 5-7 。试图在 Chrome 中使用 putImageData()将视频平铺到画布中
这显然可以通过只写单个图像并在我们写该图像时在瓦片之间放置间隙来改善。每个单幅图块都有一个图像的好处是,您可以更轻松地操作每个单幅图块,例如旋转、平移或缩放单幅图块,但是您需要管理单幅图块的列表(即,保存指向单幅图块的指针列表)。
注意 您必须从 web 服务器上运行这个示例,而不是从本地计算机上的文件中运行。原因是getImageData()
不能跨站点工作,安全检查将确保它只能在同一个 http 域上工作。这排除了本地文件访问。
草稿画布
最后一种方法是将带有drawImage()
的视频图像存储到一个中间画布中——我们称之为 scratch canvas,因为它的唯一目的是保存像素数据,并且它甚至不在屏幕上显示。一旦完成,您使用drawImage()
和来自草稿画布的输入在显示的画布上进行绘制。我们的期望是,画布中的图像已经是一种可以一片一片复制到显示的画布中的形式,而不是像以前的幼稚方法那样不断缩放。清单 5-7 中的代码展示了如何使用草稿画布。
清单 5-7 。使用临时画布在画布中重新实现视频平铺
<video controls autoplay height="240" width="360">
<source src="video/HelloWorld.mp4" type="video/mp4">
<source src="video/HelloWorld.webm" type="video/webm">
</video>
<canvas width="400" height="300" style="border: 1px solid black;">
</canvas>
<canvas id="scratch" width="360" height="240" style="display: none;"></canvas>
<script>
var context, sctxt, video;
video = document.getElementsByTagName("video")[0];
canvases = document.getElementsByTagName("canvas");
canvas = canvases[0];
scratch = canvases[1];
context = canvas.getContext("2d");
sctxt = scratch.getContext("2d");
video.addEventListener("play", paintFrame, false);
function paintFrame() {
// set up scratch frames
w = 360; h = 240;
sctxt.drawImage(video, 0, 0, w, h);
// create 4x4 tiling
tiles = 4;
gap = 5;
tw = w/tiles; th = h/tiles;
for (x = 0; x < tiles; x++) {
for (y = 0; y < tiles; y++) {
context.drawImage(scratch, x*tw, y*th, tw, th,
x*(tw+gap), y*(th+gap), tw, th);
}
}
if (video.paused || video.ended) {
return;
}
requestAnimationFrame(paintFrame);
}
</script>
注意 HTML 中第二个带有id=``scratch
的画布。它必须设置得足够大,以便能够包含视频帧。如果您没有给它一个宽度和高度属性,它将默认为 300 × 150,您可能会丢失边缘周围的数据。这个草稿画布的目的是在视频帧被传递到画布之前接收和缩放视频帧。我们不想显示它,这就是为什么它被设置为display:none
。然后使用清单 5-4 中所示的扩展drawImage()
函数将图块绘制(参见图 5-8 )到显示的画布中。
图 5-8 。在 Chrome 中使用“scratch canvas”技术
注意 这是最有效的平铺实现方式,因为它不需要重复复制视频中的帧,也不需要不断调整原始帧的大小。它也适用于所有浏览器,包括 IE。它也不需要在网络服务器上运行,这是一个额外的优势。
正如你可能已经收集到的,在画布上平铺视频提供了一些相当有趣的创意可能性。由于每个区块可以单独操作,因此每个区块可以使用不同的变换或其他技术。Sean Christmann 的“放大你的视频”展示了一个将平铺与其他画布效果(如变换)相结合的惊人例子(见http://craftymind.com/factory/html5video/CanvasVideo.html
)。当你点击视频时,该区域被平铺,平铺散开,如图 5-9 所示,产生了爆炸效果。
图 5-9 。平铺为你提供了一些严肃的创作可能性
式样
现在我们知道了如何在画布中处理视频,让我们对画布像素进行一些简单的操作,这会产生一些非常有趣的结果。我们将从使视频中的某些像素透明开始。
替换背景的像素透明度
在 HTML5 video 出现之前,Flash video 的标志之一是能够在动画或静态图像上使用 alpha 通道视频。这种技术不能在 HTML5 世界中使用,但是对画布的操作允许我们确定哪些颜色是透明的,并将画布覆盖在其他内容上。清单 5-8 显示了一个视频,其中除了白色之外的所有颜色在被投影到带有背景图像的画布上之前都是透明的。在浏览器中,像素由三种颜色组合而成:红色、绿色和蓝色。r、g 和 b 分量中的每一个可以具有 0 到 255 之间的值,相当于 0%到 100%的强度。当所有 rgb 值都为 0 时为黑色,当所有 RGB 值都为 1 时为白色。在清单 5-8 中,我们发现一个像素的 r、g 和 b 分量都在 180 以上,足够接近白色,所以我们也可以保留一些更“脏”的白色。
清单 5-8 。通过画布操作使视频中的某些颜色透明
function paintFrame() {
w = 360; h = 240;
context.drawImage(video, 0, 0, w, h);
frame = context.getImageData(0, 0, w, h);
context.clearRect(0, 0, w, h);
output = context.createImageData(w, h);
// Loop over each pixel of output file
for (x = 0; x < w; x++) {
for (y = 0; y < h; y++) {
// index in output image
i = x + w*y;
for (c = 0; c < 4; c++) {
output.data[4*i+c] = frame.data[4*i+c];
}
// make pixels transparent
r = frame.data[i * 4 + 0];
g = frame.data[i * 4 + 1];
b = frame.data[i * 4 + 2];
if (!(r > 180 && g > 180 && b > 180))
output.data[4*i + 3] = 0;
}
}
context.putImageData(output, 0, 0);
if (video.paused || video.ended) {
return;
}
requestAnimationFrame(paintFrame);
}
清单 5-8 显示了基本的绘画功能。页面的其余部分与清单 5-2 非常相似,只是在<画布>样式中添加了一个背景图像。所有像素都以完全相同的方式绘制,除了每个像素的第四个颜色通道设置为 0。这是a
通道,它决定了rbga
颜色模型的不透明度,所以我们将所有非白色的像素设为不透明。图 5-10 显示的结果是星星是仅存的不透明像素,在香港的图像上产生烟花的效果。
图 5-10 。将遮罩的视频投影到画布中的背景图像上
注意 这种技术也可以应用于蓝屏或绿屏视频。在这种情况下,视频中构成纯蓝色或绿色背景的像素变成透明的。如果屏幕光线不均匀,这将不起作用。
缩放像素切片以获得 3D 效果
视频通常放置在 3D 显示器中,通过使用透视使它们看起来更像真实世界的屏幕。这需要将视频的形状缩放为梯形,其中宽度和高度都独立缩放。在画布中,您可以通过绘制不同高度的视频图片垂直切片并使用drawImage()
函数缩放宽度来实现这一效果。清单 5-9 展示了这种技术的一个很好的例子。
清单 5-9 。使用 3D 效果在 2D 画布中渲染视频
function paintFrame() {
// set up scratch frame
w = 270; h = 180;
sctxt.drawImage(video, 0, 0, w, h);
// width should be between -500 and +500
width = -250;
// right side scaling should be between 0 and 200%
scale = 2;
// canvas width and height
cw = 1000; ch = 400;
// number of columns to draw
columns = Math.abs(width);
// display the picture mirrored?
mirror = (width > 0) ? 1 : -1;
// origin of the output picture
ox = cw/2; oy= (ch-h)/2;
// slice width
sw = columns/w;
// slice height increase steps
sh = (h*scale-h)/columns;
// Loop over each pixel column of the output picture
for (x = 0; x < w; x++) {
// place output columns
dx = ox + mirror*x*sw;
dy = oy - x*sh/2;
// scale output columns
dw = sw;
dh = h + x*sh;
// draw the pixel column
context.drawImage(scratch, x, 0, 1, h, dx, dy, dw, dh);
}
if (video.paused || video.ended) {
return;
}
requestAnimationFrame(paintFrame);
}
在这个例子中,只显示了paintFrame()
函数,我们使用了一个 1000×400 的画布和一个 scratch 画布,我们将像素数据放入其中。
当我们将视频帧拖入草稿画布时,我们将视频缩放到我们想要应用效果的大小。然后,我们将像素一列一列地拉到显示的画布中。这样做的时候,我们将像素列的宽度和高度缩放到输出图像所需的“宽度”和高度。输出图像的宽度通过width
变量给出。输出图像的高度在输出图像左侧的原始高度和右侧的原始高度的scale
倍之间缩放。负宽度将确定我们正在通过视频的“背面”观看。
这个例子是以这样一种方式编写的,你可以通过简单地改变width
和scale
变量来实现无数的创造性效果。比如你可以通过同步改变width
和scale
的值来达到翻书的效果。
图 5-11 显示了 Chrome 中的结果。包括 IE 在内的所有浏览器都支持这个例子,并且会显示相同的结果。
图 5-11 。在 Chrome 中以 3D 视角呈现视频
环境 CSS 颜色框架
画布可以用于的另一个很好的效果通常被称为视频的环境色帧。在这种效果下,会在视频周围创建一个彩色帧或边框区域,并且该帧的颜色会根据视频的平均颜色进行调整。
如果您的视频需要在页面上添加边框,或者您希望它引人注目,这种技术尤其有效。为此,您将经常计算视频的平均颜色,并使用它来填充视频后面的一个比视频稍大的 div。清单 5-10 展示了这种技术的一个实现例子。
清单 5-10 。画布中平均颜色的计算和环境色框的显示
<style type="text/css">
#ambience {
transition-property: all;
transition-duration: 1s;
transition-timing-function: linear;
padding: 40px;
width: 366px;
outline: black solid 10px;
}
video {
padding: 3px;
background-color: white;
}
canvas {
display: none;
}
</style>
<div id="ambience">
<video controls autoplay height="240" width="360">
<source src="video/HelloWorld.mp4" type="video/mp4">
<source src="video/HelloWorld.webm" type="video/webm">
</video>
</div>
<canvas id="scratch" width="320" height="160"></canvas>
</div>
<script>
var sctxt, video, ambience;
ambience = document.getElementById("ambience");
video = document.getElementsByTagName("video")[0];
scratch = document.getElementById("scratch");
sctxt = scratch.getContext("2d");
video.addEventListener("play", paintAmbience, false);
function paintAmbience() {
// set up scratch frame
sctxt.drawImage(video, 0, 0, 360, 240);
frame = sctxt.getImageData(0, 0, 360, 240);
// get average color for frame and transition to it
color = getColorAvg(frame);
ambience.style.backgroundColor =
’rgb(’+color[0]+’,’+color[1]+’,’+color[2]+’)’;
if (video.paused || video.ended) {
return;
}
// don’t do it more often than once a second
setTimeout(function () {
paintAmbience();
}, 1000);
}
function getColorAvg(frame) {
r = 0;
g = 0;
b = 0;
// calculate average color from image in canvas
for (var i = 0; i < frame.data.length; i += 4) {
r += frame.data[i];
g += frame.data[i + 1];
b += frame.data[i + 2];
}
r = Math.ceil(r / (frame.data.length / 4));
g = Math.ceil(g / (frame.data.length / 4));
b = Math.ceil(b / (frame.data.length / 4));
return Array(r, g, b);
}
</script>
尽管前面的代码块看起来相当复杂,但也相当容易理解。
我们首先设置 CSS 样式的环境,这样视频就被放在一个单独的
元素中,它的背景色从白色开始,但是会随着视频的播放而改变。视频本身有一个 3 px 的白色填充帧,将它与变色的分开。
由于有了setTimeout()
功能,视频周围的颜色每秒钟只会改变一次。我们决定在这个例子中使用setTimeout()
而不是requestAnimationFrame()
,以减少围绕视频的取景。为了确保平滑的颜色过渡,我们使用 CSS 过渡在一秒钟内完成改变。
正在使用的画布是不可见的,因为它仅用于每秒拉出一个图像帧并计算该帧的平均颜色。然后用那个颜色更新
的背景。图 5-12 显示了结果。
图 5-12 。Opera 中环境 CSS 颜色帧的渲染
如果你正在阅读印刷版,在图 5-12 中,你可能只看到一个深灰色的视频背景。然而,颜色实际上变成了背景中占主导地位的棕色的各种阴影。
注意 尽管这项技术属于“酷技术”的范畴,但还是要谨慎使用。如果有一个令人信服的设计或品牌的理由来使用它,无论如何都要使用它。仅仅因为“我能”而使用它不是一个有效的理由。
作为模式的视频
画布提供了一个简单的功能来创建平铺有图像、另一个画布或视频帧的区域。功能是createPattern()
。这将获取一个图像并将其复制到给定区域,直到该区域充满图像或视频的副本。如果您的视频没有达到您的模式要求的大小,您将需要首先使用一个草稿画布来调整视频帧的大小。
清单 5-11 展示了它是如何完成的。
清单 5-11 。用视频图案填充矩形画布区域
<video autoplay style="display: none;" >
<source src="video/HelloWorld.mp4" type="video/mp4">
<source src="video/HelloWorld.webm" type="video/webm">
</video>
<canvas width="720" height="480" style="border: 1px solid black;">
</canvas>
<canvas id="scratch" width="180" height="120" style="display:none;">
</canvas>
<script>
var context, sctxt, video;
video = document.getElementsByTagName("video")[0];
canvas = document.getElementsByTagName("canvas")[0];
context = canvas.getContext("2d");
scratch = document.getElementById("scratch");
sctxt = scratch.getContext("2d");
video.addEventListener("play", paintFrame, false);
function paintFrame() {
sctxt.drawImage(video, 0, 0, 180, 120);
pattern = context.createPattern(scratch, ’repeat’);
context.fillStyle = pattern;
context.fillRect(0, 0, 720, 480);
if (video.paused || video.ended) {
return;
}
requestAnimationFrame(paintFrame);
}
</script>
我们隐藏了原始的视频元素,因为视频已经在输出画布上绘制了 16 次。草稿画布大约每 16 毫秒抓取一帧(假设requestAnimationFrame()
以 60 fps 运行),然后使用createPattern()
的“重复”模式将其绘制到输出画布中。
每次调用paintFrame()
函数时,视频中的当前图像被抓取并用作createPattern()
中的复制图案。HTML5 canvas 规范声明,如果图像(或画布帧或视频帧)在使用它的createPattern()
函数调用后被改变,模式不会受到影响。
我们知道无法指定正在使用的图案图像的缩放比例,因此我们必须首先将视频帧加载到草稿画布中,然后从这个草稿画布中创建图案,并将其应用到绘图区域。
图 5-13 显示了 Safari 中的结果。因为所有浏览器都显示相同的行为,所以这代表了所有浏览器。
图 5-13 。在 Safari 中渲染视频模式
渐变透明蒙版
渐变遮罩用于逐渐淡化对象的不透明度。尽管市场上几乎每个视频编辑应用都广泛提供透明遮罩,但渐变遮罩也可以在运行时以编程方式添加。这是通过将页面内容(让我们假设一个图像)放在视频下面并在视频上应用灰度渐变来实现的。使用 CSS mask
属性,我们可以对渐变不透明的灰度蒙版应用透明度。我们也可以使用画布来完成这项工作。
使用 canvas,我们有更多的灵活性,因为我们可以在渐变中使用像素的 rgba 值。在这个例子中,我们简单地重用了前面的代码块,并将视频绘制到画布的中间。通过使用径向渐变,视频被混合到环境背景中。
清单 5-12 显示了代码的关键元素。
清单 5-12 。将渐变透明标记引入环境视频
<style type="text/css">
#ambience {
transition-property: all;
transition-duration: 1s;
transition-timing-function: linear;
width: 420px; height: 300px;
outline: black solid 10px;
}
#canvas {
position: relative;
left: 30px; top: 30px;
}
</style>
<div id="ambience">
<canvas id="canvas" width="360" height="240"></canvas>
</div>
<video autoplay style="display: none;">
<source src="video/HelloWorld.mp4" type="video/mp4">
<source src="video/HelloWorld.webm" type="video/webm">
</video>
<canvas id="scratch" width="360" height="240" style="display: none;">
</canvas>
<script>
var context, sctxt, video, ambience;
ambience = document.getElementById("ambience");
video = document.getElementsByTagName("video")[0];
canvas = document.getElementsByTagName("canvas")[0];
context = canvas.getContext("2d");
context.globalCompositeOperation = "destination-in";
scratch = document.getElementById("scratch");
sctxt = scratch.getContext("2d");
gradient = context.createRadialGradient(180,120,0, 180,120,180);
gradient.addColorStop(0, "rgba( 255, 255, 255, 1)");
gradient.addColorStop(0.7, "rgba( 125, 125, 125, 0.8)");
gradient.addColorStop(1, "rgba( 0, 0, 0, 0)");
video.addEventListener("play", paintAmbience, false);
function paintAmbience() {
// set up scratch frame
sctxt.drawImage(video, 0, 0, 360, 240);
// get average color for frame and transition to it
frame = sctxt.getImageData(0, 0, 360, 240);
color = getColorAvg(frame);
ambience.style.backgroundColor =
’rgba(’+color[0]+’,’+color[1]+’,’+color[2]+’,0.8)’;
// paint video image
context.putImageData(frame, 0, 0);
// throw gradient onto canvas
context.fillStyle = gradient;
context.fillRect(0, 0, 360, 240);
if (video.paused || video.ended) {
return;
}
requestAnimationFrame(paintAmbience);
}
</script>
我们不重复在清单 5-10 中定义的getColorAvg()
函数。
我们通过将显示画布的globalCompositeOperation
属性更改为destination-in
来实现带有渐变的视频遮罩。这意味着我们能够使用放置在视频帧上的渐变来控制视频帧像素的透明度。在这种情况下,我们在initCanvas
函数中使用径向渐变,并在每个视频帧中重复使用。
图 5-14 显示了所有浏览器中的结果。
图 5-14 。在各种浏览器中将带有透明遮罩的视频渲染到环境色帧上
剪辑一个区域
另一个有用的合成效果是从画布中剪切出一个区域进行显示。这将导致随后绘制到画布上的所有其他内容都只在剪裁区域中绘制。对于这种技术,一条路径被“画”出来,它可能也包括基本的形状。然后,我们不再使用 stroke()或 fill()方法在画布上绘制这些路径,而是使用clip()
方法绘制它们,在画布上创建裁剪区域,进一步的绘制将被限制在该区域内。清单 5-13 显示了一个例子。
清单 5-13 。使用剪切路径过滤出视频区域以供显示
<canvas id="canvas" width="360" height="240"></canvas>
<video autoplay style="display: none;">
<source src="video/HelloWorld.mp4" type="video/mp4">
<source src="video/HelloWorld.webm" type="video/webm">
</video>
<script>
var canvas, context, video;
video = document.getElementsByTagName("video")[0];
canvas = document.getElementsByTagName("canvas")[0];
context = canvas.getContext("2d");
context.beginPath();
// speech bubble
context.moveTo(75,25);
context.quadraticCurveTo(25,25,25,62.5);
context.quadraticCurveTo(25,100,50,100);
context.quadraticCurveTo(100,120,100,125);
context.quadraticCurveTo(90,120,65,100);
context.quadraticCurveTo(125,100,125,62.5);
context.quadraticCurveTo(125,25,75,25);
// outer circle
context.arc(180,90,50,0,Math.PI*2,true);
context.moveTo(215,90);
// mouth
context.arc(180,90,30,0,Math.PI,false);
context.moveTo(170,65);
// eyes
context.arc(165,65,5,0,Math.PI*2,false);
context.arc(195,65,5,0,Math.PI*2,false);
context.clip();
video.addEventListener("play", drawFrame, false);
function drawFrame() {
context.drawImage(video, 0, 0, 360, 240);
if (video.paused || video.ended) {
return;
}
requestAnimationFrame(drawFrame);
}
</script>
在这个例子中,我们不显示视频元素,而只在画布上绘制它的帧。在画布的设置过程中,我们定义了一个由一个语音气泡和一个笑脸组成的剪辑路径。然后,我们为play
事件设置事件监听器,并开始回放视频。在回调中,我们只需要将视频帧绘制到画布上。
这是一个非常简单有效的方法来掩盖视频区域。图 5-15 显示了 Chrome 中的结果。它在所有浏览器中都以同样的方式工作,包括 IE。
图 5-15 。在谷歌浏览器的裁剪过的画布上渲染视频
注意 记住这个例子使用了一个相当简单的编程绘制的形状来屏蔽视频。使用标志或复杂的形状来达到同样的效果是一项艰巨的任务。
绘图文本
正如您在前面的示例中看到的,简单的形状可以用于创建视频遮罩。我们也可以使用文本作为视频的遮罩。这种技术非常简单,既易于可视化(文本颜色被视频取代),也易于实现。清单 5-14 展示了如何用画布来完成。
清单 5-14 。充满视频的文本
<canvas id="canvas" width="360" height="240"></canvas>
<video autoplay style="display: none;">
<source src="video/HelloWorld.mp4" type="video/mp4">
<source src="video/HelloWorld.webm" type="video/webm">
</video>
<script>
var canvas, context, video;
video = document.getElementsByTagName("video")[0];
canvas = document.getElementsByTagName("canvas")[0];
context = canvas.getContext("2d");
// paint text onto canvas as mask
context.font = ’bold 70px sans-serif’;
context.textBaseline = ’top’;
context.fillText(’Hello World!’, 0, 0, 320);
context.globalCompositeOperation = "source-in";
video.addEventListener("play", paintFrame, false);
function paintFrame() {
context.drawImage(video, 0, 0, 360, 240);
if (video.paused || video.ended) {
return;
}
requestAnimationFrame(paintFrame);
}
</script>
我们有一个目标画布和一个隐藏的视频元素。在 JavaScript 中,我们首先将文本绘制到画布上。然后,我们使用globalCompositeOperation
属性将文本用作随后绘制到画布上的所有视频帧的遮罩。
注意,我们使用了source-in
作为合成函数。这适用于除 Opera 之外的所有浏览器,Opera 只是简单地绘制文本,但之后会忽略fillText()
剪切部分,再次绘制完整的视频帧。图 5-16 显示了所有支持该功能的其他浏览器的结果。
图 5-16 。在谷歌浏览器中作为文本填充的视频渲染
转换
canvas 也支持 CSS 支持的常见转换。这些 CSS 变换包括平移、旋转、缩放和变换矩阵。我们可以将它们应用到从视频中提取的帧上,给视频一些特殊的效果。
反思
网页设计者和开发者常用的一种视觉效果是倒影。反射实现起来相对简单,而且非常有效,尤其是在黑暗的背景下。你需要做的就是将视频帧拷贝到源视频下面的画布上,翻转拷贝,降低其不透明度,并添加渐变,这些我们之前都学过。
如果我们能够使用使用box-reflect
属性的纯 CSS 方法来创建反射,那肯定会更容易。不幸的是,这个属性还没有标准化,因此只有 blink 和基于 webkit 的浏览器实现了它。这是坏消息。
好消息是帆布来救援了。通过使用画布,我们可以在跨浏览器环境中创建一致的反射,同时保持复制的视频和源视频同步。
清单 5-15 是一个适用于所有浏览器的例子。
清单 5-15 。使用画布的视频反射
<div style="padding: 50px; background-color: #090909;">
<video autoplay style="vertical-align: bottom;" width="360">
<source src="video/HelloWorld.mp4" type="video/mp4">
<source src="video/HelloWorld.webm" type="video/webm">
</video>
<br/>
<canvas id="reflection" width="360" height="55"
style="vertical-align: top;"></canvas>
</div>
<script>
var context, rctxt, video;
video = document.getElementsByTagName("video")[0];
reflection = document.getElementById("reflection");
rctxt = reflection.getContext("2d");
// flip canvas
rctxt.translate(0,160);
rctxt.scale(1,-1);
// create gradient
gradient = rctxt.createLinearGradient(0, 105, 0, 160);
gradient.addColorStop(0, "rgba(255, 255, 255, 1.0)");
gradient.addColorStop(1, "rgba(255, 255, 255, 0.3)");
rctxt.fillStyle = gradient;
rctxt.rect(0, 105, 360, 160);
video.addEventListener("play", paintFrame, false);
function paintFrame() {
// draw frame, and fill with the opacity gradient mask
rctxt.drawImage(video, 0, 0, 360, 160);
rctxt.globalCompositeOperation = "destination-out";
rctxt.fill();
// restore composition operation for next frame draw
rctxt.globalCompositeOperation = "source-over";
if (video.paused || video.ended) {
return;
}
requestAnimationFrame(paintFrame);
}
</script>
注意 这个例子使用了<视频>元素来显示视频,尽管第二块画布也可以用于这个目的。如果您采用这种方法,请确保移除@controls
属性,因为它会破坏反射感知。
该示例将视频和视频下方对齐的画布放入一个深色的
元素中,为倒影提供一些对比度。同样,确保给和
元素相同的宽度。虽然,在这个例子中,我们给反射的高度是原始视频的三分之一。
当我们设置画布时,我们使用scale()
和translate()
函数将其准备为镜像绘图区域。平移会将其沿视频高度向下移动,缩放会沿 x 轴镜像像素。然后,我们在镜像画布上的视频帧的底部 55 个像素上设置渐变。
paintFrame()
功能在视频开始播放后并以最大速度播放时应用反射效果。因为我们已经决定让元素显示视频,所以
可能跟不上显示,这会导致和它的反射之间有一点时间上的脱节。如果这让你感到困扰,解决方案是通过另一个画布“绘制”视频帧,并隐藏视频本身。您只需要设置第二个
元素,并在paintFrame()
函数上方的画布中添加一个 drawImage()
函数。
对于反射,我们将视频帧“绘制”到镜像画布上。当使用两个
元素时,您可能会尝试使用getImageData()
和putImageData()
来应用画布转换。但是,画布变换不适用于这些函数。你必须使用一个画布,你已经通过drawImage()
将视频数据拉进画布来应用变换。
现在我们只需要镜像图像的渐变。
为了应用梯度,我们使用视频图像的梯度的合成函数。我们之前已经使用合成将画布中的当前图像替换为下一个图像。创建新的合成属性会改变这一点。因此,我们需要在应用渐变后重置合成属性。另一个解决方案是在改变合成属性之前和应用渐变之后使用save()
和restore()
函数。如果你改变了不止一个画布属性,或者你不想知道你必须重新设置属性的先前值,使用save()
和restore()
确实是更好的方法。
图 5-17 显示了最终的效果图。
图 5-17 。具有反射的视频渲染
螺旋视频
画布变换可以使我们在本章开始时看到的基于像素的操作变得容易得多,特别是当您想要将它们应用到整个画布时。在列表 5-2 和图 5-2 中显示的例子也可以用translate()
函数来实现,除了你仍然需要计算何时点击画布的边界来改变你的translate()
函数。这是通过添加一个translate(xpos,ypos)
函数来实现的,并且总是在位置(0,0)绘制图像,这并不十分成功。
我们想在这里看一个使用转换的更复杂的例子。我们将使用一个translate()
和一个rotate()
来使视频的帧在画布上盘旋。清单 5-16 展示了我们是如何做到这一点的。
清单 5-16 。使用画布的视频螺旋
<script>
var context, canvas, video;
var i = 0;
video = document.getElementsByTagName("video")[0];
canvas = document.getElementsByTagName("canvas")[0];
context = canvas.getContext("2d");
// provide a shadow
context.shadowOffsetX = 5;
context.shadowOffsetY = 5;
context.shadowBlur = 4;
context.shadowColor = "rgba(0, 0, 0, 0.5)";
video.addEventListener("play", paintFrame, false);
function paintFrame() {
context.drawImage(video, 0, 0, 120, 80);
context.setTransform(1, 0,
0, 1,
0, 0);
i += 1;
context.translate(3 * i , 1.5 * i);
context.rotate(0.2 * i);
if (video.paused || video.ended) {
alert(i);
return;
}
requestAnimationFrame(paintFrame);
}
</script>
和
元素定义与之前的例子没有变化。我们只需要增加画布的大小来适应整个螺旋。我们也给画在画布上的帧添加了阴影,这使它们与之前画的帧产生了偏移。
注意 Chrome 中视频元素附加的阴影目前不起作用。谷歌正在解决这个问题。
我们绘制螺旋的方式是这样的,我们在平移和旋转的画布上绘制新的视频帧。为了将平移和旋转应用于正确的像素,我们需要在绘制一帧后重置变换矩阵。
这非常重要,因为先前的转换已经为画布存储,这样另一个调用——例如对translate()
的调用——将沿着旋转设置的倾斜轴进行,而不是像您预期的那样直线下降。因此,必须重置变换矩阵;否则,操作是累积的。
我们还计算了显示的帧数,以便比较不同浏览器的性能。如果你把视频从头到尾播放一遍,你会看到一个带有该数字的警告框以供比较。
图 5-18 显示了 Firefox 中的渲染结果。
图 5-18 。Firefox 中螺旋形视频帧的渲染
您可能会认为这很棒,但是这给浏览器带来了什么样的性能“冲击”呢?我们来做一个小的浏览器之间的性能比较。
视频文件的持续时间为 6.06 秒。requestAnimationFrame()
功能以 60Hz 探测视频,因此理论上在视频持续时间内拾取大约 363 帧。Chrome、Safari、Opera 和 IE 都实现了这么多帧的渲染。Firefox 只能达到 165 帧。经过一些实验后,发现画布的大小是问题所在——drawImage()
要绘制的画布越大,Firefox 就越慢。我们希望这只是我们观察到的暂时问题。
这一比较是在没有为图形操作设置额外硬件加速的情况下,在 Mac OS X 上下载并安装的浏览器上进行的。
结果是,使用这种技术必须有一个合理的理由,因为您无法控制用户选择哪种浏览器。
动画和交互性
我们已经使用requestAnimationFrame()
和setTimeout()
通过画布从视频帧中创建与视频时间轴同步的动画图形。在这一节,我们想看看另一种方式来制作画布动画:通过用户交互。
这里的关键“要点”是画布只知道像素,没有对象的概念。因此,它不能将事件与绘图中的特定形状相关联。然而,作为一个整体,画布接受事件,这意味着您可以将一个click
事件附加到
元素,然后将点击事件的[
x,y
]坐标与画布的坐标进行比较,以确定它可能与哪个对象相关。
在这一节中,我们将看一个有点像简单游戏的例子。通过单击开始播放视频后,您可以随时再次单击以从引用集合中检索引用。就当是一场幸运饼干赌博吧。清单 5-17 显示了我们是如何做到的。
清单 5-17 。在画布中与用户互动的幸运饼干和视频
<script>
var quotes = ["Of those who say nothing,/ few are silent.",
"Man is born to live,/ not to prepare for life.",
"Time sneaks up on you/ like a windshield on a bug.",
"Simplicity is the/ peak of civilization.",
"Only I can change my life./ No one can do it for me."];
var canvas, context, video;
var w = 640, h = 320;
video = document.getElementsByTagName("video")[0];
canvas = document.getElementsByTagName("canvas")[0];
context = canvas.getContext("2d");
context.lineWidth = 5;
context.font = ’bold 25px sans-serif’;
context.fillText(’Click me!’, w/4+20, h/2, w/2);
context.strokeRect(w/16,h/4,w*7/8,h/2);
canvas.addEventListener("click", procClick, false);
video.addEventListener("play", paintFrame, false);
video.addEventListener("pause", showRect, false);
function paintFrame() {
if (video.paused || video.ended) {
return;
}
context.drawImage(video, 0, 0, w, h);
context.strokeStyle=’white’;
context.strokeRect(w/16,h/4,w*7/8,h/2);
requestAnimationFrame(paintFrame);
}
function isPlaying(video) {
return (!video.paused && !video.ended);
}
function showRect(e) {
context.clearRect(w/16,h/4,w*7/8,h/2);
quote = quotes[Math.floor(Math.random()*quotes.length)].split("/");
context.fillStyle = ’blue’;
context.fillText(quote[0], w/4+5, h/2-10, w/2-10);
context.fillText(quote[1], w/4+5, h/2+30, w/2-10);
context.fillStyle = ’white’;
context.fillText("click again",w/10,h/8);
}
function procClick(e) {
var pos = canvasPosition(e);
if ((pos[0] < w/4) || (pos[0] > 3*w/4)) return;
if ((pos[1] < h/4) || (pos[1] > 3*h/4)) return;
!isPlaying(video) ? video.play() : video.pause();
}
</script>
在这个例子中,我们使用一组引号作为显示的“幸运 cookies”的来源。请注意,字符串中有一个“/”标记,用于将字符串分成多行。这样做是为了便于在单个字符串中存储。
我们继续设置一个空画布,其中有一个矩形,文本为:"Click me
!".回调是为画布上的click
事件注册的,也为视频上的pause
和play
事件注册的。诀窍是使用“click”回调来暂停和播放视频,这将触发与视频暂停和播放事件相关联的相应效果。我们将可点击区域限制为矩形区域,以展示区域如何在画布中进行交互,即使不知道有什么形状。
pause
事件触发在视频中间的矩形区域显示幸运饼。play
事件触发视频帧的继续显示,从而清除了幸运 cookie。注意,如果视频暂停,我们在paintFrame()
不做任何事情。这将处理从setTimeout()
函数到paintFrame()
的任何潜在排队调用。
你可能已经注意到我们在上面的例子中缺少了一个函数——函数canvasPosition()
和函数。这个函数有助于获得画布中单击的 x 和 y 坐标。它已经被提取到清单 5-18 (你可以在http://diveintohtml5.org/canvas.html
)
找到这个例子,因为它将是任何使用 canvas 进行交互工作的人的忠实伴侣。
清单 5-18 。获取画布中点击的 x 和 y 坐标的典型函数
function canvasPosition(e) {
// from http://www.naslu.com/resource.aspx?id=460
// and http://diveintohtml5.org/canvas.html
if (e.pageX || e.pageY) {
x = e.pageX;
y = e.pageY;
} else {
x = e.clientX + document.body.scrollLeft +
document.documentElement.scrollLeft;
y = e.clientY + document.body.scrollTop +
document.documentElement.scrollTop;
}
// make coordinates relative to canvas
x -= canvas.offsetLeft;
y -= canvas.offsetTop;
return [x,y];
}
图 5-19 用不同浏览器的截图展示了这个例子的渲染。
图 5-19 。通过带有视频的交互式画布呈现幸运 cookies 示例
我们可以进一步改进这个例子,当鼠标悬停在盒子上时,将鼠标指针显示为抓手。为此,我们在画布上为mousemove
事件注册了一个回调,调用清单 5-19 中的函数,该函数在框内改变指针。
清单 5-19 。函数来改变鼠标光标在白盒顶部的位置
function procMove(e) {
var pos = canvasPosition(e);
var x = pos[0], y = pos[1];
if (x > (w/16) && x < (w*15/16) && y > (h/4) && y < (h*3/4)) {
document.body.style.cursor = "pointer";
} else {
document.body.style.cursor = "default";
}
}
您必须重用前面的canvasPosition()
函数来获取光标位置,然后在将它设置为“pointer”之前决定光标是否在框内
注意 注意不同浏览器的字体呈现方式不同,但除此之外,它们都支持相同的功能。
摘要
在本章中,我们利用了 canvas 的一些功能来处理视频图像。
我们首先了解到,drawImage()
函数允许我们将图像从元素中取出,并作为像素数据放入画布中。然后,我们确定了处理画布中的视频帧的最有效方式,并发现“草稿画布”是一个有用的准备空间,用于处理需要作为模式操作一次并重复使用多次的视频帧。
我们认为getImageData()
和putImageData()
函数是操纵视频帧数据的强大助手。
然后,我们利用像素操作功能,如改变某些像素的透明度以实现蓝屏效果,缩放像素切片以实现 3D 效果,或计算视频帧的平均颜色以创建周围环境。我们还利用了createPattern()
函数在给定的矩形上复制一个视频帧。
然后我们转向画布的合成功能,将几个独立的功能放在一起。我们使用渐变从视频渐变到环境背景、剪辑路径和文本作为模板,从视频中剪切出某些区域。
有了画布转换功能,我们终于能够创建一个跨浏览器工作的视频反射。我们还用它来旋转视频帧,从而让它们在画布上盘旋。
我们通过将用户在画布上的点击与视频活动联系起来,总结了我们对画布的看法。因为画布上没有可寻址的对象,只有可寻址的像素位置,所以它不像 SVG 那样适合捕捉对象上的事件。
在下一章,我们将深入音频 API。我们在那里见。
六、通过网络音频 API 操纵音频
说到网络,音频是...“嗯”...它就在那里。这并不是贬低音频,但是,在许多方面,音频被视为一种事后的想法或一种烦恼。然而,它的重要性不能低估。从作为用户反馈的点击声等简单效果到描述产品或事件的画外音,音频是一种主要的沟通媒介,正如一位作者喜欢说的那样,“达成交易”
音频的关键在于,当它被数字化时,它可以被操纵。要做到这一点,我们需要停止将音频视为声音,并看到它的真实面目:可以被操纵的数据。这就把我们带到了本章的主题:如何在网络浏览器中操作声音数据。
Web Audio API(应用编程接口)补充了我们刚刚了解的处理视频数据的功能。这使得开发复杂的基于 web 的游戏或音频制作应用成为可能,在这些应用中,可以用 JavaScript 动态地创建和修改音频。它还支持音频数据的可视化和数据分析,例如,确定节拍或识别正在演奏的乐器,或者您听到的声音是女性还是男性。
Web 音频 API ( http://webaudio.github.io/web-audio-api/
)是 W3C 音频工作组正在开发的规范。这个规范已经在除了 IE 之外的所有主流桌面浏览器中实现。微软已经将它添加到它的开发路线图中,所以我们可以假设它会得到普遍支持。Safari 目前在实现中使用了一个webkit
前缀。Mozilla 曾经实现了一个更简单的音频处理规范,称为“音频数据 API ”,但后来也用 Web 音频 API 规范取代了它。
注 W3C 音频工作组也开发了一个 Web MIDI API 规范(www.w3.org/TR/webmidi/
),但目前只提供给 Google Chrome 作为试用实现,所以我们暂时不解释这个 API。
在我们开始之前,回顾一下数字音频的基础知识是很有用的。
位深度和采样率
我们传统上将声音想象成正弦波——波越靠近,频率越高,因此声音也越高。至于波的高度,那叫做信号的振幅,波越高,声音越大。这些波,例如图 6-1 中的所示的,被称为波形 。横线是时间,如果信号没有离开横线,那就是沉默。
图 6-1 。Adobe Audition CC 2014 的典型波形
对于任何要数字化的声音,比如 Fireworks 或 Photoshop 中的彩色图像,都需要对波形进行采样。样本只不过是以固定间隔采样的波形的快照。以一张音频 CD 为例,以每秒 44100 次的频率采样,传统上认定为 44.1kHz,在快照时刻采样的值是代表当时音量的数字。每秒钟采样波形的频率称为采样率。采样率越高,原始模拟声音的数字表现就越准确。当然,这样做的缺点是采样率越高,文件越大。
Bitdepth 是样本值的分辨率。8 位的位深度意味着快照表示为范围从–128 到 127 的数字(即,该值适合 8 位)。16 位的位深度意味着该数字在–32,768 到 32,767 之间。如果你计算一下,你会发现一个 8 位快照每个样本有 256 个潜在值,而它的 16 位副本每个样本只有 65,000 个潜在值。样本潜在值的数量越多,数字文件可以代表的动态范围就越大。
立体声信号 每只耳朵都有一个波形。这些波形中的每一个都被单独数字化成一系列样本。它们通常被存储为一系列的对,这些对被分开以回放到它们各自的通道中。
当这些数字按照采样的顺序和频率播放时,它们会重现声音的波形。显然,更大的位深度和更高的采样速率意味着回放波形的精度更高,对波形拍摄的快照越多,波形的表现就越准确。这解释了为什么一张专辑中的歌曲有如此大的文件大小。它们以尽可能高的位深度被采样。
最常用的三种采样率是 11.025kHz、22.05kHz 和 44.1kHz。如果将采样率从 44.1kHz 降低到 22.05kHz,文件大小将减少 50%。如果速率降低到 11.025kHz,您会获得更显著的降低,另一个 50%。问题是降低采样速率会降低音频质量。以 11.025 千赫的频率听贝多芬的第九交响曲会让音乐听起来像是从锡罐里放出来的。
作为一名网页设计者或开发者,你的主要目标是以最小的文件大小获得最好的音质。尽管许多开发人员会告诉你 16 位、44.1kHz 立体声是最佳选择,但你会很快意识到这不一定是真的。例如,16 位、44.1kHz 的立体声鼠标点击声或持续时间不到几秒钟的声音——如物体滑过屏幕时发出的嗖嗖声——都是对带宽的浪费。持续时间如此之短,声音中表现的频率如此有限,以至于如果你点击的是 8 位、22.05kHz 的单声道声音,普通用户不会意识到这一点。他们听到咔哒声后继续前进。音乐文件也是如此。普通用户最有可能通过购买电脑时附赠的廉价扬声器收听。在这种情况下,一个 16 位、22.05kHz 的音轨听起来会像它的 CD 质量丰富的表亲一样好。
HTML5 音频格式
在第一章中,我们已经讨论了用于 HTML 5 的三种音频格式:MP3、WAV 和 OGG Vorbis。这些都是编码音频格式(即,音频波形的原始样本被压缩以占用更少的空间,并能够在互联网上更快地传输)。所有这些格式都使用感知编码,这意味着它们从音频流中丢弃了人类通常无法感知的所有信息。当信息以这种方式被丢弃时,文件大小会相应减小。用于编码的信息包括你的狗能听到但你听不到的声音频率。简而言之,你只能听到人类能够感知的声音(这也解释了为什么动物不太喜欢 iPods)。
所有感知编码器允许你选择多少音频是不重要的。大多数编码器使用不超过 16 Kbps 的速度来创建语音记录,从而产生高质量的文件。例如,当你创作一首 MP3 时,你需要注意带宽。格式是好的,但是如果带宽没有针对其预期用途进行优化,您的结果将是不可接受的,这就是为什么创建 MP3 文件的应用要求您设置带宽和采样率。
在这一章中,我们将处理原始音频样本,并对其进行处理以获得专业的音频效果。浏览器负责为我们解码压缩的音频文件。
泛泛而谈就到此为止;让我们从实际出发,通过使用 Web 音频 API 来处理音频数据核心的 1 和 0。我们从过滤图和AudioContext
开始。
过滤图和音频上下文
Web Audio API 规范基于构建连接的AudioNode
对象的图形来定义整体音频渲染的思想。这非常类似于作为许多媒体框架基础的过滤图思想,包括 DirectShow、GStreamer 以及 JACK 音频连接工具包。
一个滤波器图 背后的思想是,通过以特定方式修改输入数据的一系列滤波器(声音修改器)发送输入信号,将一个或多个输入信号(在我们的例子中:声音信号)连接到目的渲染器(声音输出)。
术语音频滤波器 可以指改变音频信号的音色、谐波含量、音高或波形的任何东西。该规格包括用于各种音频用途的滤波器,包括:
空间化的音频在 3D 空间中移动声音。
模拟声学空间的卷积引擎。
实时频率分析,以确定声音的组成。
提取特定频率区域的频率滤波器。
样本精确的预定声音回放。
Web Audio API 中的过滤图包含在一个AudioContext
中,由连接的AudioNode
对象组成,如图图 6-2 所示。
图 6-2 。网络音频 API 中过滤图的概念
正如您所看到的,存在没有传入连接的 AudioNode 对象—这些被称为源节点。它们也只能连接到一个音频节点。示例包括麦克风输入、媒体元素、远程麦克风输入(通过 WebRTC 连接时)、存储在内存缓冲区中的纯音频样本或振荡器等人工声源。
注****WebRTC (Web Real-Time Communication)是万维网联盟(W3C)起草的 API 定义,它支持浏览器到浏览器的应用进行语音通话、视频聊天和 P2P 文件共享,而无需内部或外部插件。这是一个很大的话题,超出了本书的范围。
没有传出连接的对象称为目的节点,它们只有一个传入连接。例如音频输出设备(扬声器)和远程输出设备(通过 WebRTC 连接时)。
中间的其他AudioNode
对象可能有多个输入连接和/或多个输出连接,是中间处理节点。
开发者不用担心两个AudioNode
对象连接时的低级流格式细节;正确的事情就会发生。例如,如果单声道音频滤波器连接了立体声输入,它将只接收左右声道的混音。
首先,让我们创建一个网络音频应用的“Hello World”。为了跟随示例,在http://html5videoguide.net/
中提供了完整的示例。清单 6-1 显示了一个简单的例子,一个振荡器源连接到默认的扬声器目的节点。你会听到频率为 1 千赫的声波。一句警告的话:我们将处理音频样本和文件,你可能要确保你的电脑音量降低。
清单 6-1 。振荡器源节点和声音输出目的节点的简单过滤图
// create web audio api context
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
// create Oscillator node
var oscillator = audioCtx.createOscillator();
oscillator.connect(audioCtx.destination);
oscillator.type = ’square’;
oscillator.frequency.value = 1000; // value in hertz
oscillator.start(0);
注意 如果你想听到不同的音调,只需改变oscillator.frequency.value
中的数字。如果你不添加频率值,默认是 440 赫兹,对于音乐倾向,是一个高于中音 c 的 A。为什么是这个值?如果你曾经去过交响乐团,听到乐队成员调整他们的乐器,那种音调已经成为音乐会音高的标准。
oscillator.start()
函数有一个可选参数来描述声音应该在什么时间开始播放。不幸的是,在 Safari 中它不是可选的。所以一定要加上 0。
图 6-3 显示了我们创建的过滤器图。
图 6-3 。网络音频 API 中的简单过滤图示例
让我们深入研究一下这个例子刚刚介绍的不同结构。
音频上下文接口
AudioContext
提供了创建AudioNode
对象并将其相互连接的环境。所有类型的AudioNode
对象都在下面代码所示的AudioContext
中定义。有许多AudioNode
对象,我们将一步一步地接近这个对象,并在本章的每一步解释我们需要的一些东西。
[Constructor]
interface AudioContext : EventTarget {
readonly attribute AudioDestinationNode destination;
readonly attribute float sampleRate;
readonly attribute double currentTime;
Promise suspend ();
Promise resume ();
Promise close ();
readonly attribute AudioContextState state;
attribute EventHandler onstatechange;
OscillatorNode createOscillator ();
...
};
interface AudioDestinationNode : AudioNode {
readonly attribute unsigned long maxChannelCount;
};
enum AudioContextState {
"suspended",
"running",
"closed"
};
每个AudioContext
包含一个只读AudioDestinationNode
。目的地通常是计算机连接的音频输出设备,如扬声器或耳机。脚本会将所有用户想要听到的音频连接到这个节点,作为AudioContext
的过滤图中的“终端”节点。你可以看到我们已经通过使用AudioContext
对象并在其上调用createOscillator()
创建了振荡器,并设置了振荡器的一些参数。然后我们通过调用振荡器对象上的connect()
函数将其连接到扬声器/耳机输出的目的地。
AudioContext
的sampleRate
在AudioContext
的生命周期内是固定的,并设置上下文中所有AudioNodes
的采样率。因此,在AudioContext
中不可能进行采样率转换。默认情况下,sampleRate
为 44,100Hz。
AudioContext
的currentTime
是以秒为单位的时间,表示AudioContext
的年龄(即,当上下文被创建时,它从零开始,并且实时增加)。所有预定时间都是相对于它的。重要的是要记住AudioContext
中的所有事件都是针对这个时钟运行的,它在几分之一秒内进行。
suspend()
、resume()
和close()
呼叫将影响currentTime
并暂停、恢复或停止其增加。他们也影响着AudioContext
是否能控制音频硬件。调用close()
后,AudioContext
不可用于创建新节点。AudioContextState
表示AudioContext
所处的状态:暂停、运行或关闭。
注意 浏览器目前不支持suspend()
、resume()
、close()
功能和状态属性。
清单 6-2 显示了一个AudioContext
的几个参数,结果显示在图 6-4 中。
清单 6-2 。音频上下文的参数
<div id="display"></div>
<script type="text/javascript">
var display = document.getElementById("display");
var context = new (window.AudioContext || window.webkitAudioContext)();
display.innerHTML = context.sampleRate + " sampling rate<br/>";
display.innerHTML += context.destination.numberOfChannels
+ " output channels<br/>";
display.innerHTML += context.currentTime + " currentTime<br/>";
</script>
图 6-4 。Chrome 中默认的 AudioContext 的参数
在通过AudioDestinationNode
播放声音之前,不知道输出通道的数量。
让我们再看看那个振荡器。它属于类型OscillatorNode
,包含一些我们可以操作的属性。
interface OscillatorNode : AudioNode {
attribute OscillatorType type;
readonly attribute AudioParam frequency;
readonly attribute AudioParam detune;
void start (optional double when = 0);
void stop (optional double when = 0);
void setPeriodicWave (PeriodicWave periodicWave);
attribute EventHandler onended;
};
enum OscillatorType {
"sine",
"square",
"sawtooth",
"triangle",
"custom"
};
第一个属性是OscillatorType
,我们在例子中将其设置为“square”。您可以在示例中改变它,并会注意到音调的音色如何变化,而其频率保持不变。
频率是一个AudioParam
对象,我们马上就会看到。它有一个可以设置的值,在我们的示例中,我们将其设置为 1,000Hz。
OscillatorNode
还有一个detune
属性,它将频率偏移给定的百分比量。其默认值为 0。去谐有助于使音符听起来更自然。
OscillatorNode
上的start()
和stop()
方法参照AudioContext
的currentTime
确定振荡器的开始和结束时间。请注意,您只能调用start()
和stop()
一次,因为它们定义了声音存在的范围。但是,您可以将振荡器与AudioDestinationNode
(或滤波器图中的下一个AudioNode
)连接或断开,以暂停/取消暂停声音。
setPeriodicWave()
功能允许设置自定义振荡器波形。使用AudioContext
的createPeriodicWave()
功能,使用傅立叶系数阵列创建自定义波形,作为周期波形的分波。除非你在写合成器,否则你可能不需要理解这个。
AudioParam 接口
我们刚刚用于OscillatorNode
的frequency
和detune
属性的AudioParam
对象类型实际上相当重要,所以让我们试着更好地理解它。它是AudioNode
在滤波器图中进行的任何音频处理的核心,因为它保存了控制AudioNodes
关键方面的参数。在我们的例子中,它是振荡器运行的频率。我们可以通过value
参数随时改变频率。这意味着一个事件被安排在下一个可能的时刻改变振荡器的频率。
由于浏览器可能会非常繁忙,所以无法预测这一事件何时会发生。在我们的例子中,这可能没问题,但如果你是一名音乐家,你会希望你的计时非常准确。因此,每个AudioParam
维护一个按时间顺序排列的变更事件列表。计划更改的时间在AudioContext’s currentTime
属性的时间坐标系中。这些事件要么立即启动更改,要么启动/结束更改。
以下是AudioParam
界面的组件:
interface AudioParam {
attribute float value;
readonly attribute float defaultValue;
void setValueAtTime (float value, double startTime);
void linearRampToValueAtTime (float value, double endTime);
void exponentialRampToValueAtTime (float value, double endTime);
void setTargetAtTime (float target, double startTime, float timeConstant);
void setValueCurveAtTime (Float32Array values, double startTime,
double duration);
void cancelScheduledValues (double startTime);
};
事件列表由AudioContext
在内部维护。以下方法可以通过向列表中添加新事件来更改事件列表。事件的类型由方法定义。这些方法被称为自动化 方法。
setValueAtTime()
告诉AudioNode
在给定的startTime
处将其AudioParam
改为值 。
linearRampToValueAtTime()
告知AudioNode
通过给定的endTime
将其AudioParam
值提升至值 。这意味着它要么从“现在”开始,要么从AudioNode
事件列表中的前一个事件开始。
exponentialRampToValueAtTime()
告诉AudioNode
使用指数连续变化从先前预定的参数值到给定的value
通过给定的endTime
增加其AudioParam
值。由于人类感知声音的方式,表示滤波器频率和回放速率的参数最好以指数方式变化。
setTargetAtTime()
告诉AudioNode
以给定的startTime
开始以给定timeConstant
的速率指数逼近目标value
。在其他用途中,这对于实现 ADSR(起音-衰减-延音-释放)包络的“衰减”和“释放”部分很有用。参数值不会在给定时间立即变为目标值,而是逐渐变为目标值。timeConstant
越大,过渡越慢。
setValueCurveAtTime()
告诉AudioNode
按照从给定startTime
开始的任意参数values
的数组为给定duration
修改其值。值的数量将被缩放以适合所需的duration
。
cancelScheduledValues()
告诉AudioNode
取消所有时间大于或等于startTime
的预定参数变更。
预定事件对于包络、音量渐变、lfo(低频振荡)、滤波器扫描或颗粒窗口等任务非常有用。我们不打算解释这些,但是专业的音乐人会知道它们是什么。重要的是,您要理解自动化方法提供了一种在给定时间实例中将参数值从一个值更改为另一个值的机制,并且这种更改可以遵循不同的曲线。这样,可以在任何AudioParam
上设定任意基于时间线的自动化曲线。我们将在一个例子中看到这意味着什么。
使用时间线,图 6-5 显示了一个自动计划,用于使用前面介绍的所有方法改变振荡器的频率。黑色的是setValueAtTime
调用,每个调用的value
是一个黑色的叉号,它们的startTime
在currentTime
时间线上。exponentialRampToValueAtTime
和linearRampToValueAtTime
调用在endTime
(时间线上的灰色线)处有一个目标value
(灰色十字)。setTargetAtTime
调用有一个startTime
(时间线上的灰色线)和一个目标value
(灰色十字)。setValueCurveAtTime
调用有一个startTime
、一个duration
和在此期间它经历的多个values
。所有这些结合在一起就产生了哔哔声和你在测试代码时听到的变化。
图 6-5 。振荡器频率的 AudioParam 自动化
清单 6-3 展示了我们如何用这种自动化来改编清单 6-1 。
清单 6-3 。振荡器的频率自动化
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var oscillator = audioCtx.createOscillator();
var freqArray = new Float32Array(5);
freqArray[0] = 4000;
freqArray[1] = 3000;
freqArray[2] = 1500;
freqArray[3] = 3000;
freqArray[4] = 1500;
oscillator.type = ’square’;
oscillator.frequency.value = 100; // value in hertz
oscillator.connect(audioCtx.destination);
oscillator.start(0);
oscillator.frequency.cancelScheduledValues(audioCtx.currentTime);
oscillator.frequency.setValueAtTime(500, audioCtx.currentTime + 1);
oscillator.frequency.exponentialRampToValueAtTime(4000,
audioCtx.currentTime + 4);
oscillator.frequency.setValueAtTime(3000, audioCtx.currentTime + 5);
oscillator.frequency.linearRampToValueAtTime(1000,
audioCtx.currentTime + 8);
oscillator.frequency.setTargetAtTime(4000, audioCtx.currentTime + 10, 1);
oscillator.frequency.setValueAtTime(1000, audioCtx.currentTime + 12);
oscillator.frequency.setValueCurveAtTime(freqArray,
audioCtx.currentTime + 14, 4);
AudioNodes
实际上可以对每个单独的音频样本或 128 个样本的块进行值计算。当需要对块的每个样本单独进行数值计算时,AudioParam
将指定它是一个 a-rate 参数。它将指定它是一个 k-rate 参数,此时只计算块的第一个样本,结果值用于整个块。
振荡器的frequency
和detune
参数都是 a 速率参数,因为每个单独的音频样本都可能需要调整频率和失谐。
a-rate AudioParam
获取音频信号的每个样本帧的当前音频参数值。
一个 k-rate AudioParam
对处理的整个块(即 128 个样本帧)使用相同的初始音频参数值。
音频节点接口
前面我们已经熟悉了OscillatorNode
,它是AudioNode
的一种,是过滤图的构建模块。一个OscillatorNode
是一个源节点。我们还熟悉了过滤图中的一种目的节点类型:??。是时候我们深入了解一下AudioNode
接口本身了,因为这是OscillatorNode
的connect()
功能的来源。
interface AudioNode : EventTarget {
void connect (AudioNode destination, optional unsigned long output = 0,
optional unsigned long input = 0);
void connect (AudioParam destination, optional unsigned long output = 0 );
void disconnect (optional unsigned long output = 0);
readonly attribute AudioContext context;
readonly attribute unsigned long numberOfInputs;
readonly attribute unsigned long numberOfOutputs;
attribute unsigned long channelCount;
attribute ChannelCountMode channelCountMode;
attribute ChannelInterpretation channelInterpretation;
};
一个AudioNode
只能属于一个AudioContext
,存储在context
属性中。
AudioNode
中的第一个connect()
方法将它连接到另一个AudioNode
。在一个特定节点的给定输出和另一个节点的给定输入之间只能有一个连接。output
参数指定要连接的输出索引,类似地input
参数指定要连接到目的地AudioNode
的哪个输入索引。
AudioNode
的numberOfInputs
属性提供了输入到AudioNode
的输入数量,而numberOfOutputs
提供了从AudioNode
输出的数量。源节点有 0 个输入,目的节点有 0 个输出。
一个AudioNode
可能有比输入更多的输出;因此支持扇出。它的输入可能多于输出,这支持扇入。将一个AudioNode
连接到另一个AudioNode
并返回是可能的,从而形成一个循环。这只有在循环中至少有一个DelayNode
时才被允许,否则你会得到一个NotSupportedError
异常。
非源和非目标AudioNode
的每个输入和输出都有一个或多个通道。输入、输出及其通道的确切数量取决于AudioNode
的类型。
channelCount
属性包含AudioNode
固有处理的通道数量。默认情况下,它是 2,但是可能会被这个属性的一个显式的新值覆盖,或者通过channelCountMode
属性覆盖。
enum ChannelCountMode { "max", "clamped-max", "explicit" };
这些值具有以下含义:
当channelCountMode
为“max”时,AudioNode
处理的通道数是所有输入和输出连接的最大通道数,channelCount
被忽略。
当channelCountMode
为“clamped-max”时,AudioNode
处理的通道数是所有输入和输出连接的最大通道数,但也是channelCount
的最大值。
当channelCountMode
为“显式”时,AudioNode
处理的通道数量由channelCount
决定。
对于每个输入,AudioNode
混合(通常是上混合)到该节点的所有连接。当输入的通道需要向下或向上混合时,channelInterpretation
属性决定如何处理这种向下或向上混合。
enum ChannelInterpretation { "speakers", "discrete" };
当channelInterpretation
为“离散”时,上混是通过填充通道直到它们用完,然后将剩余通道归零,下混是通过填充尽可能多的通道,然后丢弃剩余通道。
如果channelInterpretation
设置为“speaker”,则为特定的声道布局定义上混和下混:
1 通道:单声道(通道 0)
2 个通道:左通道(通道 0),右通道(通道 1)
4 声道:左声道(通道 0)、右声道(通道 1)、环绕左声道(通道 2)、环绕右声道(通道 3)
5.1 声道:左声道(ch 0)、右声道(ch 1)、中声道(ch 2)、低音炮(ch 3)、左环绕声道(ch 4)、右环绕声道(ch 5)
向上混合的工作方式如下:
单声道:复制到左右声道(2 和 4 声道),复制到中央(5.1 声道)
立体声:复制到左右声道(适用于 4 声道和 5.1 声道)
4 声道:复制到左侧和右侧,环绕左侧和环绕右侧(适用于 5.1)
每隔一个频道保持在 0。
缩混的工作原理如下:
单声道缩混:
2 -> 1: 0.5 *(左+右)
4 -> 1: 0.25 *(左+右+环绕左+环绕右)
5.1 -> 1: 0.7071 *(左+右)+居中
立体声缩混:
四声道缩混:
图 6-6 描述了一个AudioNode
的假设输入和输出场景,其中每个场景都有不同的通道集。如果通道数不在 1、2、4 和/或 6 中,则使用“离散”解释。
图 6-6 。音频节点的通道和输入/输出
最后,AudioNode
对象上的第二个connect()
方法将一个AudioParam
连接到一个AudioNode
。这意味着参数值由音频信号控制。
可以将一个AudioNode
输出连接到一个以上的AudioParam
,并多次调用connect()
,从而支持扇出并通过一个音频信号控制多个AudioParam
设置。也可以通过对connect();
的多次调用将多个AudioNode
输出连接到单个AudioParam
,从而支持扇入并控制具有多个音频输入的单个AudioParam
。
一个特定节点的给定输出和一个特定的AudioParam
之间只能有一个连接。同一AudioNode
和同一AudioParam
之间的多个连接被忽略。
一个AudioParam
将从连接到它的任何AudioNode
输出获取渲染的音频数据,并通过缩混将其转换为单声道(如果它还不是单声道)。接下来,它会将它与任何其他此类输出以及内在参数值(没有任何音频连接时,AudioParam
通常会具有的值)混合在一起,包括为该参数安排的任何时间线更改。
我们将通过一个操纵GainNode
增益设置的振荡器的例子来演示这一功能,即所谓的 LFO。增益仅仅意味着增加信号的功率,这导致其音量增加。GainNode
的增益设置被置于频率固定的振荡器和目的节点之间,从而使固定音调以振荡增益呈现(见清单 6-4 )。
清单 6-4 。一个振荡器的增益由另一个振荡器控制
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var oscillator = audioCtx.createOscillator();
// second oscillator that will be used as an LFO
var lfo = audioCtx.createOscillator();
lfo.type = ’sine’;
lfo.frequency.value = 2.0; // 2Hz: low-frequency oscillation
// create a gain whose gain AudioParam will be controlled by the LFO
var gain = audioCtx.createGain();
lfo.connect(gain.gain);
// set up the filter graph and start the nodes
oscillator.connect(gain);
gain.connect(audioCtx.destination);
oscillator.start(0);
lfo.start(0);
当运行清单 6-3 时,您将听到频率为 440Hz(振荡器的默认频率)的音调以每秒两次的频率在增益 0 和 1 之间波动。图 6-7 解释了过滤图的设置。请特别注意,lfo OscillatorNode
连接到GainNode
的增益参数,而不是增益节点本身。
图 6-7 。音频节点的通道和输入/输出
我们只是使用了AudioContext
的另一个函数来创建一个GainNode
。
[Constructor] interface AudioContext : EventTarget {
...
GainNode createGain ();
...
}
为了完整起见,以下是GainNode
的定义:
interface GainNode : AudioNode {
readonly attribute AudioParam gain;
};
gain
参数表示要应用的增益量。其默认值为 1(增益不变)。标称值minValue
为 0,但对于反相可能为负。简而言之,相位反转就是“翻转信号”——想想我们一开始用来解释音频信号的正弦波——反转它们的相位意味着在时间轴上镜像它们的值。名义上的maxValue
是 1,但是允许更高的值。这个参数是一个 a-rate。
一个GainNode
接受一个输入并创建一个输出,ChannelCountMode
是“max”(即,它处理给定的尽可能多的通道),而ChannelInterpretation
是“speakers”(即,对输出执行上混合或下混合)。
读取和生成音频数据
到目前为止,我们已经通过振荡器创建了音频数据。然而,一般来说,您会想要读取一个音频文件,然后获取音频数据并对其进行操作。
AudioContext
提供了这样的功能。
[Constructor] interface AudioContext : EventTarget {
...
Promise<AudioBuffer> decodeAudioData (ArrayBuffer audioData,
optional DecodeSuccessCallback successCallback,
optional DecodeErrorCallback errorCallback);
AudioBufferSourceNode createBufferSource();
...
}
callback DecodeErrorCallback = void (DOMException error);
callback DecodeSuccessCallback = void (AudioBuffer decodedData);
decodeAudioData()
函数异步解码ArrayBuffer
中包含的音频文件数据。要使用它,我们首先必须将音频文件提取到一个ArrayBuffer
中。然后我们可以将它解码成一个AudioBuffer
,并将那个AudioBuffer
交给一个AudioBufferSourceNode
。现在它在AudioNode
中,并且可以通过过滤图连接(例如,通过目的节点回放)。
XHR ( HTMLHttpRequest
)接口用于从服务器获取数据。我们将使用它将文件数据放入一个ArrayBuffer
中。我们假设您熟悉 XHR,因为它不是特定于媒体的界面。
在清单 6-5 的中,我们使用 XHR 检索文件“transition.wav ”,然后通过调用AudioContext
的decodeAudioData()
函数将接收到的数据解码成一个AudioBufferSourceNode
。
注意 感谢 CadereSounds 在 freesound 的知识共享许可下提供“transition.wav”样本(见www.freesound.org/people/CadereSounds/sounds/267125/
)。
清单 6-5 。使用 XHR 获取媒体资源
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var source = audioCtx.createBufferSource();
var request = new XMLHttpRequest();
var url = ’audio/transition.wav’;
function requestData(url) {
request.open(’GET’, url, true);
request.responseType = ’arraybuffer’;
request.send();
}
function receivedData() {
if ((request.status === 200 || request.status === 206)
&& request.readyState === 4) {
var audioData = request.response;
audioCtx.decodeAudioData(audioData,
function(buffer) {
source.buffer = buffer;
source.connect(audioCtx.destination);
source.loop = true;
source.start(0);
},
function(error) {
"Error with decoding audio data" + error.err
}
);
}
}
request.addEventListener(’load’, receivedData, false);
requestData(url);
首先我们定义一个函数,它对文件进行 XHR 请求,然后在网络检索之后调用receivedData()
函数。如果检索成功,我们将得到的ArrayBuffer
交给decodeAudioData()
。
注意 你必须把这个上传到网络服务器,因为 XHR 认为文件:URL 是不可信的。你也可以使用更现代的 API 来代替 XHR,它没有同样的问题。
我们按顺序来看一下涉及的对象。
首先,XHR 从服务器获取音频文件的字节,并将它们放入一个ArrayBuffer
中。浏览器可以解码任何格式的数据,?? 元素也可以解码。 decodeAudioData()
功能将音频数据解码成线性 PCM。如果成功,它将被重采样到AudioContext
的采样率,并存储在一个AudioBuffer
对象中。
音频缓冲接口
interface AudioBuffer {
readonly attribute float sampleRate;
readonly attribute long length;
readonly attribute double duration;
readonly attribute long numberOfChannels;
Float32Array getChannelData (unsigned long channel);
void copyFromChannel (Float32Array destination, long channelNumber,
optional unsigned long startInChannel = 0);
void copyToChannel (Float32Array source, long channelNumber,
optional unsigned long startInChannel = 0);
};
该接口表示驻留在存储器中的音频资产(例如,用于单镜头声音和其他短音频剪辑)。其格式为非交错 IEEE 32 位线性 PCM,标称范围为-1 至+1。它可以包含一个或多个通道,并可由一个或多个AudioContext
对象使用。
您通常使用AudioBuffer
来播放短声音——对于较长的声音,例如音乐配乐,您应该使用带有音频元素和MediaElementAudioSourceNode
的流。
sampleRate
属性包含音频资产的采样率。
length
属性包含样本帧中音频资源的长度。
duration
属性包含以秒为单位的音频资产的持续时间。
numberOfChannels
属性包含音频资产的离散通道的数量。
getChannelData()
方法返回特定通道 PCM 音频数据的 float 32 数组。
copyFromChannel()
方法将样本从AudioBuffer
的指定通道复制到目的 Float32Array。可在startInChannel
参数中提供从通道复制数据的可选偏移量。
copyToChannel()
方法将样本从 source Float32Array 复制到AudioBuffer
的指定通道。可在startInChannel
参数中提供从通道复制数据的可选偏移量。
可以将AudioBuffer
添加到AudioBufferSourceNode
中,以便音频资产进入过滤网络。
您可以使用AudioContext
的createBuffer()
方法直接创建一个AudioBuffer
。
[Constructor]
interface AudioContext : EventTarget {
...
AudioBuffer createBuffer (unsigned long numberOfChannels,
unsigned long length,
float sampleRate);
...
};
它将用给定长度(以采样帧为单位)、采样率和通道数的样本填充,并且只包含无声段。然而,最常见的是,AudioBuffer
用于存储解码样本。
AudioBufferSourceNode 接口
interface AudioBufferSourceNode : AudioNode {
attribute AudioBuffer? buffer;
readonly attribute AudioParam playbackRate;
readonly attribute AudioParam detune;
attribute boolean loop;
attribute double loopStart;
attribute double loopEnd;
void start (optional double when = 0, optional double offset = 0,
optional double duration);
void stop (optional double when = 0);
attribute EventHandler onended;
};
一个AudioBufferSourceNode
表示一个音频源节点,在一个AudioBuffer
中有一个存储器内音频资产。因此,它有 0 个输入和 1 个输出。这对于播放短音频资源很有用。输出的通道数总是等于分配给buffer
属性的AudioBuffer
的通道数,或者如果buffer
为空,则为一个无声通道。
buffer
属性包含音频资产。
playbackRate
属性包含渲染音频资源的速度。其默认值为 1。这个参数是 k-rate。
detune
属性调节音频资源渲染的速度。其默认值为 0。其标称范围为[-1200;1,200].这个参数是 k-rate。
playbackRate
和detune
两者一起用于确定随时间 t 变化的computedPlaybackRate
值:
computedPlaybackRate(t) = playbackRate(t) * pow(2, detune(t) / 1200)
computedPlaybackRate
是该AudioBufferSourceNode
的AudioBuffer
必须播放的有效速度。默认情况下是 1。
loop
属性表示音频数据是否应该循环播放。默认值为 false。
loopStart
和loopEnd
属性以秒为单位提供了循环运行的时间间隔。默认情况下,它们从 0 到缓冲持续时间。
start()
方法用于安排声音回放的时间。当缓冲区的音频数据播放完毕时(如果 loop 属性为 false),或者当调用了stop()
方法并且到达了指定的时间时,播放将自动停止。对于一个给定的AudioBufferSourceNode
,不能多次发出start()
和stop()
。
由于AudioBufferSourceNode
是一个AudioNode
,它有一个connect()
方法参与过滤网络(例如,连接到音频目的地进行回放)。
MediaElementAudioSourceNode 接口
另一种可用于将音频数据放入过滤图的源节点是MediaElementAudioSourceNode
。
interface MediaElementAudioSourceNode : AudioNode {
};
AudioContext
提供了创建这样一个节点的功能。
[Constructor] interface AudioContext : EventTarget {
...
MediaElementAudioSourceNode createMediaElementSource(HTMLMediaElement
mediaElement);
...
}
总之,它们允许将来自
或元素的音频作为源节点引入。因此,MediaElementAudioSourceNode
有 0 个输入和 1 个输出。输出的通道数对应于HTMLMediaElement
引用的媒体的通道数,作为参数传递给createMediaElementSource()
,如果HTMLMediaElement
没有音频,则为一个无声通道。一旦连接,HTMLMediaElement
的音频不再直接播放,而是通过过滤图播放。
对于较长的媒体文件,MediaElementAudioSourceNode
应该优先于AudioBufferSourceNode
使用,因为MediaElementSourceNode
传输资源。清单 6-6 显示了一个例子。
清单 6-6 。将音频元素流式传输到音频上下文中
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var mediaElement = document.getElementByTagName(’audio’)[0];
mediaElement.addEventListener(’play’, function() {
var source = audioCtx.createMediaElementSource(mediaElement);
source.connect(audioCtx.destination);
source.start(0);
});
我们必须等待 play 事件触发,以确保音频已经加载并被解码,这样AudioContext
就可以获得数据。清单 6-6 中音频元素的音频恰好回放一次。
MediaStreamAudioSourceNode 接口
可用于将音频数据放入过滤器图的最后一种源节点是MediaStreamAudioSourceNode
。
interface MediaStreamAudioSourceNode : AudioNode {
};
该接口表示来自MediaStream
的音频源,它基本上是一个现场音频输入源——麦克风。我们不会在本书中描述MediaStream
API——这超出了本书的范围。然而,一旦你有了这样一个MediaStream
对象,AudioContext
就提供了将MediaStream
的第一个AudioMediaStreamTrack
(音轨)转换成过滤图中的一个音频源节点的功能。
[Constructor] interface AudioContext : EventTarget {
...
MediaStreamAudioSourceNode createMediaStreamSource(MediaStream
mediaStream);
...
}
因此,MediaStreamAudioSourceNode
有 0 个输入和 1 个输出。输出的通道数对应于AudioMediaStreamTrack
的通道数,通过参数传递给createMediaStreamSource
(),如果MediaStream
没有音频,则为一个无声通道。
清单 6-7 显示了一个例子。
清单 6-7 。将音频流的音频提取到 AudioContext
navigator.getUserMedia = (navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia);
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var mediaElement = document.getElementsByTagName(’audio’)[0];
var source;
onSuccess = function(stream) {
mediaElement.src = window.URL.createObjectURL(stream) || stream;
mediaElement.onloadedmetadata = function(e) {
mediaElement.play();
mediaElement.muted = ’true’;
};
source = audioCtx.createMediaStreamSource(stream);
source.connect(audioCtx.destination);
};
onError = function(err) {
console.log(’The following getUserMedia error occured: ’ + err);
};
navigator.getUserMedia ({ audio: true }, onSuccess, onError);
清单 6-6 中音频元素的音频通过过滤网络播放,尽管在音频元素上被静音。
注意 还有一个类似的MediaStreamAudioDestinationNode
,用于将过滤图的输出渲染到一个MediaStream
对象,为通过对等连接到另一个浏览器的音频流做准备。AudioContext
的createMediaStreamDestination()
函数创建了这样一个目的节点。然而,这目前只在 Firefox 中实现。
操纵音频数据
到目前为止,我们已经了解了如何通过四种不同的机制为我们的音频滤波器图创建音频数据:振荡器、音频缓冲区、音频文件和麦克风源。接下来让我们看看AudioContext
提供给 web 开发者的一组音频操作函数。这些是标准的音频操作功能,音频专业人员会很好地理解。
这些操作功能中的每一个都通过一个处理节点在过滤图中表示,并通过AudioContext
中的 create-function 创建:
[Constructor] interface AudioContext : EventTarget {
...
GainNode createGain ();
DelayNode createDelay(optional double maxDelayTime = 1.0);
BiquadFilterNode createBiquadFilter ();
WaveShaperNode createWaveShaper ();
StereoPannerNode createStereoPanner ();
ConvolverNode createConvolver ();
ChannelSplitterNode createChannelSplitter(optional unsigned long
numberOfOutputs = 6 );
ChannelMergerNode createChannelMerger(optional unsigned long
numberOfInputs = 6 );
DynamicsCompressorNode createDynamicsCompressor ();
...
}
增益节点接口
GainNode
表示音量的变化,用AudioContext
的createGain()
方法创建。
interface GainNode : AudioNode {
readonly attribute AudioParam gain;
};
它使输入数据在传播到输出之前获得给定的增益。一个GainNode
总是正好有一个输入和一个输出,两者都有相同数量的通道:
| 输入数量 | one |
| 产出数量 | one |
| 通道计数模式 | "麦克斯" |
| 频道计数 | Two |
| 渠道解释 | "扬声器" |
gain
参数是一个无单位值,标称值介于 0 和 1 之间,其中 1 表示无增益变化。该参数为 a-rate,因此增益应用于每个样本帧,并乘以所有输入通道的每个相应样本。
增益可以随着时间而改变,并且使用去压缩算法来应用新的增益,以防止在所得到的音频中出现不美观的“咔哒声”。
清单 6-8 展示了一个通过滑动条操纵音频信号增益的例子。确保释放滑块,这样当您自己尝试时,滑块的值实际上会改变。过滤图由一个MediaElementSourceNode
、一个GainNode
和一个AudioDestinationNode
组成。
清单 6-8 。操纵音频信号的增益
<audio autoplay controls src="audio/Shivervein_Razorpalm.wav"></audio>
<input type="range" min="0" max="1" step="0.05" value="1"/>
<script>
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var mediaElement = document.getElementsByTagName(’audio’)[0];
var source = audioCtx.createMediaElementSource(mediaElement);
var gainNode = audioCtx.createGain();
source.connect(gainNode);
gainNode.connect(audioCtx.destination);
var slider = document.getElementsByTagName(’input’)[0];
slider.addEventListener(’change’, function() {
gainNode.gain.value = slider.value;
});
</script>
注意 记得把这个例子上传到网络服务器,因为 XHR 认为文件的 URL 是不可信的。
延迟节点接口
DelayNode
将输入的音频信号延迟一定的秒数,并使用AudioContext
的createDelay()
方法创建。
interface DelayNode : AudioNode {
readonly attribute AudioParam dealyTime;
};
默认delayTime
为 0 秒(无延迟)。当延迟时间改变时,过渡是平滑的,没有明显的滴答声或毛刺。
| 输入数量 | one |
| 产出数量 | one |
| 通道计数模式 | "麦克斯" |
| 频道计数 | Two |
| 渠道解释 | "扬声器" |
最小值为 0,最大值由AudioContext
方法createDelay
的maxDelayTime
参数决定。
该参数为 a-rate,因此延迟应用于每个样本帧,并乘以所有输入声道的每个相应样本。
一个DelayNode
通常用于创建一个滤波器节点循环(例如,结合一个GainNode
来创建一个重复的、衰减的回声)。当在一个周期中使用一个DelayNode
时,delayTime
属性的值被限制在最少 128 帧(一个块)。
清单 6-9 显示了一个衰减回声的例子。
清单 6-9 。通过增益和延迟滤波器衰减回声
<audio autoplay controls src="audio/Big%20Hit%201.wav"></audio>
<script>
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var mediaElement = document.getElementsByTagName(’audio’)[0];
mediaElement.addEventListener(’play’, function() {
var source = audioCtx.createMediaElementSource(mediaElement);
var delay = audioCtx.createDelay();
delay.delayTime.value = 0.5;
var gain = audioCtx.createGain();
gain.gain.value = 0.8;
// play once
source.connect(audioCtx.destination);
// create decaying echo filter graph
source.connect(delay);
delay.connect(gain);
gain.connect(delay);
delay.connect(audioCtx.destination);
});
</script>
使用createMediaElementSource()
将音频重定向到滤波器图中。源声音直接连接到目的地进行正常回放,然后也馈入延迟和增益滤波器循环,衰减回声也连接到目的地。图 6-8 显示了创建的过滤器图形。
图 6-8 。衰减回声的滤波器图
注意 感谢 robertmcdonald 在 freesound 上制作了《Big Hit 1.wav》的知识共享许可样本(见www.freesound.org/people/robertmcdonald/sounds/139501/
)。
BiquadFilterNode 接口
BiquadFilterNode
代表低阶滤波器(详见http://en.wikipedia.org/wiki/Digital_biquad_filter
),由AudioContext
的createBiquadFilter()
方法创建。低阶滤波器是基本音调控制(低音、中音、高音)、图形均衡器和更高级滤波器的构件。
interface BiquadFilterNode : AudioNode {
attribute BiquadFilterType type;
readonly attribute AudioParam frequency;
readonly attribute AudioParam detune;
readonly attribute AudioParam Q;
readonly attribute AudioParam gain;
void getFrequencyResponse (Float32Array frequencyHz,
Float32Array magResponse,
Float32Array phaseResponse);
};
滤波器参数 s 可以随时间变化(例如,频率变化产生滤波器扫描)。
| 输入数量 | one |
| 产出数量 | one |
| 通道计数模式 | "麦克斯" |
| 频道计数 | 输出中的数量与输入中的数量相同 |
| 渠道解释 | "扬声器" |
每个BiquadFilterNode
都可以配置为多种常见滤波器类型之一。
enum BiquadFilterType {
"lowpass",
"highpass",
"bandpass",
"lowshelf",
"highshelf",
"peaking",
"notch",
"allpass"
};
默认的滤镜类型是lowpass
( http://webaudio.github.io/web-audio-api/#the-biquadfilternode-interface
)。
frequency
参数的默认值为 350Hz,从 10Hz 开始,一直到奈奎斯特频率的一半(对于AudioContext
的默认 44.1kHz 采样速率为 22,050Hz)。它根据滤波器类型提供频率特性,例如,低通滤波器和高通滤波器的截止频率,或者带通滤波器的频带中心。
detune
参数提供了一个失谐频率的百分比值,使其更加自然。默认为 0。
Frequency
和detune
是速率参数,它们共同决定滤波器的计算频率:
computedFrequency(t) = frequency(t) * pow(2, detune(t) / 1200)
Q
参数是双二阶滤波器的品质因数,默认值为 1,标称范围为 0.0001 至 1,000(尽管 1 至 100 最为常见)。
gain
参数提供应用于双二阶滤波器的升压(dB ),默认值为 0,标称范围为-40 至 40(尽管 0 至 10 最为常见)。
getFrequencyResponse()
方法计算frequencyHz
频率数组中指定频率的频率响应,并返回magResponse
输出数组中的线性幅度响应值和phaseResponse
输出数组中以弧度表示的相位响应值。这对于可视化过滤器形状特别有用。
清单 6-10 显示了应用于音频源的不同滤波器类型的例子。
清单 6-10 。应用于音频源的不同双二阶滤波器类型
<audio autoplay controls src="audio/Shivervein_Razorpalm.wav"></audio>
<select class="type">
<option>lowpass</option>
<option>highpass</option>
<option>bandpass</option>
<option>lowshelf</option>
<option>highshelf</option>
<option>peaking</option>
<option>notch</option>
<option>allpass</option>
</select>
<script>
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var mediaElement = document.getElementsByTagName(’audio’)[0];
var source = audioCtx.createMediaElementSource(mediaElement);
var bass = audioCtx.createBiquadFilter();
// Set up the biquad filter node with a low-pass filter type
bass.type = "lowpass";
bass.frequency.value = 6000;
bass.Q.value = 1;
bass.gain.value = 10;
mediaElement.addEventListener(’play’, function() {
// create filter graph
source.connect(bass);
bass.connect(audioCtx.destination);
});
// Update the biquad filter type
var type = document.getElementsByClassName(’type’)[0];
type.addEventListener(’change’, function() {
bass.type = type.value;
});
</script>
输入音频文件连接到一个双二阶滤波器,频率值为 6,000Hz,品质因数为 1,增益为 10 dB。过滤器的类型可以通过下拉菜单在所有八种不同的过滤器之间进行更改。这样,您可以很好地了解这些滤波器对音频信号的影响。
在这个例子中使用getFrequencyResponse()
方法,我们可以可视化过滤器(参见http://webaudio-io2012.appspot.com/#34
)。清单 6-11 展示了如何绘制频率增益图。
清单 6-11 。绘制频率增益图
<canvas width="600" height="200"></canvas>
<canvas width="600" height="200" style="display: none;"></canvas>
<script>
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var canvas = document.getElementsByTagName(’canvas’)[0];
var ctxt = canvas.getContext(’2d’);
var scratch = document.getElementsByTagName(’canvas’)[1];
var sctxt = scratch.getContext(’2d’);
var dbScale = 60;
var width = 512;
var height = 200;
var pixelsPerDb = (0.5 * height) / dbScale;
var nrOctaves = 10;
var nyquist = 0.5 * audioCtx.sampleRate;
function dbToY(db) {
var y = (0.5 * height) - pixelsPerDb * db;
return y;
}
function drawAxes() {
ctxt.textAlign = "center";
// Draw frequency scale (x-axis).
for (var octave = 0; octave <= nrOctaves; octave++) {
var x = octave * width / nrOctaves;
var f = nyquist * Math.pow(2.0, octave - nrOctaves);
var value = f.toFixed(0);
var unit = ’Hz’;
if (f > 1000) {
unit = ’KHz’;
value = (f/1000).toFixed(1);
}
ctxt.strokeStyle = "black";
ctxt.strokeText(value + unit, x, 20);
ctxt.beginPath();
ctxt.strokeStyle = "gray";
ctxt.lineWidth = 1;
ctxt.moveTo(x, 30);
ctxt.lineTo(x, height);
ctxt.stroke();
}
// Draw decibel scale (y-axis).
for (var db = -dbScale; db < dbScale - 10; db += 10) {
var y = dbToY(db);
ctxt.strokeStyle = "black";
ctxt.strokeText(db.toFixed(0) + "dB", width + 40, y);
ctxt.beginPath();
ctxt.strokeStyle = "gray";
ctxt.moveTo(0, y);
ctxt.lineTo(width, y);
ctxt.stroke();
}
// save this drawing to the scratch canvas.
sctxt.drawImage(canvas, 0, 0);
}
</script>
我们为此使用了两个画布,因此我们有一个画布来存储准备好的网格和轴。我们将频率轴(x 轴)绘制为从音频环境的奈奎斯特频率向下 10 个八度音阶。我们绘制了从-60 dB 到 40 dB 的增益轴(y 轴)。图 6-9 显示了结果。
图 6-9 。频率增益图
现在,我们需要做的就是将滤波器的频率响应绘制到这张图中。清单 6-12 显示了用于此的函数。
清单 6-12 。应用于音频源的不同双二阶滤波器类型
function drawGraph() {
// grab the axis and grid from scratch canvas.
ctxt.clearRect(0, 0, 600, height);
ctxt.drawImage(scratch, 0, 0);
// grab the frequency response data.
var frequencyHz = new Float32Array(width);
var magResponse = new Float32Array(width);
var phaseResponse = new Float32Array(width);
for (var i = 0; i < width; ++i) {
var f = i / width;
// Convert to log frequency scale (octaves).
f = nyquist * Math.pow(2.0, nrOctaves * (f - 1.0));
frequencyHz[i] = f;
}
bass.getFrequencyResponse(frequencyHz, magResponse, phaseResponse);
// draw the frequency response.
ctxt.beginPath();
ctxt.strokeStyle = "red";
ctxt.lineWidth = 3;
for (var i = 0; i < width; ++i) {
var response = magResponse[i];
var dbResponse = 20.0 * Math.log(response) / Math.LN10;
var x = i;
var y = dbToY(dbResponse);
if ( i == 0 ) {
ctxt.moveTo(x, y);
} else {
ctxt.lineTo(x, y);
}
}
ctxt.stroke();
}
首先,我们从草稿画布中抓取前面的图形,并将其添加到一个空的画布中。然后,我们准备想要检索响应的频率数组,并对其调用getFrequencyResponse()
方法。最后,我们通过绘制从数值到数值的直线来绘制频率响应曲线。对于完整的例子,组合清单 6-10 、清单 6-11 清单和清单 6-12 清单,并调用 play 事件处理程序中的drawGraph()
函数(参见http://html5videoguide.net
)。
图 6-10 显示了清单 6-10 中的低通滤波器的结果。
图 6-10 。清单 6-10 的低通滤波器频率响应
波形节点接口
WaveShaperNode
代表非线性失真效果,使用AudioContext
的createWaveShaper()
方法创建。失真效果通过压缩或削波声波的波峰来产生“温暖”和“肮脏”的声音,从而产生大量添加的泛音。此AudioNode
使用曲线将波形失真应用于信号。
interface WaveShaperNode : AudioNode {
attribute Float32Array? curve;
attribute OverSampleType oversample;
};
curve
数组包含整形曲线的采样值。
oversample
参数指定在应用整形曲线时,应该对输入信号应用何种类型的过采样。
enum OverSampleType {
"none",
"2x",
"4x"
};
默认值为“无”,表示曲线直接应用于输入样本。值“2x”或“4x”可以通过避免一些锯齿来提高处理质量,其中“4x”产生最高质量。
| 输入数量 | one |
| 产出数量 | one |
| 通道计数模式 | "麦克斯" |
| 频道计数 | 输出中的数量与输入中的数量相同 |
| 渠道解释 | "扬声器" |
在这里,造型曲线是需要理解的重要概念。它由 x 轴区间[-1]中的曲线提取组成;1]的值仅在-1 和 1 之间。值为 0 时,曲线的值为 0。默认情况下,curve
数组为空,这意味着WaveShaperNode
不会对输入声音信号进行任何修改。
创造一个好的塑形曲线是一种艺术形式,需要对数学有很好的理解。这里有一个关于波形如何工作的很好的解释:http://music.columbia.edu/cmc/musicandcomputers/chapter4/04_06.php
。
我们将使用 y = 0.5x 3 作为我们的波形整形器。图 6-11 显示了它的形状。
图 6-11 。波形整形器示例
清单 6-13 展示了如何将这个函数应用到一个过滤图中。
清单 6-13 。将波形整形器应用于输入信号
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var mediaElement = document.getElementsByTagName(’audio’)[0];
var source = audioCtx.createMediaElementSource(mediaElement);
function makeDistortionCurve() {
var n_samples = audioCtx.sampleRate;
var curve = new Float32Array(n_samples);
var x;
for (var i=0; i < n_samples; ++i ) {
x = i * 2 / n_samples - 1;
curve[i] = 0.5 * Math.pow(x, 3);
}
return curve;
};
var distortion = audioCtx.createWaveShaper();
distortion.curve = makeDistortionCurve();
distortion.oversample = ’4x’;
mediaElement.addEventListener(’play’, function() {
// create filter graph
source.connect(distortion);
distortion.connect(audioCtx.destination);
});
在makeDistortionCurve()
函数中,我们通过在AudioContext
的samplingRate
处采样 0.5x 3 函数来创建波形曲线。然后,我们用整形曲线和 4 倍过采样创建波形整形器,并将滤波器图形放在音频输入文件中。回放时,您会注意到声音变得安静了很多,这是因为这个特定的波形整形器只有-0.5 到 0.5 之间的值。
立体面板节点接口
StereoPannerNode
表示一个简单的立体声声相器节点,可以用来向左或向右移动音频流,它是用AudioContext
的createStereoPanner()
方法创建的。
interface StereoPannerNode : AudioNode {
readonly attribute AudioParam pan;
};
它使给定的声相位置在传播到输出之前应用于输入数据。
| 输入数量 | one |
| 产出数量 | one |
| 通道计数模式 | "最大箝位" |
| 频道计数 | Two |
| 渠道解释 | "扬声器" |
该节点始终处理两个通道,channelCountMode
始终为“箝位最大值”来自具有更少或更多通道的节点的连接将被适当地向上混合或向下混合。
pan
参数描述输入在输出立体图像中的新位置。
它的默认值是 0,其标称范围是从-1 到 1。这个参数是一个 a-rate。
声相可以随时间改变,从而产生移动声源的效果(例如,从左到右)。这是通过修改左右声道的增益来实现的。
清单 6-14 显示了一个通过滑块操纵音频信号的声相位置的例子。确保释放滑块,这样当您自己尝试时,滑块的值实际上会改变。过滤图由一个MediaElementSourceNode
、一个StereoPannerNode
和一个AudioDestinationNode
组成。
清单 6-14 。操纵音频信号的声相位置
<audio autoplay controls src="audio/Shivervein_Razorpalm.wav"></audio>
<input type="range" min="-1" max="1" step="0.05" value="0"/>
<script>
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var mediaElement = document.getElementsByTagName(’audio’)[0];
var source = audioCtx.createMediaElementSource(mediaElement);
var panNode = audioCtx.createStereoPanner();
source.connect(panNode);
panNode.connect(audioCtx.destination);
var slider = document.getElementsByTagName(’input’)[0];
slider.addEventListener(’change’, function() {
panNode.pan.value = slider.value;
});
</script>
回放时,您可能想要使用耳机来更好地感受滑块移动如何影响信号的立体声位置。
卷积器节点接口
ConvolverNode
代表一个处理节点,对AudioBuffer
进行线性卷积,用AudioContext
的createConvolver()
方法创建。
interface ConvolverNode : AudioNode {
attribute AudioBuffer? buffer;
attribute boolean normalize;
};
我们可以想象一个线性卷积器代表一个房间的声学特性,而ConvolverNode
的输出代表该房间中输入信号的混响。声学特性存储在一种叫做脉冲响应的东西中。
AudioBuffer buffer
属性包含单声道、立体声或四声道脉冲响应,由ConvolverNode
用来创建混响效果。它是作为音频文件本身提供的。
normalize
属性决定来自buffer
的脉冲响应是否将通过等功率归一化进行缩放。默认是true
。
| 输入数量 | one |
| 产出数量 | one |
| 通道计数模式 | "最大箝位" |
| 频道计数 | Two |
| 渠道解释 | "扬声器" |
ConvolverNode
可以接受单声道音频输入,并应用双声道或四声道脉冲响应,以产生立体声音频输出信号。来自具有更少或更多通道的节点的连接将被适当地向上混合或向下混合,但是最多允许两个通道。
清单 6-15 展示了一个应用于音频文件的三种不同脉冲响应的例子。过滤图由一个MediaElementSourceNode
、一个ConvolverNode
和一个AudioDestinationNode
组成。
清单 6-15 。对音频信号应用三种不同的卷积
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var mediaElement = document.getElementsByTagName(’audio’)[0];
var source = audioCtx.createMediaElementSource(mediaElement);
var convolver = audioCtx.createConvolver();
// Pre-Load the impulse responses
var impulseFiles = [
"audio/filter-telephone.wav",
"audio/kitchen.wav",
"audio/cardiod-rear-levelled.wav"
];
var impulseResponses = new Array();
var allLoaded = 0;
function loadFile(url, index) {
var request = new XMLHttpRequest();
function requestData(url) {
request.open(’GET’, url, true);
request.responseType = ’arraybuffer’;
request.send();
}
function receivedData() {
if ((request.status === 200 || request.status === 206)
&& request.readyState === 4) {
var audioData = request.response;
audioCtx.decodeAudioData(audioData,
function(buffer) {
impulseResponses[index] = buffer;
if (++allLoaded == impulseFiles.length) {
createFilterGraph();
}
},
function(error) {
"Error with decoding audio data" + error.err
}
);
}
}
request.addEventListener(’load’, receivedData, false);
requestData(url);
}
for (i = 0; i < impulseFiles.length; i++) {
loadFile(impulseFiles[i], i);
}
// create filter graph
function createFilterGraph() {
source.connect(convolver);
convolver.buffer = impulseResponses[0];
convolver.connect(audioCtx.destination);
}
var radioButtons = document.getElementsByTagName(’input’);
for (i = 0; i < radioButtons.length; i++){
radioButtons[i].addEventListener(’click’, function() {
convolver.buffer = impulseResponses[this.value];
});
}
你会注意到我们正在通过XMLHttpRequest
加载三个脉冲响应,如清单 6-5 中的。然后,我们将它们存储在一个数组中,当用户在输入单选按钮之间切换时,我们可以在它们之间进行切换。我们只能在所有脉冲响应都已加载(即 allLoaded = 2)后将滤波器图放在一起。
它的 HTML 将有三个输入元素作为单选按钮,用于在不同的脉冲响应之间切换。当您玩这个例子时,您会注意到“电话”、“厨房”和“仓库”脉冲响应之间的混响差异。
ChannelSplitterNode 和 ChannelMergeNode 接口
ChannelSplitterNode
和ChannelMergerNode
表示用于在滤波器图中将音频流的各个声道分开和合并在一起的AudioNode
。
ChannelSplitterNode
是用AudioContext
的createChannelSplitter()
方法创建的,该方法带一个可选的numberOfOutputs
参数,该参数表示扇出AudioNode
s 的大小,默认为 6。哪个输出实际上具有音频数据取决于ChannelSplitterNode
的输入音频信号中可用的通道数量。例如,将一个立体声信号扇出到六个输出,只会产生两个带信号的输出,其余的都是无声的。
ChannelMergerNode
与ChannelSplitterNode
相反,使用AudioContext
的createChannelMerger()
方法创建,该方法采用一个可选的numberOfInputs
参数,表示扇入AudioNode
的大小。默认情况下为 6,但并非所有扇入都需要连接,也并非所有扇入都需要包含音频信号。例如,扇入六个输入,其中只有前两个具有立体声音频信号,每个输入创建六声道流,其中第一和第二输入被下混合为单声道,其余声道为静音。
interface ChannelSplitterNode : AudioNode {};
interface ChannelMergerNode : AudioNode {};
| | ChannelSplitterNode
| ChannelMergeNode
|
| 输入数量 | one | n(默认值:6) |
| 产出数量 | n(默认值:6) | one |
| 通道计数模式 | "麦克斯" | "麦克斯" |
| 频道计数 | 扇出至多个单声道输出 | 扇入多个缩混单声道输入 |
| 渠道解释 | "扬声器" | "扬声器" |
对于ChannelMergerNode
,channelCount
和channelCountMode
属性不可更改——所有输入都被视为单声道信号。
ChannelSplitterNode
和ChannelMergerNode
的一个应用是进行“矩阵混合”,其中每个通道的增益被单独控制。
清单 6-16 显示了我们示例音频文件的矩阵混合示例。您可能希望使用耳机来更好地听到左右声道的单独音量控制。
清单 6-16 。对音频文件的左右声道应用不同的增益
<audio autoplay controls src="audio/Shivervein_Razorpalm.wav"></audio>
<p>Left Channel Gain:
<input type="range" min="0" max="1" step="0.1" value="1"/>
</p>
<p>Right Channel Gain:
<input type="range" min="0" max="1" step="0.1" value="1"/>
</p>
<script>
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var mediaElement = document.getElementsByTagName(’audio’)[0];
var source = audioCtx.createMediaElementSource(mediaElement);
var splitter = audioCtx.createChannelSplitter(2);
var merger = audioCtx.createChannelMerger(2);
var gainLeft = audioCtx.createGain();
var gainRight = audioCtx.createGain();
// filter graph
source.connect(splitter);
splitter.connect(gainLeft, 0);
splitter.connect(gainRight, 0);
gainLeft.connect(merger, 0, 0);
gainRight.connect(merger, 0, 1);
merger.connect(audioCtx.destination);
var sliderLeft = document.getElementsByTagName(’input’)[0];
sliderLeft.addEventListener(’change’, function() {
gainLeft.gain.value = sliderLeft.value;
});
var sliderRight = document.getElementsByTagName(’input’)[1];
sliderRight.addEventListener(’change’, function() {
gainRight.gain.value = sliderRight.value;
});
</script>
这个例子很简单,两个输入滑块分别控制两个增益节点的音量,每个通道一个。需要理解的一件事是在AudioNode
上使用AudioNode
的connect()
方法的第二个和第三个参数,它们允许连接ChannelSplitterNode
或ChannelMergerNode
的独立通道。
图 6-12 显示了该示例的过滤图。
图 6-12 。左右声道音量控制的滤波图
DynamicCompressorNode 接口
DynamicCompressorNode
提供压缩效果,降低信号中最响亮部分的音量,以防止多个声音一起播放和多路复用时可能发生的削波和失真。总体来说,可以实现更响亮、更丰富、更饱满的声音。它是用AudioContext
的createDynamicCompressor()
方法创建的。
interface DynamicsCompressorNode : AudioNode {
readonly attribute AudioParam threshold;
readonly attribute AudioParam knee;
readonly attribute AudioParam ratio;
readonly attribute float reduction;
readonly attribute AudioParam attack;
readonly attribute AudioParam release;
};
| 输入数量 | one |
| 产出数量 | one |
| 通道计数模式 | “明确” |
| 频道计数 | Two |
| 渠道解释 | "扬声器" |
threshold
参数提供分贝值,超过该值压缩将开始生效。其默认值为-24,标称范围为-100 到 0。
knee
参数提供一个分贝值,代表曲线平滑过渡到压缩部分的阈值以上的范围。其默认值为 30,标称范围为 0 到 40。
ratio
参数代表输出变化 1 dB 时输入所需的 dB 变化量。其默认值为 12,标称范围为 1 到 20。
reduction
参数代表压缩器当前应用于信号的增益降低量,单位为 dB。如果没有输入信号,该值将为 0(无增益降低)。
attack
参数代表将增益降低 10 dB 的时间量(秒)。其默认值为 0.003,标称范围为 0 到 1。
release
参数表示增益增加 10 dB 所需的时间(秒)。其默认值为 0.250,标称范围为 0 到 1。
所有参数都是 k-rate。
清单 6-17 显示了一个动态压缩的例子。
清单 6-17 。音频信号的动态压缩
<audio autoplay controls src="audio/Shivervein_Razorpalm.wav"></audio>
<p>Toggle Compression: <button value="0">Off</button></p>
<script>
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var mediaElement = document.getElementsByTagName(’audio’)[0];
var source = audioCtx.createMediaElementSource(mediaElement);
// Create a compressor node
var compressor = audioCtx.createDynamicsCompressor();
compressor.threshold.value = -50;
compressor.knee.value = 20;
compressor.ratio.value = 12;
compressor.reduction.value = -40;
mediaElement.addEventListener(’play’, function() {
source.connect(audioCtx.destination);
});
var button = document.getElementsByTagName(’button’)[0];
button.addEventListener(’click’, function() {
if (this.value == 1) {
this.value = 0;
this.innerHTML = "Off";
source.disconnect(audioCtx.destination);
source.connect(compressor);
compressor.connect(audioCtx.destination);
} else {
this.value = 1;
this.innerHTML = "On";
source.disconnect(compressor);
compressor.disconnect(audioCtx.destination);
source.connect(audioCtx.destination);
}
});
</script>
在这个例子中,通过点击一个按钮,可以在过滤器图形中包括和排除压缩机。
图 6-13 描绘了所使用的压缩机。您可以看到,在-50 dB 以下,没有应用压缩。在接下来的 20 dB 内,平滑过渡到压缩曲线。该比率决定了超过阈值时应用的压缩量,我们选择输入变化 12 dB,输出变化 1 dB。1 dB 的变化不会导致任何变化,比率值越大,压缩图变平的速度越快。
图 6-13 。示例中使用的动态压缩图
总体效果是音频信号的音量降低,但只是在先前的高音量部分,而不是在较安静的部分。
我们对AudioNode
接口的了解到此结束,这些接口是操纵音频信号不同方面的标准功能,包括增益、动态、延迟、波形、通道、立体声位置和频率滤波器。
3D 空间化和平移
在本节中,我们将了解音频信号的三维定位,这在游戏中特别有用,因为游戏中需要根据听众的位置将多个信号以不同方式混合在一起。Web Audio API 带有内置的硬件加速定位音频功能。
我们处理两个构造来操纵 3D 音频信号:听众的位置和PannerNode
,它是一个过滤器节点,用来操纵声音相对于听众的位置。听者的位置由AudioContext
中的AudioListener
属性描述,而PannerNode
是通过也是AudioContext
一部分的函数创建的。
[Constructor] interface AudioContext : EventTarget {
...
readonly attribute AudioListener listener;
PannerNode createPanner ();
...
}
AudioListener 接口
该界面表示收听音频场景的人的位置和方向。
interface AudioListener {
void setPosition (float x, float y, float z);
void setOrientation (float xFront, float yFront, float zFront,
float xUp, float yUp, float zUp);
void setVelocity (float x, float y, float z);
};
AudioContext
假设听者所处的三维右手笛卡尔坐标空间(见图 6-14 )。默认情况下,侦听器站在(0,0,0)处。
图 6-14 。听者所处的右手笛卡尔坐标空间
setPosition()
方法允许我们改变位置。虽然坐标是无单位的,但通常会指定相对于特定空间尺寸的位置,并使用百分比值来指定位置。
setOrientation()
方法允许我们在 3D 笛卡尔坐标空间中改变听者耳朵指向的方向。提供了一个Front
位置和一个Up
位置。用简单的人类术语来说,Front
位置代表人的鼻子指向哪个方向,默认为(0,0,-1),表示 z 方向与耳朵指向的位置相关。Up
位置代表人的头顶所指的方向,默认为(0,1,0),表示 y 方向与人的身高有关。图 6-14 也显示了Front
和Up
位置。
setVelocity()
方法允许我们改变听者的速度,这控制了 3D 空间中行进的方向和速度。该速度相对于音频源的速度可用于确定要应用多少多普勒频移(音高变化)。默认值为(0,0,0),表示侦听器是静止的。
用于该矢量的单位是米/秒 ,并且独立于用于位置和方向矢量的单位。例如,值(0,0,17)表示收听者以 17 米/秒的速度在 z 轴方向上移动。
清单 6-18 显示了一个改变听众位置和方向的例子。默认情况下,音频声音也位于(0,0,0)。
清单 6-18 。改变听者的位置和方向
<p>Position:
<input type="range" min="-1" max="1" step="0.1" value="0" name="pos0"/>
<input type="range" min="-1" max="1" step="0.1" value="0" name="pos1"/>
<input type="range" min="-1" max="1" step="0.1" value="0" name="pos2"/>
</p>
<p>Orientation:
<input type="range" min="-1" max="1" step="0.1" value="0" name="dir0"/>
<input type="range" min="-1" max="1" step="0.1" value="0" name="dir1"/>
<input type="range" min="-1" max="1" step="0.1" value="-1" name="dir2"/>
</p>
<p>Elevation:
<input type="range" min="-1" max="1" step="0.1" value="0" name="hei0"/>
<input type="range" min="-1" max="1" step="0.1" value="1" name="hei1"/>
<input type="range" min="-1" max="1" step="0.1" value="0" name="hei2"/>
</p>
<script>
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var source = audioCtx.createBufferSource();
var request = new XMLHttpRequest();
var url = ’audio/ticking.wav’;
request.addEventListener(’load’, receivedData, false);
requestData(url);
var inputs = document.getElementsByTagName(’input’);
var pos = [0, 0, 0]; // position
var ori = [0, 0, -1]; // orientation
var ele = [0, 1, 0]; // elevation
for (i=0; i < inputs.length; i++) {
var elem = inputs[i];
elem.addEventListener(’change’, function() {
var type = this.name.substr(0,3);
var index = this.name.slice(3);
var value = parseFloat(this.value);
switch (type) {
case ’pos’:
pos[index] = value;
audioCtx.listener.setPosition(pos[0], pos[1], pos[2]);
break;
case ’ori’:
ori[index] = value;
audioCtx.listener.setOrientation(ori[0], ori[1], ori[2],
ele[0], ele[1], ele[2]);
break;
case ’ele’:
ele[index] = value;
audioCtx.listener.setOrientation(ori[0], ori[1], ori[2],
ele[0], ele[1], ele[2]);
break;
default:
console.log(’no match’);
}
});
}
</script>
在这个例子中,我们使用清单 6-5 中介绍的函数将一个循环声音加载到一个AudioBuffer
中,并且我们操纵三个参数的三维。
注意 感谢 Izkhanilov 在 freesound 的知识共享许可下提供了“滴答响的时钟. wav”样本(见www.freesound.org/people/Izkhanilov/sounds/54848/
)。
有趣的是,当您复制我们的示例时,您会注意到参数变化对声音回放没有影响。这是因为没有明确指定声源的位置。因此,我们认为AudioContext
假设听者和声源位于同一位置。它需要一个PannerNode
来指定声源的位置。
PannerNode 接口
这个接口代表一个处理节点,它在 3D 空间中相对于听众定位/空间化输入的音频流。它是用AudioContext
的createPanner()
方法创建的。
interface PannerNode : AudioNode {
void setPosition (float x, float y, float z);
void setOrientation (float x, float y, float z);
attribute PanningModelType panningModel;
attribute DistanceModelType distanceModel;
attribute float refDistance;
attribute float maxDistance;
attribute float rolloffFactor;
attribute float coneInnerAngle;
attribute float coneOuterAngle;
attribute float coneOuterGain;
};
一种思考平移者和听者的方式是考虑一个游戏环境,其中对手正在 3D 空间中奔跑,声音来自场景中的各种来源。这些源中的每一个都有一个与之相关联的PannerNode
。
物体有一个方向向量,代表声音发出的方向。此外,它们有一个音锥来代表声音的方向性。例如,声音可以是全向的,在这种情况下,无论它的方向如何,它都可以在任何地方听到,或者它可以是更具方向性的,只有当它面对听众时才能听到。在渲染过程中,PannerNode
计算方位角(听众对声源的角度)和仰角(听众上方或下方的高度)。浏览器使用这些值来呈现空间化效果。
| 输入数量 | one |
| 产出数量 | 1(立体声) |
| 通道计数模式 | "最大箝位" |
| 频道计数 | 2(固定) |
| 渠道解释 | "扬声器" |
PannerNode
的输入可以是单声道(一个声道)或立体声(两个声道)。来自具有更少或更多通道的节点的连接将被适当地向上混合或向下混合。此节点的输出被硬编码为立体声(两个声道),当前无法配置。
setPosition()
方法设置音频源相对于听众的位置。默认值为(0,0,0)。
setOrientation()
方法描述了音频源在 3D 笛卡尔坐标空间中指向的方向。根据声音的方向性(由圆锥体属性控制),远离听众的声音可以非常安静或完全无声。默认值为(1,0,0)。
panningModel
属性指定这个PannerNode
使用哪个平移模型。
enum PanningModelType {
"equalpower",
"HRTF"
};
平移模型描述如何计算声音空间化。“等功率”模式使用等功率平移,忽略仰角。HRTF (头部相关传递函数)模型使用与来自人的测量脉冲响应的卷积,从而模拟人的空间化感知。panningModel
默认为 HRTF。
distanceModel
属性决定了当音频源远离听众时,将使用哪种算法来降低其音量。
enum DistanceModelType {
"linear",
"inverse",
"exponential"
};
“线性”模型假设随着声源远离听众,增益线性降低。“逆”模型假设增益降低越来越小。“指数”模型假设增益降低越来越大。distanceModel
默认为“反相”
refDistance
属性包含一个参考距离,用于随着源远离收听者而减小音量。默认值为 1。
maxDistance
属性包含源和收听者之间的最大距离,超过该距离后,音量将不再降低。默认值为 10,000。
rolloffFactor
属性描述了当源远离收听者时音量降低的速度。默认值为 1。
coneInnerAngle
、coneOuterAngle
和coneOuterGain
一起描述了一个内部体积减少比外部少得多的圆锥体。有一个内锥体和一个外锥体,它们将声音强度描述为来自声源方向向量的声源/听者角度的函数。因此,直接指向听众的声源会比离轴指向的声源更响。
图 6-15 直观地描述了音盆概念。
图 6-15 。相对于收听者的全景声节点的源锥体的可视化
coneInnerAngle
提供了一个角度,以度为单位,在该角度范围内,体积不会减少。默认值是 360,使用的值是模 360。
coneOuterAngle
提供了一个角度,以度为单位,超出该角度,音量将减小到恒定值coneOuterGain
。默认值为 360,该值以 360 为模。
coneOuterGain
提供了coneOuterAngle
之外的音量减小量。默认值为 0,该值以 360 为模。
让我们扩展清单 6-18 中的例子,并引入一个PannerNode
。这具有将声源定位在不同于听众位置的固定位置的效果。我们还包括一个音盆,这样我们可以更容易地听到增益降低的影响。参见清单 6-19 中的变化。
清单 6-19 。引入声源的位置和声锥
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var source = audioCtx.createBufferSource();
var panner = audioCtx.createPanner();
panner.coneOuterGain = 0.5;
panner.coneOuterAngle = 180;
panner.coneInnerAngle = 90;
source.connect(panner);
panner.connect(audioCtx.destination);
现在,当我们改变位置、方向和仰角时,我们相对于声源移动听者,声源保持在声音场景的中间。例如,当我们将位置的 x 值向右移动时,声音向我们的左侧移动,我们将声音向右侧避开。一旦声音在我们的左边,当我们在-0.1 和+0.1 之间移动方向的 z 值时,声音在左右之间移动-在 0 时,我们面对声音,在+0.1 时,我们将我们的右侧转向它,在-0.1 时,我们将我们的左侧转向它。
请注意,我们实际上并没有移动声源的位置,而只是移动了听者的位置。你可以使用PannerNode
的setPosition()
和setOrientation()
来实现,你可以用多种声音来实现。我们将把这作为一个练习留给读者。
注意 Web Audio API 规范用于为PannerNode
提供一个setVelocity()
方法,该方法将计算移动声源和听众的多普勒频移。这已被否决,并将在版本 45 后从 Chrome 中删除。有计划引入一个新的SpatializerNode
界面来取代它。现在,你需要自己计算多普勒频移,可能使用DelayNode
接口或者改变playbackRate
。
音频数据的 JavaScript 操作
Web Audio API 规范目前的实现状态包括一个名为ScriptProcessorNode
的接口,可以直接使用 JavaScript 生成、处理或分析音频。这种节点类型已被弃用,目的是用AudioWorker
接口替换它,但我们仍将解释它,因为它在当前的浏览器中实现,而AudioWorker
接口没有。
ScriptProcessorNode
和AudioWorker
界面的区别在于,第一个界面运行在主浏览器线程上,因此必须与布局、渲染和浏览器中进行的大多数其他处理共享处理时间。Web Audio API 的所有其他音频节点都在单独的线程上运行,这使得音频更有可能不受其他大任务的干扰。这将随着AudioWorker
而改变,它也将在音频线程上运行 JavaScript 音频处理。它将能够以更少的延迟运行,因为它避免了线程边界的改变和必须与主线程共享资源。
这听起来很棒,但是现在我们不能使用AudioWorker
,因此将首先查看ScriptProcessorNode
。
ScriptProcessorNode 接口
该接口允许编写您自己的 JavaScript 代码来生成、处理或分析音频,并将其集成到滤波器图中。
通过AudioContext
上的createScriptProcessor()
方法创建一个ScriptProcessorNode
:
[Constructor] interface AudioContext : EventTarget {
...
ScriptProcessorNode createScriptProcessor(
optional unsigned long bufferSize = 0 ,
optional unsigned long numberOfInputChannels = 2 ,
optional unsigned long numberOfOutputChannels = 2 );
...
}
它只接受可选参数,建议将这些参数的设置留给浏览器。然而,以下是他们的解释:
a bufferSize
以 256,512,1,024,2,048,4,096,8,192,16,384 个样本帧为单位。这控制了发送音频处理事件的频率以及每个调用中需要处理的样本帧的数量。
numberOfInputChannels
默认为 2,但最多可达 32。
numberOfOutputChannels
默认为 2,但最多可达 32。
ScriptProcessorNode
的界面定义如下:
interface ScriptProcessorNode : AudioNode {
attribute EventHandler onaudioprocess;
readonly attribute long bufferSize;
};
bufferSize
属性反映了创建节点时的缓冲区大小,而onaudioprocess
将一个 JavaScript 事件处理程序与节点关联起来,当节点被激活时就会调用这个处理程序。处理程序接收的事件是一个AudioProcessingEvent
。
interface AudioProcessingEvent : Event {
readonly attribute double playbackTime;
readonly attribute AudioBuffer inputBuffer;
readonly attribute AudioBuffer outputBuffer;
};
它包含以下只读数据:
一个playbackTime
,这是音频将在与AudioContext
的currentTime
相同的时间坐标系中播放的时间。
一个inputBuffer
,包含输入音频数据,其声道数等于createScriptProcessor()
方法的numberOfInputChannels
参数。
一个outputBuffer
,用于保存事件处理程序的输出音频数据。它的通道数必须等于createScriptProcessor()
方法的numberOfOutputChannels
参数。
ScriptProcessorNode
不改变其通道或输入数量。
| 输入数量 | one |
| 产出数量 | one |
| 通道计数模式 | “明确” |
| 频道计数 | 输入通道的数量 |
| 渠道解释 | "扬声器" |
使用ScriptProcessorNode
的一个简单例子是给音频样本添加一些随机噪声。清单 6-20 显示了一个这样的例子。
清单 6-20 。在 ScriptProcessorNode 中向音频文件添加随机噪声
<audio autoplay controls src="audio/ticking.wav"></audio>
<script>
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var mediaElement = document.getElementsByTagName(’audio’)[0];
var source = audioCtx.createMediaElementSource(mediaElement);
var noiser = audioCtx.createScriptProcessor();
source.connect(noiser);
noiser.connect(audioCtx.destination);
noiser.onaudioprocess = function(event) {
var inputBuffer = event.inputBuffer;
var outputBuffer = event.outputBuffer;
for (var channel=0; channel < inputBuffer.numberOfChannels; channel++) {
var inputData = inputBuffer.getChannelData(channel);
var outputData = outputBuffer.getChannelData(channel);
for (var sample = 0; sample < inputBuffer.length; sample++) {
outputData[sample] = inputData[sample] + (Math.random() * 0.01);
}
}
};
</script>
当我们将输入数据复制到输出数据数组时,我们将 0 到 1 之间的一个随机数的 10%添加到输入数据中,从而创建一个带有一些白噪声的输出样本。
注意 你可能想看看规范中新的AudioWorker
接口以及它是如何取代ScriptProcessorNode
的。我们不能在这里描述它,因为在我写这篇文章的时候,它每天都在变化。原则是创建一个 JavaScript 文件,其中包含一个AudioWorkerNode
应该执行的脚本,然后在现有的AudioContext
上调用一个createAudioWorker()
函数,将这个脚本交给一个Worker
,后者在一个单独的线程中执行它。在AudioWorkerNode
和AudioContext
之间会引发事件来处理每个线程中的状态变化,并且能够向AudioWo
rkerNode
提供AudioParams
。
离线音频处理
OfflineAudioContext
接口是一种AudioContext
接口,它不将滤波器图形的音频输出呈现给设备硬件,而是呈现给一个AudioBuffer
。这使得处理音频数据的速度可能比实时更快,如果您只想分析音频流的内容(例如,当检测节拍时),这非常有用。
[Constructor(unsigned long numberOfChannels,
unsigned long length,
float sampleRate)]
interface OfflineAudioContext : AudioContext {
attribute EventHandler oncomplete;
Promise<AudioBuffer> startRendering ();
};
构建一个OfflineAudioContext
的工作方式类似于用AudioContext
的createBuffer()
方法创建一个新的AudioBuffer
,并采用相同的三个参数。
numberOfChannels
属性包含AudioBuffer
应该拥有的离散通道的数量。
length
属性包含样本帧中音频资源的长度。
sampleRate
属性包含音频资产的采样率。
OfflineAudioContext
提供了一个oncomplete
事件处理程序,在处理完成时调用。
它还提供了一个startRendering()
方法。当OfflineAudioContext
被创建时,它处于“暂停”状态。对该函数的调用启动了过滤器图形的处理。
使用OfflineAudioContext
的一个简单例子是从一个音频文件中抓取音频数据到一个OfflineAudioContext
中,而不打扰可能正在做其他工作的将军AudioContext
。清单 6-21 展示了如何通过调整清单 6-5 来实现这一点。
清单 6-21 。在 ScriptProcessorNode 中向音频文件添加随机噪声
// AudioContext that decodes data
var offline = new window.OfflineAudioContext(2,44100*20,44100);
var source = offline.createBufferSource();
var offlineReady = false;
// AudioContext that renders data
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var sound;
var audioBuffer;
var request = new XMLHttpRequest();
var url = ’audio/transition.wav’;
function receivedData() {
if ((request.status === 200 || request.status === 206)
&& request.readyState === 4) {
var audioData = request.response;
offline.decodeAudioData(audioData,
function(buffer) {
source.buffer = buffer;
source.connect(offline.destination);
source.start(0);
offlineReady = true;
},
function(error) {
"Error with decoding audio data" + error.err
}
);
}
}
request.addEventListener(’load’, receivedData, false);
requestData(url);
function startPlayback() {
sound = audioCtx.createBufferSource();
sound.buffer = audioBuffer;
sound.connect(audioCtx.destination);
sound.start(0);
}
var stop = document.getElementsByTagName(’button’)[0];
stop.addEventListener(’click’, function() {
sound.stop();
});
var start = document.getElementsByTagName(’button’)[1];
start.addEventListener(’click’, function() {
if (!offlineReady) return;
offline.startRendering().then(function(renderedBuffer) {
audioBuffer = renderedBuffer;
startPlayback();
}).catch(function(err) {
// audioData has already been rendered
startPlayback();
});
});
我们在清单 6-5 的例子中添加了第二个按钮,现在手动启动音频文件。下载音频文件后,离线上下文正在对其进行解码并开始呈现(当我们单击 start 按钮时)。在渲染例程中,我们保存解码后的AudioBuffer
数据,以便在稍后阶段重新加载。就是这个AudioBuffe
r
数据,我们交给直播AudioContext
回放。
音频数据可视化
我们需要了解的最后一个接口是AnalyserNode
接口。该接口表示能够提供实时频率和时域样本信息的节点。这些节点不会对直接通过的音频流进行任何更改。因此,它们可以放在过滤器图中的任何位置。该界面的主要用途是可视化音频数据。
通过AudioContext
上的createAnalyser()
方法创建一个AnalyserNode
:
[Constructor] interface AudioContext : EventTarget {
...
AnalyserNode createAnalyser ();
...
}
AnalyserNode
的界面定义如下:
interface AnalyserNode : AudioNode {
attribute unsigned long fftSize;
readonly attribute unsigned long frequencyBinCount;
attribute float minDecibels;
attribute float maxDecibels;
attribute float smoothingTimeConstant;
void getFloatFrequencyData (Float32Array array);
void getByteFrequencyData (Uint8Array array);
void getFloatTimeDomainData (Float32Array array);
void getByteTimeDomainData (Uint8Array array);
};
这些属性包含以下信息:
fftSize
:用于分析的缓冲区大小。它必须是 32 到 32,768 范围内的 2 的幂,默认值为 2,048。
frequencyBinCount
:FFT(快速傅立叶变换)大小一半的固定值。
minDecibels
、maxDecibels
:将 FFT 分析数据换算成无符号字节值的功率值范围。默认范围是从minDecibels
= -100 到maxDecibels
= -30。
smoothingTimeConstant
:介于 0 和 1 之间的值,表示平滑结果的滑动窗口的大小。0 表示没有时间平均,因此结果波动很大。默认值为 0.8。
这些方法将下列数据复制到提供的数组中:
getFloatFrequencyData
、getByteFrequencyData
:不同数据类型的当前频率数据。如果数组的元素比frequencyBinCount
少,多余的元素将被丢弃。如果数组的元素比frequencyBinCount
多,多余的元素将被忽略。
getFloatTimeDomainData
、getByteTimeDomainData
:当前时域(波形)数据。如果数组中的元素少于fftSize
的值,多余的元素将被丢弃。如果数组的元素多于fftSize
,超出的元素将被忽略。
AnalyserNode
不改变其通道或输入数量,输出可以不连接:
| 输入数量 | one |
| 产出数量 | one |
| 通道计数模式 | "麦克斯" |
| 频道计数 | one |
| 渠道解释 | "扬声器" |
清单 6-22 显示了一个将波形渲染到画布上的简单例子。
清单 6-22 。呈现音频上下文的波形数据
<audio autoplay controls src="audio/ticking.wav"></audio>
<canvas width="512" height="200"></canvas>
<script>
// prepare canvas for rendering
var canvas = document.getElementsByTagName("canvas")[0];
var sctxt = canvas.getContext("2d");
sctxt.fillRect(0, 0, 512, 200);
sctxt.strokeStyle = "#FFFFFF";
sctxt.lineWidth = 2;
// prepare audio data
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var mediaElement = document.getElementsByTagName(’audio’)[0];
var source = audioCtx.createMediaElementSource(mediaElement);
// prepare filter graph
var analyser = audioCtx.createAnalyser();
analyser.fftSize = 2048;
analyser.smoothingTimeConstant = 0.1;
source.connect(analyser);
analyser.connect(audioCtx.destination);
// data from the analyser node
var buffer = new Uint8Array(analyser.frequencyBinCount);
function draw() {
analyser.getByteTimeDomainData(buffer);
// do the canvas painting
var width = canvas.width;
var height = canvas.height;
var step = parseInt(buffer.length / width);
sctxt.fillRect(0, 0, width, height);
sctxt.drawImage(canvas, 0, 0, width, height);
sctxt.beginPath();
sctxt.moveTo(0, buffer[0] * height / 256);
for(var i=1; i< width; i++) {
sctxt.lineTo(i, buffer[i*step] * height / 256);
}
sctxt.stroke();
window.requestAnimationFrame(draw);
}
mediaElement.addEventListener(’play’, draw , false);
</script>
我们使用一个画布来绘制波浪,并用黑色背景和白色绘图颜色来准备它。我们为样本输入实例化了AudioContext
和音频元素,准备好analyser
并把它们都连接到一个过滤图。
一旦音频元素开始回放,我们就开始绘图,从analyser
中抓取波形字节。这些通过一个getByteTimeDomainData()
方法公开,该方法填充一个提供的Uint8Array
。我们获取这个数组,从先前的绘图中清除画布,并将这个新数组作为连接所有值的线绘制到画布中。然后在一个requestAnimationFrame()
调用中再次调用draw()
方法来抓取下一个无符号 8 位字节数组进行显示。这将连续地将波形绘制到画布上。
使用requestAnimationFrame
的另一种更传统的方法是使用超时为 0 的setTimeout()
函数。我们建议使用requestAnimationFrame
进行所有绘图,因为它是为渲染而构建的,并确保在下一个可能的屏幕重绘机会正确安排绘图。
图 6-16 显示了运行清单 6-22 的结果。
图 6-16 。在 Web 音频 API 中呈现音频波形
我们对 Web 音频 API 的探索到此结束。
摘要
在这一章中,我们了解了现有的关于音频 API 的提议,它提供了对样本的访问,无论它们是由算法创建的音频源、音频文件还是麦克风提供的。Web Audio API 还为此类音频数据提供了硬件加速的操作和可视化方法,以及将您自己的 JavaScript 操作例程与音频过滤器图挂钩的方法。