SickGear/lib/hachoir/parser/audio/s3m.py
2023-02-09 13:41:15 +00:00

716 lines
24 KiB
Python

"""
The ScreamTracker 3.0x module format description for .s3m files.
Documents:
- Search s3m on Wotsit
http://www.wotsit.org/
Author: Christophe GISQUET <christophe.gisquet@free.fr>
Creation: 11th February 2007
"""
from hachoir.parser import Parser
from hachoir.field import (StaticFieldSet, FieldSet, Field,
Bit, Bits,
UInt32, UInt16, UInt8, Enum,
PaddingBytes, RawBytes, NullBytes,
String, GenericVector, ParserError)
from hachoir.core.endian import LITTLE_ENDIAN
from hachoir.core.text_handler import textHandler, hexadecimal
from hachoir.core.tools import alignValue
class Chunk:
def __init__(self, cls, name, offset, size, *args):
# Todo: swap and have None=unknown instead of now: 0=unknown
assert size is not None and size >= 0
self.cls = cls
self.name = name
self.offset = offset
self.size = size
self.args = args
class ChunkIndexer:
def __init__(self):
self.chunks = []
# Check if a chunk fits
def canHouse(self, chunk, index):
if index > 1:
if chunk.offset + chunk.size > self.chunks[index - 1].offset:
return False
# We could test now that it fits in the memory
return True
# Farthest element is last
def addChunk(self, new_chunk):
index = 0
# Find first chunk whose value is bigger
while index < len(self.chunks):
offset = self.chunks[index].offset
if offset < new_chunk.offset:
if not self.canHouse(new_chunk, index):
raise ParserError("Chunk '%s' doesn't fit!" %
new_chunk.name)
self.chunks.insert(index, new_chunk)
return
index += 1
# Not found or empty
# We could at least check that it fits in the memory
self.chunks.append(new_chunk)
def yieldChunks(self, obj):
while len(self.chunks) > 0:
chunk = self.chunks.pop()
current_pos = obj.current_size // 8
# Check if padding needed
size = chunk.offset - current_pos
if size > 0:
obj.info("Padding of %u bytes needed: curr=%u offset=%u" %
(size, current_pos, chunk.offset))
yield PaddingBytes(obj, "padding[]", size)
current_pos = obj.current_size // 8
# Find resynch point if needed
count = 0
old_off = chunk.offset
while chunk.offset < current_pos:
count += 1
chunk = self.chunks.pop()
# Unfortunaly, we also pass the underlying chunks
if chunk is None:
obj.info("Couldn't resynch: %u object skipped to reach %u" %
(count, current_pos))
return
# Resynch
size = chunk.offset - current_pos
if size > 0:
obj.info("Skipped %u objects to resynch to %u; chunk offset: %u->%u" %
(count, current_pos, old_off, chunk.offset))
yield RawBytes(obj, "resynch[]", size)
# Yield
obj.info("Yielding element of size %u at offset %u" %
(chunk.size, chunk.offset))
field = chunk.cls(obj, chunk.name, chunk.size, *chunk.args)
yield field
if hasattr(field, "getSubChunks"):
for sub_chunk in field.getSubChunks():
obj.info("Adding sub chunk: position=%u size=%u name='%s'" %
(sub_chunk.offset, sub_chunk.size, sub_chunk.name))
self.addChunk(sub_chunk)
# Let missing padding be done by next chunk
class S3MFlags(StaticFieldSet):
format = (
(Bit, "st2_vibrato", "Vibrato (File version 1/ScreamTrack 2)"),
(Bit, "st2_tempo", "Tempo (File version 1/ScreamTrack 2)"),
(Bit, "amiga_slides", "Amiga slides (File version 1/ScreamTrack 2)"),
(Bit, "zero_vol_opt",
"Automatically turn off looping notes whose volume is zero for >2 note rows"),
(Bit, "amiga_limits", "Disallow notes beyond Amiga hardware specs"),
(Bit, "sb_processing", "Enable filter/SFX with SoundBlaster"),
(Bit, "vol_slide", "Volume slide also performed on first row"),
(Bit, "extended", "Special custom data in file"),
(Bits, "unused[]", 8)
)
def parseChannelType(val):
val = val.value
if val < 8:
return "Left Sample Channel %u" % val
if val < 16:
return "Right Sample Channel %u" % (val - 8)
if val < 32:
return "Adlib channel %u" % (val - 16)
return "Value %u unknown" % val
class ChannelSettings(FieldSet):
static_size = 8
def createFields(self):
yield textHandler(Bits(self, "type", 7), parseChannelType)
yield Bit(self, "enabled")
class ChannelPanning(FieldSet):
static_size = 8
def createFields(self):
yield Bits(self, "default_position", 4, "Default pan position")
yield Bit(self, "reserved[]")
yield Bit(self, "use_default", "Bits 0:3 specify default position")
yield Bits(self, "reserved[]", 2)
# Provide an automatic constructor
class SizeFieldSet(FieldSet):
"""
Provide an automatic constructor for a sized field that can be aligned
on byte positions according to ALIGN.
Size is ignored if static_size is set. Real size is stored
for convenience, but beware, it is not in bits, but in bytes.
Field can be automatically padded, unless:
- size is 0 (unknown, so padding doesn't make sense)
- it shouldn't be aligned
If it shouldn't be aligned, two solutions:
- change _size to another value than the one found through aligment.
- derive a class with ALIGN = 0.
"""
ALIGN = 16
def __init__(self, parent, name, size, desc=None):
FieldSet.__init__(self, parent, name, desc)
if size:
self.real_size = size
if self.static_size is None:
self.setCheckedSizes(size)
def setCheckedSizes(self, size):
# First set size so that end is aligned, if needed
self.real_size = size
size *= 8
if self.ALIGN:
size = alignValue(self.absolute_address + size, 8 * self.ALIGN) \
- self.absolute_address
if self._parent._size:
if self._parent.current_size + size > self._parent._size:
size = self._parent._size - self._parent.current_size
self._size = size
def createFields(self):
yield from self.createUnpaddedFields()
size = (self._size - self.current_size) // 8
if size > 0:
yield PaddingBytes(self, "padding", size)
class Header(SizeFieldSet):
def createDescription(self):
return "%s (%u patterns, %u instruments)" % \
(self["title"].value, self["num_patterns"].value,
self["num_instruments"].value)
def createValue(self):
return self["title"].value
# Header fields may have to be padded - specify static_size
# or modify _size in a derived class if never.
def createUnpaddedFields(self):
yield String(self, "title", 28, strip='\0')
yield textHandler(UInt8(self, "marker[]"), hexadecimal)
yield from self.getFileVersionField()
yield UInt16(self, "num_orders")
yield UInt16(self, "num_instruments")
yield UInt16(self, "num_patterns")
yield from self.getFirstProperties()
yield String(self, "marker[]", 4)
yield from self.getLastProperties()
yield GenericVector(self, "channel_settings", 32,
ChannelSettings, "channel")
# Orders
yield GenericVector(self, "orders", self.getNumOrders(), UInt8, "order")
yield from self.getHeaderEndFields()
class S3MHeader(Header):
"""
0 1 2 3 4 5 6 7 8 9 A B C D E F
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
0000: | Song name, max 28 chars (end with NUL (0)) |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
0010: | |1Ah|Typ| x | x |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
0020: |OrdNum |InsNum |PatNum | Flags | Cwt/v | Ffi |'S'|'C'|'R'|'M'|
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
0030: |g.v|i.s|i.t|m.v|u.c|d.p| x | x | x | x | x | x | x | x |Special|
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
0040: |Channel settings for 32 channels, 255=unused,+128=disabled |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
0050: | |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
0060: |Orders; length=OrdNum (should be even) |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
xxx1: |Parapointers to instruments; length=InsNum*2 |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
xxx2: |Parapointers to patterns; length=PatNum*2 |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
xxx3: |Channel default pan positions |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
xxx1=70h+orders
xxx2=70h+orders+instruments*2
xxx3=70h+orders+instruments*2+patterns*2
"""
def __init__(self, parent, name, size, desc=None):
Header.__init__(self, parent, name, size, desc)
# Overwrite real_size
size = 0x60 + self["num_orders"].value + \
2 * (self["num_instruments"].value + self["num_patterns"].value)
if self["panning_info"].value == 252:
size += 32
# Deduce size for SizeFieldSet
self.setCheckedSizes(size)
def getFileVersionField(self):
yield UInt8(self, "type")
yield RawBytes(self, "reserved[]", 2)
def getFirstProperties(self):
yield S3MFlags(self, "flags")
yield UInt8(self, "creation_version_minor")
yield Bits(self, "creation_version_major", 4)
yield Bits(self, "creation_version_unknown", 4, "(=1)")
yield UInt16(self, "format_version")
def getLastProperties(self):
yield UInt8(self, "glob_vol", "Global volume")
yield UInt8(self, "init_speed", "Initial speed (command A)")
yield UInt8(self, "init_tempo", "Initial tempo (command T)")
yield Bits(self, "volume", 7)
yield Bit(self, "stereo")
yield UInt8(self, "click_removal", "Number of GUS channels to run to prevent clicks")
yield UInt8(self, "panning_info")
yield RawBytes(self, "reserved[]", 8)
yield UInt16(self, "custom_data_parapointer",
"Parapointer to special custom data (not used by ST3.01)")
def getNumOrders(self):
return self["num_orders"].value
def getHeaderEndFields(self):
instr = self["num_instruments"].value
patterns = self["num_patterns"].value
# File pointers
if instr > 0:
yield GenericVector(self, "instr_pptr", instr, UInt16, "offset")
if patterns > 0:
yield GenericVector(self, "pattern_pptr", patterns, UInt16, "offset")
# S3M 3.20 extension
if self["creation_version_major"].value >= 3 \
and self["creation_version_minor"].value >= 0x20 \
and self["panning_info"].value == 252:
yield GenericVector(self, "channel_panning", 32, ChannelPanning, "channel")
# Padding required for 16B alignment
size = self._size - self.current_size
if size > 0:
yield PaddingBytes(self, "padding", size // 8)
def getSubChunks(self):
# Instruments - no warranty that they are concatenated
for index in range(self["num_instruments"].value):
yield Chunk(S3MInstrument, "instrument[]",
16 * self["instr_pptr/offset[%u]" % index].value,
S3MInstrument.static_size // 8)
# Patterns - size unknown but listed in their headers
for index in range(self["num_patterns"].value):
yield Chunk(S3MPattern, "pattern[]",
16 * self["pattern_pptr/offset[%u]" % index].value, 0)
class PTMHeader(Header):
# static_size should prime over _size, right?
static_size = 8 * 608
def getTrackerVersion(self, val):
val = val.value
return "ProTracker x%04X" % val
def getFileVersionField(self):
yield UInt16(self, "type")
yield RawBytes(self, "reserved[]", 1)
def getFirstProperties(self):
yield UInt16(self, "channels")
yield UInt16(self, "flags") # 0 => NullBytes
yield UInt16(self, "reserved[]")
def getLastProperties(self):
yield RawBytes(self, "reserved[]", 16)
def getNumOrders(self):
return 256
def getHeaderEndFields(self):
yield GenericVector(self, "pattern_pptr", 128, UInt16, "offset")
def getSubChunks(self):
# It goes like this in the BS: patterns->instruments->instr. samples
if self._parent._size:
min_off = self.absolute_address + self._parent._size
else:
min_off = 99999999999
# Instruments and minimal end position for last pattern
count = self["num_instruments"].value
addr = self.absolute_address
for index in range(count):
offset = (self.static_size + index *
PTMInstrument.static_size) // 8
yield Chunk(PTMInstrument, "instrument[]", offset,
PTMInstrument.static_size // 8)
offset = self.stream.readBits(
addr + 8 * (offset + 18), 32, LITTLE_ENDIAN)
min_off = min(min_off, offset)
# Patterns
count = self["num_patterns"].value
prev_off = 16 * self["pattern_pptr/offset[0]"].value
for index in range(1, count):
offset = 16 * self["pattern_pptr/offset[%u]" % index].value
yield Chunk(PTMPattern, "pattern[]", prev_off, offset - prev_off)
prev_off = offset
# Difficult to account for
yield Chunk(PTMPattern, "pattern[]", prev_off, min_off - prev_off)
class SampleFlags(StaticFieldSet):
format = (
(Bit, "loop_on"),
(Bit, "stereo", "Sample size will be 2*length"),
(Bit, "16bits", "16b sample, Intel LO-HI byteorder"),
(Bits, "unused", 5)
)
class S3MUInt24(Field):
static_size = 24
def __init__(self, parent, name, desc=None):
Field.__init__(self, parent, name, size=24, description=desc)
addr = self.absolute_address
val = parent.stream.readBits(addr, 8, LITTLE_ENDIAN) << 20
val += parent.stream.readBits(addr + 8, 16, LITTLE_ENDIAN) << 4
self.createValue = lambda: val
class SampleData(SizeFieldSet):
def createUnpaddedFields(self):
yield RawBytes(self, "data", self.real_size)
class PTMSampleData(SampleData):
ALIGN = 0
class Instrument(SizeFieldSet):
static_size = 8 * 0x50
def createDescription(self):
info = [self["c4_speed"].display]
if "flags/stereo" in self:
if self["flags/stereo"].value:
info.append("stereo")
else:
info.append("mono")
info.append("%u bits" % self.getSampleBits())
return ", ".join(info)
# Structure knows its size and doesn't need padding anyway, so
# overwrite base member: no need to go through it.
def createFields(self):
yield self.getType()
yield String(self, "filename", 12, strip='\0')
yield from self.getInstrumentFields()
yield String(self, "name", 28, strip='\0')
yield String(self, "marker", 4, "Either 'SCRS' or '(empty)'", strip='\0')
def createValue(self):
return self["name"].value
class S3MInstrument(Instrument):
"""
In fact a sample. Description follows:
0 1 2 3 4 5 6 7 8 9 A B C D E F
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
0000: |[T]| Dos filename (12345678.ABC) | MemSeg |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
0010: |Length |HI:leng|LoopBeg|HI:LBeg|LoopEnd|HI:Lend|Vol| x |[P]|[F]|
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
0020: |C2Spd |HI:C2sp| x | x | x | x |Int:Gp |Int:512|Int:lastused |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
0030: | Sample name, 28 characters max... (incl. NUL) |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
0040: | ...sample name... |'S'|'C'|'R'|'S'|
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
xxxx: sampledata
"""
MAGIC = b"SCRS"
PACKING = {0: "Unpacked", 1: "DP30ADPCM"}
TYPE = {0: "Unknown", 1: "Sample", 2: "adlib melody", 3: "adlib drum2"}
def getType(self):
return Enum(UInt8(self, "type"), self.TYPE)
def getSampleBits(self):
return 8 * (1 + self["flags/16bits"].value)
def getInstrumentFields(self):
yield S3MUInt24(self, "sample_offset")
yield UInt32(self, "sample_size")
yield UInt32(self, "loop_begin")
yield UInt32(self, "loop_end")
yield UInt8(self, "volume")
yield UInt8(self, "reserved[]")
yield Enum(UInt8(self, "packing"), self.PACKING)
yield SampleFlags(self, "flags")
yield UInt32(self, "c4_speed", "Frequency for middle C note")
yield UInt32(self, "reserved[]", 4)
yield UInt16(self, "internal[]", "Sample address in GUS memory")
yield UInt16(self, "internal[]", "Flags for SoundBlaster loop expansion")
yield UInt32(self, "internal[]", "Last used position (SB)")
def getSubChunks(self):
size = self["sample_size"].value
if self["flags/stereo"].value:
size *= 2
if self["flags/16bits"].value:
size *= 2
yield Chunk(SampleData, "sample_data[]",
self["sample_offset"].value, size)
class PTMType(FieldSet):
TYPES = {0: "No sample", 1: "Regular",
2: "OPL2/OPL2 instrument", 3: "MIDI instrument"}
static_size = 8
def createFields(self):
yield Bits(self, "unused", 2)
yield Bit(self, "is_tonable")
yield Bit(self, "16bits")
yield Bit(self, "loop_bidir")
yield Bit(self, "loop")
yield Enum(Bits(self, "origin", 2), self.TYPES)
# class PTMType(StaticFieldSet):
# format = (
# # (Bits, "unused", 2),
# # (Bit, "is_tonable"),
# # (Bit, "16bits"),
# # (Bit, "loop_bidir"),
# # (Bit, "loop"),
# # (Bits, "origin", 2),
# )
class PTMInstrument(Instrument):
MAGIC = b"PTMI"
ALIGN = 0
def getType(self):
return PTMType(self, "flags") # Hack to have more common code
# PTM doesn't pretend to manage 16bits
def getSampleBits(self):
return 8
def getInstrumentFields(self):
yield UInt8(self, "volume")
yield UInt16(self, "c4_speed")
yield UInt16(self, "sample_segment")
yield UInt32(self, "sample_offset")
yield UInt32(self, "sample_size")
yield UInt32(self, "loop_begin")
yield UInt32(self, "loop_end")
yield UInt32(self, "gus_begin")
yield UInt32(self, "gus_loop_start")
yield UInt32(self, "gus_loop_end")
yield textHandler(UInt8(self, "gus_loop_flags"), hexadecimal)
yield UInt8(self, "reserved[]") # Should be 0
def getSubChunks(self):
# Samples are NOT padded, and the size is already the correct one
size = self["sample_size"].value
if size:
yield Chunk(PTMSampleData, "sample_data[]", self["sample_offset"].value, size)
class S3MNoteInfo(StaticFieldSet):
"""
0=end of row
&31=channel
&32=follows; BYTE:note, BYTE:instrument
&64=follows; BYTE:volume
&128=follows; BYTE:command, BYTE:info
"""
format = (
(Bits, "channel", 5),
(Bit, "has_note"),
(Bit, "has_volume"),
(Bit, "has_effect")
)
class PTMNoteInfo(StaticFieldSet):
format = (
(Bits, "channel", 5),
(Bit, "has_note"),
(Bit, "has_effect"),
(Bit, "has_volume")
)
class Note(FieldSet):
def createFields(self):
# Used by Row to check if end of Row
info = self.NOTE_INFO(self, "info")
yield info
if info["has_note"].value:
yield UInt8(self, "note")
yield UInt8(self, "instrument")
if info["has_volume"].value:
yield UInt8(self, "volume")
if info["has_effect"].value:
yield UInt8(self, "effect")
yield UInt8(self, "param")
class S3MNote(Note):
NOTE_INFO = S3MNoteInfo
class PTMNote(Note):
NOTE_INFO = PTMNoteInfo
class Row(FieldSet):
def createFields(self):
addr = self.absolute_address
while True:
# Check empty note
byte = self.stream.readBits(addr, 8, self.endian)
if not byte:
yield NullBytes(self, "terminator", 1)
return
note = self.NOTE(self, "note[]")
yield note
addr += note.size
class S3MRow(Row):
NOTE = S3MNote
class PTMRow(Row):
NOTE = PTMNote
class Pattern(SizeFieldSet):
def createUnpaddedFields(self):
count = 0
while count < 64 and not self.eof:
yield self.ROW(self, "row[]")
count += 1
class S3MPattern(Pattern):
ROW = S3MRow
def __init__(self, parent, name, size, desc=None):
Pattern.__init__(self, parent, name, size, desc)
# Get real_size from header
addr = self.absolute_address
size = self.stream.readBits(addr, 16, LITTLE_ENDIAN)
self.setCheckedSizes(size)
class PTMPattern(Pattern):
ROW = PTMRow
class Module(Parser):
# MARKER / HEADER are defined in derived classes
endian = LITTLE_ENDIAN
def validate(self):
marker = self.stream.readBits(0x1C * 8, 8, LITTLE_ENDIAN)
if marker != 0x1A:
return "Invalid start marker %u" % marker
marker = self.stream.readBytes(0x2C * 8, 4)
if marker != self.MARKER:
return "Invalid marker %s!=%s" % (marker, self.MARKER)
return True
def createFields(self):
# Index chunks
indexer = ChunkIndexer()
# Add header - at least 0x50 bytes
indexer.addChunk(Chunk(self.HEADER, "header", 0, 0x50))
yield from indexer.yieldChunks(self)
class S3MModule(Module):
PARSER_TAGS = {
"id": "s3m",
"category": "audio",
"file_ext": ("s3m",),
"mime": ('audio/s3m', 'audio/x-s3m'),
"min_size": 64 * 8,
"description": "ScreamTracker3 module"
}
MARKER = b"SCRM"
HEADER = S3MHeader
# def createContentSize(self):
# # hdr = Header(self, "header")
# # max_offset = hdr._size//8
# # instr_size = Instrument._size//8
# for index in xrange(self["header/num_instruments"].value):
# # offset = 16*hdr["instr_pptr/offset[%u]" % index].value
# # max_offset = max(offset+instr_size, max_offset)
# # addr = self.absolute_address + 8*offset
class PTMModule(Module):
PARSER_TAGS = {
"id": "ptm",
"category": "audio",
"file_ext": ("ptm",),
"min_size": 64 * 8,
"description": "PolyTracker module (v1.17)"
}
MARKER = b"PTMF"
HEADER = PTMHeader