关于javascript:基于-React-Flow-与-Web-Audio-API-的音频应用开发

4次阅读

共计 15353 个字符,预计需要花费 39 分钟才能阅读完成。

明天咱们来学习通过 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>
);

这里有三个重要的事件要留神

  1. 记得 导入 React Flow CSS 款式,来保障所有的性能能够失常运行
  2. React Flow 渲染器须要位于具备已知高度和宽度的元素内,因而咱们将蕴含 <div /> 设置为占据整个屏幕
  3. 要应用 React Flow 提供的一些 hook,你的组件须要位于 <ReactFlowProvider /> 外部或 <ReactFlow /> 组件自身外部,因而咱们将整个应用程序包裹在 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 图的节点和连接线、一些其余状态以及一些更新该状态的 actions

要取得一个根底的交互式 React Flow 图,咱们须要做这三个步骤:

  1. onNodesChange 解决节点被挪动或者删除
  2. onEdgesChange 解决 连接线 被挪动或者删除
  3. addEdge 连贯两个节点

接下来咱们要创立一个文件 store.js,并增加以下内容

src/store.js

import {applyNodeChanges, applyEdgeChanges} from 'reactflow';
import {nanoid} from 'nanoid';
import {create} from 'zustand';

export const useStore = create((set, get) => ({nodes: [],
  edges: [],

  onNodesChange(changes) {
    set({nodes: applyNodeChanges(changes, get().nodes),
    });
  },

  onEdgesChange(changes) {
    set({edges: applyEdgeChanges(changes, get().edges),
    });
  },

  addEdge(data) {const id = nanoid(6);
    const edge = {id, ...data};

    set({edges: [edge, ...get().edges] });
  },
}));

Zustand 非常容易应用。咱们创立一个函数,它接管一个 set 和一个 get 函数,并返回一个具备初始状态的对象以及咱们能够用来更新该状态的操作。

更新是不可变的,咱们能够应用 set 函数来进行更新。get 函数是咱们读取以后状态的形式。仅此而已。

onNodesChangeonEdgesChange 中的 changes 参数示意节点或连接线被挪动或删除等事件。侥幸的是,React Flow 提供了一些帮忙函数来为咱们解决这些变更。咱们只须要用新的节点数组更新 store。

只有两个节点连贯,就会调用 addEdgedata 参数简直是一个无效的连接线,它只是短少一个 id。在这里,咱们让 nanoid 生成一个 6 个字符的随机 id,而后将连接线增加到咱们的图中

如果咱们跳回 <App /> 组件,咱们能够将 React Flow 与咱们的操作分割起来并让一些性能能够运行。

src/App.jsx

import React from 'react';
import ReactFlow, {Background} from 'reactflow';
import {shallow} from 'zustand/shallow';

import {useStore} from './store';

const selector = (store) => ({
  nodes: store.nodes,
  edges: store.edges,
  onNodesChange: store.onNodesChange,
  onEdgesChange: store.onEdgesChange,
  addEdge: store.addEdge,
});

export default function App() {const store = useStore(selector, shallow);

  return (
    <ReactFlow
      nodes={store.nodes}
      edges={store.edges}
      onNodesChange={store.onNodesChange}
      onEdgesChange={store.onEdgesChange}
      onConnect={store.addEdge}
    >
      <Background />
    </ReactFlow>
  );
}

这个 selector 到底是什么呢?Zustand 让咱们提供一个 selector 函数来从 store 中提取咱们须要的 state。联合 shallow 比照函数,这意味着当咱们不关怀状态变更时,通常组件不会进行从新渲染。

当初咱们的 store 很小,咱们实际上须要它的所有内容来帮忙渲染咱们的 React Flow 图,然而当咱们扩大它时,这个 selector 将确保咱们不会始终从新渲染所有内容。

这就是咱们创立交互式图形所需的所有:咱们能够到处挪动节点,将它们连贯在一起,而后删除它们。为了演示,临时向 store 增加一些虚构节点:

src/store.js

