(NIME2026のドラフトです)
概要
本稿では、筆者の開発している関数型音楽プログラミング言語mimiumで用いている、デジタル信号処理を対象のドメインに含めたライブコーディングシステムの設計を述べる。
音声信号処理をターゲットにしたプログラミング言語では、ソースコードを更新して評価し直すたびにディレイやフィルタなどの信号処理プロセッサの内部状態がリセットされることが一般的である。これはライブコーディングのように、実行中にソースコードを書き換えて演奏をするようなユースケースを阻む壁の一つである。
そこで筆者の開発する音楽プログラミング言語mimiumの機能を拡張し、信号処理で使われる内部状態の構造を変更前後で比較し、可能な限り変更前の状態を持ち越して新しいソースコードで評価できる仕組みを設計した。
このシステムの特徴は、ソースコード自体の変更増分を解析せずに、全てのソースコードを毎回再コンパイルし直し、コールツリーに基づく内部状態の構造の比較のみを行う点である。この方法を採用することで、既存のコンパイラやVMの定義の変更を最小限にしたままライブ評価を実現できる。
背景とモチベーション
音楽のためのプログラミングにおけるライブコーディングとは、音楽を生成するプログラムのソースコードをリアルタイムで書き換えながら演奏するパフォーマンスのスタイルである[@magnussonAlgorithmsScoresCoding2011]。
既存の信号処理をターゲットにした音楽プログラミング言語における問題の一つとして、コードの変更時に信号処理の内部状態がリセットされる問題がある。ディレイやフィルターは、内部状態(メモリ)への継続的な書き込みと読み込みを行うことで処理を実現しているが、その内部状態のインスタンスはコードのコンパイル後、信号処理を実際に始める前に0埋めで初期化されることが一般的である。
SuperCollider[@McCartney2002]のJITLibにおける信号処理のように、信号処理のインスタンスのグラフ構成自体を実行中に変更できるような仕組みの場合、内部状態はキープされる。TidaiCycles[@McLean2014]やSonic Pi[@Aaron2013]のようなSuperColliderのクライアントとして実装される言語も同様である一方、信号処理を使った表現の幅はSuperColliderのプリミティブとして用意されたUnit Generatorの組み合わせに留まることになる。
Faust[@Orlarey2009]やMaxのGen、のように、サンプル単位レベルでの信号処理の記述ができるプログラミング言語の場合は、コードを一度低レベルな命令(FaustであればLLVM IRなど)に変換し、そのコードをインスタンス化してから実行するために、インスタンス化のタイミングで毎回内部状態はリセットされる。
同様に例えばChucK[@wang_chuck_2015]はShredという単位で信号処理インスタンスを実行中に追加、削除、更新する形でライブコーディングを実現している言語だが、1つのShredが更新されるごとに内部状態はリセットされる。そのため、複数のShredが実行されていればどれか1つのShredを更新するたびに無音が挟まるようなことはないものの、Shredの中でディレイやリバーブを使用していた場合、そのディレイやリバーブのテールは更新時に途切れてしまう。
こうした特徴をまとめると、音楽プログラミング言語の設計には記述できる信号処理の最小単位を小さくしていくほど、コードの動的変更に対応することが難しくなるトレードオフがあるといえる。
こうした課題に対し、Reachは関数型のUnit Generatorを組み合わせて信号処理を記述する言語で、ソースコードの変更差分を解析して信号処理の内部状態を可能な限り保持する仕組み:Incremental Functional Reactive Programming(以下本稿ではIcFRPと呼ぶ)を提案している[@reach_incremental_2013]。この仕組みは、SuperColliderのJITLibのようなシステムと比べるとユーザーが現在の信号処理インスタンスに対して削除や追加などの命令を行うのではなく、常にその時のソースコードに望む信号処理を書けば必要な状態の更新はランタイム側が自動で担ってくれるという点で、ユーザーの演奏中の思考モデルが大きく異なると言える。
ただ、Reachによる実装としては、ソースコードの単なるテキスト差分の解析では、複数の変更のパターンの可能性を絞り込めないため、各項に隠れたラベルを紐づけた上で、テキストエディタEmacsの拡張機能として、切り取りや貼り付けといった操作の履歴を取得することで実装されている。
本稿では、筆者が開発してきた関数型音楽プログラミング言語mimiumに、IcFRPの考え方を応用しつつ、単純なテキスト比較のみで動作するライブコーディングシステムを提案する。
以下、本論文はmimiumのこれまでの言語設計の簡単な説明と、導入される2種類の機能拡張について順番に説明する。その後、本ライブコーディングシステムの他のシステムと比較した特徴および問題点を議論する。
mimium and lambda-mmm
mimiumは、Rustに近いシンタックスを持った関数型の音楽信号処理をターゲットドメインにしたプログラミング言語である[@matsuura_mimium_2021]。現在の内部実行モデルとして、値呼び単純型付きラムダ計算を拡張し、最小限の内部状態を持つプリミティブ操作:ディレイとフィードバックを加えたLambda-mmm[@matsuura_lambda-mmm_2024]という計算体系を持っている。
mimiumはコードを専用のVMバイトコードへコンパイルし実行する。実行モデルは、一般的なレジスタマシンの命令セットに、内部状態操作用の操作が加わったものとなる。ディレイやフィードバックで用いられる内部状態は、状態ストレージという1次元の配列領域と単一の読み出し位置ポインタを組み合わせたデータ領域に保存される。
コンパイラは、状態ストレージの読み出し位置ポインタを相対的に前後させる命令を適切に出力することで、VM実行時にはストレージの特定領域をフィードバックの状態変数やディレイ用のリングバッファとして解釈しデータを読み書きする。
過去のmimiumでは高階関数などを使うことによって任意の数のオシレーターバンクのような、パラメトリックなプロセッサを生成することもできたが、こうしたプロセッサは本質的に状態ストレージのレイアウトとメモリサイズをコンパイル時に決定できない。これはif文などを通じて複数サイズの状態ストレージを操作する関数が一つの変数に縮約されうるためである。
そのためmimiumでは、クロージャ(実行時に高階関数から生成される関数)と、そうでない自由変数をキャプチャしない関数の呼び出しを区別し、クロージャのインスタンスには個別の状態ストレージを生成し、クロージャ呼び出し時に使用する状態ストレージそのものを切り替えることで対応している。
今回提案するライブコーディング機能は、2つの機能追加によって実現される。
1つは、状態ストレージのレイアウトをコンパイル時に可能な限り確定させるために、多段階計算という型安全なマクロの体系を言語に導入することである。
もう1つは、生成されたプログラム同士の状態ストレージのレイアウトを比較することで、可能な限り前の内部状態を引き継いだ新しい状態ストレージを計算する解析プログラムの導入である。
この2機能を組み合わせることで、mimiumは単純にソースコードを毎回最初からコンパイルし、前のプログラムとの比較を行い、新しい内部状態ストレージを含むVMを生成しインスタンスを入れ替えることで、エディタとの連携なしにライブコーディング機能を実現する。
多段階計算
多段階計算は、型付きラムダ計算に対して、計算のステージを複数段階に分割する明示的なシンタックスを導入するものである。Lisp系言語のquote/splice機能[@lisp]のように、部分的に計算したコード片を埋め込むようなものを想定しているが、不正な値が埋め込まれないことを型システムとして保証することが特徴である(逆に、通常マクロに期待される型システムの範囲を超えたメタ操作を行うことは許されない)。実用的な例では、Scala 3でのマクロや、関数型組版処理エンジンSaTysFi[@suwa2024]のように、言語内DSLを型安全にライブラリとして実装することを想定しているものがある。mimiumにおいては、多段階計算はコンパイル時に行う計算(=シグナルグラフのルーティングの決定)と、ランタイム時に行う計算(=実際のオーディオ処理)を明示的に区別するのに用いている。
シンタックスの拡張
図nにmimiumの内部表現Lambda-mmmに多段階計算の体系を加えた新しい内部表現 (Multi-stage version of )のシンタックスを定義する。
型にはCode Type を加える。(一般的な多段階計算言語では)
\begin{align}<!-- line:69 --> \tau ::=<!-- line:70 --> &\quad R \quad &\\<!-- line:71 --> |&\quad I_n \quad &n \in \mathbb{N} \\<!-- line:72 --> |&\quad \tau_1 → \tau_2 &\\<!-- line:73 --> |&\quad (\tau_1,\tau_2,...) &\\<!-- line:74 --> |&\quad ` \tau<!-- line:75 --> \end{align}<!-- line:76 --> $$<!-- line:77 --> <!-- line:78 --> 式にはクオート($`e$)とスプライス($\$e$)を加える。これはMetaMLなど代表的な多段階計算言語における$.\langle e\rangle.$と$\textasciitilde e$という記法に対応するものである。<!-- line:79 --> $$<!-- line:80 --> \begin{align}<!-- line:81 --> e ::=<!-- line:82 --> &\quad R & R \in \mathbb{R}\ [number]&\\<!-- line:83 --> |&\quad x & [var] \\<!-- line:84 --> |&\quad e_1 \ e_2 \quad& [app]&\\<!-- line:85 --> |&\quad (e_1,e_2,...) \quad& [tuple]&\\<!-- line:86 --> |&\quad e.n \quad& n \in \mathbb{N}\ [proj]&\\<!-- line:87 --> |&\quad \lambda x.e& [abs]&\\<!-- line:88 --> |&\quad let\; x\; =\; e_1\; in\; e_2& [let]&\\<!-- line:89 --> |&\quad `e &[quote]&\\<!-- line:90 --> |&\quad $e &[splice]&\\<!-- line:91 --> |&\quad delay(n,e_{time},v_{bound})&[delay]&\\<!-- line:92 --> |&\quad mem(e) &[mem]&\\<!-- line:93 --> |&\quad feed\ x.e &[feed]&<!-- line:94 --> \end{align}<!-- line:95 --> $$<!-- line:96 --> <!-- line:97 --> $$<!-- line:98 --> \begin{align}<!-- line:99 --> v ::=<!-- line:100 --> |&\quad R & R \in \mathbb{R} [number]&\\<!-- line:101 --> |&\quad openFn(x,e,statesize) &[closure]&\\<!-- line:102 --> |&\quad closure(x,e,E) &[closure]&\\<!-- line:103 --> |&\quad \langle e \rangle &[code]&\\<!-- line:104 --> \end{align}<!-- line:105 --> $$<!-- line:106 --> <!-- line:107 --> <!-- line:108 --> delay,mem,feedは信号処理用のプリミティブである。mem(e)はシングルサンプルのディレイだが、これは遅延時間のデータを保存する必要がないため通常のディレイと区別して使用される。これら3つのプリミティブはステージ1に束縛された組み込み関数のように扱われ、ステージ0評価で現れるとエラーになる。<!-- line:109 --> <!-- line:110 --> また、多段階計算において重要な、`run`プリミティブを用いて一つ上のステージで定義されたものをその場で使用する越段階埋め込み(Cross-Stage Persistence)はmimiumでは実装されていない。一方、コンパイル時に計算した数値などをランタイムで使用するために、プリミティブをコード型へ持ち上げる`lift`関数はプリミティブとして用意されている。mimiumの型システムは現時点でジェネリクスを搭載していないため、各基本型毎に異なる名前の組み込み関数(floatに対するlift_fなど)が用意される。<!-- line:111 --> <!-- line:112 --> #### シンタックスシュガー<!-- line:113 --> <!-- line:114 --> mimiumでは、多段階計算の体系を直感的に扱えるように2つのシンタックスシュガーを導入している。<!-- line:115 --> <!-- line:116 --> 1つは、ステージ0でコード辺を返す関数`mymacro`を実行した結果をスプライスでステージ1に埋め込む操作、`$(mymacro(arg))`を、`mymacro!(arg)`と書けるようにするものである。<!-- line:117 --> <!-- line:118 --> もう1つは、変数のスコープを維持したままだとスプライスとクオートの括弧がネストされてしまう問題を回避するために、グローバルスコープに限定し、#stage(macro)/#stage(main)と書くことで、スコープを維持したままその行以下は指定したステージになるというものである。<!-- line:119 --> <!-- line:120 --> ### 多段階計算によるメタ操作の実例<!-- line:121 --> <!-- line:122 --> 以下、実際のmimiumのコードを用いて実例を解説する。以下で見せるコードはいずれもmimiumで実際に動作する、パラメトリックな加算合成シンセサイザーのコードである。<!-- line:123 --> <!-- line:124 --> まず、Code.1は多段階計算を使わずに、再帰関数でクロージャを生成する例である。この例では、コードは一度コンパイルされ、ランタイムでグローバル環境が評価されるタイミングでadditive関数が実行され、myoscにクロージャが束縛される。この時、osc関数の内部状態はランタイムのクロージャインスタンスの上にそれぞれ確保される。<!-- line:125 --> <!-- line:126 --> ```rust fn additive(n,gen){<!-- line:128 --> let g = gen()<!-- line:129 --> if (n>1){<!-- line:130 --> let next = additive(n-1 ,gen)<!-- line:131 --> |rate| next(rate) + g(rate*n)/n<!-- line:132 --> }else{<!-- line:133 --> |rate| g(rate) <!-- line:134 --> }<!-- line:135 --> }<!-- line:136 --> let PI = 3.14159265359<!-- line:137 --> fn phasor_shift (freq,phase_shift){<!-- line:138 --> (self + freq/samplerate + phase_shift)%1.0<!-- line:139 --> }<!-- line:140 --> fn sinwave = (freq,phase){<!-- line:141 --> phasor_shift(freq,phase)*2.0*PI |> sin<!-- line:142 --> }<!-- line:143 --> fn osc = (freq){<!-- line:144 --> sinwave(freq,0.0) * 0.5<!-- line:145 --> }<!-- line:146 --> let myosc = additive(5, | | osc);<!-- line:147 --> fn dsp(){<!-- line:148 --> let f = 200<!-- line:149 --> let r = f |> myosc<!-- line:150 --> (r,r)<!-- line:151 --> }<!-- line:152 --> ``` <!-- line:154 --> 次に、Code.2が、多段階計算を用いたバージョンである。このコードは、シンタックスシュガーを外して純粋なクオートとスプライスをネストした表現に直すとCode.3のように展開される。ここで、additive関数はバイトコード生成の前の段階で、ステージ0で評価されCode.4のように展開される。<!-- line:155 --> <!-- line:156 --> ```rust #stage(macro)<!-- line:158 --> fn additive(n,gen){<!-- line:159 --> if (n>1){<!-- line:160 --> let n_lifted = lift_f(n)<!-- line:161 --> let next = additive(n-1 ,gen)<!-- line:162 --> `|rate| ($next)(rate) + ($gen)(rate*$n_lifted) / $n_lifted<!-- line:163 --> }else{<!-- line:164 --> `|rate| ($gen)(rate)<!-- line:165 --> }<!-- line:166 --> }<!-- line:167 --> #stage(main)<!-- line:168 --> let PI = 3.14159265359<!-- line:169 --> ...<!-- line:170 --> fn osc = (freq){<!-- line:171 --> ...//as same as non-macro version<!-- line:172 --> }<!-- line:173 --> fn dsp(){<!-- line:174 --> let f = 200<!-- line:175 --> let r = f |> additive!(5,`osc)<!-- line:176 --> (r,r)<!-- line:177 --> }<!-- line:178 --> ``` <!-- line:180 --> ```rust `{ // after desugar<!-- line:182 --> ${<!-- line:183 --> let additive = |n,gen|{<!-- line:184 --> if (n>1){<!-- line:185 --> let n_lifted = lift_f(n)<!-- line:186 --> let next = additive(n-1 ,gen)<!-- line:187 --> `|rate| ($next)(rate) + ($gen)(rate*$n_lifted) / $n_lifted<!-- line:188 --> }else{<!-- line:189 --> `|rate| ($gen)(rate)<!-- line:190 --> }<!-- line:191 --> }<!-- line:192 --> `{<!-- line:193 --> let osc = | | {<!-- line:194 --> ...<!-- line:195 --> }<!-- line:196 --> let dsp = | |{<!-- line:197 --> let f = 200<!-- line:198 --> let r = f |> $additive(5,`osc)<!-- line:199 --> (r,r)<!-- line:200 --> }<!-- line:201 --> }<!-- line:202 --> }<!-- line:203 --> }<!-- line:204 --> ``` <!-- line:206 --> ステージ1で定義されているosc関数は、ステージ0でadditive関数の実行を通じてdsp関数の中に埋め込まれる。ここで、変数nは元々ステージ0で評価される変数のため、lift_fプリミティブ関数を用いてステージ1へ持ち越されている。<!-- line:207 --> <!-- line:208 --> ```rust // after macro expansion<!-- line:210 --> let osc = | | {<!-- line:211 --> ...<!-- line:212 --> }<!-- line:213 --> let dsp = | |{<!-- line:214 --> let f = 200<!-- line:215 --> let r = f |> |f| osc(f*5)/5 + osc(f*4)/4 + ... osc(f)<!-- line:216 --> (r,r)<!-- line:217 --> }<!-- line:218 --> ``` <!-- line:220 --> マクロが展開された時点で、自由変数をキャプチャするクロージャはコードから消えていることがわかる。実行時にクロージャインスタンスは生成されないため、内部状態ストレージはグローバル環境に単なる配列としてアロケートすることが可能になる。<!-- line:221 --> <!-- line:222 --> ## コールツリーの解析<!-- line:223 --> <!-- line:224 --> ```rust enum WordSize(u64);<!-- line:226 --> enum MaxTime(u64);<!-- line:227 --> enum StateTree{<!-- line:228 --> Feed(WordSize),<!-- line:229 --> Mem(WordSize),<!-- line:230 --> Delay(MaxTime),<!-- line:231 --> OpenFnCall(Vec<StateTree>)<!-- line:232 --> }<!-- line:233 --> ``` <!-- line:235 --> - クロージャ<!-- line:236 --> - 関数定義内でグローバルでもローカルでもない変数を参照している場合。<!-- line:237 --> <!-- line:238 --> クロージャ(ほとんどの場合、高階関数)に関わる内部状態は、クロージャインスタンス上に保持される。<!-- line:239 --> <!-- line:240 --> (非クロージャなダイレクトな関数呼び出しだとしても、関数の変数に対して破壊的代入で別の関数代入するとぶっ壊れる可能性あるな)<!-- line:241 --> <!-- line:242 --> 内部状態を参照するのは、ディレイ、Feed、非クロージャ関数呼び出しのいずれか。<!-- line:243 --> <!-- line:244 --> エントリポイント`dsp`からの非クロージャ関数呼び出しを辿っていけば静的に使用する内部状態の木構造が導出できる。<!-- line:245 --> <!-- line:246 --> 再帰関数を用いて複雑な信号処理を実現する場合でも、多段階計算を使用してコードを記述した場合は、実行時にクロージャを生成することなく静的な関数呼び出し<!-- line:247 --> <!-- line:248 --> ## 状態構造ツリー同士の比較<!-- line:249 --> <!-- line:250 --> ソースコードをなるべく頻繁に編集するのであれば、内部状態の構造の変化は木の要素のうち1箇所の削除、追加、置き換えである可能性が高い。<!-- line:251 --> <!-- line:252 --> まず先頭、末尾からのリニアスキャンで比較<!-- line:253 --> <!-- line:254 --> まだ完了していなければ、最長共通部分列の検出を行う。<!-- line:255 --> <!-- line:256 --> 再帰的なマッチの仕方 パーシャルマッチみたいなものが起きうる<!-- line:257 --> <!-- line:258 --> 内部状態構造を比較する木は、あくまで線形メモリ上の参照範囲を保持しており、データそのものを木構造として保持しているわけではない。<!-- line:259 --> <!-- line:260 --> まず、新しい内部状態木構造から必要なメモリサイズを算出し、メモリをアロケートする。<!-- line:261 --> <!-- line:262 --> その後、木構造の比較を行い、必要な差分変更処理のパッチを作成する。<!-- line:263 --> <!-- line:264 --> <!-- line:265 --> <!-- line:266 --> (ここまではオーディオスレッドをブロックせずに実行可能)<!-- line:267 --> 古い内部メモリと木構造にパッチを適用し、新しいメモリへ必要なデータをコピーする。<!-- line:268 --> <!-- line:269 --> 信号処理インスタンスを、新たに確保したメモリと共にオーディオバッファ処理の間に入れ替える。<!-- line:270 --> <!-- line:271 --> ### サンプルコード<!-- line:272 --> <!-- line:273 --> ```diff fn phasor(freq){<!-- line:275 --> (self+(1/freq))%1.0<!-- line:276 --> }<!-- line:277 --> fn osc(freq){<!-- line:278 --> - phasor(freq)* 2 * PI |> sin<!-- line:279 --> + phasor(freq+(phasor(freq/10))) * 2 * PI |> sin<!-- line:280 --> }<!-- line:281 --> ``` <!-- line:283 --> <!-- line:284 --> ## 議論<!-- line:285 --> <!-- line:286 --> <!-- line:287 --> Reachのシステムと比較したときに、再帰によるパラメトリックなDSPコード生成に対応したままライブコーディング機能を実現できていること、またその実現にテキストエディタ固有の機能に依存せず、単にソースコード同士の比較によって構造比較を行えている点がメリットと言える。<!-- line:288 --> <!-- line:289 --> エッジケースとして最長共通部分列の仕様上、`osc1()+osc2()`から`osc2()+osc1()`への変更のような、明らかに問題のない入れ替えでもどちらか片方しか引き継がれないという問題がある。また、どちらが引き継がれるかはLCSのバックトラックの戦略に依存する。関連して、引き継いではいけないはずのデータを引き継ぐ可能性がある。<!-- line:290 --> <!-- line:291 --> ```rust fn phasor1(freq){ //0 ~ samplerate/freq<!-- line:293 --> (self+1.0)% (samplerate/freq)<!-- line:294 --> }<!-- line:295 --> fn osc1(freq){<!-- line:296 --> phasor1(freq)*freq/samplerate |> sin<!-- line:297 --> }<!-- line:298 --> fn myfreq(){<!-- line:299 --> 1000 + osc1(1.0)*100 //myfreqはFncall[FnCall[Feed]]を持つ<!-- line:300 --> }<!-- line:301 --> fn phasor2(freq){ //0 ~ 1<!-- line:302 --> (self+freq/samplerate)% 1.0<!-- line:303 --> }<!-- line:304 --> fn osc2(freq){<!-- line:305 --> phasor2(freq) |> sin<!-- line:306 --> }<!-- line:307 --> fn myamp(){<!-- line:308 --> (osc2(1.0)+1.0) / 2 //myampもFncall[FnCall[Feed]]<!-- line:309 --> }<!-- line:310 --> <!-- line:311 --> //変更前<!-- line:312 --> fn dsp(){ //dspはFncall[Fncall[FnCall[Feed]],Fncall[Feed]]<!-- line:313 --> let f = myfreq()<!-- line:314 --> osc2(f)<!-- line:315 --> }<!-- line:316 --> //変更後<!-- line:317 --> fn dsp(){//dspはFncall[Fncall[FnCall[Feed]],Fncall[Feed]]で変化なし<!-- line:318 --> myamp() * osc2(1000) <!-- line:319 --> }<!-- line:320 --> ``` <!-- line:322 --> 上のサンプルでは、はじめlfoを使って周波数をモジュレーションしている状態から、周波数は固定にして音量をモジュレーションする処理へと切り替えた例である。myfreq()とmyamp()はそれぞれどちらもosc関数を1度だけ呼び出すため、dsp関数の内部状態ツリーの構成は共通しており、再コンパイル時にデータが引き継がれる。この時、myampにはmyfreqの最後の位相が引き継がれることになるが、今回実装しているphasor1はselfに保存される値が0~samplerate/freq、例えば1000Hzなら0~48の値のレンジを取り、これがmyampの中で使われているphasor2のselfのその値の本来のレンジは0~1であるべきにも関わらず引き継がれてしまう。<!-- line:323 --> <!-- line:324 --> ただ、実際には結局phasor2を実行したときには0~1のレンジに丸まるので大きな問題にはならない。ある関数がある範囲に収まることが保証されているということは、仮にそこで使われているselfに不正な値が差し込まれたとしても、その関数が計算し終わったときには元の範囲に収まる可能性が高いからだ。似たようなケースで関連性のないデータがコピーされたとしても、結局は聴感上違和感がない程度に留まる可能性もある。<!-- line:325 --> <!-- line:326 --> 現実的なユースケースを考えれば、2つの点においてこれらの問題点は気にならなくなる。<!-- line:327 --> <!-- line:328 --> 1つは、コードの規模が大きくなっていくにつれて、関数呼び出しの深さが深くなっていくため、偶然関数呼び出しの構造が一致する可能性は小さくなっていくことである。2つ目は、実際のライブコーディングにおいては複雑な構造変化を一度に行うことは少なく、あったとしてもそれは精々一箇所へのコード挿入、削除、置き換えといったアクションの連続として捉えられる。一度に大規模のコード変更をコミットせずに、個々の変更毎にセーブと更新を行えば、本システムでも問題なく対応できる。<!-- line:329 --> <!-- line:330 --> <!-- line:331 --> <!-- line:332 --> ## 将来的な展望<!-- line:333 --> <!-- line:334 --> <!-- line:335 --> 原理的にはFaustでも実現できるはず。<!-- line:336 --> <!-- line:337 --> ディレイ、Feed以外に、外部定義の関数呼び出しにもこの仕組みを応用できるか?LuaのUserData的な仕組み。<!-- line:338 --> Faustにおけるrwtableのように、単にCell的な仕組みを用意すればDelayやMemもこの上に乗っかる形で全部カバーできるはず<!-- line:339 --> <!-- line:340 --> rwtable(read_index:float,write_index:float,input:float,size:const-float)<!-- line:341 -->