前篇导航:CPAL 使用笔记 ①】了解与基本配置
本篇将介绍大体的结构,并以正弦波为例讲解代码细节。
下一篇会继续讲解其他波形的构造方法,并示例完整代码。
构思
我们想制作一个振荡器,发出一些由简单的波形组成的声音。
我们希望它可以有如下的功能:
- 可以选择不同的波形
- 可以指定频率(也就是周期)
工作流程
按我们的设想,这个振荡器想要发出声音,实际上向 Stream 的回调函数中周期性发送数据。我们把振荡器作为一个对象(也就是rust中的结构体),我们希望这个结构体可以实现构造不同波形的方法,返回每个时刻的振幅。
我们要用到的东西有:
struct Oscillator; // 振荡器的结构体
enum Waveform; // 不同波形的枚举类型
fn process_frame; // 回调函数调用它获得每个时刻的振幅
fn make_stream; // 构造 stream 的函数
制作
主体结构
波形的枚举:
enum Waveform {
Sine,
Square,
Saw,
Triangle,
}
构造一个 Oscillator:
struct Oscillator {
pub sample_rate: f32, // 采样率
pub waveform: Waveform, // 波形(用到上面的枚举)
pub current_sample_index: f32, // 第几个采样点
pub frequency_hz: f32, // 频率
}
为 Oscillator 实现方法
以正弦波为例。
我们都知道正弦函数的数学表达式
y
=
s
i
n
(
x
)
y = sin(x)
y=sin(x) 。但在这里,我们需要实现一些量纲的转换。
具体地说,我们的自变量的单位是采样数
x
x
x 。
每个采样的时间为
1
S
\frac{1}{S}
S1(其中
S
S
S 是采样率)。则第
x
x
x 个采样点经过的时间为
x
S
\frac{x}{S}
Sx 。
我们希望该波的频率为
f
f
f ,则有
y
=
k
⋅
sin
(
2
π
f
⋅
x
S
)
y = k \cdot \sin \Big( 2 \pi f \cdot \frac{x}{S} \Big)
y=k⋅sin(2πf⋅Sx)
事实上,要求的振幅的值域为
[
−
1
,
1
]
[-1,1]
[−1,1] ,因此
k
=
1
k=1
k=1 。即
y
=
sin
(
2
π
f
x
/
S
)
y = \sin \big( 2 \pi f x / S \big)
y=sin(2πfx/S)
在实际的设计中,我们将所有的运算放在一个周期内进行。所以每次更新
x
x
x 时都将
x
x
x 对
S
S
S 取余,这样可以避免数据溢出。
impl Oscillator {
// 更新当前采样数(每次取余)
fn advance_sample(&mut self) {
self.current_sample_index = (self.current_sample_index + 1.0) % self.sample_rate;
}
// 计算振幅(用上面推出来的式子)
fn calculate_sine_output_from_freq(&self, freq: f32) -> f32 {
let two_pi = 2.0 * std::f32::consts::PI;
(self.current_sample_index * freq * two_pi / self.sample_rate).sin()
}
// 每次调用获取当前帧的振幅
fn sine_wave(&mut self) -> f32 {
self.advance_sample();
self.calculate_sine_output_from_freq(self.frequency_hz)
}
// 其实 tick() 才是每次调用的入口,它来选择到底用哪个波形生成振幅
fn tick(&mut self) -> f32 {
match self.waveform {
Waveform::Sine => self.sine_wave(),
Waveform::Square => self.square_wave(),
Waveform::Saw => self.saw_wave(),
Waveform::Triangle => self.triangle_wave(),
}
}
}
回调函数(process_frame)
如其名字所示,它是计算每个帧采样的振幅的入口函数,它负责对采样数据从具体操作,而计算的部分交给 Oscillator 下实现的方法。
fn process_frame<SampleType>(
output: &mut [SampleType], // stream 打包的 data 数据引用。我们就是在改它
oscillator: &mut Oscillator, // 振荡器的引用
num_channels: usize, // 通道数
) where
SampleType: Sample + FromSample<f32>,
{
for frame in output.chunks_mut(num_channels) {
// 调用 tick() 函数得到当前帧的振幅,并通过 from_sample() 统一数据类型
let value: SampleType = SampleType::from_sample(oscillator.tick());
// 把得到的数据复制到每个通道(相当于在改数据)
for sample in frame.iter_mut() {
*sample = value;
}
}
}
构造 Stream
fn make_stream<T>(
device: &cpal::Device,
config: &cpal::StreamConfig,
) -> Result<cpal::Stream, anyhow::Error>
where
T: SizedSample + FromSample<f32>,
{
//获取通道数
let num_channels = config.channels as usize;
// 声明一个频率为 440Hz 的振荡器
let mut oscillator = Oscillator {
waveform: Waveform::Sine,
sample_rate: config.sample_rate.0 as f32,
current_sample_index: 0.0,
frequency_hz: 440.0,
};
// 申明错误处理函数
let err_fn = |err| eprintln!("Error building output sound stream: {}", err);
let time_at_start = std::time::Instant::now();
println!("Time at start: {:?}", time_at_start);
// 构造 stream
let stream = device.build_output_stream(
config,
move |output: &mut [T], _: &cpal::OutputCallbackInfo| {
// 调用 process_frame() 函数
process_frame(output, &mut oscillator, num_channels)
},
err_fn,
None,
)?;
// 没问题就返回 stream 并移交所有权。
Ok(stream)
}
– Have a try –
想想上一节的内容,看看能不能将返回的 stream 在 device 上运行起来。
答案下节公布