【CPAL 使用笔记 ②】制作一个振荡器(一)

本文介绍了如何使用Rust语言和CPAL库创建一个可定制波形的音频振荡器,详细阐述了Oscillator结构体、Waveform枚举以及如何实现正弦波功能,包括频率设置和采样过程。后续将探讨其他波形和完整代码示例。
摘要由CSDN通过智能技术生成

前篇导航:CPAL 使用笔记 ①】了解与基本配置

本篇将介绍大体的结构,并以正弦波为例讲解代码细节。
下一篇会继续讲解其他波形的构造方法,并示例完整代码。


构思

我们想制作一个振荡器,发出一些由简单的波形组成的声音。
我们希望它可以有如下的功能:

  1. 可以选择不同的波形
  2. 可以指定频率(也就是周期)

工作流程

按我们的设想,这个振荡器想要发出声音,实际上向 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=ksin(2πfSx)
事实上,要求的振幅的值域为 [ − 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 上运行起来。
答案下节公布

  • 51
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值