m13o.net

2020-12-11 Fri 23:09
Rustでwavを解析する その4   AdventCalendar2020 Rust programming

前回でエラー関連の方針はなんとなく決まったので, 解析処理を実装していこうと思います.

最終的には無圧縮PCMデータに含まれるヘッダ情報と波形データを抽出する物を作ろうかなと思っているので, そういう雰囲気のあるstructを構築します.

まずはwavの共通フォーマットヘッダから.

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

Windows API のmmreg.hにあるWAVFORMATをRustのstructで表現しただけです. 無圧縮PCMの場合, この後にサンプル毎の量子化bit数を表す2バイトがあるので, それを設定したstructを定義します.

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

では, PcmWavParser に実際のバイト列をパースするメソッドを追加しましょう.

impl<'a> PcmWavParser<'a> {
    // ...


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

ひとまずインターフェースだけを. 正常にパースされると, PcmWavFormatと波形データへのスライスのタプルを返し, 失敗するとstd::error::Errorを返す形を取っています.

このメソッドはその内部でいくつかの複雑な処理を行うので, このメソッドのテストそのものは, 正規の無圧縮PCMのバイト列をテストデータとして用意しそれを読み取れたかどうかと, 無効な無圧縮PCMのバイト列が渡された場合はエラーを返すというのを検査する形で良いかと思います. 忘れないために, 完成するまでは失敗し続ける正常系テストを記述しつつ, 個別のメソッドを実装していきましょう.

#[test]
fn parse_success() {
    let buffer = vec![
        0x52u8, 0x49, 0x46, 0x46, // RIFF
        0x2C, 0x00, 0x00, 0x00, // size
        0x57, 0x41, 0x56, 0x45, // WAVE
        0x66, 0x6D, 0x74, 0x20, // fmt
        0x10, 0x00, 0x00, 0x00, // fmt size(16)
        0x01, 0x00, 0x01, 0x00, // format tag(1), channels(1)
        0x22, 0x56, 0x00, 0x00, // sample rate(22050)
        0x44, 0xAC, 0x00, 0x00, // 1秒あたりのバイト数(44100) sample rate(22050) * channels(1) * bit width(2)
        0x02, 0x00, 0x10, 0x00, // block size (2) channels(1) * bit width(2), bit/sample(16)
        0x64, 0x61, 0x74, 0x61, // data
        0x08, 0x00, 0x00, 0x00, // data size(8)
        0x00, 0x00, 0xFF, 0x7F, // 0, 32767
        0xFF, 0xFF, 0x00, 0x00, // -32768, 0
    ];
    let mut parser = PcmWavParser::new(&buffer);

    let result = parser.parse();
    assert!(result.is_ok());
    let (pcm_wav_header, data) = result.unwrap();
    assert_eq!(pcm_wav_header.wav_format.format_tag, WAVE_FORMAT_PCM);
    assert_eq!(pcm_wav_header.wav_format.channels, 1);
    assert_eq!(pcm_wav_header.wav_format.samples_per_sec, 22050);
    assert_eq!(pcm_wav_header.wav_format.avg_bytes_per_sec, 44100);
    assert_eq!(pcm_wav_header.wav_format.block_align, 2);
    assert_eq!(pcm_wav_header.bit_per_sample, 16);
    assert_eq!(data.len(), 8);
}

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

    let result = parser.parse();
    assert!(result.is_err());
}

完成するまで, 基本的に上は失敗し続け, に下は常にパスし続けるテストです. テストデータをコードに直接打ち込んでいる都合で内容がよくわからなくなるので雰囲気をコメントに記載しています.

この状態で試しにcargo testを走らせてみると, parse_fail()テストも失敗します. unimplemented!()マクロにより必ず失敗するようになっているからです. まずは, このテストが通るようになるために, 実装を進めていきます.

parse()は呼び出されると, バイト列の先頭から順にデータを解析していくようになるはずなので, parse_chunk_header()を呼び出して, RIFFのFourCCと全体のサイズを取得する必要があります.

