Source code for quasimoto.riff
"""
A module implementing interfaces for RIFF files.
"""
# built-in
from contextlib import contextmanager
import os
from pathlib import Path
from typing import BinaryIO, Iterator, Optional, Type, TypeVar, cast
# third-party
from runtimepy.primitives import Uint32
from runtimepy.primitives.byte_order import ByteOrder
from vcorelib.logging import LoggerMixin
# internal
from quasimoto.enums import ChunkType
from quasimoto.riff.chunk import NULL_BYTE, Chunk
T = TypeVar("T", bound="RiffInterface")
[docs]
class RiffInterface(LoggerMixin):
"""A class for reading and writing RIFF files."""
def __init__(self, stream: BinaryIO, is_writer: bool = True) -> None:
"""Initialize this instance."""
super().__init__()
self.stream = stream
# Write the header.
self.is_writer = is_writer
if self.is_writer:
ChunkType.RIFF.to_stream(self.stream)
# Leave a placeholder for actual size.
self.write_size(0)
else:
header = self.read()
assert header is not None
self.header: Chunk = header
self.logger.info("Header: %s.", self.header)
assert self.header.kind is ChunkType.RIFF
[docs]
def read_size(self) -> int:
"""Read a size from the stream."""
return cast(
int,
Uint32.kind.read(self.stream, byte_order=ByteOrder.LITTLE_ENDIAN),
)
[docs]
def read(self) -> Optional[Chunk]:
"""Read the next chunk."""
result = None
kind = ChunkType.from_stream(self.stream)
if kind is not None:
size = self.read_size()
data = None
form = None
if not kind.is_container:
data = self.stream.read(size)
if size % 2 == 1:
self.stream.read(1) # pragma: nocover
else:
form = ChunkType.from_stream(self.stream)
result = Chunk(kind, size, data=data, form=form)
return result
[docs]
def chunks(self) -> Iterator[Chunk]:
"""Read file chunks."""
result = self.read()
while result is not None:
yield result
result = self.read()
[docs]
def write_size(self, size: int, seek: int = None) -> None:
"""An interface for writing a size field."""
# Validate size.
prim = Uint32.kind
bounds = prim.int_bounds
assert bounds is not None
assert bounds.validate(size), size
if seek is not None:
self.stream.seek(seek)
# Write size.
Uint32.kind.write(
size, self.stream, byte_order=ByteOrder.LITTLE_ENDIAN
)
def _write_data(self, data: bytes) -> None:
"""Write chunk data."""
size = len(data)
self.write_size(size)
# Write data.
self.stream.write(data)
if size % 2 == 1:
self.stream.write(NULL_BYTE) # pragma: nocover
[docs]
def write(self, chunk: Chunk) -> None:
"""Write a chunk to the file."""
assert self.is_writer
# Can't write container chunks this way.
assert not chunk.kind.is_container
# Write header.
chunk.kind.to_stream(self.stream)
# Write data.
if chunk.data is not None:
self._write_data(chunk.data)
[docs]
def finalize(self) -> None:
"""Finalize the header size."""
if self.is_writer:
self.stream.seek(0, os.SEEK_END)
size = self.stream.tell() - 8
self.write_size(size, seek=4)
else:
remaining = self.stream.read()
if remaining:
self.logger.warning(
"%d bytes remaining in file!", len(remaining)
)
[docs]
@classmethod
@contextmanager
def from_path(
cls: Type[T], path: Path, is_writer: bool = True
) -> Iterator[T]:
"""Create a RIFF interface from a path."""
with path.open("wb" if is_writer else "rb") as out_fd:
result = cls(out_fd, is_writer=is_writer)
try:
yield result
finally:
result.finalize()