Files
quartz-research-note/content/多段階計算と増分関数型リアクティブプログラミングのによる信号処理のライブコーディング.md
松浦 知也 Matsuura Tomoya 4008cd0382
All checks were successful
Build / build (push) Successful in 10m8s
[obsidian] vault backup: 2025-11-03 11:42:18[
2025-11-03 11:42:18 -05:00

136 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
date: 2025-09-25 11:42
---
#paper #research
(NIME2026のドラフトです)
## 概要
本稿では、筆者の開発している関数型音楽プログラミング言語[[mimium]]で用いている、デジタル信号処理を対象のドメインに含めたライブコーディングシステムの設計を述べる。
音声信号処理をターゲットにしたプログラミング言語では、ソースコードを更新して評価し直すたびにディレイやフィルタなどの信号処理プロセッサの内部状態がリセットされることが一般的である。これはライブコーディングのように、実行中にソースコードを書き換えて演奏をするようなユースケースを阻む壁の一つである。
そこで筆者の開発する音楽プログラミング言語mimiumの機能を拡張し、信号処理で使われる内部状態の構造を変更前後で比較し、可能な限り変更前の状態を持ち越して新しいソースコードで評価できる仕組みを設計した。
このシステムの特徴は、ソースコード自体の変更増分を解析せずに、全てのソースコードを毎回再コンパイルし直し、コールツリーに基づく内部状態の構造の比較のみを行う点である。この方法を採用することで、既存のコンパイラやVMの定義の変更を最小限にしたままライブ評価を実現できる。
## 背景とモチベーション
音楽のためのプログラミングにおけるライブコーディングとは、音楽を生成するプログラムのソースコードをリアルタイムで書き換えながら演奏するパフォーマンスのスタイルである[@magnussonAlgorithmsScoresCoding2011]。
既存の信号処理をターゲットにした音楽プログラミング言語における問題の一つとして、コードの変更時に信号処理の内部状態がリセットされる問題がある。ディレイやフィルターは、内部状態メモリへの継続的な書き込みと読み込みを行うことで処理を実現しているが、その内部状態のインスタンスはコードのコンパイル後、信号処理を実際に始める前に0埋めで初期化されることが一般的である。
MaxMSP[@Max]やPureData[@puckettePureData1997]、SuperCollider[@McCartney2002]のJITLibにおける信号処理のように、信号処理のインスタンスのグラフ構成自体を実行中に変更できるような仕組みの場合、内部状態はキープされる。TidaiCycles[@McLean2014]やSonic Pi[@Aaron2013]のようなSuperColliderのクライアントとして実装される言語も同様である一方、信号処理を使った表現の幅はSuperColliderのプリミティブとして用意されたUnit Generatorの組み合わせに留まることになる。
Faust[@Orlarey2009]やMaxのGen、のように、サンプル単位レベルでの信号処理の記述ができるプログラミング言語の場合は、コードを一度低レベルな命令FaustであればLLVM IRなどに変換し、そのコードをインスタンス化してから実行するために、インスタンス化のタイミングで毎回内部状態はリセットされる。
同様に例えばChucK[@Wang2015]はShredという単位で信号処理インスタンスを実行中に追加、削除、更新ができるが、1つのShredが更新されるごとに内部状態はリセットされる。そのため、複数のShredが実行されていればどれか1つのShredを更新するたびに無音が挟まるようなことはないものの、Shredの中でディレイやリバーブを使用していた場合、そのディレイやリバーブのテールは更新時に途切れてしまう。
こうした特徴をまとめると、音楽プログラミング言語の設計には記述できる信号処理の最小単位を小さくしていくほど、コードの動的変更に対応することが難しくなるトレードオフがあるといえる。
こうした課題に対し、Reachは関数型のUnit Generatorを組み合わせて信号処理を記述する言語で、ソースコードの変更差分を解析して信号処理の内部状態を可能な限り保持する仕組み:**Incremental Functional Reactive Programming**(IcFRP)を提案している[@reach_incremental_2013]。この仕組みは、SuperColliderのJITLibのようなシステムと比べるとユーザーが現在の信号処理インスタンスに対して削除や追加などの命令を行うのではなく、常にその時のソースコードに望む信号処理を書けば必要な状態の更新はランタイム側が自動で担ってくれるという点で、ユーザーの演奏中の思考モデルが大きく異なると言える。
ただ、実装としては、ソースコードの単なるテキスト差分の解析では、複数の変更のパターンの可能性を絞り込めないため、テキストエディタEmacsの拡張として操作の履歴を取得することで実装しているため、実装は複雑なものと言える。
本稿では、筆者が開発してきた関数型音楽プログラミング言語mimiumに、IcFRPの考え方を応用したライブコーディングシステムを提案する。
以下、本論文はmimiumのこれまでの言語設計の簡単な説明と、導入される2種類の機能拡張について順番に説明する。
その後、本ライブコーディングシステムの他のシステムと比較した特徴および問題点を議論する。
## mimium and lambda-mmm
mimiumは、Rustに近いシンタックスを持った関数型の音楽信号処理をターゲットにしたプログラミング言語である[@matsuura2021]。現在の内部実行モデルとして、値呼び単純型付きラムダ計算を拡張し、最小限の内部状態を持つプリミティブ操作ディレイとフィードバックを加えたLambda-mmm[@matsuura_lambda-mmm_2024]という計算体系を持っている。
mimiumはコードを専用
内部状態はDelayかMem、Feedのいずれかに還元される。
Memは1サンプルのみのディレイであるため、最終的な計算結果としてDelayに与える遅延時間を1としたものと同一に見做せるが、実行モデル的に1サンプルのみのディレイはリングバッファの読み書きインデックスの追跡が必要ないため、区別しておく方が効率的に実行できる。
## コールツリーの解析
- クロージャ
- 関数定義内でグローバルでもローカルでもない変数を参照している場合。
クロージャ(ほとんどの場合、高階関数)に関わる内部状態は、クロージャインスタンス上に保持される。
内部状態を参照するのは、ディレイ、Feed、非クロージャ関数呼び出しのいずれか。
エントリポイント`dsp`からの非クロージャ関数呼び出しを辿っていけば静的に使用する内部状態の木構造が導出できる。
再帰関数を用いて複雑な信号処理を実現する場合でも、多段階計算を使用してコードを記述した場合は、実行時にクロージャを生成することなく静的な関数呼び出し
## 状態構造ツリー同士の比較
ソースコードをなるべく頻繁に編集するのであれば、内部状態の構造の変化は木の要素のうち1箇所の削除、追加、置き換えである可能性が高い。
まず先頭、末尾からのリニアスキャンで比較
まだ完了していなければ、最長共通部分列の検出を行う。
内部状態構造を比較する木は、あくまで線形メモリ上の参照範囲を保持しており、データそのものを木構造として保持しているわけではない。
まず、新しい内部状態木構造から必要なメモリサイズを算出し、メモリをアロケートする。
その後、木構造の比較を行い、必要な差分変更処理のパッチを作成する。
(ここまではオーディオスレッドをブロックせずに実行可能)
古い内部メモリと木構造にパッチを適用し、新しいメモリへ必要なデータをコピーする。
信号処理インスタンスを、新たに確保したメモリと共にオーディオバッファ処理の間に入れ替える。
## 問題点
差分処理を実行している間に内部状態が更新されてはいけない。なので、新しいソースコードのパースや内部状態構造導出、VMコード生成、木の比較までは非同期で行えるが、コピー中はオーディオ処理全てを一度中断しなくてはならない。
構造が大きくなった時にドロップアウトしないか。木のサイズでざっくりベンチを取りたい。バッファサイズの参考にできるはず
たまたま入れ替えた時に、引き継いではいけないはずのデータを引き継ぐ可能性がある。
```rust
fn osc(freq){
let phase = samplerate/freq
(self+phase)%1.0 |> sin
}
fn myfreq(){
1000 + osc(1.0)*100 //oscは1Feedを持つ
}
fn myamp(){
(osc(1.0)+1.0) / 2
}
//変更前
fn dsp(){
osc(myfreq())
}
//変更後
fn dsp(){
osc(1000)*myamp()
}
```
上のサンプルでは、はじめlfoを使って周波数をモジュレーションしている状態から、周波数は固定にして音量をモジュレーションする処理へと切り替えた例である。myfreq()とmyamp()はそれぞれどちらもosc関数を1度だけ呼び出すため、dsp関数の内部状態ツリーの構成は共通しており、再コンパイル時にデータが引き継がれる。
この時、myampにはmyfreqの最後の位相が引き継がれることになるが、これは特に望ましいというわけではないどうでもよさそうではあるけど
myfreqとmyampをそれぞれselfを使って個別に実装していた場合、値の範囲がおかしくなる可能性がある。
ツリー構築の際に、関数ラベルをつけるようにすればいいが、ラベルのネーミング方法に一貫性がないとダメ
親の関数名+child1,2,3...みたいにすれば多分大丈夫。
## 将来的な展望
原理的にはFaustでも実現できるはず。
ディレイ、Feed以外に、外部定義の関数呼び出しにもこの仕組みを応用できるかLuaのUserData的な仕組み。
Faustにおけるrwtableのように、単にCell的な仕組みを用意すればDelayやMemもこの上に乗っかる形で全部カバーできるはず
rwtable(read_index:float,write_index:float,input:float,size:const-float)