【Rust】WAV音频命令行工具`zrtstr`解析

381 阅读9分钟

Awesome-Rust常年身居第一,是Rust早期版本的一个工具。它的主要功能是:检查立体声 WAV 文件是否具有相同的通道(假立体声)并将其转换为单声道。

项目连接: zrtstr

梗概

  1. 数字音频基本功
    1. wav文件格式
    2. 抖动阈值
    3. 采样点
    4. 假立体声转单声道
  2. 项目设计思路

数字音频基本功

wav文件格式

当我们谈论 WAV 文件格式时,通常是指标准的 WAV 文件格式,也称为 WAV RIFF(Resource Interchange File Format)。WAV 是一种无损音频文件格式,最初由 Microsoft 和 IBM 在 1991 年共同开发。它广泛用于存储音频数据,因为它不会导致任何音频质量损失。

以下是 WAV 文件格式的一般结构:

  1. 文件头(RIFF Header):WAV 文件以 RIFF 文件格式开始。文件头包含四个字节的文件类型标记 "RIFF",以及文件的总体大小(不包括文件头的大小)。

  2. 文件大小(Chunk Size):指定文件中数据块的总大小(以字节为单位)。

  3. 文件格式类型(Format Type):通常为 "WAVE",指示文件是 WAV 文件。

  4. 格式块(Format Chunk):包含了音频数据的格式信息,如采样率、声道数、位深度等。

  5. 数据块(Data Chunk):包含实际的音频数据。数据块以 "data" 标记开头,紧随其后的是音频数据的总大小。

在 WAV 文件中,音频数据是以 PCM(脉冲编码调制)方式存储的。PCM 是一种无损的音频编码方式,它直接将模拟音频信号转换成数字信号,并将每个采样点的振幅值保存为一个整数,以表示音频样本的幅度。

总的来说,WAV 文件格式是一种简单而通用的音频文件格式,它存储了音频数据的原始采样值,因此保留了音频的完整质量。这使得 WAV 文件成为音频处理和编辑的首选格式之一。

抖动阈值

抖动是一种在数字音频处理中常用的技术,用于改善低位精度的音频信号质量。在将音频从高位精度转换为低位精度的过程中,会引入量化误差,抖动技术可以通过在量化误差上添加一个小的随机信号来减少量化误差带来的不良影响,从而提高音频的动态范围和声音的质量。设置合适的抖动阈值可以控制抖动的效果。

当在数字音频处理中从高位精度转换为低位精度时,会出现量化误差。量化误差是指将连续的音频信号转换为离散的数字值时引入的误差。这些误差可能导致音频信号失真或丢失细节,特别是在较低的位深度下。

抖动是一种技术,用于减少量化误差对音频质量的影响。它通过在量化误差上添加一个小的随机信号来平均化和分散误差,从而使得误差不集中在固定的值上,而是分散在一个范围内。这样可以减少量化噪声的影响,提高音频的动态范围和声音的质量。

在处理抖动音频时,可以设置一个阈值来控制抖动的效果。阈值表示允许的样本级别差异,即允许的振幅变化大小。如果阈值设置为0,表示进行严格的检查,抖动效果会更加保守,不会引入较大的振幅变化。而如果阈值设置为一个自然数(比如1、2等),则表示允许更大的振幅变化,抖动效果会更明显,从而减少了量化误差的感知。

因此抖动技术可以帮助改善低位精度的音频信号质量,而设置合适的阈值可以调整抖动的效果,从而平衡音频质量和文件大小等因素。

阈值设为0时,表示进行严格的检查,而非严格检查则允许较小的振幅变化。

当将阈值设置为100时,意味着允许每个样本之间的振幅变化达到100个自然数的幅度。这样的抖动效果会非常明显,量化误差的影响会被大幅减少。具体效果取决于音频数据的位深度和抖动算法的实现,但一般来说,较大的阈值会导致以下效果:

  • 动态范围增加:较大的阈值可以使得音频信号的动态范围更加宽广,音频的细节和动态变化将得到保留,使得音频听起来更加自然和逼真。

  • 减少量化噪声:抖动技术的使用可以减少由于量化误差引起的噪声,音频的清晰度和纯净度会得到提升。

  • 降低失真:较大的阈值可以减少音频信号的失真,特别是在低位深度的情况下,音频的还原效果会更好。

然而需要注意的是,过度使用抖动和设置过大的阈值可能会导致音频听起来过于平滑,失去了原始音频的细节和真实感。因此,在应用抖动技术时,需要根据具体的音频数据和需求来选择合适的阈值,以达到最佳的效果。

采样点

在处理音频数据时,会检查每一对采样点的数据是否相同,或者是否在给定的差异范围内。

而在 WAV 文件或其他音频文件中,音频数据由一系列采样点组成。每个采样点代表一段时间内的声音波形的振幅值。

  • 当处理音频数据时,有时需要检查相邻两个采样点的数据是否相同,或者是否在某个范围内有微小的差异。
  • 这种检查通常是为了检测音频中是否存在无声或静音部分,或者为了检测音频中是否存在过渡平滑的变化。

