音声信号処理の勉強ノート - 3. 音声入力/出力
この記事では、リアルタイムDSPの基本的な理解を深めるために、CPALを使用したシンプルなオーディオ入出力の例を勉強します。この例では、リアルタイムで録音した音声に対して、エフェクトを適用し、それを出力するという流れになります。また、Rustの文法やライブラリについても軽く説明します。
CPALとは
CPALは、Rust言語でオーディオ入出力を扱うためのクロスプラットフォームライブラリです。CPALを使用することで、マイクやスピーカーなどのオーディオデバイスにアクセスし、リアルタイムで音声データの録音や再生を行うことができます。CPALは、さまざまなOS(Windows、macOS、Linux、iOS、Androidなど)に対応しており、同じコードで異なるプラットフォーム上でも動作させることが可能です。
オーディオ入出力
これから、オーディオ入出力関数のコードを詳しく見ていきます。この関数は、入力デバイス(マイクなど)からオーディオデータを取得し、リアルタイムで処理を行い、その結果を出力デバイス(スピーカーなど)に送信する仕組みを実装しています。
使用されるライブラリや構造体
use {
audio_module::{AudioModule, AudioProcessor, Command, CommandHandler},
cpal::traits::{DeviceTrait, HostTrait, StreamTrait},
ringbuf::RingBuffer,
};
pub struct AudioStreams {
pub output: cpal::Stream,
pub input: cpal::Stream,
}
オーディオ入出力のコア部分 - 関数シグネチャ
pub fn start_audio<Module: AudioModule>(
command_receiver: crossbeam_channel::Receiver<Command>,
sample_rate: usize,
) -> Result<AudioStreams, ()>
-
pub fn start_audio<Module: AudioModule>
ModuleはAudioModuleトレイトを実装する型で、オーディオ処理の機能を提供することが期待されます。これにより、異なるオーディオモジュールを柔軟に使用できるようになっています。Rustのモノモーフィゼーション(Monomorphization)により、コンパイル時にこのジェネリック型は具体的な型に置き換えられます。そのため、ランタイムでのオーバーヘッドが少なくなり、高速に処理が行われます。 -
command_receiver: crossbeam_channel::Receiver<Command>
別スレッドや非同期タスクからコマンドを受け取るチャンネルです。このコマンドにより、リアルタイムでオーディオ処理を制御することができます。 -
sample_rate: usize
サンプリングレートを指定します。これは、1秒間に処理されるサンプル数を表し、例えば44100
などの値が使用されます。 -
Result<AudioStreams, ()>
成功時には、入力ストリームと出力ストリームを持つAudioStreams
構造体を返し、失敗時には()
(エラー)を返します。また、成功時にAudioStreams
を返すことで、音声ストリームの制御は呼び出し元で行えることができます。
オーディオ入出力のコア部分 - 関数の実装
let mut processor = Module::create_processor(sample_rate);
const CHANNELS: usize = 2;
const FRAMES_PER_BUFFER: usize = 128;
const SAMPLES_PER_BUFFER: usize = FRAMES_PER_BUFFER * CHANNELS;
-
processor
の初期化:Module::create_processor(sample_rate)
を使って、指定されたサンプリングレートに基づいたオーディオプロセッサを生成しています。ここでは、processor
の具体的な実装は重要ではありませんが、知っておくべき点は次の2つです。- 入力データを処理し、出力を生成するメソッドを提供していること
- コマンドを処理するメソッドがあること: これにより、リアルタイムでのオーディオ操作が可能になります
-
SAMPLES_PER_BUFFER
: バッファ内のサンプル数。フレーム数にチャンネル数を掛けることで、1バッファ内に存在するサンプルの総数を求めています。この例ではステレオ(2チャンネル)で128フレームなので、256サンプルが1バッファに含まれます。デバイスごとに推奨されるバッファサイズや処理の最適化の要件が異なるため、デバイスの特性に応じて調整が必要になる場合があります。バッファサイズが小さいと、処理が頻繁に行われるためレイテンシーが低くなりますが、その分システムに負荷がかかりやすくなります。
let host = cpal::default_host();
let input_device = host
.default_input_device()
.expect("failed to find a default input device");
let output_device = host
.default_output_device()
.expect("failed to find a default output device");
let stream_config = cpal::StreamConfig {
channels: CHANNELS as u16,
sample_rate: cpal::SampleRate(sample_rate as u32),
buffer_size: cpal::BufferSize::Fixed(FRAMES_PER_BUFFER as u32),
};
cpal::default_host()
を使ってシステムのデフォルトオーディオホストを取得し、それに基づいて入力デバイスと出力デバイスを設定しています。次に、cpal::StreamConfig
を使ってオーディオストリームの設定を行います。ここでは、ステレオ(2チャンネル)で、指定されたサンプリングレートと、固定されたバッファサイズ(FRAMES_PER_BUFFER
)を使ってオーディオデータ処理の準備がされています。
また、オーディオデバイスが指定されたsample_rate
やbuffer_size
を必ずしもそのまま採用するとは限りません。もし問題が発生した場合、デバイスの実際の設定を確認する必要があります。
let mut process_buffer = [0.0f32; SAMPLES_PER_BUFFER];
let ring_buffer = RingBuffer::new(SAMPLES_PER_BUFFER * 2);
let (mut to_output, mut from_input) = ring_buffer.split();
-
process_buffer
: 入力されたオーディオサンプルに対してエフェクトなどの処理が行われ、データを一時的に保持するためのバッファです。 -
ring_buffer
: リングバッファ(循環バッファ)は、一定のサイズを持つバッファで、データがいっぱいになると最初の位置に戻り、再びデータが書き込まれる構造です。このバッファは、リアルタイム処理におけるデータのやり取りをスムーズに行うために使用されます。 -
to_output
とfrom_input
: リングバッファを読み書きするための2つの構造体です。
let input = input_device
.build_input_stream(
&stream_config,
move |data: &[f32], _info: &cpal::InputCallbackInfo| {
// バッファが準備できるたびに呼び出され、音声データが 'data' に渡されている
debug_assert!(data.len() == SAMPLES_PER_BUFFER);
while let Ok(command) = command_receiver.try_recv() {
processor.handle_command(command);
}
processor.process_stereo(data, &mut process_buffer);
to_output.push_slice(&process_buffer);
},
move |err| eprintln!("Error on audio input stream: {}", err),
)
.expect("Failed to create input audio stream");
let output = output_device
.build_output_stream(
&stream_config,
move |data: &mut [f32], _info: &cpal::OutputCallbackInfo| {
// バッファに音声データが必要になるたびに呼び出され、'data' に音声データを書き込む必要がある
debug_assert!(data.len() == SAMPLES_PER_BUFFER);
let consumed = from_input.pop_slice(data);
if consumed < SAMPLES_PER_BUFFER {
println!("output underflowed");
}
},
move |err| eprintln!("Error on audio output stream: {}", err),
)
.expect("Failed to create input audio stream");
この部分では、オーディオの入出力ストリームを構築しています。
-
input
ストリーム:input_device.build_input_stream
で入力ストリームを作成しています。コールバックで、入力データ(data
)がprocessor
によって処理され、その結果をto_output
にプッシュします。また、command_receiver
で受け取ったコマンドも処理しています。 -
output
ストリーム:output_device.build_output_stream
で出力ストリームを作成しています。ここでは、リングバッファからデータをポップしてdata
に書き込みます。もしバッファに十分なデータがない場合、アンダーフロー(データ不足)が発生したことを通知します。 - ここの処理では、
input
とoutput
のバッファ(data
)サイズが同じであることが重要です。
if let Err(error) = input.play() {
eprintln!("Error while starting input audio stream: {}", error);
return Err(());
}
if let Err(error) = output.play() {
eprintln!("Error while starting output audio stream: {}", error);
return Err(());
}
Ok(AudioStreams { input, output })
入力と出力のオーディオストリームを開始します。
音声入出力を行ってみる
- 必要なリポジトリ: https://github.com/irh/freeverb-rs
- 実行してみたい例は
/examples/app_gtk
のディレクトリにあります: - 必要な環境: Rustの開発環境とGUIツールキット
GTK
(筆者はMacOSでテストしていました) -
/examples/app_gtk
内でcargo run
を実行すると、GUIアプリケーションが起動します。デフォルトのマイクからの音声をリバーブエフェクトをかけて再生することができます。
リアルタイムで変更可能なパラメータ
アプリケーションを実行すると、以下のパラメータをリアルタイムで調整することができます。これらはリバーブの特性に影響を与え、音声に異なる効果を与えます。
- dampening: リバーブの高周波数成分をどれだけ減衰させるかを調整します。値を上げると、高音が抑えられ、リバーブが柔らかくなります。
- width: ステレオフィールドの広がりを調整します。値を大きくすると、リバーブが広がり、空間の広さを感じやすくなります。
- room_size: リバーブがかかる仮想の部屋の大きさを調整します。部屋が大きいほど、リバーブの持続時間が長くなり、音が反響する空間が大きく感じられます。
- dry: 元の音声の音量を調整します。これにより、リバーブのかかっていない生の音がどれくらい聞こえるかが決まります。
- wet: リバーブがかかった音の音量を調整します。これを上げるとリバーブの効果が強調され、反響が目立つようになります。
音量の大きさには注意してください!!
フィードバックループが発生しないように注意してください!!
Rustのライブラリが古くて起動できない場合
1. gtkのバージョンをあげて:
examples/app_gtk/Cargo.toml
[package]
name = "freeverb-gtk"
version = "0.1.0"
authors = ["irh <ian.r.hobson@gmail.com>"]
edition = "2021"
[dependencies]
audio_module = { path = "../../src/audio_module" }
freeverb_module = { path = "../../src/freeverb_module" }
audio_thread_priority = "0.3"
cpal = "0.13.1"
crossbeam-channel = "0.3"
gtk = "0.18"
ringbuf = "0.2.2"
examples/app_gtk/src/gtk_parameter_slider.rs
use {
audio_module::{Command, Parameter},
gtk::prelude::*,
gtk::{Label, Orientation, PositionType, Scale},
};
pub fn make_slider(
parameter: Box<dyn Parameter>,
id: usize,
command_sender: crossbeam_channel::Sender<Command>,
) -> gtk::Box {
let container = gtk::Box::new(Orientation::Vertical, 2);
container.set_margin_top(2);
let label = Label::new(Some(parameter.name().as_str()));
container.pack_start(&label, false, false, 0);
let scale = Scale::with_range(Orientation::Vertical, 0.0, 1.0, 0.01);
scale.set_inverted(true);
scale.set_value_pos(PositionType::Bottom);
let value_converter = parameter.make_value_converter();
let string_converter = parameter.make_string_converter();
scale.set_value(value_converter.user_to_linear(parameter.default_user_value()) as f64);
scale.connect_format_value({
move |_, x| string_converter.to_string(value_converter.linear_to_user(x as f32))
});
container.pack_start(&scale, true, true, 0);
container.set_margin_bottom(2);
scale.connect_value_changed(move |scale| {
command_sender
.send(Command::SetParameter(id, scale.value() as f32))
.unwrap();
});
container
}
examples/app_gtk/src/gtk_parameter_toggle.rs
use {
audio_module::{Command, Parameter},
gtk::prelude::*,
gtk::{Orientation, ToggleButton},
};
pub fn make_toggle(
parameter: Box<dyn Parameter>,
id: usize,
command_sender: crossbeam_channel::Sender<Command>,
) -> gtk::Box {
let button = ToggleButton::with_label(parameter.name().as_str());
button.set_active(parameter.default_user_value() != 0.0);
button.connect_toggled(move |button| {
command_sender
.send(Command::SetParameter(
id,
if button.is_active() { 1.0f32 } else { 0.0f32 },
))
.unwrap();
});
let container = gtk::Box::new(Orientation::Vertical, 2);
container.pack_start(&button, true, false, 0);
container
}
examples/app_gtk/src/main.rs
use {
audio_module::{AudioModule, Widget},
freeverb_module::FreeverbModule,
gtk::{prelude::*, Orientation, Window, WindowPosition, WindowType},
};
mod audio_thread;
mod gtk_parameter_slider;
mod gtk_parameter_toggle;
fn main() {
run_main::<FreeverbModule>();
}
fn run_main<Module: AudioModule>() {
if gtk::init().is_err() {
println!("Error initializing GTK");
return;
}
let (command_sender, command_receiver) = crossbeam_channel::bounded(1024);
let sample_rate = 44100;
let _audio_streams = audio_thread::start_audio::<Module>(command_receiver, sample_rate)
.expect("Failed to start audio");
let window = Window::new(WindowType::Toplevel);
window.set_title("freeverb-rs");
window.set_default_size(350, 300);
window.set_position(WindowPosition::Center);
window.connect_delete_event(|_, _| {
gtk::main_quit();
false.into()
});
let container = gtk::Box::new(Orientation::Horizontal, 4);
window.add(&container);
for id in 0..Module::parameter_count() {
let parameter = Module::parameter(id);
let widget = match parameter.widget() {
Widget::Slider => {
gtk_parameter_slider::make_slider(parameter, id, command_sender.clone())
}
Widget::Button => {
gtk_parameter_toggle::make_toggle(parameter, id, command_sender.clone())
}
};
container.pack_start(&widget, false, true, 5);
}
window.show_all();
gtk::main();
}
2. 再ビルドして実行してみてください
まとめと次のステップ
この記事では、RustとCPALライブラリを使用して、リアルタイムで音声の入出力を行う基本的な仕組みについて学びました。
次のステップでは、エフェクトの実装に注目していきます。これまで見てきたリバーブのようなオーディオエフェクトを、どのようにコードで実装するのか、その仕組みやアルゴリズムについて紹介する予定です。