前言
最近正在开发一款虚拟钢琴,这款应用的主要功能是模拟实体的88键钢琴,用户点击按键后,会发出相应的音调。
最初,我觉得这项任务很简单,只需要在点击事件上添加播放功能就行。随着开发的深入,我发现这件事并不像我想象的那么简单,Web音频开发其实很复杂。后来我发现了一个名为Tone.js的音频处理库。这个库的官方文档过于简洁,而且要理解它的一些概念,需要有一定的音学基础。通过查阅资料,我总结了一些关于Tone.js的知识,想在此与大家分享。
才疏学浅,纰漏难免(尤其是音学的概念部分),欢迎大家指正。
Web Audio API
介绍Tone.js之前,我们需要先了解一下Web Audio API。
Web Audio API是一个基于浏览器的音频处理系统,使用Web Audio API处理音频,首先需要创建一个上下文环境(Audio Context),在这个环境中我们可以对音频进行创建、调整、输出等一系列操作。音频输入可以是从音视频文件中读取出来的数据流(比如AudioBufferSourceNode
),也可以是通过代码进行运算得到的音频数据(比如可以产生音波信号的振荡器OscillatorNode
),然后我们可以对这段音频进行各种处理(比如用于调整音量的GainNode
,音频处理完毕之后一般都需要将音频输出(比如播放到扬声器destination
)。一个简化的流程模型如下:
Tone.js
Tone.js就是一款基于Web Audio API开发的音频库。它对Web Audio API进行了一定程度的抽象,极大地简化了通过编码进行音频处理、音频合成的复杂度,但万变不离其宗,它始终绕不开我们上边提到的简化模型。
安装导入
安装方法很基础:
arduino
npm install tone
// or
yarn add tone
导入也同样简单:
javascript
import * as Tone from 'tone'
音频上下文
Tone.js在加载时已经自动创建了一个AudioContext,同时这个上下文也针对各类浏览器做了最大程度的兼容,我们无需过多关注AudioContext的手动创建。
如果你有需要,你也可以通过 Tone.getContext()
来获取当前上下文,或者通过 Tone.setContext(xxx)
来手动设置一个上下文。
音源输入
我们之前的简化模型中已经提到,音频处理的第一步离不开音频的输入,也就是音频源。针对我们开发虚拟钢琴引用来说,我们的目标是点击各个琴键,播放出相应的音调,这里我们会用到 Sampler
API。Sampler
允许我们传入一个音符对应的资源地址,然后在需要播放该音符时自动处理该音符的Attack
(起音)和Release
(释音)。
js
const sampler = new Tone.Sampler({
urls: {
A1: "A1.mp3",
C4: "C4.mp3",
},
baseUrl: "<https://tonejs.github.io/audio/casio/>",
})
如上这段代码,当我们需要播放音符 A1
对应的音调时,实际上播放的是 baseUrl
+ urls.A1
对应的音频资源,也就是 https://tonejs.github.io/audio/casio/A1.mp3
。
我们需要做的是88键的钢琴,那么在创建Sampler
时,我们需要把88个音符对应的资源全部传入urls对象中,这样不会很麻烦吗?
的确会很麻烦,但其实我们也不必定义所有音符。当我们定义两个音符时,Tone.Sampler
会自动根据这两个音符生成它们之间所空缺的其他音符对应的音调。
刚才有提到
attack
和release
的概念,大多数人可能对这个概念会感到陌生。其实这是音学中定义的声音变化中的两个阶段。控制声音的振幅或音量随时间的变化的概念叫做amplitude envelope
(振幅包络)。简单来说,包络控制的就是声音从诞生到消亡的整个阶段。这一整个阶段可以细分为四个小阶段:Attack
(起音)、Decay
(衰减)、Sustain
(延音)、Release
(释音)。在Tone.js中我们会看到很多和这几个词相关的API,此处只要大概了解就好。
音频输出
在本例中,我们不需要对声音做额外的特殊处理。让我们直接将声音输出到电脑的扬声器。代码也很简单:
js
sampler.toDestination();
搞定了音频输出的对象之后,我们还有一件很关键的事情没有做,那就是音调的触发。触发音调最简单的API就是 triggerAttackRelease
,它的函数定义如下:
ts
triggerAttackRelease (
notes:Frequency[]|Frequency,
// 需要播放的音符或频率
duration: Time|Time[],
// 音符持续的时间
time?:Time,
// 何时开始播放音符
velocity= 1:NormalRange
// 起音强度0~1
) => this
那么再结合上边的示例,我们要实现一个按键完整的代码就是:
jsx
import * as Tone from 'tone'
import {useEffect,useRef} from 'react'
function Piano(){
const SamplerRef = useRef()
const NoteA1Ref = useRef()
useEffect(()=>{
SamplerRef.current = new Tone.Sampler({
urls: {
A1: "A1.mp3",
C4: "C4.mp3",
},
baseUrl: "<https://tonejs.github.io/audio/casio/>",
}).toDestination()
},[])
function playNoteA1(){
// 确保资源已加载完毕
Tone.loaded().then(()=>{
if (Tone.context.state !== 'running') {
// 在播放音频前务必启动Tone
Tone.start();
}
SamplerRef.current.triggerAttackRelease('A1')
})
}
return <div ref='NoteA1' onClick='playNoteA1'>A1</div>
}
结语
至此,我们使用Tone.js开发web钢琴的核心部分就已经完成了。Tone.js的强大之处当然不止于此,尝试起来吧!