这个项目中,我们比较相邻两个采样点的数据,并判断它们是否相同,或者它们之间的差异是否在预先定义的范围内。

如果数据相同或者差异在允许的范围内,则满足条件;

否则,可能需要进行一些处理或者标记这些部分的数据。这有助于确保音频数据的质量和准确性。

假立体声转单声道

当相邻两个采样点的数据相同时,意味着音频数据在这两个采样点之间没有发生任何变化,没有声音波形的振幅变化。

这种情况通常表示音频中的无声或静音部分。

当相邻两个采样点的数据有微小的差异时,意味着音频数据在这两个采样点之间发生了一些变化,声音波形的振幅略微改变。这种情况通常表示音频中发生了平滑的变化或过渡。

在处理音频数据时,检查相邻两个采样点的数据是否相同,或者在给定的差异范围内,来判断音频中是否存在静音或平滑变化的部分。这可能在一些音频处理任务中很有用,例如消除静音部分、音量平滑处理或转化为单声道(如果适用)等。

项目实现

当我们要设计一个音频处理文件的工具时,我们需要考虑3点:

  1. 该工具原始文件是什么?
  2. 输出文件是什么?
  3. 该工具的功能是什么?

1,2假设都以WAV来作为原始文件和输出文件,3则是本项目的重点。

以命令行工具为例,我们需要考虑的是:

  1. 如何使用命令行处理和输入
  2. 怎么处理
  3. 如何输出和保存

1. 如何使用命令行处理和输入

如果我们要使用命令行的话,首先就要有一个提示用户输入的命令行,这里我们使用clap来实现。

extern crate clap;

use clap::{Arg, Command};

    let matches = Command::new("zrtstr")
        .author(crate_authors!())
        .version(crate_version!())
        .about("Check stereo WAV-file for identical channels, detecting \
                faux-stereo files generated by some audio-editing software and \
                DAWs.\nOutputs a true-mono WAV file on detection of \
                faux-stereo. Takes left channel of the input file, writes in the \
                same location with -MONO suffix in file name.")
        .arg(Arg::new("INPUT")
                .help("Path to the input file to process. If no input given, \
                process all WAV files in current directory.")
                .index(1)
                .value_parser(validate_path))
        .arg(Arg::new("dither")
                .short('d')
                .long("dither")
                .value_name("THRESHOLD")
                .required(false)
                .help("Set threshold for sample level difference to process \
                        dithered audio.{n}Natural number (amplitude delta). Set to 0 for strict checking.")
                .next_line_help(true)
                .num_args(1)
                .default_missing_value("10")
                .default_value("10")
                .value_parser(clap::value_parser!(u32)))
        .arg(Arg::new("license")
                .short('l')
                .long("license")
                .action(clap::ArgAction::SetTrue)
                .help("Display the license information."))
    .get_matches();

再次判断一下临界条件:

  1. 如果打印帮助信息,那么就不需要处理了,直接退出。
  2. 如果选择处理,则进行调度

退出:

    if matches.get_flag("license") {
        println!("{}", LICENSE);
        println!("\nclap (Command Line Argument Parser) License:");
        println!(include_str!("../LICENSE-CLAP"));
        process::exit(0);
    }

至于调度,我们则要考虑下是否设置抖动阈值?此处先设置一个Conf的struct.

pub struct Conf {
    pub dither: u32,
    ...
}

怎么处理

面对这个问题,其实上面的基础则说得很清楚,我们只需要考虑实现2点:

  1. 遍历WAV的采样点
  2. 判断采样点是否相同,是否满足抖动阈值

在这个基础上,其实只有一块核心,那就是如下:

// dither_threshold就是抖动阈值 dither
iter.map(|s| s[0] - s[1])
    .inspect(|_| update_pb())
    .any(predicate)

let predicate = |diff: i32| -> bool {
    if dither_threshold == 0 {
        diff != 0
    } else {
        diff.unsigned_abs() > dither_threshold
    }
};

在这个代码段中,diff是相邻的两个采样点的差值,用于判断这两个采样点是否相同或在给定的差异范围内。

dither_threshold是一个阈值,它表示允许的最大采样点差值。如果dither_threshold设置为0,表示不允许任何差异,即相邻的两个采样点必须完全相同。因此,diff != 0用于判断相邻的两个采样点是否不相同,如果不相同,就意味着音频数据不满足双声道特征,是真正的立体声。

如果dither_threshold不为0,表示允许一定的差异范围。在这种情况下,diff.unsigned_abs() > dither_threshold用于判断相邻的两个采样点的差值的绝对值是否大于dither_threshold,如果大于,就意味着音频数据满足伪立体声的特征。

故这个代码段通过dither_threshold的设置来判断音频数据是否满足双声道或伪立体声的特征。如果dither_threshold为0,则要求相邻的两个采样点完全相同,否则允许一定的差异范围。

如何输出和保存

核心问题已经解决,其实保存和输出则是很简单的调用。建议参考源码zrtstr