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 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>みたいなのを適用する関数としてあらわせ、、、る?

考えうるユースケース

type Region = {
 start:float,
 dur:float,
 content: ()->float,
} 

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

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

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)

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

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!(_)

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

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 = {
   start: 100ms,
   dur: 1000ms,
   content: || sinosc($track1(), 1.0)
}
 
let regions = [r1,r2,...]//regionの列はstage0のvalue
 
let track3 = track_regions(regions)
 
 
fn track_regions(regions:[{start:float,dur:float,generator:()->float}]){
	Track()
}
 

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

 
fn Track(content:()->float)->`()->float{
	let thunk = bind_to_runtime(content)//ここでprobeも行うことで、波形とメーター表示をする
	thunk
}
 
fn master(tracks:[`()->float])->`()->float{
	`{ || map(tracks,|t| $t() ) |> sum}
}
let dsp = Track!($master([t1,t2,t3,...]))
 

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

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

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

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

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

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

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

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

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

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

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)