fn parse(&mut self) -> Result<(PcmWavFormat, &'a[u8]), Box<dyn Error + 'static>> {
    match self.parse_chunk_header() {
        Ok(header) if header.four_cc == FOUR_CC_RIFF => {
            todo!("WAVE以降の処理はここで行う")
        },
        Ok(header) => Err(Box::new(error::RiffParseError::InvalidChunkFourCC(header.four_cc))),
        Err(e) => Err(e),
    }
}

parse()の中では, parse_chunk_header()を最初に呼び出し, 読み取ったヘッダのFourCCがRIFFか否かを確認するようにします. この状態ではtodo!マクロの箇所は未実装なので, さらに解析を進めるための実装をしていきます.

仕様に従えば, この次に出てくるFourCCはWAVEで, そこからは各サブチャンク毎に読み取っていく処理になります. 全部のサブチャンクに対する実装を今は行いませんが, 今後の事とテスト容易性のために更なる解析を行うメソッドを用意してそこに実装していきましょう.

fn parse_wave_chunks(&mut self, size: u32) -> Result<(PcmWavFormat, &'a[u8]), Box<(dyn Error + 'static)>> {
    match self.reader.read_le_u32() {
        Ok((v, _)) if v == FOUR_CC_WAVE => {
            todo!()
        }
        Ok((v, _)) => return Err(Box::new(error::RiffParseError::InvalidChunkFourCC(v))),
        Err(e) => return Err(Box::new(e)),
    }
}

上記のような parse_wave_chunks()メソッドを作り, match構文でparse()と同じような形のエラー処理をしつつ, todo!()となっている箇所で, 必要なサブチャンクを解析していうようにします. また, このメソッドを作成したので, parse()メソッドのtodo!マクロをこのメソッドに置きかえましょう.

fn parse(&mut self) -> Result<(PcmWavFormat, &'a[u8]), Box<dyn Error + 'static>> {
    match self.parse_chunk_header() {
        Ok(header) if header.four_cc == FOUR_CC_RIFF => self.parse_wave_chunks(header.chunk_size),
        Ok(header) => Err(Box::new(error::RiffParseError::InvalidChunkFourCC(header.four_cc))),
        Err(e) => Err(e),
    }
}

では, parse_wave_chunks()を実装していきましょう. WAVEから始まるバイト列を処理していくイメージなので, 利用形態としては以下のような形を想定しています.

#[test]
fn parse_wave_chunks_success() {
    let buffer = vec![
        0x57, 0x41, 0x56, 0x45, // WAVE
        0x66, 0x6D, 0x74, 0x20, // fmt
        0x10, 0x00, 0x00, 0x00, // fmt size(16)
        0x01, 0x00, 0x01, 0x00, // format tag(1), channels(1)
        0x22, 0x56, 0x00, 0x00, // sample rate(22050)
        0x44, 0xAC, 0x00, 0x00, // 1秒あたりのバイト数(44100) sample rate(22050) * channels(1) * bit width(2)
        0x02, 0x00, 0x10, 0x00, // block size (2) channels(1) * bit width(2), bit/sample(16)
        0x64, 0x61, 0x74, 0x61, // data
        0x08, 0x00, 0x00, 0x00, // data size(8)
        0x00, 0x00, 0xFF, 0x7F, // 0, 32767
        0xFF, 0xFF, 0x00, 0x00, // -32768, 0
    ];
    let mut parser = PcmWavParser::new(&buffer);

    let result = parser.parse_wave_chunks(buffer.len() as u32);
    let (pcm_wav_header, data) = result.unwrap();
    assert_eq!(pcm_wav_header.wav_format.format_tag, WAVE_FORMAT_PCM);
    assert_eq!(pcm_wav_header.wav_format.channels, 1);
    assert_eq!(pcm_wav_header.wav_format.samples_per_sec, 22050);
    assert_eq!(pcm_wav_header.wav_format.avg_bytes_per_sec, 44100);
    assert_eq!(pcm_wav_header.wav_format.block_align, 2);
    assert_eq!(pcm_wav_header.bit_per_sample, 16);
    assert_eq!(data.len(), 8);
}

parse()そのもののテストと似通っていますが, parse()メソッドのOk(T)の戻り値は, 実質このメソッドの戻り値なので, 両テストはRIFFから始まるか, WAVEから始まるかの違いしかありません.

RIFFはサブチャンクのサイズ毎にデータがまとまっているので, 読み取り済みサイズがデータの全サイズに到達したらEOFに到達したという事になります. また今回はfmtチャンクとdataチャンク以外は無視する事にしているので, parse_wave_cunks()メソッドではその2つを解析し, それ以外をスキップすれば良い事になります.

fn parse_wave_chunks(&mut self, size: u32) -> Result<(PcmWavFormat, &'a[u8]), Box<(dyn Error + 'static)>> {
    let mut remain_size = size - std::mem::size_of_val(&size) as u32;
    let mut pcm_wav_format: Option<PcmWavFormat> = None;
    let mut wave_form: Option<&[u8]> = None;
    while remain_size > 0 {
        let parse_result = self.parse_chunk_header();
        match parse_result {
            Ok(header) => {
                remain_size -= std::mem::size_of::<RiffCommonHeader>() as u32;
                match header.four_cc {
                    FOUR_CC_FMT => todo!(),
                    FOUR_CC_DATA => todo!(),
                    _ => {
                        let _ = self.reader.read(header.chunk_size as usize)?; 
                    },
                }
                remain_size -= header.chunk_size;
            },
            Err(e) => Err(e),
        }
    }

    Ok((pcm_wav_format.unwrap(), wave_form.unwrap()))
}

戻り値はPcmWavFormat structとデータのスライスのタプルなのでそれを返すようにする必要があります. が, いつどのようにサブチャンクが出現するのかわからないので, Option型のmutな変数として, 戻り値の2つを用意して, 最後にタプルとして入力されているであろうデータを結合して返します.

while でデータの読み取りサイズを減算しつつ, 0になるまで探索し続け, fmtとdataが表われた時だけ処理を分岐させます.

では, fmtを読み取るメソッドを追加してみましょう. fmtはdata部を解釈するために必要な情報を格納しているチャンクで, その中身はPcmWavFormat structそのものなので, メソッドのインターフェースは以下のような形になります.

fn parse_fmt_chunk(&mut self, size: u32) -> Result<PcmWavFormat, Box<(dyn Error + 'static)>> {
    todo!()
}

これを呼び出す形でparse_wae_chunksのfmtの場合のtodo!()マクロを置き換えます.

match header.four_cc {
    FOUR_CC_FMT => FOUR_CC_FMT => pcm_wav_format = Some(self.parse_fmt_chunk(header.chunk_size)?),
    FOUR_CC_DATA => todo!(),
    _ => self.reader.read(header.chunk_size as usize),
}

戻り値を先に用意したmut pcm_wav_format変数に保持して次のチャンクへ進みます.

#[test]
fn parse_fmt_chunks() {
    let buffer = vec![
        0x01, 0x00, 0x01, 0x00, // format tag(1), channels(1)
        0x22, 0x56, 0x00, 0x00, // sample rate(22050)
        0x44, 0xAC, 0x00, 0x00, // 1秒あたりのバイト数(44100) sample rate(22050) * channels(1) * bit width(2)
        0x02, 0x00, 0x10, 0x00, // block size (2) channels(1) * bit width(2), bit/sample(16)
    ];

    let mut parser = PcmWavParser::new(&buffer);
    let format = parser.parse_fmt_chunk(buffer.len() as u32).unwrap();
    assert_eq!(format.wav_format.format_tag, WAVE_FORMAT_PCM);
    assert_eq!(format.wav_format.channels, 1);
    assert_eq!(format.wav_format.samples_per_sec, 22050);
    assert_eq!(format.wav_format.avg_bytes_per_sec, 44100);
    assert_eq!(format.wav_format.block_align, 2);
    assert_eq!(format.bit_per_sample, 16);
}

このような形のテストがパスできれば良いはずなので, これがパスするように実装をしていきます. といっても, やる事は仕様に従ってバイト列を解釈していくだけです.

fn parse_fmt_chunk(&mut self, size: u32) -> Result<PcmWavFormat, Box<(dyn Error + 'static)>> {
    let (format_tag, _) = self.reader.read_le_u16()?;
    let (channels, _) = self.reader.read_le_u16()?;
    let (samples_per_sec, _) = self.reader.read_le_u32()?;
    let (avg_bytes_per_sec, _) = self.reader.read_le_u32()?;
    let (block_align, _) = self.reader.read_le_u16()?;
    let(bit_per_sample, _) = self.reader.read_le_u16()?;

    OK(PcmWavFormat {
        wav_format: WavFormat {
            format_tag,
            channels,
            samples_per_sec,
            avg_bytes_per_sec,
            block_align
        },
        bit_per_sample
    })
}

次にdata部を読み取ります. 以下のような読み取りサイズとPcmWaveFormatを引数に取るインターフェースのメソッドを作成します.

fn parse_data_chunk(&mut self, size: u32) -> Result<&'a [u8], Box<(dyn Error + 'static)>> {
    todo!()
}

これを呼び出すようにしてみましょう.

match header.four_cc {
    // ...
    FOUR_CC_DATA => {
        if pcm_wav_format.is_none() {
            return Err(Box::new(?????)); // これは仕様を満たしていないがどうしよう
        };
        wave_form = Some(self.parse_data_chunk(header.chunk_size)?);
    },
    // ...
}

pcm_wave_formatがNoneの時, wavの仕様を満たしていないデータとなるので, 何かエラーを返さないといけません. RiffParseErrorにエラーを追加して, それを返すようにしましょう.

#[derive(Debug, Eq, PartialEq)]
pub enum RiffParseError {
    InvalidChunkFourCC(u32),
    FmtChunkBeforeDataChunk, // <- new!!
}

impl Display for RiffParseError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            // ...
            RiffParseError::FmtChunkBeforeDataChunk => {
                write!(f, "fmtチャンクよりも先にdataチャンクがあります.")
            }
        }
    }
}

