GB/音频/Rust Cpal 库介绍
对于数字音频来说, 通常其波形是已知的, 这在 Game Boy 中也不例外. 为了在 PC 上播放这些波形图所代表的音频, 需要能将波形数据推送到音频设备(例如音响, 耳机等)的手段.
cpal 是 Rust 下的一个底层音频 IO 库, 通过它可以播放(到音频设备)和收集音频流(从耳麦). 本小节将聚焦在使用 cpal 播放音频上. 在 cpal 中有以下几个概念:
- Device. Device 是一个音频设备, 该设备能提供音频输入流, 音频输出流或两者兼备. 常见比如麦克风, 音响, 耳麦等, 都是合法的 Device.
- Stream. Stream 是开放的音频通道. Input streams 允许你接收音频数据, Ouput streams 允许你播放音频数据. 在创建新的 streams 的时候, 必须指定要处理该 stream 的 device.
- EventLoop. EventLoop 是由一个或多个设备运行的流的集合. 每个流必须属于 EventLoop, 并且属于 EventLoop 的所有流都是一起管理的.
使用 cpal 的第一步是创建一个 EventLoop:
use cpal::EventLoop;
let event_loop = EventLoop::new();
然后需要选择一个播放设备. 最简单的方法是通过 cpal::default_output_device() 函数使用默认输出设备, 这通常与当前操作系统设置的默认音频播放设备一样. 或者, 可以使用 devices() 函数枚举所有可用设备. 请注意, 如果系统上没有可用于该流类型的设备, 则 default_output_device() 函数将会返回 None.
let device = cpal::default_output_device().expect("no output device available");
在创建 Stream 之前, 必须先行决定音频样本的格式. 使用 cpal::supported_output_formats() 方法查询所有支持的格式. 它生成一个 SupportedFormat 结构列表, 在之后可以将其转换为实际的 Format 结构. 除了查询格式列表再选择合适的格式外, 也可以手动构建自己的格式, 但如果设备不支持该格式, 则在构建流时可能会导致错误, 因此后者并不推荐使用. 注意, supported_output_formats() 方法可能会返回错误, 例如, 当设备已断开连接的时候.
let mut supported_formats_range = device.supported_output_formats()
.expect("error while querying formats");
let format = supported_formats_range.next()
.expect("no supported format?!")
.with_max_sample_rate();
为了创建 Stream 所需要的所有内容均已准备就绪, 现在从 EventLoop 中创建它:
let stream_id = event_loop.build_output_stream(&device, &format).unwrap();
build_output_stream() 返回的值是一个 StreamId, 通过该 StreamId 可以控制流的运行细节. 现在可以安全的启动流了, 通过 EventLoop 上的 play_stream() 方法完成它:
event_loop.play_stream(stream_id);
音频设备正在等待你传入数据! 在 event_loop 上调用 run() 来开始处理:
event_loop.run(move |_stream_id, _stream_data| {
// read or write stream data here
});
当 run() 运行时, 指定的音频设备将定期调用我们传递给此函数的回调函数. 回调函数接收 StreamId 和 StreamData 类型的实例, 可以从该实例读取或写入的数据. StreamData 的数据类型可以是 I16, U16 或 F32 中的一个, 具体取决于传递给build_output_stream的格式.
在下面这个例子中, 程序会输出一个正弦波, 运行后将能听到"哔哔"的声音. 完整代码如下所示:
use cpal;
fn main() {
let device = cpal::default_output_device().expect("Failed to get default output device");
let format = device.default_output_format().expect("Failed to get default output format");
let event_loop = cpal::EventLoop::new();
let stream_id = event_loop.build_output_stream(&device, &format).unwrap();
event_loop.play_stream(stream_id.clone());
let sample_rate = format.sample_rate.0 as f32;
let mut sample_clock = 0f32;
// Produce a sinusoid of maximum amplitude.
let mut next_value = || {
sample_clock = (sample_clock + 1.0) % sample_rate;
(sample_clock * 440.0 * 2.0 * 3.141592 / sample_rate).sin()
};
event_loop.run(move |_, data| {
match data {
cpal::StreamData::Output { buffer: cpal::UnknownTypeOutputBuffer::U16(mut buffer) } => {
for sample in buffer.chunks_mut(format.channels as usize) {
let value = ((next_value() * 0.5 + 0.5) * std::u16::MAX as f32) as u16;
for out in sample.iter_mut() {
*out = value;
}
}
},
cpal::StreamData::Output { buffer: cpal::UnknownTypeOutputBuffer::I16(mut buffer) } => {
for sample in buffer.chunks_mut(format.channels as usize) {
let value = (next_value() * std::i16::MAX as f32) as i16;
for out in sample.iter_mut() {
*out = value;
}
}
},
cpal::StreamData::Output { buffer: cpal::UnknownTypeOutputBuffer::F32(mut buffer) } => {
for sample in buffer.chunks_mut(format.channels as usize) {
let value = next_value();
for out in sample.iter_mut() {
*out = value;
}
}
},
_ => (),
}
});
}