--- date: 2024-10-30 15:42 --- #mimium ## 要件 どこまでMIDIインプットをmimiumの世界の外側として捉えるか ### 先行例 [[SuperCollider]]や[[ChucK]]はコールバックを登録するようなイメージ [Using MIDI | SuperCollider 3.12.2 Help](https://doc.sccode.org/Guides/UsingMIDI.html ```smalltalk s.boot; ( var notes, on, off; MIDIClient.init; MIDIIn.connectAll; notes = Array.newClear(128); // array has one slot per possible MIDI note on = MIDIFunc.noteOn({ |veloc, num, chan, src| notes[num] = Synth(\default, [\freq, num.midicps, \amp, veloc * 0.00315]); }); off = MIDIFunc.noteOff({ |veloc, num, chan, src| notes[num].release; }); q = { on.free; off.free; }; ) // when done: q.value; ``` まあこれはDSPアウトプットの合成が.playで暗黙的に行える(加算で合成されるという想定)だからできることかな、、、 ChuckもボイスごとにShredを生やす方向で対応してるからちょっと微妙だ [Chuck - Input & Output](https://chuck.stanford.edu/doc/reference/io.html#MidiIn) --- 現実的には、[[Faust]]のように、ボイスアロケーターは外側で実装してしまい、非同期に更新されうるAtomicな値のセルをノートやccのデータとして受け取れるようにすれば当面は十分 が、最終的にはMIDIエフェクト(MIDI信号自体のディレイやクォンタイズ、スロットリングとか)を[[Functional Reactive Programming|FRP]]っぽく書けると嬉しい --- Noteのバインドの記法 ```rust let cell:()->float = bind_midi_note_mono(channel) let (note,vel) = cell(); //値の取り出し ``` ```rust //rust fn bind_midi_note_mono(&mut self ,vm:&mut Machine)->ReturnCode{ let ch = Machine::get_as::(vm.get_stack(0)); let cell = Arc::new((AtomicF64::new(),AtomicF64::new() )); self.add_midi_listener(ch,|note,vel|{ let (note_c,vel_c) = cell.clone(); note_c.write(note); vel_c.write(vel); }); let cls = |vm:&mut Machine|->ReturnCode{ let (note_c,vel_c) = cell.clone(); vm.set_stack(0,Machine::to_value(note_c)); vm.set_stack(1,Machine::to_value(vel_c)); 2 } vm::set_stack(0, wrap_rust_closure(cls)) 1 } ``` あとは頑張って`add_midi_listener`相当の何かを作ろうって感じだな(関数型っぽくはないけども) --- IOの順序保証とか考える 正格評価だとIOモナドとかはそもそも考える必要がない [Algebraic Effects入門](https://v2.aintek.xyz/posts/introduction-to-algebraic-effects) > `type 'a io = unit -> 'a` > 純粋な値は以下のようなIOモナドにリフトできる。 > `let return x = fun () -> x` > 計算はバインド演算子によってつなげることができる。 > `let (>>=) c1 c2 = fun () -> c2 (c1 ())` mimiumだとバインド演算子ってこうか(ジェネリクスが必要だけども) ```rust fn bind(f1,f2){ | | f1() |> f2 } ``` --- ### FRPっぽい感じで考えてみる 課題:4ポリのMIDIボイスアロケーターを考える アロケーターを実行すると4つのCell(Behavior)が返ってくる いや、ストリームを返すべき? ```rust fn alloc_midi_voice_4(channel)->[Stream;4] ``` 状態のフィードバックはSodiumでは次の2つを持って実行できる - hold: 任意のStreamが発火した時に最後の値をキープしたCellを作る - snapshot:任意のStreamが発火したとき、Cellをサンプリングして別のStreamとして発火する 状態の保持として必要なのは、`Busymap([Option;4])`みたいなやつ。 ```rust fn bind_midi_event()->Stream fn bind_note_event()->Stream<(Channel,Note)>{ bind_midi_event |> filter(|byte| byte |> is_note_event ) |> map(|byte| (byte[0],Note(byte[1],byte[2]) ) ) } fn overwrite_arr(idx,v){ |arr| { let mut narr = arr; narr[idx] = v narr } } fn get_4_voice(note){ let (last_stream,last_busymap) = self; if last_busymap |> is_busy{ (last_stream,last_busymap) }else{ match note{ Note::On(pitch,vel)=>{ let voice_idx = last_busymap|>find_available_voice; let new_busymap = last_busymap |> overwritearr(voice_idx,pitch); let new_str = last_stream |> overwritearr(idx,(pitch,vel)) } Note::Off=>{ } } } } fn alloc_midi_voice_4(channel){ let busymap = Cell<_>([None,None,None,None]); bind_note_event |> filter(|(c,_note)| c==channel) |> merge(busymap |> snapshot, |(note,busycell)| { busycell.hold } ) } ```