hello 大家好,我是 superZidan,这篇文章想跟大家聊聊 基于 React Flow 与 Web Audio API 的音频应用开发
,如果大家遇到任何问题,欢迎联系我 或者直接微信添加 superZidan41
今天我们来学习通过 React Flow 和 Web Audio API 来创建一个可交互的语音广场。我们将会从最小的场景开始,在学习 React Flow(包括:状态管理,实现自定义节点,添加交互能力) 之前,我们会先学习 Web Audio API。
这个教程会一步一步地带你完善这个应用,当然你也可以跳过中间的一些步骤。但如果你是一名新手,还是建议你从头到尾按顺序看完。
Web Audio API
让我们来看一些 Web Audio API 。以下的高亮是你需要知道的知识点:
- Web Audio API 提供了许多不同的音频节点,包括:音频源(比如: OscillatorNode 和 MediaElementAudioSourceNode ),音频效果(比如:GainNode, DelayNode , ConvolverNode )输出(比如:AudioDestinationNode)
- 音频节点可以互相连接在一起来形成一个「图」,我们一般称之为「音源处理图」或者「信号图」或者「信号链」
- 音频处理在原生代码中是在一个单独的进程中处理的,这就意味着即使主线程正在忙于处理其他的任务,我们也可以持续进行音频任务处理
- AudioContext 充当音频处理图的大脑。 我们可以使用它来创建新的音频节点并进行暂停或恢复音频处理。
你好,声音
让我们看看这些东西的一些实际应用并构建我们的第一个网络音频应用程序!我们暂时不会做太复杂的事情:我们将制作一个简单的鼠标电子琴。我们将使用 React 来处理这些示例,并使用 vite
来打包和热更新
当然,你也可以使用其他的打包工具比如 parcel 或者 CRA ,也可以使用 Typescript 来替换 Javascript 。为了让应用足够的简单,我们暂时都不使用他们,但是 React Flow 是类型完整的(完全由 Typescript 编写)。
npm create vite@latest
// Project name: audio-hello
// Select a framework: › React
// Select a variant: › JavaScript
Vite 会为我们创建一个简单的 React 应用,但我们可以删掉一些不需要的资源。跳转到 App.jsx
,删掉默认创建的组件内容,创建一个新的 AudioContext 并将我们需要的节点放在一起。我们需要一个 OscillatorNode 来生成一些音调和一个 GainNode 来控制音量。
src/App.jsx
// 创建音频处理图的大脑
const context = new AudioContext();
// 创建一个 oscillator 节点来生成音调
const osc = context.createOscillator();
// 创建一个 gain 节点来控制音量
const amp = context.createGain();
// 通过 gain 节点将 oscillator 的输出传递到扬声器
osc.connect(amp);
amp.connect(context.destination);
// 开始生成这些音调
osc.start();
OSCILLATOR 节点需要启动 不要忘记调用 osc.start ,否则音调不会生成
对于我们的应用程序,我们将跟踪鼠标在屏幕上的位置并使用它来设置 oscillator(振荡器) 节点的音高和 gain(增益)节点的音量。
src/App.jsx
import React from 'react';
const context = new AudioContext();
const osc = context.createOscillator();
const amp = context.createGain();
osc.connect(amp);
amp.connect(context.destination);
osc.start();
const updateValues = (e) => {
const freq = (e.clientX / window.innerWidth) * 1000;
const gain = e.clientY / window.innerHeight;
osc.frequency.value = freq;
amp.gain.value = gain;
};
export default function App() {
return <div style={
{ width: '100vw', height: '100vh' }} onMouseMove={updateValues} />;
}
osc.frequency.value
amp.gain.value
Web Audio API 区分简单对象属性和音频节点参数。 这种区别以AudioParam
的形式出现。 你可以在 MDN 文档中阅读它们,但现在只需要知道使用 .value 来设置 AudioParam 的值而不是直接为属性分配值就足够了。
如果你现在尝试使用我们的应用,你会发现什么事情都没有发生。AudioContext 一直处于挂起的状态下启动,这样可以避免广告劫持我们的扬声器。我们可以在 <div>
元素上添加一个点击事件,判断如果当前 AudioContext 处于挂起状态就恢复它,这样就可以快速的修复上述问题。
const toggleAudio = () => {
if (context.state === 'suspended') {
context.resume();
} else {
context.suspend();
}
};
export default function App() {
return (
<div ...
onClick={
toggleAudio}
/>
);
};
这就是我们开始使用 Web Audio API 制作声音所需的一切内容,让我们再整理一下代码,让它的可读性更高一点
src/App.jsx
import {
useState } from 'react'
import './App.css'
const context = new AudioContext();
const osc = context.createOscillator();
const amp = context.createGain();
osc.connect(amp);
amp.connect(context.destination);
osc.start();
const updateValues = (e) => {
const freq = (e.clientX / window.innerWidth) * 1000;
const gain = e.clientY / window.innerHeight;
osc.frequency.value = freq;
amp.gain.value = gain;
};
export default function App() {
const [ isRunning, setIsRunning ] = useState(false)
const toggleAudio = () => {
if (context.state === 'suspended') {
context.resume();
setIsRunning(true)
} else {
context.suspend();
setIsRunning(false)
}
};
return <div
style={
{
width: '100vw', height: '100vh' }}
onMouseMove={
updateValues} >
<button onClick={
toggleAudio}>{
isRunning ? '🔊' : '🔇'}</button>
</div>;
}
现在让我们把这些知识先抛到一边,看看如何从头开始构建一个 React Flow 项目。
搭建 React Flow 项目
稍后,我们将利用所了解的有关 Web Audio API、oscillators(振荡器)和gain(增益)节点的知识,并使用 React Flow 以交互方式构建音频处理图。 不过现在,我们需要组装一个空的 React Flow 应用程序
我们已经有一个基于 Vite 的 React 应用,我们将继续使用它。
我们需要在项目中额外安装三个依赖:使用 reactflow
来处理 UI ,使用 zustand
来进行状态管理,使用 nanoid
来生成 id
npm install reactflow zustand nanoid
我们将删除 Web Audio 章节的所有内容,并从头开始。 首先修改 main.jsx
以匹配以下内容:
src/main.jsx
import App from './App';
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ReactFlowProvider } from 'reactflow';
// 👇 不要忘记导入样式文件
import 'reactflow/dist/style.css';
import './index.css';
const root = document.querySelector('#root');
// React flow 需要在一个已知高度和宽度的元素内才能工作
ReactDOM.createRoot(root).render(
<React.StrictMode>
<div style={
{ width: '100vw', height: '100vh' }}>
<ReactFlowProvider>
<App />
</ReactFlowProvider>
</div>
</React.StrictMode>
);
这里有三个重要的事情要注意
- 记得导入 React Flow CSS 样式,来保证所有的功能可以正常运行
- React Flow 渲染器需要位于具有已知高度和宽度的元素内,因此我们将包含
设置为占据整个屏幕
- 要使用 React Flow 提供的一些 hook,你的组件需要位于 内部或 组件本身内部,因此我们将整个应用程序包裹在 Provider 中以确保
接下来,跳转到 App.jsx
中并创建一个空流程
src/App.jsx
import React from 'react';
import ReactFlow, { Background } from 'reactflow';
export default function App() {
return (
<ReactFlow>
<Background />
</ReactFlow>
);
}
后续我们将扩展并添加到该组件。 现在我们添加了 React Flow 的一个插件 - <Background />
- 来检查一切是否设置正确。 继续运行 npm run dev
并检查你的浏览器。 你应该可以看到一个空流程:
让开发服务器保持运行。 然后继续我们的工作
1.Zustand 的状态管理
Zustand 的 store 将保存我们应用程序的所有 UI 状态。 实际上,这意味着它将保存我们的 React Flow 图的节点和连接线、一些其他状态以及一