m13o.net

2020-12-09 Wed 23:00
Rustでwavを解析する その2   AdventCalendar2020 Rust programming

前回は無圧縮PCMのwavのフォーマットについてざっと概要を記述しました. 今回はこの内容を元にwavデータの構造をRustで表現するために, 以前作成したワークスペースのwavプロジェクト上に実装していこうと思います.

何をすればいいかの確認のために, 前回提示した大枠の処理の流れを再掲します.

  1. 先頭4バイトを取得し, RIFFのFourCCの有無を確認
  2. 次の4バイトを取得し, データサイズを確認
  3. 次の4バイトを取得し, RIFFのデータ形式が何であるかを確認
  4. 次の4バイトを取得し, サブチャンクが何であるかを確認
  5. 次の4バイトを取得し, データサイズを確認
  6. サブチャンクがLISTならば, 4に戻る
  7. 6. でないならば, 5のデータサイズまでバイト列をそのサブチャンクの種別毎に解析
  8. EOFまで4を繰り返す

今はひとまず, 無圧縮PCMのwavについてのみフォーカスしているので, 以下のようなPcmWavFormatというstructと, 波形データの集合のスライスを返すパーサーを記述していきます.

なにはなくとも, RIFFチャンクの解析をしなければ始まらないので,まずはRIFF共通のヘッダ情報を取得しましょう. RIFFには各チャンクは共通でFourCCとチャンクサイズという並びのバイト列が必ず存在するのでそれを示すstructの作成と, バイト列を読み込んでそのstructを返すパーサーを書きます.

struct RiffCommonHeader {
    four_cc: u32,
    chunk_size: u32,
}

four_ccが[char; 4]や[u8; 4]ではなくu32なのは, 比較する時に面倒だなと思ったからです. そのため, 4つのcharをu32に変換する関数を用意しましょう.

fn make_fourcc_to_le_u32(cc0: char, cc1: char, cc2: char, cc3: char) -> u32 {
    (cc3 as u32) << 24 | (cc2 as u32) << 16 | (cc1 as u32) << 8 | (cc0 as u32)
}

一応テストコードも書いておきましょう.

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn make_four_cc_to_le_u32_test() {
        let value = make_fourcc_to_le_u32('A', 'B', 'C', 'D');
        assert_eq!(value,
                   0x44434241);
    }
}

cargo testを実行してテストが……おや?

error[E0015]: calls in constants are limited to constant functions, tuple structs and tuple variants

コンパイルに失敗しました. なるほど, const functions. どうやらコンパイル時に値が定まる関数として利用したい場合は constをfnの前に付ける必要があるようです.

const fn make_fourcc_to_le_u32(cc0: char, cc1: char, cc2: char, cc3: char) -> u32 {
    (cc3 as u32) << 24 | (cc2 as u32) << 16 | (cc1 as u32) << 8 | (cc0 as u32)
}

これでテストが通りました.

では, これを元に必要なFourCCをconstとして定義します.

const FOUR_CC_RIFF: u32 = make_fourcc_to_le_u32('R', 'I', 'F', 'F');
const FOUR_CC_WAVE: u32 = make_fourcc_to_le_u32('W', 'A', 'V', 'E');
const FOUR_CC_FMT: u32 = make_fourcc_to_le_u32('f', 'm', 't', ' ');
const FOUR_CC_DATA: u32 = make_fourcc_to_le_u32('d', 'a', 't', 'a');

今の処, 読み解く必要なのは上記4つで, それ以外のチャンクはスキップするようにするので, 定義としてはこれで十分です.

また, フォーマットタグでの読み分けも必要なので, 無圧縮PCM用のフォーマットタグだけ定義しておきます.

const WAVE_FORMAT_PCM: u16 = 0x0001;

これ以外は全部今は無効な物として扱うようにします.

ではRIFFの各チャンクのヘッダ情報を読み取る処理を書いていきたいのですが, どういった流れで処理をしたいかを明確にするためにまずはテストコードを書いてみます.

#[cfg(test)]
mod tests {
    use super::*;

    // ...

    #[test]
    fn parse_riff_chunk_header() {
        let buffer = vec![0x52u8, 0x49, 0x46, 0x46, 0x04, 0x00, 0x00, 0x00, 0x57, 0x41, 0x56, 0x45];
        let mut parser = PcmWavParser::new(&buffer);

        let chunk_header = parser.parse_chunk_header().unwrap();
        assert_eq!(chunk_header.four_cc, FOUR_CC_RIFF);
        assert_eq!(chunk_header.chunk_size, 4);
    }
}

PcmWaveParserというパーサーを作り, parse_chunk_header()メソッドからRiffCommonHeaderを受け取れるようにします. parse_chunk_header()をunwrap()しているのは, 入力されたバイト列が正しいデータではない場合にエラーを返す必要があるため, Result<T, E>を返す方が良いと思われるためです. ここでResult<T, E>のEに何を置くかですが, ひとまずは, BinaryReaderが返すBufferOverRunErrorを返すようにしておきます.

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

impl<'a> PcmWavParser<'a> {
    fn new(buffer: &'a Vec<u8>) -> Self {
        PcmWavParser {
            reader: BinaryReader::new(buffer)
        }
    }

    fn parse_chunk_header(&mut self) -> Result<RiffCommonHeader, BufferOverRunError> {
        match self.reader.read_le_u32() {
            Ok((four_cc, _rest)) => {
                match self.reader.read_le_u32() {
                    Ok((chunk_size, _rest)) => {
                        Ok(RiffCommonHeader {
                            four_cc,
                            chunk_size
                        })
                    },
                    Err(e) => Err(e),
                }
            },
            Err(e) => Err(e)
        }
    }
}

ネストが深い気もしますが, まぁとりあえずこんな感じで実装しつつ, cargo testを実行してテストがパスする事を確認します. また, エラーケースのテストが抜けているので, four_cc, chunk_sizeそれぞれのバイトが足りない場合を確認します.

#[test]
fn parse_chunk_header_not_enough_four_cc_bytes() {
    let buffer = vec![0x52u8, 0x49,];
    let mut parser = PcmWavParser::new(&buffer);

    let result = parser.parse_chunk_header();
    assert_eq!(result.err().unwrap().type_id(), std::any::TypeId::of::<BufferOverRunError>());
}

#[test]
fn parse_chunk_header_not_enough_chunk_size_bytes() {
    let buffer = vec![0x52u8, 0x49, 0x46, 0x46, 0x03, 0x00, 0x01];
    let mut parser = PcmWavParser::new(&buffer);

    let result = parser.parse_chunk_header();
    assert_eq!(result.err().unwrap().type_id(), std::any::TypeId::of::<BufferOverRunError>());
}

std::any::TypeIdを利用してエラーがBufferOverRunErrorかどうかで判断するようにしています. テストそのものはこれでパスしますが, あまりうまくなさそうなのでこの辺りのエラー関連は後々書き直す予感がしてきました. ああ, なんだかとても大事な事のような気がします. してきました.

というわけで, 次回はエラーハンドリング周りの整備を行ってみようと思います.