web虚拟钢琴中Tone.js的应用

前言

最近正在开发一款虚拟钢琴,这款应用的主要功能是模拟实体的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 会自动根据这两个音符生成它们之间所空缺的其他音符对应的音调。

刚才有提到 attackrelease 的概念,大多数人可能对这个概念会感到陌生。其实这是音学中定义的声音变化中的两个阶段。控制声音的振幅或音量随时间的变化的概念叫做 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的强大之处当然不止于此,尝试起来吧!

相关推荐
森叶4 分钟前
Electron 主进程中使用Worker来创建不同间隔的定时器实现过程
前端·javascript·electron
霸王蟹13 分钟前
React 19 中的useRef得到了进一步加强。
前端·javascript·笔记·学习·react.js·ts
霸王蟹13 分钟前
React 19版本refs也支持清理函数了。
前端·javascript·笔记·react.js·前端框架·ts
繁依Fanyi18 分钟前
ColorAid —— 一个面向设计师的色盲模拟工具开发记
开发语言·前端·vue.js·编辑器·codebuddy首席试玩官
codelxy21 分钟前
vue引用cesium,解决“Not allowed to load local resource”报错
javascript·vue.js
程序猿阿伟1 小时前
《社交应用动态表情:RN与Flutter实战解码》
javascript·flutter·react native
明似水1 小时前
Flutter 开发入门:从一个简单的计数器应用开始
前端·javascript·flutter
沐土Arvin1 小时前
前端图片上传组件实战:从动态销毁Input到全屏预览的全功能实现
开发语言·前端·javascript
Zww08912 小时前
el-dialog鼠标在遮罩层松开会意外关闭,教程图文并茂
javascript·vue.js·计算机外设
爱编程的鱼2 小时前
C#接口(Interface)全方位讲解:定义、特性、应用与实践
java·前端·c#