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

424 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 range(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")
yield from self.fixInstrumentHeader()
# This part probably wrong
sample_size = []
for index in range(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:
yield from self.fixInstrumentHeader()
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 range(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 range(rows):
yield Row(self, "row[]")
def createDescription(self):
return "Pattern with %i rows" % self["rows"].value
class Header(FieldSet):
MAGIC = b"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": (
'audio/xm', 'audio/x-xm',
'audio/module-xm', 'audio/mod', '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 range(self["/header/patterns"].value):
yield Pattern(self, "pattern[]")
for index in range(self["/header/instruments"].value):
yield Instrument(self, "instrument[]")
# Metadata added by ModPlug - can be discarded
yield from ParseModplugMetadata(self)
def createContentSize(self):
# Header size
size = Header.static_size
# Add patterns size
for index in range(self["/header/patterns"].value):
size += createPatternContentSize(self, size)
# Add instruments size
for index in range(self["/header/instruments"].value):
size += createInstrumentContentSize(self, size)
# Not reporting Modplug metadata
return size
def createDescription(self):
return self["header"].description