はじめに

正月休みに暇を持て余していたので, 暇つぶしにRustで信号処理をしようと思い立ちました. 信号処理の走りとして, WAVファイルを扱う方法を調査したので, そのメモです.

WAVファイルの構造

WAVファイルはMicrosoftのRIFF(Resource Interchange File Format)のサブセットとして定義されています. WAVはMP3やAACなどの圧縮音声ファイルとは異なり, 非圧縮の音声ファイルです. 非常にシンプルなフォーマットで, 読み込みや書き込みが容易なので信号処理でよく使用されます.

WAVファイルは以下のような構造をしています.

descriptionfield nameendianfile offsetfield size
RIFFヘッダーChunkIDbig04
ChunkSizelittle44
Formatbig84
fmtチャンクSubchunk1IDbig124
Subchunk1Sizelittle164
AudioFormatlittle202
NumChannelslittle222
SampleRatelittle244
ByteRatelittle284
BlockAlignlittle322
BitsPerSamplelittle342
DataチャンクSubchunk2IDbig364
Subchunk2Sizelittle404
Datalittle44Subchunk2Size

Rustの構造体を使ってこの構造を表現すると以下のようになります.

struct RiffDescChunk {
    id: [u8; 4],
    size: u32,
    form_type: [u8; 4],
}

struct FmtChunk {
    id: [u8; 4],
    size: u32,
    audio_format: u16,
    num_channels: u16,
    sample_rate: u32,
    byte_rate: u32,
    block_align: u16,
    bits_per_sample: u16,
}

struct DataChunk {
    id: [u8; 4],
    size: u32,
    data: Vec<i16>,
}

WARNING

ビット深度を16bitと決め打ちしているため, DataChunkのdataVec<i16>としています.(本当はジェネリクスを使って可変にした方が良いかもしれない)

これらのChunkの共通実装として, Chunkトレイトを定義します.

trait Chunk {
    fn to_bytes(&self) -> Vec<u8>;
}


impl Chunk for RiffDescChunk {
    fn to_bytes(&self) -> Vec<u8> {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&self.id);
        bytes.extend_from_slice(&self.size.to_le_bytes());
        bytes.extend_from_slice(&self.form_type);
        bytes
    }
}

impl Chunk for FmtChunk {
    fn to_bytes(&self) -> Vec<u8> {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&self.id);
        bytes.extend_from_slice(&self.size.to_le_bytes());
        bytes.extend_from_slice(&self.audio_format.to_le_bytes());
        bytes.extend_from_slice(&self.num_channels.to_le_bytes());
        bytes.extend_from_slice(&self.sample_rate.to_le_bytes());
        bytes.extend_from_slice(&self.byte_rate.to_le_bytes());
        bytes.extend_from_slice(&self.block_align.to_le_bytes());
        bytes.extend_from_slice(&self.bits_per_sample.to_le_bytes());
        bytes
    }
}


impl Chunk for DataChunk {
    fn to_bytes(&self) -> Vec<u8> {
        let mut bytes = Vec::new();
        bytes.extend_from_slice(&self.id);
        bytes.extend_from_slice(&self.size.to_le_bytes());
        bytes.extend_from_slice(
            &self
                .data
                .iter()
                .flat_map(|&s| s.to_le_bytes())
                .collect::<Vec<u8>>(),
        );
        bytes
    }
}

少し冗長ですが, これでWAVファイルを扱うための基本的な構造体とトレイトが定義できました.

WAVファイルの書き出し

試しに, sin波を書き出してみます.

use std::f32::consts::PI;
use std::fs::File;
use std::io::Write;

// WAVファイルのフォーマットに関する定数
const SAMPLE_RATE: u32 = 44100;
const BITS_PER_SAMPLE: u16 = 16; // サンプルあたりのビット数
const NUM_CHANNELS: u16 = 1; // チャネル数(モノラル)
const LINEAR_PCM_FORMAT: u16 = 1; // PCMフォーマット
const FMT_CHUNK_SIZE: u32 = 16;

// サンプルの生成に使用する定数
const DURATION_SECS: f32 = 2.0; // 音の再生時間(秒)
const AMPLITUDE: f32 = std::i16::MAX as f32; // 音の振幅
const FREQUENCY: f32 = 440.0; // 生成する音の周波数(Hz)

