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

317 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)