mirror of
https://github.com/SickGear/SickGear.git
synced 2024-12-26 20:53:38 +00:00
318 lines
10 KiB
Python
318 lines
10 KiB
Python
|
"""
|
||
|
Modplug metadata inserted into module files.
|
||
|
|
||
|
Doc:
|
||
|
- http://modplug.svn.sourceforge.net/viewvc/modplug/trunk/modplug/soundlib/
|
||
|
|
||
|
Author: Christophe GISQUET <christophe.gisquet@free.fr>
|
||
|
Creation: 10th February 2007
|
||
|
"""
|
||
|
|
||
|
from hachoir.field import (FieldSet,
|
||
|
UInt32, UInt16, UInt8, Int8, Float32,
|
||
|
RawBytes, String, GenericVector, ParserError)
|
||
|
from hachoir.core.endian import LITTLE_ENDIAN
|
||
|
from hachoir.core.text_handler import textHandler, hexadecimal
|
||
|
|
||
|
MAX_ENVPOINTS = 32
|
||
|
|
||
|
|
||
|
def parseComments(parser):
|
||
|
size = parser["block_size"].value
|
||
|
if size > 0:
|
||
|
yield String(parser, "comment", size)
|
||
|
|
||
|
|
||
|
class MidiOut(FieldSet):
|
||
|
static_size = 9 * 32 * 8
|
||
|
|
||
|
def createFields(self):
|
||
|
for name in ("start", "stop", "tick", "noteon", "noteoff",
|
||
|
"volume", "pan", "banksel", "program"):
|
||
|
yield String(self, name, 32, strip='\0')
|
||
|
|
||
|
|
||
|
class Command(FieldSet):
|
||
|
static_size = 32 * 8
|
||
|
|
||
|
def createFields(self):
|
||
|
start = self.absolute_address
|
||
|
size = self.stream.searchBytesLength(b"\0", False, start)
|
||
|
if size > 0:
|
||
|
self.info("Command: %s" % self.stream.readBytes(start, size))
|
||
|
yield String(self, "command", size, strip='\0')
|
||
|
yield RawBytes(self, "parameter", (self._size // 8) - size)
|
||
|
|
||
|
|
||
|
class MidiSFXExt(FieldSet):
|
||
|
static_size = 16 * 32 * 8
|
||
|
|
||
|
def createFields(self):
|
||
|
for index in range(16):
|
||
|
yield Command(self, "command[]")
|
||
|
|
||
|
|
||
|
class MidiZXXExt(FieldSet):
|
||
|
static_size = 128 * 32 * 8
|
||
|
|
||
|
def createFields(self):
|
||
|
for index in range(128):
|
||
|
yield Command(self, "command[]")
|
||
|
|
||
|
|
||
|
def parseMidiConfig(parser):
|
||
|
yield MidiOut(parser, "midi_out")
|
||
|
yield MidiSFXExt(parser, "sfx_ext")
|
||
|
yield MidiZXXExt(parser, "zxx_ext")
|
||
|
|
||
|
|
||
|
def parseChannelSettings(parser):
|
||
|
size = parser["block_size"].value // 4
|
||
|
if size > 0:
|
||
|
yield GenericVector(parser, "settings", size, UInt32, "mix_plugin")
|
||
|
|
||
|
|
||
|
def parseEQBands(parser):
|
||
|
size = parser["block_size"].value // 4
|
||
|
if size > 0:
|
||
|
yield GenericVector(parser, "gains", size, UInt32, "band")
|
||
|
|
||
|
|
||
|
class SoundMixPluginInfo(FieldSet):
|
||
|
static_size = 128 * 8
|
||
|
|
||
|
def createFields(self):
|
||
|
yield textHandler(UInt32(self, "plugin_id1"), hexadecimal)
|
||
|
yield textHandler(UInt32(self, "plugin_id2"), hexadecimal)
|
||
|
yield UInt32(self, "input_routing")
|
||
|
yield UInt32(self, "output_routing")
|
||
|
yield GenericVector(self, "routing_info", 4, UInt32, "reserved")
|
||
|
yield String(self, "name", 32, strip='\0')
|
||
|
yield String(self, "dll_name", 64, desc="Original DLL name", strip='\0')
|
||
|
|
||
|
|
||
|
class ExtraData(FieldSet):
|
||
|
|
||
|
def __init__(self, parent, name, desc=None):
|
||
|
FieldSet.__init__(self, parent, name, desc)
|
||
|
self._size = (4 + self["size"].value) * 8
|
||
|
|
||
|
def createFields(self):
|
||
|
yield UInt32(self, "size")
|
||
|
size = self["size"].value
|
||
|
if size:
|
||
|
yield RawBytes(self, "data", size)
|
||
|
|
||
|
|
||
|
class XPlugData(FieldSet):
|
||
|
|
||
|
def __init__(self, parent, name, desc=None):
|
||
|
FieldSet.__init__(self, parent, name, desc)
|
||
|
self._size = (4 + self["size"].value) * 8
|
||
|
|
||
|
def createFields(self):
|
||
|
yield UInt32(self, "size")
|
||
|
while not self.eof:
|
||
|
yield UInt32(self, "marker")
|
||
|
if self["marker"].value == 'DWRT':
|
||
|
yield Float32(self, "dry_ratio")
|
||
|
elif self["marker"].value == 'PORG':
|
||
|
yield UInt32(self, "default_program")
|
||
|
|
||
|
|
||
|
def parsePlugin(parser):
|
||
|
yield SoundMixPluginInfo(parser, "info")
|
||
|
|
||
|
# Check if VST setchunk present
|
||
|
size = parser.stream.readBits(
|
||
|
parser.absolute_address + parser.current_size, 32, LITTLE_ENDIAN)
|
||
|
if 0 < size < parser.current_size + parser._size:
|
||
|
yield ExtraData(parser, "extra_data")
|
||
|
|
||
|
# Check if XPlugData is present
|
||
|
size = parser.stream.readBits(
|
||
|
parser.absolute_address + parser.current_size, 32, LITTLE_ENDIAN)
|
||
|
if 0 < size < parser.current_size + parser._size:
|
||
|
yield XPlugData(parser, "xplug_data")
|
||
|
|
||
|
|
||
|
# Format: "XXXX": (type, count, name)
|
||
|
EXTENSIONS = {
|
||
|
# WriteInstrumentHeaderStruct@Sndfile.cpp
|
||
|
"XTPM": {
|
||
|
"..Fd": (UInt32, 1, "Flags"),
|
||
|
"..OF": (UInt32, 1, "Fade out"),
|
||
|
"..VG": (UInt32, 1, "Global Volume"),
|
||
|
"...P": (UInt32, 1, "Panning"),
|
||
|
"..EV": (UInt32, 1, "Volume Envelope"),
|
||
|
"..EP": (UInt32, 1, "Panning Envelope"),
|
||
|
".EiP": (UInt32, 1, "Pitch Envelope"),
|
||
|
".SLV": (UInt8, 1, "Volume Loop Start"),
|
||
|
".ELV": (UInt8, 1, "Volume Loop End"),
|
||
|
".BSV": (UInt8, 1, "Volume Sustain Begin"),
|
||
|
".ESV": (UInt8, 1, "Volume Sustain End"),
|
||
|
".SLP": (UInt8, 1, "Panning Loop Start"),
|
||
|
".ELP": (UInt8, 1, "Panning Loop End"),
|
||
|
".BSP": (UInt8, 1, "Panning Substain Begin"),
|
||
|
".ESP": (UInt8, 1, "Padding Substain End"),
|
||
|
"SLiP": (UInt8, 1, "Pitch Loop Start"),
|
||
|
"ELiP": (UInt8, 1, "Pitch Loop End"),
|
||
|
"BSiP": (UInt8, 1, "Pitch Substain Begin"),
|
||
|
"ESiP": (UInt8, 1, "Pitch Substain End"),
|
||
|
".ANN": (UInt8, 1, "NNA"),
|
||
|
".TCD": (UInt8, 1, "DCT"),
|
||
|
".AND": (UInt8, 1, "DNA"),
|
||
|
"..SP": (UInt8, 1, "Panning Swing"),
|
||
|
"..SV": (UInt8, 1, "Volume Swing"),
|
||
|
".CFI": (UInt8, 1, "IFC"),
|
||
|
".RFI": (UInt8, 1, "IFR"),
|
||
|
"..BM": (UInt32, 1, "Midi Bank"),
|
||
|
"..PM": (UInt8, 1, "Midi Program"),
|
||
|
"..CM": (UInt8, 1, "Midi Channel"),
|
||
|
".KDM": (UInt8, 1, "Midi Drum Key"),
|
||
|
".SPP": (Int8, 1, "PPS"),
|
||
|
".CPP": (UInt8, 1, "PPC"),
|
||
|
".[PV": (UInt32, MAX_ENVPOINTS, "Volume Points"),
|
||
|
".[PP": (UInt32, MAX_ENVPOINTS, "Panning Points"),
|
||
|
"[PiP": (UInt32, MAX_ENVPOINTS, "Pitch Points"),
|
||
|
".[EV": (UInt8, MAX_ENVPOINTS, "Volume Enveloppe"),
|
||
|
".[EP": (UInt8, MAX_ENVPOINTS, "Panning Enveloppe"),
|
||
|
"[EiP": (UInt8, MAX_ENVPOINTS, "Pitch Enveloppe"),
|
||
|
".[MN": (UInt8, 128, "Note Mapping"),
|
||
|
"..[K": (UInt32, 128, "Keyboard"),
|
||
|
"..[n": (String, 32, "Name"),
|
||
|
".[nf": (String, 12, "Filename"),
|
||
|
".PiM": (UInt8, 1, "MixPlug"),
|
||
|
"..RV": (UInt16, 1, "Volume Ramping"),
|
||
|
"...R": (UInt16, 1, "Resampling"),
|
||
|
"..SC": (UInt8, 1, "Cut Swing"),
|
||
|
"..SR": (UInt8, 1, "Res Swing"),
|
||
|
"..MF": (UInt8, 1, "Filter Mode"),
|
||
|
},
|
||
|
|
||
|
# See after "CODE tag dictionary", same place, elements with [EXT]
|
||
|
"STPM": {
|
||
|
"...C": (UInt32, 1, "Channels"),
|
||
|
".VWC": (None, 0, "CreatedWith version"),
|
||
|
".VGD": (None, 0, "Default global volume"),
|
||
|
"..TD": (None, 0, "Default tempo"),
|
||
|
"HIBE": (None, 0, "Embedded instrument header"),
|
||
|
"VWSL": (None, 0, "LastSavedWith version"),
|
||
|
".MMP": (None, 0, "Plugin Mix mode"),
|
||
|
".BPR": (None, 0, "Rows per beat"),
|
||
|
".MPR": (None, 0, "Rows per measure"),
|
||
|
"@PES": (None, 0, "Chunk separator"),
|
||
|
".APS": (None, 0, "Song Pre-amplification"),
|
||
|
"..MT": (None, 0, "Tempo mode"),
|
||
|
"VTSV": (None, 0, "VSTi volume"),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
class MPField(FieldSet):
|
||
|
|
||
|
def __init__(self, parent, name, ext, desc=None):
|
||
|
FieldSet.__init__(self, parent, name, desc)
|
||
|
self.ext = ext
|
||
|
self.info(self.createDescription())
|
||
|
self._size = (6 + self["data_size"].value) * 8
|
||
|
|
||
|
def createFields(self):
|
||
|
# Identify tag
|
||
|
code = self.stream.readBytes(self.absolute_address, 4)
|
||
|
if code in self.ext:
|
||
|
cls, count, comment = self.ext[code]
|
||
|
else:
|
||
|
cls, count, comment = RawBytes, 1, "Unknown tag"
|
||
|
|
||
|
# Header
|
||
|
yield String(self, "code", 4, comment)
|
||
|
yield UInt16(self, "data_size")
|
||
|
|
||
|
# Data
|
||
|
if not cls:
|
||
|
size = self["data_size"].value
|
||
|
if size > 0:
|
||
|
yield RawBytes(self, "data", size)
|
||
|
elif cls in (String, RawBytes):
|
||
|
yield cls(self, "value", count)
|
||
|
else:
|
||
|
if count > 1:
|
||
|
yield GenericVector(self, "values", count, cls, "item")
|
||
|
else:
|
||
|
yield cls(self, "value")
|
||
|
|
||
|
def createDescription(self):
|
||
|
return "Element '%s', size %i" % \
|
||
|
(self["code"]._description, self["data_size"].value)
|
||
|
|
||
|
|
||
|
def parseFields(parser):
|
||
|
# Determine field names
|
||
|
ext = EXTENSIONS[parser["block_type"].value]
|
||
|
if ext is None:
|
||
|
raise ParserError("Unknown parent '%s'" % parser["block_type"].value)
|
||
|
|
||
|
# Parse fields
|
||
|
addr = parser.absolute_address + parser.current_size
|
||
|
while not parser.eof and parser.stream.readBytes(addr, 4) in ext:
|
||
|
field = MPField(parser, "field[]", ext)
|
||
|
yield field
|
||
|
addr += field._size
|
||
|
|
||
|
# Abort on unknown codes
|
||
|
parser.info("End of extension '%s' when finding '%s'" %
|
||
|
(parser["block_type"].value, parser.stream.readBytes(addr, 4)))
|
||
|
|
||
|
|
||
|
class ModplugBlock(FieldSet):
|
||
|
BLOCK_INFO = {
|
||
|
"TEXT": ("comment", True, "Comment", parseComments),
|
||
|
"MIDI": ("midi_config", True, "Midi configuration", parseMidiConfig),
|
||
|
"XFHC": ("channel_settings", True, "Channel settings", parseChannelSettings),
|
||
|
"XTPM": ("instrument_ext", False, "Instrument extensions", parseFields),
|
||
|
"STPM": ("song_ext", False, "Song extensions", parseFields),
|
||
|
}
|
||
|
|
||
|
def __init__(self, parent, name, desc=None):
|
||
|
FieldSet.__init__(self, parent, name, desc)
|
||
|
self.parseBlock = parsePlugin
|
||
|
|
||
|
t = self["block_type"].value
|
||
|
self.has_size = False
|
||
|
if t in self.BLOCK_INFO:
|
||
|
self._name, self.has_size, desc, parseBlock = self.BLOCK_INFO[t]
|
||
|
if callable(desc):
|
||
|
self.createDescription = lambda: desc(self)
|
||
|
if parseBlock:
|
||
|
self.parseBlock = lambda: parseBlock(self)
|
||
|
|
||
|
if self.has_size:
|
||
|
self._size = 8 * (self["block_size"].value + 8)
|
||
|
|
||
|
def createFields(self):
|
||
|
yield String(self, "block_type", 4)
|
||
|
if self.has_size:
|
||
|
yield UInt32(self, "block_size")
|
||
|
|
||
|
if self.parseBlock:
|
||
|
yield from self.parseBlock()
|
||
|
|
||
|
if self.has_size:
|
||
|
size = self["block_size"].value - (self.current_size // 8)
|
||
|
if size > 0:
|
||
|
yield RawBytes(self, "data", size, "Unknown data")
|
||
|
|
||
|
|
||
|
def ParseModplugMetadata(parser):
|
||
|
while not parser.eof:
|
||
|
block = ModplugBlock(parser, "block[]")
|
||
|
yield block
|
||
|
if block["block_type"].value == "STPM":
|
||
|
break
|
||
|
|
||
|
# More undocumented stuff: date ?
|
||
|
size = (parser._size - parser.absolute_address - parser.current_size) // 8
|
||
|
if size > 0:
|
||
|
yield RawBytes(parser, "info", size)
|