const useStore = create((set, get) => ({
  nodes: [{ id: 'a', data: { label: 'oscillator'}, position: {x: 0, y: 0} },
    {id: 'b', data: { label: 'gain'}, position: {x: 150, y: 150} },
    {id: 'c', data: { label: 'output'}, position: {x: 350, y: 200} }
  ],
  ...
}));

2. 自定义节点

十分好,咱们当初曾经有了一个可交互的 React Flow 实例,并且能够操作它。咱们增加了一些虚构的节点但它们当初仅仅是默认无款式的。在此步骤中,咱们将增加三个带有交互式控件的自定义节点:

  1. 一个振荡器(oscillator)节点和管制音高和波形类型。
  2. 一个增益器(gain)节点和管制音量
  3. 一个输入节点和一个用于关上和敞开音频解决的按钮。

让咱们创立一个新文件夹 nodes/,并为咱们要创立的每个自定义节点创立一个文件。从振荡器开始,咱们须要两个控件和一个源句柄来将振荡器的输入连贯到其余节点。

src/nodes/Osc.jsx

import React from 'react';
import {Handle} from 'reactflow';

import {useStore} from '../store';

export default function Osc({id, data}) {
  return (
    <div>
      <div>
        <p> 振荡器节点 </p>

        <label>
          <span> 频率 </span>
          <input
            className="nodrag"
            type="range"
            min="10"
            max="1000"
            value={data.frequency} />
          <span>{data.frequency}赫兹 </span>
        </label>

        <label>
          <span> 波形 </span>
          <select className="nodrag" value={data.type}>
            <option value="sine"> 正弦波 </option>
            <option value="triangle"> 三角波 </option>
            <option value="sawtooth"> 锯齿波 </option>
            <option value="square"> 方波 </option>
          </select>
          </label>
      </div>

      <Handle type="source" position="bottom" />
    </div>
  );
};

NODRAG”很重要 留神增加到 <input /><select /> 元素的 “nodrag” 类。记住增加这个类是十分重要的,否则你会发现 React Flow 拦挡鼠标事件并且你将永远被困在拖动节点!

如果咱们尝试渲染这个自定义节点,咱们会发现输出没有做任何事件。那是因为输出值由 data.frequencydata.type 固定,但咱们没有监听变动的事件处理程序,也没有更新节点数据的机制!

为了解决这个问题,咱们须要跳回咱们的 store 并增加一个 updateNode 操作:

src/store.js

export const useStore = create((set, get) => ({
  // ...

  updateNode(id, data) {
    set({nodes: get().nodes.map(node =>
        node.id === id
          ? {...node, data: Object.assign(node.data, data) }
          : node
      )
    });
  },

  // ...
}));