// ...


#[cfg(test)]
mod tests {
    // ...

    #[test]
    fn display_fmt_chunk_before_data_chunk() {
        let error = RiffParseError::FmtChunkBeforeDataChunk;
        assert_eq!(format!("{}", error), "fmtチャンクよりも先にdataチャンクがあります.")
    }
}

そして, pcm_wave_formatがNoneの時はこのエラーを返すようにparse_wave_chunks()メソッドに追記します.

if pcm_wav_format.is_none() {
    return Err(Box::new(RiffParseError::FmtChunkBeforeDataChunk));
};

ではparse_data_chunk()メソッドを実装していきましょう. といっても, ここでは, chunkのサイズ分読み取ったデータのスライスを返すだけで十分です.

fn parse_data_chunk(&mut self, size: u32) -> Result<&'a [u8], Box<(dyn Error + 'static)>> {
    let (data, _) = self.reader.read(size as usize)?;
    Ok(data)
}

// ...


#[test]
fn parse_data_chunk() {
    let buffer = vec! [
        0x00u8, 0x01, 0x05, 0x1F,
        0xFF, 0xFF, 0xFF, 0x7F,
        0x00, 0x00, 0x00, 0x80,
    ];

    let mut parser = PcmWavParser::new(&buffer);
    let data = parser.parse_data_chunk(buffer.len() as u32).unwrap();
    assert_eq!(data.len(), buffer.len());
    assert_eq!(data[0], 0x00);
    assert_eq!(data[1], 0x01);
    assert_eq!(data[2], 0x05);
    assert_eq!(data[3], 0x1F);
    assert_eq!(data[4], 0xFF);
    assert_eq!(data[5], 0xFF);
    assert_eq!(data[6], 0xFF);
    assert_eq!(data[7], 0x7F);
    assert_eq!(data[8], 0x00);
    assert_eq!(data[9], 0x00);
    assert_eq!(data[10], 0x00);
    assert_eq!(data[11], 0x80);
}

cargo testを実行すると全てのテストがパスするようになりました. これで無圧縮PCMなwavのデータのフォーマット情報と波形データを取得する事ができるようになったはず! なので, 次は実際にwavファイルを読み取ってそのフォーマット情報を取得する簡単なCLIを作ってみようと思います.