2023/04/14
React FlowとWeb Audio APIの統合

今日は、React FlowとWeb Audio APIを使用してインタラクティブなオーディオプレイグラウンドを作成する方法を見ていきます。 最初にWeb Audio APIについて学習してから、状態管理、カスタムノードの実装、インタラクティブ機能の追加など、React Flowにおける多くの一般的なシナリオの処理方法を見ていきます。

しばらく前に、React FlowのDiscordサーバーで取り組んでいたプロジェクトを共有しました。 それはbleep.cafeと呼ばれ、ブラウザ内でデジタルシンセシスを学習するための小さなWebアプリです。多くの人がそのようなものの作成方法に興味を持っていました。ほとんどの人は、**ブラウザにシンセエンジンが組み込まれていることさえ知りません!**
このチュートリアルでは、ステップバイステップで同様のものを構築します。 ここではいくつかの部分を省略しますが、ほとんどの場合、React FlowとWeb Audio APIのどちらにも慣れていない場合でも、最後まで手順に従って、最後に何かが機能するようになるはずです。
すでにReact Flowの達人である場合は、Web Audio APIに関する最初のセクションを読んでから、3番目のセクションに進んで、ものがどのように結び付けられているかを確認することをお勧めします!
しかし、まず…
デモ!
このチュートリアルの他の例と同様に、音が出ます。
アバンギャルドな傑作を作成しないように、先に進む前に各例の音声をミュートすることを忘れないでください!
Web Audio API
React Flowとインタラクティブなノードエディタの良さに入る前に、Web Audio APIの集中講義を受ける必要があります。 知っておくべき重要なポイントを以下に示します。
- Web Audio APIは、ソース(例:OscillatorNode、MediaElementAudioSourceNode)、エフェクト(例:GainNode、DelayNode、ConvolverNode)、および出力(例:AudioDestinationNode)など、さまざまなオーディオノードを提供します。
- オーディオノードは接続して(循環する可能性のある)グラフを形成できます。 これをオーディオ処理グラフ、シグナルグラフ、またはシグナルチェーンと呼びます。
- オーディオ処理は、ネイティブコードによって別のスレッドで処理されます。 つまり、メインのUIスレッドがビジー状態またはブロックされている場合でも、サウンドの生成を続けることができます。
- AudioContextは、オーディオ処理グラフの中枢として機能します。 これを用いて新しいオーディオノードを作成し、オーディオ処理全体を一時停止または再開できます。
サウンド、こんにちは!
これらの機能を実際に使用して、最初のWeb Audioアプリを構築してみましょう!ここでは、あまり複雑なことはしません。単純なマウステルミンを作成します。 これらの例とそれ以降のすべての例ではReactを使用し(結局のところ、React Flowと呼ばれています!)、vite
を使用してバンドルとホットリロードを処理します。
parcelやCreate React Appなどの別のバンドラを好む場合でも大丈夫です。それらはすべてほぼ同じことを行います。 JavaScriptの代わりにTypeScriptを使用することもできます。単純にするために、今日は使用しませんが、React Flowは完全に型付けされており(そして完全にTypeScriptで記述されているため)、使用するのは簡単です!
npm create vite@latest -- --template react
Viteは単純なReactアプリケーションをスキャフォールディングしますが、アセットを削除してApp.jsx
に直接ジャンプできます。 生成されたデモコンポーネントを削除し、新しいAudioContextを作成し、必要なノードをまとめることから始めます。 音を生成するOscillatorNodeと、音量を制御するGainNodeが必要です。
// Create the brain of our audio-processing graph
const context = new AudioContext();
// Create an oscillator node to generate tones
const osc = context.createOscillator();
// Create a gain node to control the volume
const amp = context.createGain();
// Pass the oscillator's output through the gain node and to our speakers
osc.connect(amp);
amp.connect(context.destination);
// Start generating those tones!
osc.start();
オシレータノードは開始する必要があります。
osc.start
への呼び出しを忘れないでください。 オシレータはそれがないと音を生成しません!
私たちのアプリでは、画面上のマウスの位置を追跡し、それを用いてオシレータノードのピッチとゲインノードの音量を設定します。
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 />
にクリックハンドラを追加して、一時停止されている場合はコンテキストを再開することで、簡単に修正できます。
const toggleAudio = () => {
if (context.state === 'suspended') {
context.resume();
} else {
context.suspend();
}
};
export default function App() {
return (
<div ...
onClick={toggleAudio}
/>
);
};
そして、Web Audio APIでサウンドを生成するために必要なものはすべて揃いました!家で一緒に作業していなかった場合に備えて、私たちがまとめたものをここに示します。
さて、ここで一旦この知識は脇に置いて、React Flowプロジェクトをゼロから構築する方法を見てみましょう。
既にReact Flowのプロですか?既にReact Flowに精通している場合は、次のセクションをスキップして、サウンドの作成に直接進むことができます。そうでない方は、React Flowプロジェクトをゼロから構築する方法を見ていきましょう。
React Flowプロジェクトのスキャフォールディング
後で、Web Audio API、オシレーター、ゲインノードについて学んだことを活用し、React Flowを使用してオーディオ処理グラフをインタラクティブに構築します。しかし今のところは、空のReact Flowアプリを準備する必要があります。
既にViteでReactアプリが設定されているので、それを使い続けます。前のセクションをスキップした場合は、npm create vite@latest -- --template react
を実行して開始しました。ただし、お好きなバンドラーや開発サーバーを使用できます。ここで説明する内容はVite固有のものではありません。
このプロジェクトには、UI向けに@xyflow/react
(言うまでもなく!)、シンプルな状態管理ライブラリとしてzustand
(React Flow内部で使用しているもの)、軽量なIDジェネレーターとしてnanoid
の3つの追加依存関係のみが必要です。
npm install @xyflow/react zustand nanoid
Web Audioの集中講義からすべてを削除し、ゼロから始めます。main.jsx
を以下の内容に変更して開始します。
import App from './App';
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ReactFlowProvider } from '@xyflow/react';
// 👇 Don't forget to import the styles!
import '@xyflow/react/dist/style.css';
import './index.css';
const root = document.querySelector('#root');
ReactDOM.createRoot(root).render(
<React.StrictMode>
{/* React flow needs to be inside an element with a known height and width to work */}
<div style={{ width: '100vw', height: '100vh' }}>
<ReactFlowProvider>
<App />
</ReactFlowProvider>
</div>
</React.StrictMode>,
);
ここでは、3つの重要な点に注意する必要があります。
- すべてが正しく機能するように、**React FlowのCSSスタイルをインポートする**必要があります。
- React Flowレンダラーは、高さおよび幅が既知の要素内にある必要があるため、包含する
<div />
を画面全体を占めるように設定しました。 - React Flowが提供するいくつかのフックを使用するには、コンポーネントを
<ReactFlowProvider />
内、または<ReactFlow />
コンポーネント自体の中に配置する必要があります。確実に動作させるために、アプリ全体をプロバイダーでラップしました。
次に、App.jsx
に移動して、空のフローを作成します。
import React from 'react';
import { ReactFlow, Background } from '@xyflow/react';
export default function App() {
return (
<ReactFlow>
<Background />
</ReactFlow>
);
}
このコンポーネントは時間をかけて拡張していきます。今のところは、React Flowのプラグインの1つである<Background />
を追加して、すべてが正しく設定されているかどうかを確認しています。npm run dev
(またはViteを選択しなかった場合は、開発サーバーを起動するために必要な操作)を実行して、ブラウザーを確認してください。空のフローが表示されるはずです。

