ys2-intro/loader/samples/minexample/furC64/chipchune/furnace/wavetable.py

102 lines
3.6 KiB
Python
Raw Normal View History

2025-11-26 07:42:34 -05:00
from io import BytesIO
from typing import Optional, Union, BinaryIO, List
from chipchune._util import read_short, read_int, read_str
from .data_types import WavetableMeta
from .enums import _FurWavetableImportType
FILE_MAGIC_STR = b'-Furnace waveta-'
EMBED_MAGIC_STR = b'WAVE'
class FurnaceWavetable:
def __init__(self, file_name: Optional[str] = None) -> None:
"""
Creates or opens a new Furnace wavetable as a Python object.
:param file_name: (Optional)
If specified, then it will parse a file as a FurnaceWavetable. If file name (str) is
given, it will load that file.
Defaults to None.
"""
self.file_name: Optional[str] = None
"""
Original file name, if the object was initialized with one.
"""
self.meta: WavetableMeta = WavetableMeta()
"""
Wavetable metadata.
"""
self.data: List[int] = []
"""
Wavetable data.
"""
if isinstance(file_name, str):
self.load_from_file(file_name)
def load_from_file(self, file_name: Optional[str] = None) -> None:
if isinstance(file_name, str):
self.file_name = file_name
if self.file_name is None:
raise RuntimeError('No file name set, either set self.file_name or pass file_name to the function')
# since we're loading from an uncompressed file, we can just check the file magic number
with open(self.file_name, 'rb') as f:
detect_magic = f.peek(len(FILE_MAGIC_STR))[:len(FILE_MAGIC_STR)]
if detect_magic == FILE_MAGIC_STR:
return self.load_from_stream(f, _FurWavetableImportType.FILE)
else: # uncompressed for sure
raise ValueError('No recognized file type magic')
def load_from_bytes(self, data: bytes, import_as: Union[int, _FurWavetableImportType]) -> None:
"""
Load a wavetable from a series of bytes.
:param data: Bytes
"""
return self.load_from_stream(
BytesIO(data),
import_as
)
def load_from_stream(self, stream: BinaryIO, import_as: Union[int, _FurWavetableImportType]) -> None:
"""
Load a wavetable from an **uncompressed** stream.
:param stream: File-like object containing the uncompressed wavetable.
:param import_as: int
- 0 = wavetable file
- 1 = wavetable embedded in module
"""
if import_as == _FurWavetableImportType.FILE:
if stream.read(len(FILE_MAGIC_STR)) != FILE_MAGIC_STR:
raise ValueError('Bad magic value for a wavetable file')
version = read_short(stream)
read_short(stream) # reserved
self.__load_embed(stream)
elif import_as == _FurWavetableImportType.EMBED:
return self.__load_embed(stream)
else:
raise ValueError('Invalid import type')
def __load_embed(self, stream: BinaryIO) -> None:
if stream.read(len(EMBED_MAGIC_STR)) != EMBED_MAGIC_STR:
raise RuntimeError('Bad magic value for a wavetable embed')
blk_size = read_int(stream)
if blk_size > 0:
wt_data = BytesIO(stream.read(blk_size))
else:
wt_data = stream
self.meta.name = read_str(wt_data)
self.meta.width = read_int(wt_data)
read_int(wt_data) # reserved
self.meta.height = read_int(wt_data) + 1 # serialized height is 1 lower than actual value
self.data = [read_int(wt_data) for _ in range(self.meta.width)]