mirror of
https://github.com/ScrelliCopter/VGM-Tools
synced 2025-02-21 04:09:25 +11:00
generalise python wave writer
This commit is contained in:
37
common/riffwriter.py
Normal file
37
common/riffwriter.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import BinaryIO, List
|
||||
|
||||
|
||||
class AbstractRiffChunk(ABC):
|
||||
@abstractmethod
|
||||
def fourcc(self) -> bytes: raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def size(self) -> int: raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def write(self, f: BinaryIO): raise NotImplementedError
|
||||
|
||||
|
||||
class RiffFile(AbstractRiffChunk):
|
||||
def fourcc(self) -> bytes: return b"RIFF"
|
||||
|
||||
def size(self) -> int: return 4 + sum(8 + c.size() for c in self._chunks)
|
||||
|
||||
def __init__(self, type: bytes, chunks: List[AbstractRiffChunk]):
|
||||
self._chunks = chunks
|
||||
if len(type) != 4: raise ValueError
|
||||
self._type = type
|
||||
|
||||
def write(self, f: BinaryIO):
|
||||
f.writelines([
|
||||
self.fourcc(),
|
||||
self.size().to_bytes(4, "little", signed=False),
|
||||
self._type])
|
||||
for chunk in self._chunks:
|
||||
size = chunk.size()
|
||||
if size & 0x3: raise AssertionError("Unaligned chunks will produce malformed riff files")
|
||||
f.writelines([
|
||||
chunk.fourcc(),
|
||||
size.to_bytes(4, "little", signed=False)])
|
||||
chunk.write(f)
|
||||
107
common/wavesampler.py
Normal file
107
common/wavesampler.py
Normal file
@@ -0,0 +1,107 @@
|
||||
import struct
|
||||
from enum import Enum
|
||||
from riffwriter import AbstractRiffChunk
|
||||
from typing import BinaryIO, List
|
||||
|
||||
|
||||
class WaveSamplerSMPTEOffset:
|
||||
def __init__(self, hours: int=0, minutes: int=0, seconds: int=0, frames: int=0):
|
||||
if -23 > hours > 23: raise ValueError("Hours out of range")
|
||||
if 0 > minutes > 59: raise ValueError("Minutes out of range")
|
||||
if 0 > seconds > 59: raise ValueError("Seconds out of range")
|
||||
if 0 > frames > 0xFF: raise ValueError("Frames out of range")
|
||||
self._hours = hours
|
||||
self._minutes = minutes
|
||||
self._seconds = seconds
|
||||
self._frames = frames
|
||||
|
||||
def hours(self) -> int: return self._hours
|
||||
def minutes(self) -> int: return self._minutes
|
||||
def seconds(self) -> int: return self._seconds
|
||||
def frames(self) -> int: return self._frames
|
||||
|
||||
def pack(self) -> bytes:
|
||||
#FIXME: endianess??
|
||||
return struct.pack("<bBBB", self._hours, self._minutes, self._seconds, self._frames)
|
||||
|
||||
|
||||
class WaveSamplerLoopType(Enum):
|
||||
FORWARD = 0
|
||||
BIDIRECTIONAL = 1
|
||||
REVERSE = 2
|
||||
|
||||
|
||||
class WaveSamplerLoop:
|
||||
def __init__(self,
|
||||
cueId: int=0,
|
||||
type: int|WaveSamplerLoopType=0,
|
||||
start: int=0,
|
||||
end: int=0,
|
||||
fraction: int=0,
|
||||
loopCount: int=0):
|
||||
self._cueId = cueId
|
||||
self._type = type.value if type is WaveSamplerLoopType else type
|
||||
self._start = start
|
||||
self._end = end
|
||||
self._fraction = fraction
|
||||
self._loopCount = loopCount
|
||||
|
||||
def pack(self) -> bytes:
|
||||
return struct.pack("<IIIIII",
|
||||
self._cueId, # Cue point ID
|
||||
self._type, # Loop type
|
||||
self._start, # Loop start
|
||||
self._end, # Loop end
|
||||
self._fraction, # Fraction (none)
|
||||
self._loopCount) # Loop count (infinite)
|
||||
|
||||
|
||||
class WaveSamplerChunk(AbstractRiffChunk):
|
||||
def fourcc(self) -> bytes: return b"smpl"
|
||||
|
||||
def loopsSize(self) -> int: return len(self._loops) * 24
|
||||
|
||||
def size(self) -> int: return 36 + self.loopsSize()
|
||||
|
||||
def write(self, f: BinaryIO):
|
||||
#TODO: unused data dummied out for now
|
||||
f.write(struct.pack("<4sIiiii4sII",
|
||||
self._manufacturer, # MMA Manufacturer code
|
||||
self._product, # Product
|
||||
self._period, # Playback period (ns)
|
||||
self._unityNote, # MIDI unity note
|
||||
self._fineTune, # MIDI pitch fraction
|
||||
self._smpteFormat, # SMPTE format
|
||||
self._smpteOffset.pack(), # SMPTE offset
|
||||
|
||||
len(self._loops), # Number of loops
|
||||
self.loopsSize())) # Loop data length
|
||||
f.writelines(loop.pack() for loop in self._loops)
|
||||
|
||||
def __init__(self,
|
||||
manufacturer: bytes|None=None,
|
||||
product: int=0,
|
||||
period: int=0,
|
||||
midiUnityNote: int=0,
|
||||
midiPitchFraction: int=0,
|
||||
smpteFormat: int=0,
|
||||
smpteOffset: WaveSamplerSMPTEOffset|None=None,
|
||||
loops: List[WaveSamplerLoop]=None):
|
||||
if manufacturer is not None:
|
||||
if len(manufacturer) not in [1, 3]: raise ValueError("Malformed MIDI manufacturer code")
|
||||
self._manufacturer = len(manufacturer).to_bytes(1, byteorder="little", signed=False)
|
||||
self._manufacturer += manufacturer.rjust(3, b"\x00")
|
||||
else:
|
||||
self._manufacturer = b"\x00" * 4
|
||||
|
||||
if 0 > product > 0xFFFF: raise ValueError("Product code out of range")
|
||||
self._product = product # Arbitrary vendor specific product code, dunno if this should be signed or unsigned
|
||||
self._period = period # Sample period in ns, (1 / samplerate) * 10^9, who cares
|
||||
if 0 > midiUnityNote > 127: raise ValueError("MIDI Unity note out of range")
|
||||
self._unityNote = midiUnityNote # MIDI note that plays the sample unpitched, middle C=60
|
||||
self._fineTune = midiPitchFraction # Finetune fraction, 256 == 100 cents
|
||||
if smpteFormat not in [0, 24, 25, 29, 30]: raise ValueError("Invalid SMPTE format")
|
||||
self._smpteFormat = smpteFormat
|
||||
self._smpteOffset = smpteOffset if (smpteOffset and smpteFormat > 0) else WaveSamplerSMPTEOffset()
|
||||
if self._smpteOffset.frames() > self._smpteFormat: raise ValueError("SMPTE frame offset can't exceed SMPTE format")
|
||||
self._loops = loops if loops is not None else list()
|
||||
95
common/wavewriter.py
Normal file
95
common/wavewriter.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import struct
|
||||
from abc import abstractmethod
|
||||
from enum import Enum
|
||||
from typing import BinaryIO, List
|
||||
from riffwriter import RiffFile, AbstractRiffChunk
|
||||
|
||||
|
||||
class WaveSampleFormat(Enum):
|
||||
PCM = 0x0001
|
||||
IEEE_FLOAT = 0x0003
|
||||
ALAW = 0x0006
|
||||
MULAW = 0x0007
|
||||
EXTENSIBLE = 0xFFFE
|
||||
|
||||
|
||||
class WaveAbstractFormatChunk(AbstractRiffChunk):
|
||||
def fourcc(self) -> bytes: return b"fmt "
|
||||
|
||||
def size(self) -> int: return 16
|
||||
|
||||
@abstractmethod
|
||||
def sampleformat(self) -> WaveSampleFormat: raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def channels(self) -> int: raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def samplerate(self) -> int: raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def byterate(self) -> int: raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def align(self) -> int: raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def bitdepth(self) -> int: raise NotImplementedError
|
||||
|
||||
def write(self, f: BinaryIO):
|
||||
f.write(struct.pack("<HHIIHH",
|
||||
self.sampleformat().value,
|
||||
self.channels(),
|
||||
self.samplerate(),
|
||||
self.byterate(),
|
||||
self.align(),
|
||||
self.bitdepth()))
|
||||
|
||||
|
||||
class WavePcmFormatChunk(WaveAbstractFormatChunk):
|
||||
def sampleformat(self) -> WaveSampleFormat: return WaveSampleFormat.PCM
|
||||
|
||||
def channels(self) -> int: return self._channels
|
||||
|
||||
def samplerate(self) -> int: return self._samplerate
|
||||
|
||||
def byterate(self) -> int: return self._samplerate * self._channels * self._bytedepth
|
||||
|
||||
def align(self) -> int: return self._channels * self._bytedepth
|
||||
|
||||
def bitdepth(self) -> int: return self._bytedepth * 8
|
||||
|
||||
def __init__(self, channels: int, samplerate: int, bitdepth: int):
|
||||
if channels < 0 or channels >= 256: raise ValueError
|
||||
if samplerate < 1 or samplerate > 0xFFFFFFFF: raise ValueError
|
||||
if bitdepth not in [8, 16, 32]: raise ValueError
|
||||
self._channels = channels
|
||||
self._samplerate = samplerate
|
||||
self._bytedepth = bitdepth // 8
|
||||
|
||||
|
||||
class WaveDataChunk(AbstractRiffChunk):
|
||||
def fourcc(self) -> bytes: return b"data"
|
||||
|
||||
def size(self) -> int: return len(self._data)
|
||||
|
||||
def write(self, f: BinaryIO): f.write(self._data)
|
||||
|
||||
def __init__(self, data: bytes):
|
||||
self._data = data
|
||||
|
||||
|
||||
class WaveCommentChunk(AbstractRiffChunk):
|
||||
def fourcc(self) -> bytes: return b"clm "
|
||||
|
||||
def size(self) -> int: return len(self._comment)
|
||||
|
||||
def write(self, f: BinaryIO): f.write(self._comment)
|
||||
|
||||
def __init__(self, comment: bytes):
|
||||
self._comment = comment
|
||||
|
||||
|
||||
class WaveFile(RiffFile):
|
||||
def __init__(self, format: WaveAbstractFormatChunk, chunks: List[AbstractRiffChunk]):
|
||||
super().__init__(b"WAVE", [format] + chunks)
|
||||
Reference in New Issue
Block a user