---
date: "2023-12-06T03:51:12+0900"
---

#programming-language #sound

https://github.com/tomoyanonymous/otopoiesis

DAWをプログラマブルにする試み
```
#[param("hoge",0..8)]
let p1 = 1.0;
```

---

## 思想

Brandt(2002) の[[Temporal Type Constructor]]（以下TTC）という概念を使う。

TTCはジェネリックなタイプ`A`に対して、以下の3つの型コンストラクタを用意することでジェネリックに時間信号を取り扱う思想。

以下はRustの擬似コード。

```rust
type time = Real;
//時間に紐づいたイベント。MIDIノートとか
struct Event<A>{v:A, t:time} 
//有限ベクトル。オーディオクリップとか
type Vec<A> = std::Vec<A> 
//無限ベクトル、またはストリーム。1論理時刻毎にA型のものを返す漸化式（内部状態を持つかもしれない）
type iVec<A> = Box<dyn FnMut()->A> 
```

例えばMIDIの記録されたデータは

```rust
type NOTE= Event<(u8,u8)>//ノート番号、ベロシティ
type MIDI = Vec<NOTE> 
```

みたいになる

## 構造

基本的なイメージはこんな感じ？

```
type Project<V> = Vec<Track<_,__>> -> iVec<V>
type Track<I,O> =  Device<I> * Device<O> //デバイス情報
				*(
				  Vec<Region<O>> 
				| Vec<Event<I,O>>
				| Generator<O>
				)
type Region<V> = (time*time)* //start,duration
				(Vec<V> //　オーディオデータ
				  | Generator<V>
				  | Project<V>) //プロジェクトも再帰的に埋め込める
type Generator<T> = iVec<T>			
```

なんだけど、TrackAで使われてるGeneratorの中のParameterとしてTrackBの値をアサインしたい、みたいなことを表現できたらプログラミングとして面白くなる、という話

```
//Freq440Hz,Gain1.0,Phase0.0
let t1 = Track(Generator::SineWave(Constant(440),Constant(1.0),Constant(0.0)));
let t2 = Track(Generator::SineWave(t1,Constant(1.0),Constant(0.0)));
```
これをあんまり動的ディスパッチじゃない感じで実装したい。そしてこの辺までは別にMaxとかと同じレベルの話

ここからがDAWをプログラミングで操作できる面白いとこで、例えばリージョンに対するフェードインアウトとかを`Region<T>->Region<T>`の関数として定義できるところ

CubaseにおけるインストゥルメントトラックとかはMIDIトラック＋シンセサイザーの合成なので、
`Track<NOTE,NOTE>`に`Vec<NOTE>->iVec<Audio>`みたいなのを適用する関数としてあらわせ、、、る？

## 考えうるユースケース

```rust
type Region = {
 start:float,
 dur:float,
 content: [float],
} 

fn region(start,dur,content:(({start,dur})->float))->Region{
	let wave = render_mono(||content(start,dur)) |> probe_array 
	{
		start = start_p,
		dur = dur_p,
		content = wave
	}
}

```

### 一般的なリージョンエフェクト

```rust
type alias FX = ({s:float,start:float,dur:float})->float
fn region_transformer(origin:Region,fx:FX) -> 'Region{
	let start = origin.start
	let dur = origin.dur
	 `region({
		start:$start,
		dur:$dur,
		content: |start,dur| {s:$origin.content(),start:$start,dur:$dur} |> $fx
	})
}
```

### リージョンに対するFadeInOut

```rust
fn apply_fadeinout(s,start,tin,dur,tout){
	let end = start+dur
	if now < start | now > end | tin<=0 | tout <= 0{
		0
	}else{
		let gain_s = min(max(now - start,0)/tin ,1)
		let gain_e = min(max(end - now,0)/tout,1)
		s*gain_s*gain_e // gain_sとgain_eはどちらから0~1の区間もう片方が1
	}
}
fn finout_generator(s,start,dur){
	//パラメーターはステージ１では単なる変数参照になる
	let time_in = Param!(0.0,"fade_in",0..=f64::MAX);
	let time_out = Param!(0.0,"fade_out",0..=f64::MAX);
	apply_fadeinout(s,start,time_in,dur, time_out)
}
fn fade_inout(origin:Region)-> 'Region{
	region_transformer(origin,finout_generator)
}
...
fade_inout!(some_region)
```

### リージョンに対するリバース

```rust
fn reverse(origin:Region)->Region{
	let new_cache = origin |> render
	//クロージャとして値new_cacheを閉じ込める
	let new_generator = |s,start,dur| {//実際にはsは使わない
	//s is not used
		let index =  dur - start - now
		new_cache[index]
	}
	region_transformer(origin,new_generator)
}
...
some_region |> reverse!(_) |> fade_inout!(_)
```

## トラックの中で別のトラックの信号を参照する

```rust
fn phasor(freq){
  self+freq/samplerate % 1.0
}
fn sinosc(freq,amp){
  phase*TWOPI |> sin
}
// --ここから上はstage 1 value

