SickGear/lib/hachoir/parser/audio/xm.py
JackDandy 980e05cc99 Change Hachoir can't support PY2 so backport their PY3 to prevent a need for system dependant external binaries like mediainfo.
Backported 400 revisions from rev 1de4961-8897c5b (2018-2014).
Move core/benchmark, core/cmd_line, core/memory, core/profiler and core/timeout to core/optional/*
Remove metadata/qt*

PORT: Version 2.0a3 (inline with 3.0a3 @ f80c7d5).
Basic Support for XMP Packets.
tga: improvements to adhere more closely to the spec.
pdf: slightly improved parsing.
rar: fix TypeError on unknown block types.
Add MacRoman win32 codepage.
tiff/exif: support SubIFDs and tiled images.
Add method to export metadata in dictionary.
mpeg_video: don't attempt to parse Stream past length.
mpeg_video: parse ESCR correctly, add SCR value.
Change centralise CustomFragments.
field: don't set parser class if class is None, to enable autodetect.
field: add value/display for CustomFragment.
parser: inline warning to enable tracebacks in debug mode.
Fix empty bytestrings in makePrintable.
Fix contentSize in jpeg.py to account for image_data blocks.
Fix the ELF parser.
Enhance the AR archive parser.
elf parser: fix wrong wrong fields order in parsing little endian section flags.
elf parser: add s390 as a machine type.
Flesh out mp4 parser.

PORT: Version 2.0a1 (inline with 3.0a1).
Major refactoring and PEP8.
Fix ResourceWarning warnings on files. Add a close() method and support for the context manager protocol ("with obj: ...") to parsers, input and output streams.
metadata: get comment from ZIP.
Support for InputIOStream.read(0).
Fix sizeGe when size is None.
Remove unused new_seekable_field_set file.
Remove parser Mapsforge .map.
Remove parser Parallel Realities Starfighter .pak files.
sevenzip: fix for newer archives.
java: update access flags and modifiers for Java 1.7 and update description text for most recent Java.
Support ustar prefix field in tar archives.
Remove file_system* parsers.
Remove misc parsers 3d0, 3ds, gnome_keyring, msoffice*, mstask, ole*, word*.
Remove program parsers macho, nds, prc.
Support non-8bit Character subclasses.
Python parser supports Python 3.7.
Enhance mpeg_ts parser to support MTS/M2TS.
Support for creation date in tiff.
Change don't hardcode errno constant.

PORT: 1.9.1
Internal Only: The following are legacy reference to upstream commit messages.
Relevant changes up to b0a115f8.
Use integer division.
Replace HACHOIR_ERRORS with Exception.
Fix metadata.Data: make it sortable.
Import fixes from e7de492.
PORT: Version 2.0a1 (inline with 3.0a1 @ e9f8fad).
Replace hachoir.core.field with hachoir.field
Replace hachoir.core.stream with hachoir.stream
Remove the compatibility module for PY1.5 to PY2.5.
metadata: support TIFF picture.
metadata: fix string normalization.
metadata: fix datetime regex Fix hachoir bug #57.
FileFromInputStream: fix comparison between None and an int.
InputIOStream: open the file in binary mode.
2018-03-28 00:43:11 +01:00

412 lines
15 KiB
Python

"""
Parser of FastTrackerII Extended Module (XM) version 1.4
Documents:
- Modplug source code (file modplug/soundlib/Load_xm.cpp)
http://sourceforge.net/projects/modplug
- Dumb source code (files include/dumb.h and src/it/readxm.c
http://dumb.sf.net/
- Documents of "XM" format on Wotsit
http://www.wotsit.org
Author: Christophe GISQUET <christophe.gisquet@free.fr>
Creation: 8th February 2007
"""
from hachoir.parser import Parser
from hachoir.field import (StaticFieldSet, FieldSet,
Bit, RawBits, Bits,
UInt32, UInt16, UInt8, Int8, Enum,
RawBytes, String, GenericVector)
from hachoir.core.endian import LITTLE_ENDIAN, BIG_ENDIAN
from hachoir.core.text_handler import textHandler, filesizeHandler, hexadecimal
from hachoir.parser.audio.modplug import ParseModplugMetadata
from hachoir.parser.common.tracker import NOTE_NAME
def parseSigned(val):
return "%i" % (val.value - 128)
# From dumb
SEMITONE_BASE = 1.059463094359295309843105314939748495817
PITCH_BASE = 1.000225659305069791926712241547647863626
SAMPLE_LOOP_MODE = ("No loop", "Forward loop", "Ping-pong loop", "Undef")
class SampleType(FieldSet):
static_size = 8
def createFields(self):
yield Bits(self, "unused[]", 4)
yield Bit(self, "16bits")
yield Bits(self, "unused[]", 1)
yield Enum(Bits(self, "loop_mode", 2), SAMPLE_LOOP_MODE)
class SampleHeader(FieldSet):
static_size = 40 * 8
def createFields(self):
yield UInt32(self, "length")
yield UInt32(self, "loop_start")
yield UInt32(self, "loop_end")
yield UInt8(self, "volume")
yield Int8(self, "fine_tune")
yield SampleType(self, "type")
yield UInt8(self, "panning")
yield Int8(self, "relative_note")
yield UInt8(self, "reserved")
yield String(self, "name", 22, charset="ASCII", strip=' \0')
def createValue(self):
bytes = 1 + self["type/16bits"].value
C5_speed = int(16726.0 * pow(SEMITONE_BASE, self["relative_note"].value)
* pow(PITCH_BASE, self["fine_tune"].value * 2))
return "%s, %ubits, %u samples, %uHz" % \
(self["name"].display, 8 * bytes, self["length"].value / bytes, C5_speed)
class StuffType(StaticFieldSet):
format = (
(Bits, "unused", 5),
(Bit, "loop"),
(Bit, "sustain"),
(Bit, "on")
)
class InstrumentSecondHeader(FieldSet):
static_size = 234 * 8
def createFields(self):
yield UInt32(self, "sample_header_size")
yield GenericVector(self, "notes", 96, UInt8, "sample")
yield GenericVector(self, "volume_envelope", 24, UInt16, "point")
yield GenericVector(self, "panning_envelope", 24, UInt16, "point")
yield UInt8(self, "volume_points", r"Number of volume points")
yield UInt8(self, "panning_points", r"Number of panning points")
yield UInt8(self, "volume_sustain_point")
yield UInt8(self, "volume_loop_start_point")
yield UInt8(self, "volume_loop_end_point")
yield UInt8(self, "panning_sustain_point")
yield UInt8(self, "panning_loop_start_point")
yield UInt8(self, "panning_loop_end_point")
yield StuffType(self, "volume_type")
yield StuffType(self, "panning_type")
yield UInt8(self, "vibrato_type")
yield UInt8(self, "vibrato_sweep")
yield UInt8(self, "vibrato_depth")
yield UInt8(self, "vibrato_rate")
yield UInt16(self, "volume_fadeout")
yield GenericVector(self, "reserved", 11, UInt16, "word")
def createInstrumentContentSize(s, addr):
start = addr
samples = s.stream.readBits(addr + 27 * 8, 16, LITTLE_ENDIAN)
# Seek to end of header (1st + 2nd part)
addr += 8 * s.stream.readBits(addr, 32, LITTLE_ENDIAN)
sample_size = 0
if samples:
for index in xrange(samples):
# Read the sample size from the header
sample_size += s.stream.readBits(addr, 32, LITTLE_ENDIAN)
# Seek to next sample header
addr += SampleHeader.static_size
return addr - start + 8 * sample_size
class Instrument(FieldSet):
def __init__(self, parent, name):
FieldSet.__init__(self, parent, name)
self._size = createInstrumentContentSize(self, self.absolute_address)
self.info(self.createDescription())
# Seems to fix things...
def fixInstrumentHeader(self):
size = self["size"].value - self.current_size // 8
if size:
yield RawBytes(self, "unknown_data", size)
def createFields(self):
yield UInt32(self, "size")
yield String(self, "name", 22, charset="ASCII", strip=" \0")
# Doc says type is always 0, but I've found values of 24 and 96 for
# the _same_ song here, just different download sources for the file
yield UInt8(self, "type")
yield UInt16(self, "samples")
num = self["samples"].value
self.info(self.createDescription())
if num:
yield InstrumentSecondHeader(self, "second_header")
for field in self.fixInstrumentHeader():
yield field
# This part probably wrong
sample_size = []
for index in xrange(num):
sample = SampleHeader(self, "sample_header[]")
yield sample
sample_size.append(sample["length"].value)
for size in sample_size:
if size:
yield RawBytes(self, "sample_data[]", size, "Deltas")
else:
for field in self.fixInstrumentHeader():
yield field
def createDescription(self):
return "Instrument '%s': %i samples, header %i bytes" % \
(self["name"].value, self["samples"].value, self["size"].value)
VOLUME_NAME = (
"Volume slide down", "Volume slide up", "Fine volume slide down",
"Fine volume slide up", "Set vibrato speed", "Vibrato",
"Set panning", "Panning slide left", "Panning slide right",
"Tone porta", "Unhandled")
def parseVolume(val):
val = val.value
if 0x10 <= val <= 0x50:
return "Volume %i" % val - 16
else:
return VOLUME_NAME[val / 16 - 6]
class RealBit(RawBits):
static_size = 1
def __init__(self, parent, name, description=None):
RawBits.__init__(self, parent, name, 1, description=description)
def createValue(self):
return self._parent.stream.readBits(self.absolute_address, 1, BIG_ENDIAN)
class NoteInfo(StaticFieldSet):
format = (
(RawBits, "unused", 2),
(RealBit, "has_parameter"),
(RealBit, "has_type"),
(RealBit, "has_volume"),
(RealBit, "has_instrument"),
(RealBit, "has_note")
)
EFFECT_NAME = (
"Arppegio", "Porta up", "Porta down", "Tone porta", "Vibrato",
"Tone porta+Volume slide", "Vibrato+Volume slide", "Tremolo",
"Set panning", "Sample offset", "Volume slide", "Position jump",
"Set volume", "Pattern break", None, "Set tempo/BPM",
"Set global volume", "Global volume slide", "Unused", "Unused",
"Unused", "Set envelope position", "Unused", "Unused",
"Panning slide", "Unused", "Multi retrig note", "Unused",
"Tremor", "Unused", "Unused", "Unused", None)
EFFECT_E_NAME = (
"Unknown", "Fine porta up", "Fine porta down",
"Set gliss control", "Set vibrato control", "Set finetune",
"Set loop begin/loop", "Set tremolo control", "Retrig note",
"Fine volume slide up", "Fine volume slide down", "Note cut",
"Note delay", "Pattern delay")
class Effect(RawBits):
def __init__(self, parent, name):
RawBits.__init__(self, parent, name, 8)
def createValue(self):
t = self.parent.stream.readBits(self.absolute_address, 8, LITTLE_ENDIAN)
param = self.parent.stream.readBits(self.absolute_address + 8, 8, LITTLE_ENDIAN)
if t == 0x0E:
return EFFECT_E_NAME[param >> 4] + " %i" % (param & 0x07)
elif t == 0x21:
return ("Extra fine porta up", "Extra fine porta down")[param >> 4]
else:
return EFFECT_NAME[t]
class Note(FieldSet):
def __init__(self, parent, name, desc=None):
FieldSet.__init__(self, parent, name, desc)
self.flags = self.stream.readBits(self.absolute_address, 8, LITTLE_ENDIAN)
if self.flags & 0x80:
# TODO: optimize bitcounting with a table:
# http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetTable
self._size = 8
if self.flags & 0x01: self._size += 8
if self.flags & 0x02: self._size += 8
if self.flags & 0x04: self._size += 8
if self.flags & 0x08: self._size += 8
if self.flags & 0x10: self._size += 8
else:
self._size = 5 * 8
def createFields(self):
# This stupid shit gets the LSB, not the MSB...
self.info("Note info: 0x%02X" %
self.stream.readBits(self.absolute_address, 8, LITTLE_ENDIAN))
yield RealBit(self, "is_extended")
if self["is_extended"].value:
info = NoteInfo(self, "info")
yield info
if info["has_note"].value:
yield Enum(UInt8(self, "note"), NOTE_NAME)
if info["has_instrument"].value:
yield UInt8(self, "instrument")
if info["has_volume"].value:
yield textHandler(UInt8(self, "volume"), parseVolume)
if info["has_type"].value:
yield Effect(self, "effect_type")
if info["has_parameter"].value:
yield textHandler(UInt8(self, "effect_parameter"), hexadecimal)
else:
yield Enum(Bits(self, "note", 7), NOTE_NAME)
yield UInt8(self, "instrument")
yield textHandler(UInt8(self, "volume"), parseVolume)
yield Effect(self, "effect_type")
yield textHandler(UInt8(self, "effect_parameter"), hexadecimal)
def createDescription(self):
if "info" in self:
info = self["info"]
desc = []
if info["has_note"].value:
desc.append(self["note"].display)
if info["has_instrument"].value:
desc.append("instrument %i" % self["instrument"].value)
if info["has_volume"].value:
desc.append(self["has_volume"].display)
if info["has_type"].value:
desc.append("effect %s" % self["effect_type"].value)
if info["has_parameter"].value:
desc.append("parameter %i" % self["effect_parameter"].value)
else:
desc = (self["note"].display, "instrument %i" % self["instrument"].value,
self["has_volume"].display, "effect %s" % self["effect_type"].value,
"parameter %i" % self["effect_parameter"].value)
if desc:
return "Note %s" % ", ".join(desc)
else:
return "Note"
class Row(FieldSet):
def createFields(self):
for index in xrange(self["/header/channels"].value):
yield Note(self, "note[]")
def createPatternContentSize(s, addr):
return 8 * (s.stream.readBits(addr, 32, LITTLE_ENDIAN) +
s.stream.readBits(addr + 7 * 8, 16, LITTLE_ENDIAN))
class Pattern(FieldSet):
def __init__(self, parent, name, desc=None):
FieldSet.__init__(self, parent, name, desc)
self._size = createPatternContentSize(self, self.absolute_address)
def createFields(self):
yield UInt32(self, "header_size", r"Header length (9)")
yield UInt8(self, "packing_type", r"Packing type (always 0)")
yield UInt16(self, "rows", r"Number of rows in pattern (1..256)")
yield UInt16(self, "data_size", r"Packed patterndata size")
rows = self["rows"].value
self.info("Pattern: %i rows" % rows)
for index in xrange(rows):
yield Row(self, "row[]")
def createDescription(self):
return "Pattern with %i rows" % self["rows"].value
class Header(FieldSet):
MAGIC = "Extended Module: "
static_size = 336 * 8
def createFields(self):
yield String(self, "signature", 17, "XM signature", charset="ASCII")
yield String(self, "title", 20, "XM title", charset="ASCII", strip=' ')
yield UInt8(self, "marker", "Marker (0x1A)")
yield String(self, "tracker_name", 20, "XM tracker name", charset="ASCII", strip=' ')
yield UInt8(self, "format_minor")
yield UInt8(self, "format_major")
yield filesizeHandler(UInt32(self, "header_size", "Header size (276)"))
yield UInt16(self, "song_length", "Length in patten order table")
yield UInt16(self, "restart", "Restart position")
yield UInt16(self, "channels", "Number of channels (2,4,6,8,10,...,32)")
yield UInt16(self, "patterns", "Number of patterns (max 256)")
yield UInt16(self, "instruments", "Number of instruments (max 128)")
yield Bit(self, "amiga_ftable", "Amiga frequency table")
yield Bit(self, "linear_ftable", "Linear frequency table")
yield Bits(self, "unused", 14)
yield UInt16(self, "tempo", "Default tempo")
yield UInt16(self, "bpm", "Default BPM")
yield GenericVector(self, "pattern_order", 256, UInt8, "order")
def createDescription(self):
return "'%s' by '%s'" % (
self["title"].value, self["tracker_name"].value)
class XMModule(Parser):
PARSER_TAGS = {
"id": "fasttracker2",
"category": "audio",
"file_ext": ("xm",),
"mime": (
u'audio/xm', u'audio/x-xm',
u'audio/module-xm', u'audio/mod', u'audio/x-mod'),
"magic": ((Header.MAGIC, 0),),
"min_size": Header.static_size + 29 * 8, # Header + 1 empty instrument
"description": "FastTracker2 module"
}
endian = LITTLE_ENDIAN
def validate(self):
header = self.stream.readBytes(0, 17)
if header != Header.MAGIC:
return "Invalid signature %a" % header
if self["/header/header_size"].value != 276:
return "Unknown header size (%u)" % self["/header/header_size"].value
return True
def createFields(self):
yield Header(self, "header")
for index in xrange(self["/header/patterns"].value):
yield Pattern(self, "pattern[]")
for index in xrange(self["/header/instruments"].value):
yield Instrument(self, "instrument[]")
# Metadata added by ModPlug - can be discarded
for field in ParseModplugMetadata(self):
yield field
def createContentSize(self):
# Header size
size = Header.static_size
# Add patterns size
for index in xrange(self["/header/patterns"].value):
size += createPatternContentSize(self, size)
# Add instruments size
for index in xrange(self["/header/instruments"].value):
size += createInstrumentContentSize(self, size)
# Not reporting Modplug metadata
return size
def createDescription(self):
return self["header"].description