// サンプル生成関数
fn generate_samples(duration: u32, frequency: f32, amplitude: f32) -> Vec<i16> {
    (0..duration)
        .map(|i| {
            let t = i as f32 / SAMPLE_RATE as f32;
            let sample = amplitude * (2.0 * PI * frequency * t).sin();
            sample as i16
        })
        .collect()
}

fn main() {
    // サンプルを生成
    let duration_samples = (DURATION_SECS * SAMPLE_RATE as f32) as u32;
    let samples = generate_samples(duration_samples, FREQUENCY, AMPLITUDE);

    // Dataチャンクのサイズを計算
    let data_chunk_size = 2 * samples.len() as u32; // サンプル数 x 2バイト(16ビット)
    let riff_size = 36 + data_chunk_size; // RIFFチャンク全体のサイズ

    // RIFFヘッダー
    let riff_desc_header = RiffDescChunk {
        id: *b"RIFF",
        size: riff_size,
        form_type: *b"WAVE",
    };

    // ブロックアラインメントとバイトレートの計算
    let block_align = NUM_CHANNELS * BITS_PER_SAMPLE / 8; // チャンネル数 x サンプルあたりのバイト数
    let byte_rate = SAMPLE_RATE * block_align as u32; // サンプルレート x ブロックアラインメント

    // fmtチャンク
    let fmt_chunk = FmtChunk {
        id: *b"fmt ",
        size: FMT_CHUNK_SIZE,
        audio_format: LINEAR_PCM_FORMAT,
        num_channels: NUM_CHANNELS,
        sample_rate: SAMPLE_RATE,
        byte_rate,
        block_align,
        bits_per_sample: BITS_PER_SAMPLE,
    };

    // dataチャンク
    let data_chunk = DataChunk {
        id: *b"data",
        size: data_chunk_size,
        data: samples,
    };

    // WAVファイルの生成
    let mut file = File::create("test.wav").unwrap();
    file.write_all(&riff_desc_header.to_bytes()).unwrap();
    file.write_all(&fmt_chunk.to_bytes()).unwrap();
    file.write_all(&data_chunk.to_bytes()).unwrap();
    file.flush().unwrap();
}

これで, test.wavというファイルが生成されます.

生成されたWAVファイルをaudacityで読み込むと, 以下のようにsin波が表示されます. 波形, スペクトログラムともに期待値通りですね. また正常に音が再生されることを確認しました.

TIP

t=0付近は不連続なのでスペクトログラムが乱れています.
これは窓関数をかけることで改善できます.

audacity

一応, 生成されたWAVファイルのバイナリを開いて中身を確認してみます.

00000000: 5249 4646 34b1 0200 5741 5645 666d 7420  RIFF4...WAVEfmt
00000010: 1000 0000 0100 0100 44ac 0000 8858 0100  ........D....X..
00000020: 0200 1000 6461 7461 10b1 0200 0000 0408  ....data........
...

big endianとlittle endianが混ざって読みにくいですが, 生成されたWAVファイルのヘッダーも特に問題なさそうです.

52 49 46 46: RIFF
34 b1 02 00: 0x0002b134 = 176564[bytes]
57 41 56 45: WAVE
66 6d 74 20: fmt
10 00 00 00: 0x00000010 = 16[bytes]
01 00      : 0x0001 = 1 (PCM)
01 00      : 0x0001 = 1 (monoral)
44 ac 00 00: 0x0000ac44 = 44100[Hz]
88 58 01 00: 0x00015888 = 88200[byte/sec]
02 00      : 0x0002 = 2 (block size)
10 00      : 0x0010 = 16 (bit/sample)
64 61 74 61: data
10 b1 02 00: 0x0002b110 = 176528[bytes]

おわりに

RustでWAVファイルを扱う方法を調査しました. 体力が尽きたので実装はしませんでしたが, 読み込みは書き出しと同じような感じで実装できると思います. ちなみに, WAVファイルを扱えるクレートはいくつかあるようなので, 面倒くさくなったらそちらを使おうと思います💦.