247 lines
7.6 KiB
Markdown
247 lines
7.6 KiB
Markdown
---
|
||
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,
|
||
}
|
||
```
|
||
|
||
### 一般的なリージョンエフェクト
|
||
|
||
```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: || {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 fade_inout(origin:Region)-> 'Region{
|
||
let new_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)
|
||
};
|
||
region_transformer(origin,new_generator)
|
||
}
|
||
```
|
||
|
||
### リージョンに対するリバース
|
||
|
||
```rust
|
||
fn reverse(origin:Region)->Region{
|
||
//クロージャとして値を閉じ込める
|
||
let new_cache = origin |> render
|
||
|
||
Region{
|
||
start:origin.start,
|
||
dur:origin.dur,
|
||
content: | | {
|
||
let index = origin.dur - origin.start - now
|
||
new_cache[index]
|
||
}
|
||
}
|
||
}
|
||
|
||
```
|
||
|
||
### 他
|
||
|
||
- ドラッグしてるオブジェクトのスナップ対象は例えば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)
|