//track_1はstage0として参照する
let track1:`()->float = Track(|| phasor(1.0)|> scale(_,0.0,1.0,100,4000))

//track1をfrequencyとして参照することにする

let r1 = LiveRegion({
   start: 100ms,
   dur: 1000ms,
   content: || sinosc($track1(), 1.0)
})

let regions = [r1,r2,...]//regionの列はstage0のvalue

let track3 = track_regions(regions)

```

- リージョンの移動に応じて生成される波形が変わるみたいなこともできるようになる
- ただ実行時のトラックのサンプルに依存すると、
	- サムネイルの生成のために全てのトラックを一度走査するみたいな必要は出てくる
	- これに対するリバースとか定義できなくない？
- リージョンの書き込み＝ステージ0評価ということにするならば、ステージ0でもselfなど状態関数使える必要あり
- リージョンを、他のトラックを参照するLiveRegionとそうでないものに分けるとか？
- あるステージ0関数を評価するとステージ1配列を返すrender_lift関数のようなものを考える

```rust
fn render_lift(content:()->float,time:float)->`[float]{
	...
}
```


## トラックのランタイム構造

```rust
let content = `{ phasor(Param!("freq",20,20000)) } 


fn Track(name:string, content:()->`float)->`float{
	`{
		//stack is cleared and new track ui is placed on top of the stack	
		 $content() |> probe_to_track!(name,_)
	 }
}

