概要
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クライエントの書き方
クライエント作成:
let (client, _status) = jack::Client::new(
"polyhost", //クライエントの名前
jack::ClientOptions::NO_START_SERVER,
).unwrap();
シンセなので, MIDI入力x1とオーディオ出力x2を登録:
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クライエントの信号処理部分はコールバック関数に包まれる:
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
};
クライエント起動:
let active_client = client
.activate_async((), jack::ClosureProcessHandler::new(callback))
.unwrap();
std::thread::park();
// mainがブロックされたのでdeactivateは不要
// active_client.deactivate().unwrap();
シンセの状態と MIDI イベント
手元のキーボードにはsostenuto pedal(ソステヌートペダル)ポートを持っていないから, 今度対応する信号はkeyとsustain pedal(サスティンペダル)のみです. その場合, 音の終止は3つのケースがある:
-
音のsustainが終わりまでpedal downかkey downを押し続け
-
(既に pedal up 状態で)key upによってリリースの終わり
-
(既に key up 状態で)pedal upによってリリースの終わり
この状態グラフに応じて次のようなデータ構造を作る.
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が必要:
// 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ポリシンセを安易に作成できる.
#[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)
}
}
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なし)なので,信号カットのポップノイズが出る.