はじめに
rustlings を終えたので、とりあえず少し具体的な事をしてみようと、サウンドをやってみました。
環境は下記の通りです。
- Windows 10
- rustc 1.40.0 (stable-x86_64-pc-windows-gnu)
IDE は CLion を使っているので toolchain は mingw を使っています (CLion の Rust デバッガーが現時点では msvc に対応していない) 。ちなみに JetBrains IDE としては IntelliJ IDEA でも Rust プラグインがあるのでそちらでもコーティングは可能ですが現時点で IntelliJ では Rust デバッガーがありません。
オーディオライブラリを導入する
とりあえず音を出したいので、ライブラリの導入から始めます。
検索をすると "rust-portaudio" がよさそうなので、これを使うことにします。
rust-portaudio は "PortAudio" というクロスプラットフォームかつ OSS のオーディオライブラリの Rust バインディングになります。
とりあえず Cargo.toml に rust-portaudio の Crate を入れる記述をします。
[dependencies]
portaudio = "0.7.0"
で適当にコード書いてビルドをしてみるとリンク時にエラーが出ます。
error: linking with `gcc` failed: exit code: 1
|
= note: "gcc" "-Wl,--enable-long-section-names" "-fno-use-linker-plugin" "-Wl,--nxcompat" "-nostdlib" "-m64" (略)
= note: ld: cannot find -lportaudio
PortAudio の本体がないためリンクエラーになりました。よって PortAudio 本体をビルドします。
Git から checkout するかダウンロードサイトでソースコードを落としてきます。 Git は最新 stable の "pa_stable_v190600_20161030" ブランチで。
今回は Rust toolchain が mingw (x64) なので、 PortAudio も mingw (mingw64) でビルドします。 Makefile でビルドしました。
ビルドした PortAudio の成果物のうち、 "libportaudio.dll.a" をリンカーから見えるようにする必要があります (これがないからエラーになっていた) 。最初は "%USERPROFILE%\.rustup\toolchains\stable-x86_64-pc-windows-gnu\lib\rustlib\x86_64-pc-windows-gnu\lib" にコピーしてたのですがどうにも納得がいかず、 cargo から参照先を指定できる方法があるはず、ということで調べてみました。
build.rs というソースファイルをプロジェクトのルート (cargo.toml と同じ位置) に配置し、標準出力にオプションを書き出すとそのオプションが反映されます。
fn main() {
println!("cargo:rustc-link-search=native=lib");
}
上記でプロジェクト下の "lib" フォルダーに "libportaudio.dll.a" を配置するとリンクされます。
"libportaudio-2.dll" は Rust プロジェクトでビルドして生成された .exe と同じ位置にコピーします (これも build.rs を駆使すれば配置まで自動で行えそうですが今回は省略) 。
以上で準備完了です。
PortAudio を使ってみる
ストリーミング再生をするオーディオデバイスのライブラリは何を使っても大体考え方は同じで
- 再生する音声サンプルを取得するコールバックを設定する
- バッファ内のサンプル消費状況を自分で監視し空いている事を確認したら埋めていく
のどちらかのパターンです。 DirectSound と OpenAL が後者で多くは前者かなあと思います。 PortAudio の前者のパターンです。
ということでサンプルコードを見ながら実装していきます。
let pa = portaudio::PortAudio::new()?;
let mut settings = pa.default_output_stream_settings(2, 48000.0, 48000)?;
settings.flags = portaudio::stream_flags::CLIP_OFF;
PortAudio の初期化と出力設定情報を準備します。
let mut sample_number: u32 = 0;
let callback = move |portaudio::OutputStreamCallbackArgs { buffer, frames, .. }| {
let mut i = 0;
for _ in 0..frames {
let value = (sample_number as f32 * 440.0 * 2.0 * PI / 48000.0).sin();
buffer[i] = value;
buffer[i + 1] = value;
i += 2;
sample_number += 1;
}
portaudio::Continue
};
440Hz のサイン波を生成するコールバックです。
let mut stream = pa.open_non_blocking_stream(settings, callback)?;
stream.start()?;
pa.sleep(3 * 1_000);
stream.stop()?;
stream.close()?;
設定とコールバックを指定して出力ストリームを開き、再生を行います。
割とシンプルに記述することができました。
応用
以前書いた F# の記事内のコードを Rust に移植して同じことをしてみます。 Note 構造体や NoteName 列挙型の定義などは長いので GitHub を参照してください。
コールバックでの波形生成でドミソの和音は下記のようになりました。
let time = Time::samples_to_time(SAMPLE_RATE,sample_number);
let value =
(Note::new(4, NoteName::C).pitch().square_wave(time) +
Note::new(4, NoteName::E).pitch().square_wave(time) +
Note::new(4, NoteName::G).pitch().square_wave(time)).volume(0.3);
パイプライン演算子が使えないので中間型を定義してメソッドチェーンでそれっぽい見通しのよい感じにしてみました。
おわりに
Rust なんもわからん・・・