Rust-jack で音源開発/ポリシンセ仕組みの基本

# 概要

Polyphonic Synthesizer(ポリシンセ)は*ホスト部分*と*シンセ(音色)部分*で構成される. Polyphonicホストは複数のシンセ管理して, 解析した外部のMIDI信号を対応できるシンセに送る. そのおかげで, シンセはトラフィックを気にせずに音色合成アルゴリズムの重点を置きできる. 今回はRust-jackに使ってこのPolyphonicホストをデザインして, sine楽器の例を作成する.

Rust-jack: <https://github.com/RustAudio/rust-jack>

ソースコード: <https://6e5d.com/proj/polysplit.tar.gz>

# Rust-jackクライエントの書き方

クライエント作成:

```rust
let (client, _status) = jack::Client::new(
"polyhost", //クライエントの名前
jack::ClientOptions::NO_START_SERVER,
).unwrap();
```

シンセなので, MIDI入力x1とオーディオ出力x2を登録:

```rust
let midi_in = client
.register_port("midi_in", jack::MidiIn::default())
.unwrap();
// out1/out2は左/右(ステレオ出力)
let mut audio_out1 = client
.register_port("audio_out1", jack::AudioOut::default())
.unwrap();
let mut audio_out2 = client
.register_port("audio_out2", jack::AudioOut::default())
.unwrap();
```

JACKクライエントの信号処理部分はコールバック関数に包まれる:

```rust
let callback = move |_: &jack::Client, ps: &jack::ProcessScope| -> jack::Control {
for event in midi_in.iter(ps) {
if event.bytes[0] & 0xf0 == 0x90 {
// ここにMIDIデータを読み込む
}
}
// バッファout1/out2を取得してWaveデータ書き込む
let out1 = audio_out1.as_mut_slice(ps);
let out2 = audio_out2.as_mut_slice(ps);
// 終了の場合はjack::Control::Quitを戻す
jack::Control::Continue
};
```

クライエント起動:

```rust
let active_client = client
.activate_async((), jack::ClosureProcessHandler::new(callback))
.unwrap();
std::thread::park();
// mainがブロックされたのでdeactivateは不要
// active_client.deactivate().unwrap();
```

# シンセの状態と MIDI イベント

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0%2F3511996%2Ffe8b9a52-0f64-7817-eb33-324efac38ff2.png)

手元のキーボードにはsostenuto pedal(ソステヌートペダル)ポートを持っていないから, 今度対応する信号はkeyとsustain pedal(サスティンペダル)のみです. その場合, 音の終止は3つのケースがある:

* 音のsustainが終わりまでpedal downかkey downを押し続け

* (既に pedal up 状態で)key upによってリリースの終わり

* (既に key up 状態で)pedal upによってリリースの終わり

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0%2F3511996%2F31024cf2-1733-f8f4-d7a6-74c1b9c5ad71.png)

この状態グラフに応じて次のようなデータ構造を作る.

```rust
active: HashMap<u8, Box<dyn Synth>>,
sustain: Vec<Box<dyn Synth>>,
release: Vec<Box<dyn Synth>>,
```

すべてのシンセはactive/sustain/releaseのいずれかに入る.

* Key upする時, HashMap対応のactiveシンセはpedal up/downに応じてsustain/releaseへ移動.

* Pedal upする時, sustain全体のシンセはreleaseへ移動.

* Synth は時間経って出力終了をリターンする時, どこ(active/sustain/release)にいても削除される.

# シンセの抽象

シンセを動的に作るため, 2つのtraitsが必要:

```rust
// key downの時, 新たなシンセを作成するためのsynth generator
pub trait SynthGenerator: Send {
// sample rateを設置する
fn set_sr(&mut self, sr: usize);
// シンセを作成
fn generate(&self, note: i32, velocity: f32) -> Box<dyn Synth>;
}
// 音高と音圧を固定されたシンセ, バッファに合成した信号を書き込む
pub trait Synth: Send {
fn set_end(&mut self, smp_count: usize);
fn sample(&mut self, data_l: &mut [f32], data_r: &mut [f32]) -> bool;
}
```

Synth traitの関数について:

* `set_end`: 次のバッファの第`smp_count`フレームからkey upによってリリース状態になる. この関数は最大1回しか呼び出されない. (サスティンで終了の場合は1回も呼び出されない).

* `sample`: ステレオバッファ`data_l/data_r`を書き込む. `data_l`と`data_r`が同じ長さを持つことを保証されます. 戻り値は`true`の時に出力終了し, このシンセを破棄する.

ちなみに, 以前のバージョンで出力完了したシンセをリサイクルから, シンセの音高と音圧の設置関数は必要. ハードウエア音源の場合はシンセを量産できず,定数のシンセにイベントを分配する (8/16 ボイスなど)必要があるが, ソフト音声合成は大きなallocation(ヒープ割り当て)のはほとんどしないので, 簡単性のためシンセの回収をやめた.

# 使い方

その枠組を使ってsineポリシンセを安易に作成できる.

```rust
#[derive(Default)]
struct SineGen {
sr: usize,
}
impl SynthGenerator for SineGen {
fn set_sr(&mut self, sr: usize) {
self.sr = sr;
}
fn generate(&self, note: i32, velocity: f32) -> Box<dyn Synth> {
eprintln!("{} {}", note, velocity);
// midi noteを周波数に変換
let note_frequency: f32 = 440.0 * f32::powf(2.0, (note as f32 - 69.0) / 12.0);
let ss = SineSynth {
end: None,
f: note_frequency,
phase: 0f32,
si: 2.0 * PI / self.sr as f32,
v: velocity,
};
Box::new(ss)
}
}
```

```rust
struct SineSynth {
end: Option<usize>, // 停止フレーム(Noneの場合, 次のバッファに停止されない)
f: f32, // 音高
phase: f32, // sineの位相
si: f32, // sample intervalフレームの長さ
v: f32, // 音圧
}
impl Synth for SineSynth {
fn set_end(&mut self, smp_count: usize) {
self.end = Some(smp_count);
}
fn sample(&mut self, data_l: &mut [f32], data_r: &mut [f32]) -> bool {
for (idx, (sl, sr)) in data_l.iter_mut().zip(data_r.iter_mut()).enumerate() {
let dphase = self.f * self.si;
self.phase += dphase;
let v = self.phase.sin() * self.v * 0.5;
*sl += v;
*sr += v;
if let Some(e) = self.end {
if e >= idx {
return true
}
}
while self.phase >= PI * 2.0 {
self.phase -= PI * 2.0;
}
}
return false
}
}
```

JACK Clientを起動して, キーボードでsine楽器を弹くことができた. Keyupの時シンセは即時停止(releaseなし)なので,信号カットのポップノイズが出る.