mirror of
https://github.com/SickGear/SickGear.git
synced 2024-12-01 00:43:37 +00:00
0d9fbc1ad7
This version of SickBeard uses both TVDB and TVRage to search and gather it's series data from allowing you to now have access to and download shows that you couldn't before because of being locked into only what TheTVDB had to offer. Also this edition is based off the code we used in our XEM editon so it does come with scene numbering support as well as all the other features our XEM edition has to offer. Please before using this with your existing database (sickbeard.db) please make a backup copy of it and delete any other database files such as cache.db and failed.db if present, we HIGHLY recommend starting out with no database files at all to make this a fresh start but the choice is at your own risk! Enjoy!
668 lines
24 KiB
Python
668 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 lib.hachoir_parser import Parser
|
|
from lib.hachoir_core.field import (StaticFieldSet, FieldSet, Field,
|
|
Bit, Bits,
|
|
UInt32, UInt16, UInt8, Enum,
|
|
PaddingBytes, RawBytes, NullBytes,
|
|
String, GenericVector, ParserError)
|
|
from lib.hachoir_core.endian import LITTLE_ENDIAN
|
|
from lib.hachoir_core.text_handler import textHandler, hexadecimal
|
|
from lib.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 != 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 == 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)
|
|
# Not tested, probably wrong:
|
|
#if chunk.size: field.static_size = 8*chunk.size
|
|
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 == 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):
|
|
for field in self.createUnpaddedFields():
|
|
yield field
|
|
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)
|
|
for field in self.getFileVersionField():
|
|
yield field
|
|
|
|
yield UInt16(self, "num_orders")
|
|
yield UInt16(self, "num_instruments")
|
|
yield UInt16(self, "num_patterns")
|
|
|
|
for field in self.getFirstProperties():
|
|
yield field
|
|
yield String(self, "marker[]", 4)
|
|
for field in self.getLastProperties():
|
|
yield field
|
|
|
|
yield GenericVector(self, "channel_settings", 32,
|
|
ChannelSettings, "channel")
|
|
|
|
# Orders
|
|
yield GenericVector(self, "orders", self.getNumOrders(), UInt8, "order")
|
|
|
|
for field in self.getHeaderEndFields():
|
|
yield field
|
|
|
|
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 xrange(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 xrange(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(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 xrange(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')
|
|
|
|
for field in self.getInstrumentFields():
|
|
yield field
|
|
|
|
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 = "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 = "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))
|
|
for field in indexer.yieldChunks(self):
|
|
yield field
|
|
|
|
|
|
class S3MModule(Module):
|
|
PARSER_TAGS = {
|
|
"id": "s3m",
|
|
"category": "audio",
|
|
"file_ext": ("s3m",),
|
|
"mime": (u'audio/s3m', u'audio/x-s3m'),
|
|
"min_size": 64*8,
|
|
"description": "ScreamTracker3 module"
|
|
}
|
|
MARKER = "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 = "PTMF"
|
|
HEADER = PTMHeader
|