xaac-rs 0.2.0

High-level Rust AAC/xHE-AAC encoder and decoder bindings built on libxaac
Documentation
from __future__ import annotations

from dataclasses import dataclass


PCM_SUBFORMAT_GUID = bytes(
    [
        0x01,
        0x00,
        0x00,
        0x00,
        0x00,
        0x00,
        0x10,
        0x00,
        0x80,
        0x00,
        0x00,
        0xAA,
        0x00,
        0x38,
        0x9B,
        0x71,
    ]
)


@dataclass(frozen=True)
class WavFile:
    sample_rate: int
    channels: int
    bits_per_sample: int
    channel_mask: int | None
    pcm_data: bytes

    @classmethod
    def parse(cls, data: bytes) -> "WavFile":
        if len(data) < 12 or data[0:4] != b"RIFF" or data[8:12] != b"WAVE":
            raise ValueError("input is not a RIFF/WAVE file")

        offset = 12
        channels = None
        sample_rate = None
        bits_per_sample = None
        channel_mask = None
        pcm_data = None

        while offset + 8 <= len(data):
            chunk_id = data[offset : offset + 4]
            chunk_size = int.from_bytes(data[offset + 4 : offset + 8], "little")
            offset += 8

            if offset + chunk_size > len(data):
                raise ValueError("WAV chunk extends past end of file")

            chunk = data[offset : offset + chunk_size]
            if chunk_id == b"fmt ":
                if len(chunk) < 16:
                    raise ValueError("WAV fmt chunk is too short")

                audio_format = int.from_bytes(chunk[0:2], "little")
                channels = int.from_bytes(chunk[2:4], "little")
                sample_rate = int.from_bytes(chunk[4:8], "little")
                bits_per_sample = int.from_bytes(chunk[14:16], "little")

                if audio_format == 1:
                    pass
                elif audio_format == 0xFFFE:
                    if len(chunk) < 40:
                        raise ValueError("WAV extensible fmt chunk is too short")
                    channel_mask = int.from_bytes(chunk[20:24], "little")
                    valid_bits = int.from_bytes(chunk[18:20], "little")
                    if valid_bits != 0:
                        bits_per_sample = valid_bits
                    if chunk[24:40] != PCM_SUBFORMAT_GUID:
                        raise ValueError("only PCM WAV extensible files are supported")
                else:
                    raise ValueError("only uncompressed PCM WAV files are supported")
            elif chunk_id == b"data":
                pcm_data = chunk

            offset += chunk_size
            if chunk_size % 2 == 1:
                offset += 1

        if channels is None:
            raise ValueError("WAV fmt chunk missing channel count")
        if sample_rate is None:
            raise ValueError("WAV fmt chunk missing sample rate")
        if bits_per_sample is None:
            raise ValueError("WAV fmt chunk missing bits per sample")
        if pcm_data is None:
            raise ValueError("WAV data chunk not found")
        if bits_per_sample not in (16, 24, 32):
            raise ValueError(
                "only 16-bit, 24-bit, and 32-bit PCM WAV files are supported"
            )

        bytes_per_sample = bits_per_sample // 8
        frame_size = channels * bytes_per_sample
        if frame_size == 0 or len(pcm_data) % frame_size != 0:
            raise ValueError("WAV data size is not aligned to full PCM frames")

        return cls(
            sample_rate=sample_rate,
            channels=channels,
            bits_per_sample=bits_per_sample,
            channel_mask=channel_mask,
            pcm_data=pcm_data,
        )