fn master(tracks:[`float])->`float{
	Track!("master", `{  lift_array(map(tracks,|t| $t ) ) |> sum })
}
let dsp = master([t1,t2,t3...])

```

Trackの中でSliderとかが呼び出されると、そのスライダーはトラック内のパラメーターに属していて欲しいが、引数は内側から評価されてしまう=sliderの評価の方が先に来てしまう

UIツリー系の評価は、

- 個別のパラメーターを評価時Stackに積むような処理
- TrackやリージョンではStackを消費しグループ化して自分のデータを構築(data::Trackの方)、トラックのUIを配置してStackに積む
- トップレベルでスタックを全て消費しUIをプロジェクト全体のUI構築

UI系の関数は全てサンク（レベル0）を受けて遅延評価するようなものでないとNG


## 最低限必要になるUI類

```
Project
Track
Clip
Slider
Checkbox
```

Trackの直下でTrackが呼ばれた場合、フォルダトラックとしてネストされることになる、、、とか

これ、たとえばClipはTrackの下かClipの下かのいずれかでしか使えないっつーことになるのでなんか新たに型システムで縛れる気がしてきたなあ

Sliderが呼び出された時、初期値を変数参照にするのはあんまり意味がないことになる（リテラルで固定して、それを双方向プログラムしたい）

一個ずつ順番に考えていこう

```rust
`{
let phasor = | | (self+1.0)% 44100 
let dsp:()->float = Project!("test project ",`| | {
        let gain = Slider!("gain",`0.5,0.0,1.0)
        sin(440*3.1415 * 2*phasor() /samplerate) *gain
    })
}
```

これがコンパイル通るところまではいける

ExprNodeIdをオーディオスレッドで直接参照してはいけない（session_globalsがスレッドローカルなので参照ができない）

リテラルの値を`Arc<atomic::F64>`でパースして、それを引き回す

Sliderの値はProbeIdのようにインデックス参照したいので、そのストレージを何処で保持するか問題

SystemPluginは本来スレッドを超えて共有できない状態を共有できてしまう（vmが走っているオーディオスレッドからメインスレッドの値にアクセスしてしまっている）

そうすると状態共有でArcを使用する意味があんまりない



## トラックの追加やリージョンの追加のアクション

要するにUndo可能なアクション

- 最悪、テキストに関してはdiff取って保存しておけば良いんでは
- 「新規トラックの追加」みたいなアクションをどうするか
- GUIのViewモデルからシンタックスツリーに戻すことはできない（マクロ展開後の結果だから）
- Projectに与える配列に対してNodeの参照を持っていればなんとかなるか？
- 



### 他

- ドラッグしてるオブジェクトのスナップ対象は例えばCubaseなら固定のグリッドor既存のイベントorその両方とかだけど、例えばグリッドをジェネラティブに生成できる
	- もちろんクオンタイズにも使える
- 非破壊的クォンタイズ。録音された時のイベント位置は覚えていて、一番近いグリッドに何％寄せるかも決められるし、ランダマイズも後から修正できる
- プロジェクトをリージョンとして埋め込める
	- その際、プロジェクトに与えられるグローバルなパラメーターみたいなものはどうしようね


- 結局内蔵スクリプトがフックされるタイミングって次の3つになるんよな
	- 信号グラフ確定時（ルートのコンパイル）
	- 再生前（prepareToPlay)
	- 信号再生時（process）


## 多段階計算と組み合わせる

[[mimiumの多段階計算]]で、それなりに多段階計算の実装が間に合ってきた。

FadeinOutのようなリージョン→リージョンの関数はステージ0の計算と考えることができる。

また、Generator系も、基本的には周波数や音量といったUIパラメーターは、ステージ0での評価時にUIを生成して値を受け取るチャンネルを作り、ステージ1＝再生中にそのUIからの値を受け取るという方式で捉えられる

```rust
fn audiofx(param1=100,param2=200){
	`|input|{... }
}

fn gen_component(){
 Param{..} |> sinosc
}
```

`Param`はジェネリックな関数とする

まあこれつまり、Temporal Type Constuctorだけだとパラメーター周りのIOのライブ入出力について十分に考慮されていないということになるのかな

フェードイン/アウトを掛けた状態のものを、複製とかも含めて考えるとどうなるんだろ



---
以下は昔に考えていたこと

コードの例(モジュレーションされているサイン波＋ディレイ)

```
project{
	track: [
		delay{
			sinosc{
			freq:
				sinosc:{
					freq: float{20..20000,1000,"freq"}
					phase: 0.0},
			phase: 0.0
			},
		time: 1000
		}
	]
}
```

UIは基本的にプロジェクトツリーの`Param`型と、UIだけで使われる`State`をそれぞれ可変参照として持つ
(Reference カウントするのではなく、有限なライフタイムを持つ可変参照で作る)

```rust
struct UI<'a>{
	param: &'a mut Param,
	state: &'a mut State
}
impl<'a> egui::Widget for UI<'a>{
	fn ui(self, ui: &mut egui::Ui) -> egui::Response {
		///...
	}
}
```

eguiはimmiditate モードだから毎フレームこのUI型を生成している（egui標準のSliderとかもこの方式）

オーディオプロセッサーもこのやり方にできるか？


## 開発メモ

クリップのサムネイル生成はgeneratorじゃなくてregion側でやろう

fileplayerのui実装もgeneratorからregionに移そう

そうなるとaudio側の実装もそっちに合わせるのが自然だよな・・・


完全にValueを64bitで静的型付けとして扱う時の、擬似的に動的型チェックする方法がないか

id_arenaのIDが128bitである限りちょっと厳しそう
プロジェクト、トラック、リージョンが限られた数であることを前提にすればNaNBoxingもできなくはなさそうだが、、、

コンパイラのContextをアプリ中で引き回さなくちゃいけなくなるのがやだなー
少なくともこれやるとマルチスレッドはめっちゃ難しくなるな

名前があんまり気に入ってない

mimeme(MInimal Musical Environment for Manual Editing)

---

[[PureData]]なみのミニマルな部分からの作り込みというのができない（静的型付けだからか？）



