我需要生成正弦波样本以将其作为音频播放。频率范围为 200 至 2000Hz,步长为 1Hz。这是嵌入式项目的一部分,我想花尽可能少的时间来生成音频样本。我的解决方案是生成一个周期的样本并重复播放它们,直到需要更改音调频率,此时我将生成新的样本。
use std::f32::consts::TAU;
const SAMPLE_RATE: u32 = 8000;
const BUFFER_SIZE: usize = 100;
fn main() {
let mut values = Vec::new();
let mut buffer = [0f32; BUFFER_SIZE];
let mut tone_generator = ToneGenerator { phase: 0.0 };
let count = tone_generator.generate_samples(&mut buffer, 495, 0.0);
values.extend_from_slice(&buffer[0..count]); // Pretend this is pushing the samples to a FIFO
values.extend_from_slice(&buffer[0..count]); // Repeat the same values
values.extend_from_slice(&buffer[0..count]); // Again
let count = tone_generator.generate_samples(&mut buffer, 496, 0.0); // New samples
values.extend_from_slice(&buffer[0..count]); // Etc.
values.extend_from_slice(&buffer[0..count]);
let count = tone_generator.generate_samples(&mut buffer, 497, 0.0);
values.extend_from_slice(&buffer[0..count]);
values.extend_from_slice(&buffer[0..count]);
// Plot all the samples
let mut plot = plotly::Plot::new();
let ts = values
.iter()
.enumerate()
.map(|(ts, _)| ts as f32 / SAMPLE_RATE as f32)
.collect::<Vec<_>>();
let trace = plotly::Scatter::new(ts, values.clone())
.name("sin")
.mode(plotly::common::Mode::LinesMarkers);
plot.add_trace(trace);
plot.show();
}
struct ToneGenerator {
phase: f32,
}
impl ToneGenerator {
fn generate_samples<'a>(
&'a mut self,
buffer: &'a mut [f32; BUFFER_SIZE],
frequency: u32,
volume_db: f32,
) -> usize {
let delta = frequency as f32 * TAU / SAMPLE_RATE as f32;
let amplitude = libm::powf(10.0, volume_db / 20.).min(1.0);
let mut count = 0;
for value in buffer.iter_mut() {
*value = self.phase.sin() * amplitude;
count += 1;
self.phase += delta;
if self.phase > TAU {
self.phase -= TAU;
break;
}
}
count
}
}
问题是,当我以 1Hz 为步长增加音调频率时,步长之间声音的变化不是恒定的。我认为有两个原因:
有没有一种方法可以在频率不变的情况下解决这些问题而不需要生成新的样本?对我来说,这两个要求似乎是矛盾的,但也许有一种技术可以做到这一点?
如果以特定周期(此处由样本计数确定)重复波形,则它将仅包含基频(周期的倒数)倍数的频率。做其他事是不可能的;信号数学告诉我们这一点。 (考虑信号的傅里叶变换是什么。)
因此,为了对预生成的波进行更精确的频率选择,必须将它们做得更长——特别是,为了填补“N个样本”和“N+1个样本”周期之间的间隙,您需要生成具有 (N × 2) + 1 个样本的信号,其中包含两个波周期。这不是冗余数据,因为中间部分的样本与两端不同。
或者相反,要使用更少的存储空间,您可以通过重新采样(在存储的样本之间进行插值)来更改播放期间单个波形的频率 - 但我相信这相当于使用正弦查找表生成正弦波.
另一种通用策略是通过旋转单位向量(或等效的复数)来生成波。
这需要计算归一化的平方根,但除了初始化外,不需要计算正弦或余弦。