这个动作将解决局部数据更新,例如,如果咱们只想更新节点的频率,咱们能够调用 updateNode(id, { frequency: 220}。当初咱们只须要将这个 action 带入咱们的 <Osc / > 组件并在输出更改时调用它。

src/nodes/Osc.jsx

import React from 'react';
import {Handle} from 'reactflow';
import {shallow} from 'zustand/shallow';

import {useStore} from '../store';

// 增加 selector
const selector = (id) => (store) => ({setFrequency: (e) => store.updateNode(id, { frequency: +e.target.value}),
    setType: (e) => store.updateNode(id, { type: e.target.value}),
});

export default function Osc({id, data}) {
    // 应用 useStore
    const {setFrequency, setType} = useStore(selector(id), shallow);

    return (
        <div>
        <div>
            <p> 振荡器节点 </p>

            <label>
            <span> 频率 </span>
            <input
                className="nodrag"
                type="range"
                min="10"
                max="1000"
                value={data.frequency}
                // 增加 onChange 事件
                onChange={setFrequency}
            />
            <span>{data.frequency}赫兹 </span>
            </label>

            <label>
            <span> 波形 </span>
            
            <select 
                className="nodrag" 
                value={data.type}  
                // 增加 onChange 事件
                onChange={setType}>
                <option value="sine"> 正弦波 </option>
                <option value="triangle"> 三角波 </option>
                <option value="sawtooth"> 锯齿波 </option>
                <option value="square"> 方波 </option>
            </select>
            </label>
        </div>

        <Handle type="source" position="bottom" />
        </div>
    );
};

嘿,咱们又用到 selector 了!请留神这次咱们如何应用它从个别的 updateNode 操作派生两个事件处理程序,setFrequencysetType

最初一件事就是通知 React Flow 如何渲染咱们的自定义节点。为此,咱们须要创立一个 nodeTypes 对象:键应该对应于节点的类型,值将是要渲染的 React 组件。

防止不必要的渲染<App> 组件内部定义 nodeTypes(或者是用 React 的 useMemo)是很重要的,这样能够防止每次渲染都会反复计算的问题

如果你的开发服务器正在运行,如果事件还没有扭转,请不要惊恐!咱们的长期节点还没有被赋予正确的类型,所以 React Flow 只是退回到渲染默认节点。如果咱们将其中一个节点更改为具备一些 频率 类型 初始值的 osc,咱们应该会看到正在渲染咱们的自定义节点。

src/store.js

const useStore = create((set, get) => ({
  nodes: [
    { type: 'osc',
      id: 'a',
      data: {frequency: 220, type: 'square'},
      position: {x: 200, y: 0}
    },
    ...
  ],
  ...
}));

⚠️  纠结款式问题? 如果你只是在继续执行这篇文章中的代码,你会发现自定义节点看起来不像下面预览中的节点。为了让内容易于了解,咱们在代码片段中省略了款式。

要理解如何设置自定义节点的款式,请查看 React Flow 对于主题的文档或应用 Tailwind 的示例。

具体实例代码能够查看 这里

实现 gain 节点的过程简直雷同,因而我将把这个作为作业留给你。相同,咱们将注意力转向输入节点。该节点将没有参数管制,但咱们的确想要关上和敞开信号处理。当初咱们还没有实现任何音频代码,咱们只须要向咱们的 store 增加一个标识和一个切换它的 action。

src/store.js

const useStore = create((set, get) => ({
  ...

  isRunning: false,

  toggleAudio() {set({ isRunning: !get().isRunning });
  },

  ...
}));

自定义节点自身非常简单:

src/nodes/Out.jsx

import React from 'react';
import {Handle} from 'reactflow';
import {tw} from 'twind';
import {shallow} from 'zustand/shallow';
import {useStore} from '../store';

const selector = (store) => ({
  isRunning: store.isRunning,
  toggleAudio: store.toggleAudio,
});

export default function Out({id, data}) {const { isRunning, toggleAudio} = useStore(selector, shallow);
  return (<div className={tw('rounded-md bg-white shadow-xl px-4 py-2')}>
      <Handle className={tw('w-2 h-2')} type="target" position="top" />

      <div>
        <p> 输入节点 </p>

        <button onClick={toggleAudio}>
          {isRunning ? (
            <span role="img" aria-label="mute">
              🔈
            </span>
          ) : (
            <span role="img" aria-label="unmute">
              🔇
            </span>
          )}
        </button>
      </div>
    </div>
  );
}

事件开始变得十分好!

接下来咱们看下一步

让它发声

当初咱们有一个交互式图表,咱们可能更新节点数据,当初让咱们增加 Web Audio API 的相干内容。首先创立一个新文件 audio.js,而后创立一个新的音频上下文和一个空的 Map。

src/audio.js

const context = new AudioContext();
const nodes = new Map();

咱们治理音频图的形式是 hook 咱们 store 中的不同 action。因而,咱们可能会在调用 addEdge 操作时连贯两个音频节点,或者在调用 updateNode 时更新音频节点的属性,等等。

🔥 硬编码节点 咱们在这篇文章的后面对 store 中的几个节点进行了硬编码,但咱们的音频图对它们无所不知!对于实现的我的项目,咱们能够勾销所有这些硬编码,但当初咱们还须要对一些音频节点进行硬编码, 这十分重要。咱们会这么做:

const context = new AudioContext();
const nodes = new Map();

const osc = context.createOscillator();
osc.frequency.value = 220;
osc.type = 'square';
osc.start();

const amp = context.createGain();
amp.gain.value = 0.5;

const out = context.destination;

nodes.set('a', osc);
nodes.set('b', amp);
nodes.set('c', out);

1. 节点变更

当初,咱们的图中可能产生两种类型的节点变更,咱们须要对其做出响应:更新节点的数据,以及从图中删除节点。咱们曾经对前者有了一个 action,所以让咱们先解决它。

audio.js 中,咱们将定义一个函数 updateAudioNode,咱们将应用节点的 ID 和局部数据对象调用该函数,并应用它来更新 Map 中的现有节点:

src/audio.js

export function updateAudioNode(id, data) {const node = nodes.get(id);

  for (const [key, val] of Object.entries(data)) {if (node[key] instanceof AudioParam) {node[key].value = val;
    } else {node[key] = val;
    }
  }
}

揭示 请记住,音频节点上的属性可能是非凡的 AudioParams,必须以不同的形式更新为惯例对象属性。

当初咱们要更新 store 中的 updateNode 操作以调用此函数作为更新的一部分:

src/store.js

import {updateAudioNode} from './audio';

export const useStore = create((set, get) => ({
  ...

  updateNode(id, data) {updateAudioNode(id, data);
    set({nodes: ...});
  },

  ...
}));

咱们须要解决的下一个更改是从图中删除一个节点。如果你在图中抉择一个节点并按退格键,React Flow 会将其删除。这是通过咱们连贯的 onNodesChange 操作为咱们隐式解决的,但当初咱们须要一些额定的解决,咱们须要将一个新操作连贯到 React Flow 的 onNodesDelete 事件。

src/audio.js

export function removeAudioNode(id) {const node = nodes.get(id);

  node.disconnect();
  node.stop?.();

  nodes.delete(id);
}

src/store.js

import {..., removeAudioNode} from './audio';

export const useStore = create((set, get) => ({
  ...

  removeNodes(nodes) {for (const { id} of nodes) {removeAudioNode(id)
    }
  },

  ...
}));

**src/App.jsx**

const selector = store => ({
  ...,
  onNodesDelete: store.removeNodes
});

export default function App() {const store = useStore(selector, shallow);

  return (
    <ReactFlow
      onNodesDelete={store.onNodesDelete}
      ...
    >
      <Background />
    </ReactFlow>
  )
};

惟一须要留神的是,onNodesDelete 会调用提供的回调函数,其中蕴含 一组 已删除的节点,因为有可能一次删除多个节点!

2. 连接线变更

咱们离真正收回一些声音越来越近了!剩下的就是解决图形连接线的变更。与节点变更一样,咱们曾经有一个操作来解决创立新的连接线,咱们还在 onEdgesChange 中隐式解决删除的连接线。

要解决新连贯,咱们只须要在 addEdge 操作中创立的连接线的源 ID 以及指标 ID。而后咱们能够在咱们的 Map 中查找两个节点并将它们连接起来。

src/audio.js

export function connect(sourceId, targetId) {const source = nodes.get(sourceId);
  const target = nodes.get(targetId);

  source.connect(target);
}

src/store.js

import {..., connect} from './audio';

export const useStore = create((set, get) => ({
  ...

  addEdge(data) {
    ...

    connect(data.source, data.target);
  },

  ...
}));

咱们看到 React Flow 可能接管了一个 onNodesDelete 回调函数,还有一个 onEdgesDelete 回调函数!咱们用来实现断开连接并将其关联到咱们的 store 和 React Flow 实例的办法与之前的做法简直雷同,所以咱们也将把它留给你!

3. 关上扬声器

你应该还记得咱们的 AudioContext 是以挂起的状态启动的,以避免那些令人讨厌的自动播放问题。咱们曾经为 store 中的 <Out /> 组件 mock 了所需的数据和操作,当初咱们只须要用实在上下文状态和复原与暂停的办法替换它们。

src/audio.js

export function isRunning() {return context.state === 'running';}

export function toggleAudio() {return isRunning() ? context.suspend() : context.resume();
}

尽管到目前为止咱们还没有从 audio 函数返回任何货色,但咱们须要从 toggleAudio 返回,因为这些办法是异步的,咱们不想过早地更新 store

import {..., isRunning, toggleAudio} from './audio'

export const useStore = create((set, get) => ({
  ...

  isRunning: isRunning(),

  toggleAudio() {toggleAudio().then(() => {set({ isRunning: isRunning() });
    });
  }
}));

咱们做到了!咱们当初曾经把足够多的货色组合在一起,能够真正 发出声音 了!让咱们看看咱们的成绩。

4. 创立新节点

到目前为止,咱们始终在解决图中的一组硬编码的节点。这对于原型设计来说很好,但为了让它真正有用,咱们须要一种办法来动静地将新节点增加到图形中。咱们的最终工作是增加此性能:咱们将从音频代码开始入手,最初创立一个根本工具栏。

实现 createAudioNode 函数将非常简单。咱们只须要新节点的 ID、要创立的节点类型及其初始数据:

src/audio.js

export function createAudioNode(id, type, data) {switch (type) {
    case 'osc': {const node = context.createOscillator();
      node.frequency.value = data.frequency;
      node.type = data.type;
      node.start();

      nodes.set(id, node);
      break;
    }

    case 'amp': {const node = context.createGain();
      node.gain.value = data.gain;

      nodes.set(id, node);
      break;
    }
  }
}

接下来,咱们的 store 中须要一个 createNode 函数。节点 ID 将由 nanoid 生成,咱们将为每种节点类型硬编码一些初始数据,因而咱们惟一须要传入的是要创立的节点类型:

src/store.js

import {..., createAudioNode} from './audio';

export const useStore = create((set, get) => ({
  ...

  createNode(type) {const id = nanoid();

    switch(type) {
      case 'osc': {const data = { frequency: 440, type: 'sine'};
        const position = {x: 0, y: 0};

        createAudioNode(id, type, data);
        set({nodes: [...get().nodes, {id, type, data, position}] });

        break;
      }

      case 'amp': {const data = { gain: 0.5};
        const position = {x: 0, y: 0};

        createAudioNode(id, type, data);
        set({nodes: [...get().nodes, {id, type, data, position}] });

        break;
      }
    }
  }
}));

咱们能够更智能地计算新节点的地位,但为了简略起见,咱们临时将其硬编码为 {x: 0, y: 0}

最初是创立一个能够触发 createNode 操作的工具栏组件。为此,咱们将跳回 App.jsx 并应用 <Panel /> 组件。

src/App.jsx

...
import ReactFlow, {Panel} from 'reactflow';
...

const selector = (store) => ({
  ...,
  createNode: store.createNode,
});

export default function App() {const store = useStore(selector, shallow);

  return (
    <ReactFlow>
      <Panel position="top-right">
        ...
      </Panel>
      <Background />
    </ReactFlow>
  );
};

咱们在这里不须要任何花哨的货色,只须要几个按钮来触发 createNode 操作:

src/App.jsx

<Panel className={tw('space-x-4')}  position="top-right">
  <button className={tw('px-2 py-1 rounded bg-white shadow')}  onClick={() => store.createNode('osc')}> 增加 osc</button>
  <button className={tw('px-2 py-1 rounded bg-white shadow')}  onClick={() => store.createNode('amp')}> 增加 amp</button>
</Panel>

那就是本文所有的内容啦!咱们当初领有一个功能齐全的音频图编辑器,它能够:

  • 创立新的音频节点
  • 通过 UI 更新节点数据
  • 进行节点连贯
  • 删除节点和连贯
  • 启动和进行音频解决

最初的想法

这是一个漫长的过程,但咱们做到了!因为咱们的致力,有了一个乏味的小型交互式音频游乐场,一路上学习了一些对于 Web Audio API 的常识,并且对「运行」React Flow 图有了更好的意识。

有很多办法能够持续扩大这个我的项目。如果你想持续致力,这里有一些想法:

  • 增加更多节点类型
  • 容许节点连贯到其余节点上的 AudioParams
  • 应用 AnalyserNode 可视化节点或信号的输入
  • 其余你能想到的所有事件

你能够应用 残缺的源代码 作为终点,也能够在咱们明天所做的根底上持续构建。最初感激大家对本文的反对~欢送点赞珍藏,在评论区留下你的浅见 🌹🌹🌹

本文为翻译文,原文地址 && 代码仓库地址

正文完
 0