index

Rust Sine Wave 產生器

· 3min

Conclusion

Rust 播放 WAV 檔與即時音量控制 不同的是,這次 source 是自已產生,且 decoder 的部分也要自行處理。

以下是版本一:

// 260602 Sine Wave 產生器
use cpal::{SampleFormat, Sample, FromSample};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};

fn sine_gen<T>(data: &mut [T], _: &cpal::OutputCallbackInfo)
where
    T: Sample + FromSample<f32>
{
    let sample_rate = 48000.0;
    let freq = 440.0;

    for (idx, sample) in data.iter_mut().enumerate() {
        let t = idx as f32 / sample_rate;
        let sig: T = T::from_sample((2.0 * std::f32::consts::PI * freq * t).sin());

        *sample = sig;
    }
}

fn main() {
    let host = cpal::default_host();
    let device = host.default_output_device().expect("No availible device");
    let supported_config = device
        .default_output_config()
        .expect("Error while querying configs");

    let sample_format = supported_config.sample_format();
    let config = supported_config.into();

    let err_fn = |err| eprintln!("Error: {err}");

    let stream = match sample_format {
        SampleFormat::F32 => device.build_output_stream(&config, sine_gen::<f32>, err_fn, None),
        SampleFormat::I16=> device.build_output_stream(&config, sine_gen::<i16>, err_fn, None),
        SampleFormat::U16=> device.build_output_stream(&config, sine_gen::<u16>, err_fn, None),
        other_format => panic!("Unsupported sample format '{:?}'", other_format),
    }
    .unwrap();

    stream.play().unwrap();

    std::thread::sleep(std::time::Duration::from_secs(5));
}

會發現雖然用 idx 來製造 t 可行,但不會是純 sine wave,主要還是因為 idx 會從 0 開始算,導致產生「破音」。

// 取得真實的取樣率
let sample_rate = config.sample_rate.0 as f32;
let freq = 440.0;

let stream = match sample_format {
    SampleFormat::F32 => {
        // 1. 在傳入閉包「之前」,宣告一個可變的狀態變數
        let mut sample_clock = 0f32; 
        
        device.build_output_stream(
            &config,
            // 2. 使用 move 關鍵字,把 sample_clock 的所有權「吃」進閉包裡
            move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
                for sample in data.iter_mut() {
                    // 3. 推進時鐘(加上取餘數防止時間無限大導致浮點數失真)
                    sample_clock = (sample_clock + 1.0) % sample_rate;
                    
                    let t = sample_clock / sample_rate;
                    *sample = (2.0 * std::f32::consts::PI * freq * t).sin();
                }
            },
            err_fn,
            None,
        )
    },
    // (I16, U16 的邏輯一模一樣,只需宣告各自的 clock 並轉型即可)
    // ...
    other_format => panic!("Unsupported sample format '{:?}'", other_format),
}.unwrap();

Practice

cpal

Links
cargo add cpal
  • host本機,給予各種可存取的外部音訊裝置(device)
  • device 具有 IO 的音訊裝置
  • stream 音訊流,需選擇「流向」的裝置

From / Into

From 可以多種不同型別都換為一種(例如 String)Into 則是比較方便的寫法,但因為沒有指定型別,編譯器不會知道要哪一種,所以通常需要變數手動指定。

建立 From 之後 Into 也會隨之成立。

let my_string = String::from("hello");

原始碼:

// 這是標準函式庫中,為 String 實作 From<&str> 的原碼
#[stable(feature = "rust1", since = "1.0.0")]
impl From<&str> for String {
    #[inline]
    fn from(s: &str) -> String {
        s.to_owned()
    }
}

回到實作:

// 意思是:為 StreamConfig 這個型別,實作 From<SupportedStreamConfig> 特徵
impl From<SupportedStreamConfig> for StreamConfig {
    
    // 這裡的 Self,就是 StreamConfig 的替身!
    fn from(conf: SupportedStreamConfig) -> Self {
        // 內部邏輯:把 conf 拆開,重新組裝成 StreamConfig
        StreamConfig {
            channels: conf.channels(),
            sample_rate: conf.sample_rate(),
            buffer_size: conf.buffer_size().clone(),
        }
    }
}

References