m13o.net

2020-12-12 Sat 21:00
Rustでwavを解析する その5   AdventCalendar2020 Rust programming

前回までで, 無圧縮PCMのバイナリデータからフォーマット情報と生の波形データのスライスを取得できるようになったはずですので, 実際にwavファイルからフォーマット情報を取得する単純なCLIを作成してみます.

workspaceのルートにあるCargo.tomlにメンバーを新たに追加します.

[workspace]
members = [
    "binary_reader",
    "wav",
    "wav_probe",
]

そして cargo newでwav_probeというbinary用crateを作成します.

cargo new wav_probe --bin

では依存関係の整備からしていきましょう. wav::PcmWavParserを利用して解析を進めていくので, このwav_probeはwav crateに依存します.

[dependencies]
wav = {path = "../wav"}

しかし, wav crateの中を見返してみると, 公開範囲がprivateになっているので, 外から見えるようにいくつかのstructやenum, メソッドを外部から参照できるようにします.

wav/lib.rsに実装している各要素の内, WavFormat, PcmWavFormat structそのものと各メンバーはpublicに設定します.

// wav/lib.rs

pub struct WavFormat {
    pub format_tag: u16,
    pub channels: u16,
    pub samples_per_sec: u32,
    pub avg_bytes_per_sec: u32,
    pub block_align: u16,
}

pub struct PcmWavFormat {
    pub wav_format: WavFormat,
    pub bit_per_sample: u16,
}

また, PcmWavParserもnew()やparse()メソッドは外から使われないといけないので, これらの公開範囲もpublicにします.

pub struct PcmWavParser<'a> {
    reader: BinaryReader<'a>,
}

impl<'a> PcmWavParser<'a> {
    pub fn new(buffer: &'a Vec<u8>) -> Self {
        // ...
    }

    pub fn parse(&mut self) -> Result<(PcmWavFormat, &'a[u8]), Box<dyn Error + 'static>> {
        // ...
    }

エラー関連も外に見せる必要があるので, wav/error.rsにあるRiffParseErrorもpublicにしていきます(が, その3あたりでpubにしてました).

#[derive(Debug, Eq, PartialEq)]
pub enum RiffParseError {
    InvalidChunkFourCC(u32),
    FmtChunkBeforeDataChunk,
}

wav/lib.rsの先頭に以下の設定を加えます(加えていましたが一応).

pub mod error;

では, wav_probe/main.rsに実装していきましょう.

今回は引数に与えられたwavファイルを解析して情報の表示を行うので, まずは引数を取得します.

Rustでコマンドライン引数を操作するには, std::env::args()を利用します. 今回はファイルパスとして成立する引数を1つ受け取るだけのCLIにするので, 引数が2つ以上ある時はエラーを返したいと思います. ただし, std::env::args()が返すArgs型はiterを実装しているのですが, その第一要素は必ずアプリケーションそのもののパスになっているので, 最初の要素はskipする必要があります.

fn main() {
    let args = std::env::args().skip(1).collect::<String>();
    match args.len() {
        1 => {
            todo!()
        },
        0 => println!("パラメータを指定してください."),
        _ => println!("パラメータが多すぎます"),
    }
}

PcmWavParserはメモリ上に展開されているデータを読み取る物として作られているので, ファイルにあるデータをメモリに載せる必要があります. そのため, std:fs::Fileとstd::io::BufReaderを利用して, ファイルの情報をVec<u8>のバッファにつめこみます.

use std::fs::File;
use std::io::{BufReader, Read};

//...

match args.len() {
    1 => {
        match File::open(args[0]) {
            Ok(file) => {
                let mut reader = BufReader::new(file);
                let mut buffer = Vec::new();
                match reader.read_to_end(&mut buffer){
                    Ok(_) => {
                        todo!()
                    },
                    Err(e) => println!("{}", e),
                }
            },
            Err(e) => println!("{}", e),
        }
    },

ネストが深くて若干の気持ち悪さがありますが, (面倒なので)このままやっていきます.

これで, ファイル内のデータを全てbufferというメモリ上に載せる事ができたので, PcmWavParserを呼び出して, フォーマット情報を取得していきましょう.

// ...
use wav::PcmWavParser;

// ...

match reader.read_to_end(&mut buffer){
    Ok(_) => {
        let mut parser = PcmWavParser::new(&buffer);
        match parser.parse() {
            Ok((format, data)) => {
                todo!()
            },
            Err(e) => println!("{}", e),
        }
    },
    Err(e) => println!("{}", e),
}

このあたりはテストコードと同様な書き方になるはずです. では表示用に別途関数を用意して, 取得したフォーマット情報を出力しましょう.

fn display_wav_info(format: &PcmWavFormat, data: &[u8]) {
    println!("フォーマットタグ: {}\n\
              チャンネル数: {}\n\
              サンプリングレート: {}\n\
              データ転送レート: {}\n\
              ブロックサイズ: {}\n\
              量子化bit数: {}\n\
              タイム: {}sec",
             format.wav_format.format_tag,
             format.wav_format.channels,
             format.wav_format.samples_per_sec,
             format.wav_format.avg_bytes_per_sec,
             format.wav_format.block_align,
             format.bit_per_sample,
             data.len() as f32 / format.wav_format.block_align as f32 / format.wav_format.samples_per_sec as f32);
}

表示用の関数はこのようにprintln!()で取得した情報を列挙しているだけです. これを, main()関数の中で呼び出します.

//...

let mut parser = PcmWavParser::new(&buffer);
match parser.parse() {
    Ok((format, data)) => {
        display_wav_info(&format, data);
    },
    Err(e) => println!("{}", e),
}
//...

では, 適当に作ったwavファイルを出力してみましょう.

$ cargo run --package wav_probe "tests/wav/sine.wav"
  フォーマットタグ: 1
  チャンネル数: 2
  サンプリングレート: 44100
  データ転送レート: 176400
  ブロックサイズ: 4
  量子化bit数: 16
  タイム: 2.1253288sec

はい, このように情報を表示できるようになりました. お疲れ様でした.