如何在Rust音频可视化多频点间生成贝塞尔类曲线替代插值直线
问题
这个任务相对复杂,我无法在标题中概括全部问题,具体如下:
- 我开发了一款音频可视化工具,可将原始音频数据转换为
Vec<f32>格式,向量元素按频率从低到高排序,覆盖0Hz到20000Hz范围。 - 我需要对该向量做
normalize处理,将原有的线性频率排布转换为更符合人耳听觉特性的对数排布,对应实现函数如下:
fn normalize(buffer: Vec<f32>, volume: f32) -> Vec<f32> { let mut output_buffer: Vec<f32> = vec![0.0; buffer.len()]; let mut start_pos: usize = 0; let mut end_pos: usize = 0; for i in 0..buffer.len() { // FIRST HALF let offset: f32 = (buffer.len() as f32 / (i + 1) as f32).sqrt(); if ((i as f32 * offset) as usize) < output_buffer.len() { // normalized position let pos: usize = (i as f32 * offset) as usize; // stores positions needed for filling start_pos = end_pos; end_pos = pos; let y = buffer[i]; // prevent volume loss, that could occur because of 'crunching' of higher freqs // by only setting the value of buffer if y is bigger if output_buffer[pos] < y { output_buffer[pos] = y; } } // SECOND HALF // linear filling of the values between if end_pos - start_pos > 1 && (end_pos - 1) < output_buffer.len() { for s_p in (start_pos + 1)..end_pos { let percentage: f32 = (s_p - start_pos) as f32 / ((end_pos - 1) - start_pos) as f32; let mut y: f32 = 0.0; //(output_buffer[s_p] * (1.0 - percentage) ) + (output_buffer[end_pos] * percentage); y += output_buffer[start_pos] * (1.0 - percentage); y += output_buffer[end_pos] * percentage; output_buffer[s_p] = y; } } } output_buffer }
代码上半部分将buffer中的值重排为对数分布,但这种方法会导致尤其是低频段的大量数值被跳过,可视化效果如下(未填充间隙):
| | | | | | | | | | | | ||| | | | | ||| +----+---+--+-+++
因此我在代码下半部分实现了间隙填充逻辑,填充后的效果如下:
| :|: | ::|:: :|: :::|::: ::|:| | ::::|:::|::|:||| |::::|:::|::|:||| +----+---+--+-+++
为了方便展示我减少了柱体数量,实际实现的柱体数量是演示的10倍左右,所以线性插值的直线感会非常明显。
我最终的需求是:将节点间的直线插值替换为曲线插值,更符合声音的表现特性,同时需要支持获取曲线上任意点的y坐标值。
请问有什么可行的实现方案?还是我当前的实现思路存在问题?
以上代码来自我开发的音频处理库audioviz,我还开发了配套带GUI的应用audiolizer可调用该库。
回答
你当前的实现思路没有问题,两种优化方向都可以满足需求,可根据改造成本选择:
方案1:最小成本改造现有插值逻辑
如果不想调整核心频率映射逻辑,仅替换插值算法即可:
- 余弦插值
仅需修改一行代码就能实现S型平滑过渡,计算量和线性插值几乎一致,可快速消除生硬的直线感:
// 原有计算percentage的逻辑保留,新增如下转换即可 let cos_percentage = (1.0 - (percentage * std::f32::consts::PI).cos()) * 0.5; let y = output_buffer[start_pos] * (1.0 - cos_percentage) + output_buffer[end_pos] * cos_percentage;
- Catmull-Rom样条插值
如果需要多节点连续平滑效果、支持任意点取y值,可使用这个音频可视化领域常用的算法,仅需要相邻4个锚点即可计算区间内任意位置的幅值,不会出现过冲失真:
// p0/p1/p2/p3为连续四个锚点的幅值,t为当前区间内的百分比(取值0~1) fn catmull_rom(p0: f32, p1: f32, p2: f32, p3: f32, t: f32) -> f32 { let t2 = t * t; let t3 = t2 * t; 0.5 * ( 2.0 * p1 + (-p0 + p2) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3 ) }
使用前需要把第一步映射得到的所有锚点(位置、幅值)存入单独数组,插值时取当前区间的前后两个锚点即可,首尾区间的缺失锚点可以直接用首尾锚点的值补全。
方案2:优化频率映射逻辑,从根源消除插值需求
你当前用的是正向映射逻辑:从原线性FFT下标映射到对数坐标位置,属于下采样式映射,天然会产生间隙。换用反向映射逻辑可以完全避免插值,结果更准确性能也更高:
- 遍历输出buffer的每个下标,计算该下标对应的对数频率值;
- 根据采样率、FFT长度参数,转换得到该频率对应原线性FFT的下标区间;
- 取该区间内幅值的最大值或平均值,直接赋值给输出buffer对应位置。
这种方式得到的输出天然平滑,不需要额外做间隙填充,也不会有插值带来的失真问题,更适合音频可视化场景。
内容的提问来源于stack exchange,提问作者BrunoWallner
相关产品推荐
相关产品推荐

