博客 / 詳情

返回

基於 React Flow 與 Web Audio API 的音頻應用開發

今天我們來學習通過 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.jpg

讓開發服務器保持運行。 然後繼續我們的工作

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.jpg

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 }
    },
    ...
  ],
  ...
}));

3.jpg

⚠️  糾結樣式問題? 如果你只是在繼續執行這篇文章中的代碼,你會發現自定義節點看起來不像上面預覽中的節點。 為了讓內容易於理解,我們在代碼片段中省略了樣式。

要了解如何設置自定義節點的樣式,請查看 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>
  );
}

事情開始變得非常好!

4.jpg

接下來我們看下一步

讓它發聲

現在我們有一個交互式圖表,我們能夠更新節點數據,現在讓我們添加 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>

5.jpg

那就是本文所有的內容啦! 我們現在擁有一個功能齊全的音頻圖編輯器,它可以:

  • 創建新的音頻節點
  • 通過 UI 更新節點數據
  • 進行節點連接
  • 刪除節點和連接
  • 啓動和停止音頻處理

最後的想法

這是一個漫長的過程,但我們做到了! 因為我們的努力,有了一個有趣的小型交互式音頻遊樂場,一路上學習了一些關於 Web Audio API 的知識,並且對「運行」 React Flow 圖有了更好的認識。

有很多方法可以繼續擴展這個項目。 如果你想繼續努力,這裏有一些想法:

  • 添加更多節點類型
  • 允許節點連接到其他節點上的 AudioParams
  • 使用 AnalyserNode 可視化節點或信號的輸出
  • 其他你能想到的所有事情

你可以使用 完整的源代碼 作為起點,也可以在我們今天所做的基礎上繼續構建。最後感謝大家對本文的支持~歡迎點贊收藏,在評論區留下你的高見 🌹🌹🌹

本文為翻譯文,原文地址 && 代碼倉庫地址
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.