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的强大之处当然不止于此,尝试起来吧!

相关推荐
web1309332039817 分钟前
前端下载后端文件流,文件可以下载,但是打不开,显示“文件已损坏”的问题分析与解决方案
前端
王小王和他的小伙伴20 分钟前
解决 vue3 中 echarts图表在el-dialog中显示问题
javascript·vue.js·echarts
学前端的小朱24 分钟前
处理字体图标、js、html及其他资源
开发语言·javascript·webpack·html·打包工具
outstanding木槿29 分钟前
react+antd的Table组件编辑单元格
前端·javascript·react.js·前端框架
好名字08211 小时前
前端取Content-Disposition中的filename字段与解码(vue)
前端·javascript·vue.js·前端框架
摇光931 小时前
js高阶-async与事件循环
开发语言·javascript·事件循环·宏任务·微任务
隐形喷火龙1 小时前
element ui--下拉根据拼音首字母过滤
前端·vue.js·ui
m0_748241122 小时前
Selenium之Web元素定位
前端·selenium·测试工具
风无雨2 小时前
react杂乱笔记(一)
前端·笔记·react.js
前端小魔女2 小时前
2024-我赚到自媒体第一桶金
前端·rust