Files
quartz-research-note/content/mimiumのREPLをVMで実装.md
松浦 知也 Matsuura Tomoya 5ccf09c98f
All checks were successful
Build / build (push) Successful in 8m13s
[obsidian] vault backup: 2025-07-01 18:42:02[
2025-07-01 18:42:02 +09:00

7.6 KiB
Raw Blame History

date
date
2025-06-27 17:10

#mimium #memo

今の所の問題点として

  • マシンが一つの結合されたコンパイル済みバイトコードしか受け付けていない

    • これを配列として複数保持する必要あり
  • 外部関数の参照インデックスのテーブルはVM上に載っているが、内部関数は一つのプログラム上の絶対位置しか参照できない

  • プログラム上で定義した内部関数を、他のプログラムでも参照できるようにする、シンボルテーブルがいる

  • またグローバル環境の評価も同様に、シンボル-(GlobalPos,型)変換のテーブルをコンパイラが持っていないとだめ

  • マシンをオーディオドライバ立ち上げとともにオーディオスレッドにMoveしてしまっているので、メインスレッドで新たにコンパイルしたプログラムを何かの方法でオーディオスレッドに送ってやらないといけない

  • シンボルテーブルもDashMapで作ってればなんとかなるのかな

  • VMの中にControllerみたいなDashMap系をArcで包んだものを用意しておいて、メインのマシンはオーディオスレッドに送ってしまい、Arcでコントローラーだけをメインスレッドに残しておく

  • ExecContextはRepl起動から終了まで次の情報を持ち続ける

    • グローバル変数のシンボルテーブル(型情報含む)
    • 内部関数のシンボルテーブル(型情報含む)
    • 外部関数のシンボルテーブル(型情報含む)

ExecContextは、起動時VMのインスタンスを作る。この時VMConrollerのArc参照がVM内に渡される。

ExecContextは、ソースコードを受け取るたびにコンパイラのインスタンスを作ってはプログラムを出力し、終了する。

出力されたプログラムはVMControllerのPrograms(DashMap)に非同期でインサートされ、シンボルテーブルも更新される

Replで送られた新しいプログラム一行だけのプログラムをどのスレッドで、いつ実行するかが問題

DSPの頭でReplで溜まった処理(無名関数のキュー)を順番に消化する?あるいは、

dsp = |l,r| ->(float,float){ ( new_process )}

こんな感じでREPLに送ると、実際には

fn repl_001(){
	| | {
		dsp = |l,r| ->(float,float){ ( new_process )}
	}@now
}

みたいなのを実行したことになるとか

これだとスケジューラープラグイン実装されてることが前提にはなるし、スケジューラーにタスクを登録するのをController越しに行える必要がある

というか、メインスレッドに相当するものを別途作る方がやっぱり良さそう

結局、今VMに乗ってるだいたいのものはShared Stateとして共有されて、スタック、ベースポインタ、StateStorageだけが独立する形になりそう


シングルスレッドの場合、REPLで実行されたものは必ずオーディオドライバのブロック頭で消化

マルチスレッド前提の場合、Shared State全般がRwLockとかで包まれることになってしまう

しかし、大概の場合グローバルな状態共有はAtomicな単一パラメーターで済む

あくまで、普通のグローバル変数宣言はスレッドローカルな扱いにして、メインスレッドと協調しないといけない場合は"shared"みたいなキーワードをつけるようにするとか(構文増やしたくないけど)

だし、これだと結局OSC送るサーバークライアント構成と実質的に変わらないかも

let hoge = 100;

みたいなのの代わりに、別のキーワードを持たせる

global hoge = 100

プリミティブな型であれば、Atomicな書き換えでOK。hogeへの書き込みをオーディオコンテキストの中でやると毎サンプル実行される保証がない、みたいな仕様

タプルとか合成型だと、マルチスレッドで書き換えたときに片方のパラメーターが更新されてない時に読み取られる可能性が厳密にはある

グローバルに宣言された配列型とか、合成型をどうやって扱う?

単に、グローバルな値への書き込みがいつ行われるか保証はされませんよ、という仕様にするならそれでも十分か

もしくは、

let hoge = param(100)

みたいなラップの仕方を考える。hogeはParam<Number>型をもち、Number型に自動キャストできる。

パラメトリックフィルタ複製で、個々の周波数を外側からコントロールしたいとするとどうなるか

fn replicate(n,gen){
    let g = gen(n, n*100)
    if (n>0.0){    
	let c = replicate(n - 1.0,gen)
        | | g() + c()
    }else{
        | | g()
    }
}
fn mygen(freq){
  osc(freq)
}
fn gen(n,init_f){
   let channel = param("freq_%n",init_f);
   | | mygen(channel)
}
let myfilter = replicate(5,gen)
fn dsp(){
	myfilter()
}

まあこうなるか 自動グループ化みたいなのできるか?実行してる関数の深さ的にglobal::replicate::gen::(args)で自動ネーミング自体は不可能ではなさそうだが/自動グループをやりたければ、Param<T>の中身をレコード型にすればいいのか

paramがジェネリックな関数T->Param<T>であることが重要になってくる。範囲の制限とかはRanged型みたいなのをmimium側で定義すればよくなる・・・はずまあIO周りは結局命令型になるのかなあ

let channel = param("freq_%n",init_f);
set_param("freq_%n",1000.0)
channel = 1000;//

最後の行のように、普通にAssign書き込むのもできるようにしておきたい。これできるようにするには、Assignをオーバーロードできるような型システムが必要なのかなparam(コンストラクタ)get_param_nameget_paramset_paramの4種類があれば十分か

mimiumのレコード型のデフォルト実装、関数のデフォルト引数の組み合わせに対して自動でパラメーターが作られるようにしたい

fn gen_synthesizer(){
	let s = |gate = 0,freq=1000,gain = 1.0|{
		...
	}
	s({..})
}
let mysynth = gen_synthesizer();

fn dsp(){
	mysynth()
}

これを実行すると、隠れレコード構造体定義とグローバルなデフォルト引数が作られる

...
type defaultarg = {
 "gate":number = 0,
 "freq":number = 1000,
 "gain":number = 1.0
}
let default_arg_for_mysynth = param("mysynth",defaultarg::default());
fn dsp(){
	mysynth()
}

dsp内でmysynth({..})と実行してしまうと、これがグローバルなコンテキストで作られない可能性がある・・のか?いや、でも基本的に書き換えられる心配はないからいいのかな

デフォルト引数構造体のインスタンスは必ずグローバル評価で行う、だとパラメトリックな生成はできないし問題起きそう

//自動キャストとジェネリクスの組み合わせさえうまくいけばこのくらいのことはできそう
let p = param({..});
fn dsp(){
	p |>
	|gate = 0,freq = 1000,gain = 1.0|{
		...//do something
	}
}

ということは、とりあえず雑にParamを実装して