開発サーバーは実行したままにしておきます。新しい部分を追加する際に、進捗状況を確認し続けることができます。
1. Zustandを使用した状態管理
Zustandストアは、アプリケーションのすべてのUI状態を保持します。実際には、React Flowグラフのノードとエッジ、その他の状態のいくつかの部分、そしてその状態を更新するためのいくつかのアクションを保持します。
基本的なインタラクティブなReact Flowグラフを稼働させるには、3つのアクションが必要です。
onNodesChange
は、ノードの移動や削除を処理します。onEdgesChange
は、エッジの移動や削除を処理します。addEdge
は、グラフ内の2つのノードを接続します。
新しいファイルstore.js
を作成し、以下を追加します。
import { applyNodeChanges, applyEdgeChanges } from '@xyflow/react';
import { nanoid } from 'nanoid';
import { createWithEqualityFn } from 'zustand/traditional';
export const useStore = createWithEqualityFn((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
関数は、現在の状態を読み取る方法です。そして…Zustandは以上です。
onNodesChange
とonEdgesChange
の両方にあるchanges
引数は、ノードまたはエッジの移動や削除などのイベントを表します。幸いなことに、React Flowは、それらの変更を適用するためのヘルパー 関数を提供しています。ノードの新しい配列でストアを更新するだけです。
addEdge
は、2つのノードが接続されるたびに呼び出されます。data
引数は、ほぼ有効なエッジですが、IDがありません。ここでは、nanoidを使用して6文字のランダムなIDを生成し、エッジをグラフに追加しています。特に難しいことはありません。
<App />
コンポーネントに戻ると、React Flowをアクションに接続して動作させることができます。
import React from 'react';
import { ReactFlow, Background } from '@xyflow/react';
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を使用すると、セレクター関数を指定して、ストアから必要な状態の正確な部分を抽出できます。shallow
等価関数と組み合わせることで、関心のない状態が変更された場合でも、再レンダリングが行われないことが一般的です。
現時点では、ストアは小さく、React Flowグラフのレンダリングにすべてが必要ですが、ストアを拡張する際に、このセレクターは常にすべてを再レンダリングしないようにします。
インタラクティブなグラフに必要なのはこれだけです。ノードを移動したり、接続したり、削除したりできます。デモとして、ストアにダミーノードを一時的に追加します。
const useStore = createWithEqualityFn((set, get) => ({
nodes: [
{ id: 'a', data: { label: 'oscillator' }, position: { x: 0, y: 0 } },
{ id: 'b', data: { label: 'gain' }, position: { x: 50, y: 50 } },
{ id: 'c', data: { label: 'output' }, position: { x: -50, y: 100 } }
],
...
}));
2. カスタムノード
素晴らしい、インタラクティブなReact Flowインスタンスを使用できるようになりました。ダミーノードを追加しましたが、現時点ではデフォルトのスタイルなしのノードです。このステップでは、インタラクティブなコントロールを備えた3つのカスタムノードを追加します。
- オシレーターノードと、ピッチと波形の種類のコントロール。
- ゲインノードと、ボリュームのコントロール。
- 出力ノードと、オーディオ処理のオン/オフを切り替えるボタン。
新しいフォルダnodes/
を作成し、作成する各カスタムノードのファイルを作成します。オシレーターから始めると、2つのコントロールと、オシレーターの出力を他のノードに接続するためのソースハンドルが必要です。
import React from 'react';
import { Handle } from '@xyflow/react';
import { useStore } from '../store';
export default function Osc({ id, data }) {
return (
<div>
<div>
<p>Oscillator Node</p>
<label>
<span>Frequency</span>
<input
className="nodrag"
type="range"
min="10"
max="1000"
value={data.frequency} />
<span>{data.frequency}Hz</span>
</label>
<label>
<span>Waveform</span>
<select className="nodrag" value={data.type}>
<option value="sine">sine</option>
<option value="triangle">triangle</option>
<option value="sawtooth">sawtooth</option>
<option value="square">square</option>
</select>
</div>
<Handle type="source" position="bottom" />
</div>
);
};
「nodrag」が重要です。
<input />
要素と<select />
要素の両方に追加されている"nodrag"
クラスに注意してください。このクラスを追加することを絶対に忘れないでください。そうでないと、React Flowがマウスイベントをインターセプトし、ノードを永遠にドラッグし続けることになります!
このカスタムノードのレンダリングを試みると、入力は何も行いません。data.frequency
とdata.type
によって入力値が固定されているためですが、変更をリッスンするイベントハンドラーがなく、ノードのデータを更新するメカニズムがありません!
状況を修正するために、ストアに戻ってupdateNode
アクションを追加する必要があります。
export const useStore = createWithEqualityFn((set, get) => ({
...
updateNode(id, data) {
set({
nodes: get().nodes.map(node =>
node.id === id
? { ...node, data: { ...node.data, ...data } }
: node
)
});
},
...
}));
このアクションは部分的なデータ更新を処理します。たとえば、ノードのfrequency
のみを更新したい場合は、updateNode(id, { frequency: 220 }
を呼び出すだけです。これで、アクションを<Osc />
コンポーネントに組み込み、入力が変更されるたびに呼び出すだけです。
import React from 'react';
import { Handle } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
import { useStore } from '../store';
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 }) {
const { setFrequency, setType } = useStore(selector(id), shallow);
return (
<div>
<div>
<p>Oscillator Node</p>
<label>
<span>Frequency:</span>
<input
className="nodrag"
type="range"
min="10"
max="1000"
value={data.frequency}
onChange={setFrequency}
/>
<span>{data.frequency}Hz</span>
</label>
<label>
<span>Waveform:</span>
<select className="nodrag" value={data.type} onChange={setType}>
<option value="sine">sine</option>
<option value="triangle">triangle</option>
<option value="sawtooth">sawtooth</option>
<option value="square">square</option>
</select>
</label>
</div>
<Handle type="source" position="bottom" />
</div>
);
}
おや、selector
が戻ってきました!今回は、一般的なupdateNode
アクションから、2つのイベントハンドラーsetFrequency
とsetType
を導出するために使用していることに注目してください。
パズルの最後のピースは、React Flowにカスタムノードのレンダリング方法を伝えることです。そのためには、nodeTypes
オブジェクトを作成する必要があります。キーはノードのtype
に対応し、値はレンダリングするReactコンポーネントになります。
import React from 'react';
import { ReactFlow } from '@xyflow/react';
import { shallow } from 'zustand/shallow';
import { useStore } from './store';
import Osc from './nodes/Osc';
const selector = (store) => ({
nodes: store.nodes,
edges: store.edges,
onNodesChange: store.onNodesChange,
onEdgesChange: store.onEdgesChange,
addEdge: store.addEdge,
});
const nodeTypes = {
osc: Osc,
};
export default function App() {
const store = useStore(selector, shallow);
return (
<ReactFlow
nodes={store.nodes}
nodeTypes={nodeTypes}
edges={store.edges}
onNodesChange={store.onNodesChange}
onEdgesChange={store.onEdgesChange}
onConnect={store.addEdge}
>
<Background />
</ReactFlow>
);
}
不要なレンダリングを避ける。
<App />
コンポーネントの外側でnodeTypes
を定義する(またはReactのuseMemo
を使用する)ことが重要です。そうすることで、毎回再計算するのを防ぐことができます。
開発サーバーが実行されている場合でも、まだ何も変わっていないと慌てないでください!一時的なノードにはまだ適切な型が割り当てられていないため、React Flowはデフォルトのノードのレンダリングに戻ります。frequency
とtype
の初期値を指定して、それらのノードの1つをosc
に変更すると、カスタムノードがレンダリングされるはずです。
const useStore = createWithEqualityFn((set, get) => ({
nodes: [
{ type: 'osc',
id: 'a',
data: { frequency: 220, type: 'square' },
position: { x: 0, y: 0 }
},
...
],
...
}));
ゲインノードの実装はほぼ同じプロセスなので、それはあなたにお任せします。代わりに、出力ノードに注目しましょう。このノードにはパラメーターコントロールはありませんが、信号処理のオン/オフを切り替えたいと考えています。オーディオコードをまだ実装していないため、現時点では少し困難なので、とりあえずフラグをストアに追加し、それを切り替えるアクションを追加します。
const useStore = createWithEqualityFn((set, get) => ({
...
isRunning: false,
toggleAudio() {
set({ isRunning: !get().isRunning });
},
...
}));
カスタムノード自体は非常にシンプルです。
import React from 'react';
import { Handle } from '@xyflow/react';
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>
<Handle type="target" position="top" />
<div>
<p>Output Node</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
を作成します。
const context = new AudioContext();
const nodes = new Map();
オーディオグラフの管理方法は、ストア内の様々なアクションにフックすることです。そのため、addEdge
アクションが呼び出されたときに2つのオーディオノードを接続したり、updateNode
アクションが呼び出されたときにオーディオノードのプロパティを更新したりといった処理を行います。
ハードコーディングされたノード
この記事の前半でストアにいくつかのノードをハードコーディングしましたが、オーディオグラフはそれらのノードについて何も知りません!完成したプロジェクトでは、これらのハードコーディングされた部分をすべて削除できますが、現時点では、いくつかのオーディオノードもハードコーディングすることが**非常に重要**です。
実施方法
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. ノードの変更
現在、グラフ内で発生する可能性があり、対応する必要があるノードの変更には2つのタイプがあります。ノードのdata
の更新と、グラフからのノードの削除です。前者については既にアクションを用意しているので、まずそちらを処理しましょう。
audio.js
で、ノードのIDと部分的なdata
オブジェクトを引数として呼び出し、Map
内の既存のノードを更新するために使用するupdateAudioNode
関数を作成します。
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
である場合があります。
次に、ストア内のupdateNode
アクションを更新して、更新の一部としてこの関数を呼び出すようにします。
import { updateAudioNode } from './audio';
export const useStore = createWithEqualityFn((set, get) => ({
...
updateNode(id, data) {
updateAudioNode(id, data);
set({ nodes: ... });
},
...
}));
次に処理する必要がある変更は、グラフからノードを削除することです。グラフでノードを選択してバックスペースキーを押すと、React Flowによってノードが削除されます。これはonNodesChange
アクションによって暗黙的に処理されますが、追加の処理が必要なため、React FlowのonNodesDelete
イベントに新しいアクションを接続する必要があります。
これは非常に簡単なので、コメントなしで次の3つのコードスニペットを示します。
export function removeAudioNode(id) {
const node = nodes.get(id);
node.disconnect();
node.stop?.();
nodes.delete(id);
}
唯一注意すべき点は、onNodesDelete
は一度に複数のノードを削除できるため、削除されたノードの*配列*を指定されたコールバック関数に渡すことです!
2. エッジの変更
実際に音を出す準備が整ってきました!残りはグラフのエッジの変更を処理することだけです。ノードの変更と同様に、新しいエッジの処理を行うアクションは既に用意されており、onEdgesChange
で削除されたエッジも暗黙的に処理されています。
新しい接続を処理するには、addEdge
アクションで作成されたエッジからsource
IDとtarget
IDを取得するだけです。その後、Map
で2つのノードを検索し、それらを接続できます。
export function connect(sourceId, targetId) {
const source = nodes.get(sourceId);
const target = nodes.get(targetId);
source.connect(target);
}
React FlowがonNodesDelete
ハンドラーを受け入れることが分かりましたが、onEdgesDelete
ハンドラーも存在します!disconnect
を実装し、ストアとReact Flowインスタンスに接続する方法は、以前と同じなので、これも皆さんにお任せします!
3. スピーカーのオン/オフ
AudioContext
は、潜在的に迷惑な自動再生の問題を防ぐために、サスペンドされた状態から開始されることを思い出してください。<Out />
コンポーネントに必要なデータとアクションをストアで既に偽装しているので、今度はそれらを実際のコンテキストの状態と再開/一時停止メソッドに置き換えるだけです。
export function isRunning() {
return context.state === 'running';
}
export function toggleAudio() {
return isRunning() ? context.suspend() : context.resume();
}
これまでオーディオ関数から何も返していませんでしたが、これらのメソッドは非同期であるため、ストアを早期に更新したくないので、toggleAudio
から値を返す必要があります!
import { ..., isRunning, toggleAudio } from './audio'
export const useStore = createWithEqualityFn((set, get) => ({
...
isRunning: isRunning(),
toggleAudio() {
toggleAudio().then(() => {
set({ isRunning: isRunning() });
});
}
}));
さあ、できました!これで、実際に*音を出す*ための準備が整いました!動作を確認してみましょう。
4. 新しいノードの作成
これまで、グラフではハードコーディングされたノードセットを扱ってきました。これはプロトタイピングには問題ありませんでしたが、実際に役立つためには、グラフに動的に新しいノードを追加する方法が必要です。最後のタスクは、この機能を追加することです。オーディオコードから始めて、基本的なツールバーを作成することで、逆方向に作業します。
createAudioNode
関数の実装は簡単です。必要なのは、新しいノードのID、作成するノードの種類、およびその初期データだけです。
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;
}
}
}
次に、ストアにcreateNode
関数が必要になります。ノードIDはnanoidによって生成され、各ノードタイプに対して初期データをハードコーディングするため、渡す必要があるのは作成するノードの種類だけです。
import { ..., createAudioNode } from './audio';
export const useStore = createWithEqualityFn((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 />
プラグインコンポーネントを使用します。
...
import { ReactFlow, Panel } from '@xyflow/react';
...
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
アクションを適切なタイプでトリガーするボタンをいくつか配置するだけで済みます。
<Panel position="top-right">
<button onClick={() => store.createNode('osc')}>osc</button>
<button onClick={() => store.createNode('amp')}>amp</button>
</Panel>
そして…すべて完了です!これで、次のことができる完全に機能するオーディオグラフエディターができました。
- 新しいオーディオノードの作成
- いくつかのUIコントロールを使用したノードデータの更新
- ノードの接続
- ノードと接続の削除
- オーディオ処理の開始と停止
冒頭のデモをもう一度ご覧いただけますが、今回はソースコードも確認して、何も見逃していないことを確認できます。
最後に
長かったですが、やり遂げました!その努力の甲斐あって、楽しくインタラクティブなオーディオプレイグラウンドが完成し、Web Audio APIについて少し学び、React Flowグラフを「実行」するアプローチの1つについてより深く理解できました。
ここまで読んで「Hayleigh、Web Audioアプリは二度と書きません。何か役立つことは学びましたか?」と思っているなら、幸運です。なぜなら、学びました!Web Audio APIへの接続アプローチを、behave-graphのような他のグラフベースの計算エンジンに適用できます。実際、そうした人がいてbehave-flowを作成しました!
このプロジェクトを拡張する方法は他にもたくさんあります。作業を続けたい場合は、いくつかのアイデアを以下に示します。
- ノードタイプの追加。
- ノードが他のノードの
AudioParams
に接続できるようにする。 AnalyserNode
を使用して、ノードまたは信号の出力を視覚化する。- 他に思いつくことなら何でも!
そして、インスピレーションを探しているなら、オーディオ関連のノードベースのUIを使用しているプロジェクトがいくつかあります。私のお気に入りはMax/MSP、Reaktor、そしてPure Dataです。MaxとReaktorはクローズドソースの商用ソフトウェアですが、それでもアイデアを盗むことができます .
完成したソースコードを起点として使用するか、今日作成したものの上に構築し続けることができます。皆さん作成したものが是非見たいので、DiscordサーバーまたはTwitterで共有してください。
React Flowは、ユーザーによって資金提供される独立した企業です。支援したい場合は、Githubでスポンサーになるか、Proプランのいずれかに登録することができます。