quartz-research-note/content/otopoiesis.md
松浦 知也 Matsuura Tomoya c18e2fabae
Some checks failed
Build / build (push) Has been cancelled
updated timestamp
2024-02-08 20:06:45 +09:00

5.9 KiB
Raw Permalink Blame History

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の擬似コード。

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の記録されたデータは

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 Track1 = Generator::SineWave(Constant(440),Constant(1.0),Constant(0.0));
let Track2 = Generator::SineWave(Track1,Constant(1.0),Constant(0.0));

これをあんまり動的ディスパッチじゃない感じで実装したい。そしてこの辺までは別にMaxとかと同じレベルの話

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

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

考えうるユースケース

リージョンに対するFadeInOut

fn fade_inout(origin:Region)->Region{
	let time_in = Param(0.0,"fade_in",0..=f64::MAX);
	let time_out = Param(0.0,"fade_out",0..=f64::MAX);
	//クロージャとして値を閉じ込める
	let new_generator = |s| apply_fadeinout(s,time_in,time_out);
	Region{
		start:origin.start,
		dur:origin.dur,
		content: compose(origin.content,new_generator)//この辺がよくわからん
	}
}

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

fn reverse(origin:Region)->Region{
	//クロージャとして値を閉じ込める
	let mut new_cache = vec![0;origin.get_dur_in_samples()*origin.channeks];
	origin.render(new_cache);
	let new_generator = |s| ;
	Region{
		start:origin.start,
		dur:origin.dur,
		content: compose(origin.content,new_generator)//この辺がよくわからん
	}
}

  • ドラッグしてるオブジェクトのスナップ対象は例えばCubaseなら固定のグリッドor既存のイベントorその両方とかだけど、例えばグリッドをジェネラティブに生成できる

    • もちろんクオンタイズにも使える
  • 非破壊的クォンタイズ。録音された時のイベント位置は覚えていて、一番近いグリッドに何%寄せるかも決められるし、ランダマイズも後から修正できる

  • プロジェクトをリージョンとして埋め込める

    • その際、プロジェクトに与えられるグローバルなパラメーターみたいなものはどうしようね
  • 結局内蔵スクリプトがフックされるタイミングって次の3つになるんよな

    • 信号グラフ確定時(ルートのコンパイル)
    • 再生前prepareToPlay)
    • 信号再生時process

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

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

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 カウントするのではなく、有限なライフタイムを持つ可変参照で作る)

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をアプリ中で引き回さなくちゃいけなくなるのがやだなー 少なくともこれやるとマルチスレッドはめっちゃ難しくなるな