Files
quartz-research-note/content/otopoiesis.md
松浦 知也 Matsuura Tomoya 695f3bc450
Some checks failed
Build / build (push) Failing after 7m56s
[obsidian] vault backup: 2025-09-04 03:44:03[
2025-09-04 03:44:03 +09:00

386 lines
12 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: "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){
//パラメーターはステージ1では単なる変数参照になる
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)