Source code for quasimoto.wave.reader
"""
A module implementing interfaces for reading WAVE files.
"""
# built-in
from contextlib import contextmanager
from io import BytesIO
from pathlib import Path
from typing import Iterator, cast
# third-party
from runtimepy.primitives import Int16
from vcorelib.math.time import nano_str
# internal
from quasimoto.enums import ChunkType
from quasimoto.riff import RiffInterface
from quasimoto.riff.chunk import Chunk
from quasimoto.wave.mixins import FormatMixin
[docs]
class WaveReader(FormatMixin):
"""A class for reading and writing WAVE files."""
def __init__(self, riff: RiffInterface) -> None:
"""Initialize this instance."""
super().__init__()
assert not riff.is_writer
# Only need 'fmt ' and 'data' chunks.
chunks = list(riff.chunks())
format_chunk = None
for chunk in chunks:
if chunk.kind is ChunkType.FMT:
assert format_chunk is None
format_chunk = chunk
if chunk.kind is ChunkType.DATA:
assert not hasattr(self, "data")
self.data: Chunk = chunk
# Parse format.
assert format_chunk is not None
assert format_chunk.kind is ChunkType.FMT
assert format_chunk.size == 16
assert format_chunk.data is not None
self.format.update(format_chunk.data)
# Validate format.
self.validate_header(self.format)
self.logger.info("Format header: %s.", self.format)
# Validate data chunk.
assert self.data.kind is ChunkType.DATA
# Dump some information.
self.logger.info("%s of sample data.", self.duration_str)
@property
def num_samples(self) -> int:
"""Get the number of samples contained."""
all_channels = self.channels * self.sample_bytes
assert self.data.size % all_channels == 0
return self.data.size // all_channels
@property
def duration_s(self) -> float:
"""Get the duration in seconds of this data."""
return self.num_samples / self.sample_rate
@property
def duration_str(self) -> str:
"""Get this data's duration as a human-readable string."""
return nano_str(int(self.duration_s * 1e9), is_time=True) + "s"
[docs]
def chunked_samples(self, count: int) -> Iterator[list[tuple[int, ...]]]:
"""Iterate over samples in chunks."""
chunk = []
for sample in self.samples:
chunk.append(sample)
if len(chunk) == count:
yield chunk
chunk = []
if chunk:
yield chunk
@property
def samples(self) -> Iterator[tuple[int, ...]]:
"""Get raw samples as a generator."""
assert self.data.data is not None
# Only support reading 16-bit samples.
assert self.sample_bytes == 2
num_channels = self.channels
with BytesIO(self.data.data) as stream:
with self.log_time("Processing samples", reminder=True):
for _ in range(self.num_samples):
yield tuple(
cast(
int,
Int16.kind.read(
stream, byte_order=self.byte_order
),
)
for _ in range(num_channels)
)
# Confirm we're at the end of the data.
assert stream.tell() == self.data.size
[docs]
@staticmethod
@contextmanager
def from_path(path: Path) -> Iterator["WaveReader"]:
"""Get a WAVE reader from a path."""
with RiffInterface.from_path(path, is_writer=False) as riff:
yield WaveReader(riff)