Remove submodule, just put Dependencies in ./libs
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
from hachoir_parser.audio.aiff import AiffFile
|
||||
from hachoir_parser.audio.au import AuFile
|
||||
from hachoir_parser.audio.itunesdb import ITunesDBFile
|
||||
from hachoir_parser.audio.midi import MidiFile
|
||||
from hachoir_parser.audio.mpeg_audio import MpegAudioFile
|
||||
from hachoir_parser.audio.real_audio import RealAudioFile
|
||||
from hachoir_parser.audio.xm import XMModule
|
||||
from hachoir_parser.audio.s3m import S3MModule
|
||||
from hachoir_parser.audio.s3m import PTMModule
|
||||
from hachoir_parser.audio.mod import AmigaModule
|
||||
from hachoir_parser.audio.flac import FlacParser
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"""
|
||||
Audio Interchange File Format (AIFF) parser.
|
||||
|
||||
Author: Victor Stinner
|
||||
Creation: 27 december 2006
|
||||
"""
|
||||
|
||||
from hachoir_parser import Parser
|
||||
from hachoir_core.field import (FieldSet,
|
||||
UInt16, UInt32, Float80, TimestampMac32,
|
||||
RawBytes, NullBytes,
|
||||
String, Enum, PascalString32)
|
||||
from hachoir_core.endian import BIG_ENDIAN
|
||||
from hachoir_core.text_handler import filesizeHandler
|
||||
from hachoir_core.tools import alignValue
|
||||
from hachoir_parser.audio.id3 import ID3v2
|
||||
|
||||
CODEC_NAME = {
|
||||
'ACE2': u"ACE 2-to-1",
|
||||
'ACE8': u"ACE 8-to-3",
|
||||
'MAC3': u"MAC 3-to-1",
|
||||
'MAC6': u"MAC 6-to-1",
|
||||
'NONE': u"None",
|
||||
'sowt': u"Little-endian, no compression",
|
||||
}
|
||||
|
||||
class Comment(FieldSet):
|
||||
def createFields(self):
|
||||
yield TimestampMac32(self, "timestamp")
|
||||
yield PascalString32(self, "text")
|
||||
|
||||
def parseText(self):
|
||||
yield String(self, "text", self["size"].value)
|
||||
|
||||
def parseID3(self):
|
||||
yield ID3v2(self, "id3v2", size=self["size"].value*8)
|
||||
|
||||
def parseComment(self):
|
||||
yield UInt16(self, "nb_comment")
|
||||
for index in xrange(self["nb_comment"].value):
|
||||
yield Comment(self, "comment[]")
|
||||
|
||||
def parseCommon(self):
|
||||
yield UInt16(self, "nb_channel")
|
||||
yield UInt32(self, "nb_sample")
|
||||
yield UInt16(self, "sample_size")
|
||||
yield Float80(self, "sample_rate")
|
||||
yield Enum(String(self, "codec", 4, strip="\0", charset="ASCII"), CODEC_NAME)
|
||||
|
||||
def parseVersion(self):
|
||||
yield TimestampMac32(self, "timestamp")
|
||||
|
||||
def parseSound(self):
|
||||
yield UInt32(self, "offset")
|
||||
yield UInt32(self, "block_size")
|
||||
size = (self.size - self.current_size) // 8
|
||||
if size:
|
||||
yield RawBytes(self, "data", size)
|
||||
|
||||
class Chunk(FieldSet):
|
||||
TAG_INFO = {
|
||||
'COMM': ('common', "Common chunk", parseCommon),
|
||||
'COMT': ('comment', "Comment", parseComment),
|
||||
'NAME': ('name', "Name", parseText),
|
||||
'AUTH': ('author', "Author", parseText),
|
||||
'FVER': ('version', "Version", parseVersion),
|
||||
'SSND': ('sound', "Sound data", parseSound),
|
||||
'ID3 ': ('id3', "ID3", parseID3),
|
||||
}
|
||||
|
||||
def __init__(self, *args):
|
||||
FieldSet.__init__(self, *args)
|
||||
self._size = (8 + alignValue(self["size"].value, 2)) * 8
|
||||
tag = self["type"].value
|
||||
if tag in self.TAG_INFO:
|
||||
self._name, self._description, self._parser = self.TAG_INFO[tag]
|
||||
else:
|
||||
self._parser = None
|
||||
|
||||
def createFields(self):
|
||||
yield String(self, "type", 4, "Signature (FORM)", charset="ASCII")
|
||||
yield filesizeHandler(UInt32(self, "size"))
|
||||
size = self["size"].value
|
||||
if size:
|
||||
if self._parser:
|
||||
for field in self._parser(self):
|
||||
yield field
|
||||
if size % 2:
|
||||
yield NullBytes(self, "padding", 1)
|
||||
else:
|
||||
yield RawBytes(self, "data", size)
|
||||
|
||||
class AiffFile(Parser):
|
||||
PARSER_TAGS = {
|
||||
"id": "aiff",
|
||||
"category": "audio",
|
||||
"file_ext": ("aif", "aiff", "aifc"),
|
||||
"mime": (u"audio/x-aiff",),
|
||||
"magic_regex": (("FORM.{4}AIF[CF]", 0),),
|
||||
"min_size": 12*8,
|
||||
"description": "Audio Interchange File Format (AIFF)"
|
||||
}
|
||||
endian = BIG_ENDIAN
|
||||
|
||||
def validate(self):
|
||||
if self.stream.readBytes(0, 4) != "FORM":
|
||||
return "Invalid signature"
|
||||
if self.stream.readBytes(8*8, 4) not in ("AIFF", "AIFC"):
|
||||
return "Invalid type"
|
||||
return True
|
||||
|
||||
def createFields(self):
|
||||
yield String(self, "signature", 4, "Signature (FORM)", charset="ASCII")
|
||||
yield filesizeHandler(UInt32(self, "filesize"))
|
||||
yield String(self, "type", 4, "Form type (AIFF or AIFC)", charset="ASCII")
|
||||
while not self.eof:
|
||||
yield Chunk(self, "chunk[]")
|
||||
|
||||
def createDescription(self):
|
||||
if self["type"].value == "AIFC":
|
||||
return "Audio Interchange File Format Compressed (AIFC)"
|
||||
else:
|
||||
return "Audio Interchange File Format (AIFF)"
|
||||
|
||||
def createContentSize(self):
|
||||
return self["filesize"].value * 8
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
AU audio file parser
|
||||
|
||||
Author: Victor Stinner
|
||||
Creation: 12 july 2006
|
||||
"""
|
||||
|
||||
from hachoir_parser import Parser
|
||||
from hachoir_core.field import UInt32, Enum, String, RawBytes
|
||||
from hachoir_core.endian import BIG_ENDIAN
|
||||
from hachoir_core.text_handler import displayHandler, filesizeHandler
|
||||
from hachoir_core.tools import createDict, humanFrequency
|
||||
|
||||
class AuFile(Parser):
|
||||
PARSER_TAGS = {
|
||||
"id": "sun_next_snd",
|
||||
"category": "audio",
|
||||
"file_ext": ("au", "snd"),
|
||||
"mime": (u"audio/basic",),
|
||||
"min_size": 24*8,
|
||||
"magic": ((".snd", 0),),
|
||||
"description": "Sun/NeXT audio"
|
||||
}
|
||||
endian = BIG_ENDIAN
|
||||
|
||||
CODEC_INFO = {
|
||||
1: (8, u"8-bit ISDN u-law"),
|
||||
2: (8, u"8-bit linear PCM"),
|
||||
3: (16, u"16-bit linear PCM"),
|
||||
4: (24, u"24-bit linear PCM"),
|
||||
5: (32, u"32-bit linear PCM"),
|
||||
6: (32, u"32-bit IEEE floating point"),
|
||||
7: (64, u"64-bit IEEE floating point"),
|
||||
8: (None, u"Fragmented sample data"),
|
||||
9: (None, u"DSP program"),
|
||||
10: (8, u"8-bit fixed point"),
|
||||
11: (16, u"16-bit fixed point"),
|
||||
12: (24, u"24-bit fixed point"),
|
||||
13: (32, u"32-bit fixed point"),
|
||||
18: (16, u"16-bit linear with emphasis"),
|
||||
19: (16, u"16-bit linear compressed"),
|
||||
20: (16, u"16-bit linear with emphasis and compression"),
|
||||
21: (None, u"Music kit DSP commands"),
|
||||
23: (None, u"4-bit ISDN u-law compressed (CCITT G.721 ADPCM)"),
|
||||
24: (None, u"ITU-T G.722 ADPCM"),
|
||||
25: (None, u"ITU-T G.723 3-bit ADPCM"),
|
||||
26: (None, u"ITU-T G.723 5-bit ADPCM"),
|
||||
27: (8, u"8-bit ISDN A-law"),
|
||||
}
|
||||
|
||||
# Create bit rate and codec name dictionnaries
|
||||
BITS_PER_SAMPLE = createDict(CODEC_INFO, 0)
|
||||
CODEC_NAME = createDict(CODEC_INFO, 1)
|
||||
|
||||
VALID_NB_CHANNEL = set((1,2)) # FIXME: 4, 5, 7, 8 channels are supported?
|
||||
|
||||
def validate(self):
|
||||
if self.stream.readBytes(0, 4) != ".snd":
|
||||
return "Wrong file signature"
|
||||
if self["channels"].value not in self.VALID_NB_CHANNEL:
|
||||
return "Invalid number of channel"
|
||||
return True
|
||||
|
||||
def getBitsPerSample(self):
|
||||
"""
|
||||
Get bit rate (number of bit per sample per channel),
|
||||
may returns None if you unable to compute it.
|
||||
"""
|
||||
return self.BITS_PER_SAMPLE.get(self["codec"].value)
|
||||
|
||||
def createFields(self):
|
||||
yield String(self, "signature", 4, 'Format signature (".snd")', charset="ASCII")
|
||||
yield UInt32(self, "data_ofs", "Data offset")
|
||||
yield filesizeHandler(UInt32(self, "data_size", "Data size"))
|
||||
yield Enum(UInt32(self, "codec", "Audio codec"), self.CODEC_NAME)
|
||||
yield displayHandler(UInt32(self, "sample_rate", "Number of samples/second"), humanFrequency)
|
||||
yield UInt32(self, "channels", "Number of interleaved channels")
|
||||
|
||||
size = self["data_ofs"].value - self.current_size // 8
|
||||
if 0 < size:
|
||||
yield String(self, "info", size, "Information", strip=" \0", charset="ISO-8859-1")
|
||||
|
||||
size = min(self["data_size"].value, (self.size - self.current_size) // 8)
|
||||
yield RawBytes(self, "audio_data", size, "Audio data")
|
||||
|
||||
def createContentSize(self):
|
||||
return (self["data_ofs"].value + self["data_size"].value) * 8
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
FLAC (audio) parser
|
||||
|
||||
Documentation:
|
||||
|
||||
* http://flac.sourceforge.net/format.html
|
||||
|
||||
Author: Esteban Loiseau <baal AT tuxfamily.org>
|
||||
Creation date: 2008-04-09
|
||||
"""
|
||||
|
||||
from hachoir_parser import Parser
|
||||
from hachoir_core.field import FieldSet, String, Bit, Bits, UInt16, UInt24, RawBytes, Enum, NullBytes
|
||||
from hachoir_core.stream import BIG_ENDIAN, LITTLE_ENDIAN
|
||||
from hachoir_core.tools import createDict
|
||||
from hachoir_parser.container.ogg import parseVorbisComment
|
||||
|
||||
class VorbisComment(FieldSet):
|
||||
endian = LITTLE_ENDIAN
|
||||
createFields = parseVorbisComment
|
||||
|
||||
class StreamInfo(FieldSet):
|
||||
static_size = 34*8
|
||||
def createFields(self):
|
||||
yield UInt16(self, "min_block_size", "The minimum block size (in samples) used in the stream")
|
||||
yield UInt16(self, "max_block_size", "The maximum block size (in samples) used in the stream")
|
||||
yield UInt24(self, "min_frame_size", "The minimum frame size (in bytes) used in the stream")
|
||||
yield UInt24(self, "max_frame_size", "The maximum frame size (in bytes) used in the stream")
|
||||
yield Bits(self, "sample_hertz", 20, "Sample rate in Hertz")
|
||||
yield Bits(self, "nb_channel", 3, "Number of channels minus one")
|
||||
yield Bits(self, "bits_per_sample", 5, "Bits per sample minus one")
|
||||
yield Bits(self, "total_samples", 36, "Total samples in stream")
|
||||
yield RawBytes(self, "md5sum", 16, "MD5 signature of the unencoded audio data")
|
||||
|
||||
class SeekPoint(FieldSet):
|
||||
def createFields(self):
|
||||
yield Bits(self, "sample_number", 64, "Sample number")
|
||||
yield Bits(self, "offset", 64, "Offset in bytes")
|
||||
yield Bits(self, "nb_sample", 16)
|
||||
|
||||
class SeekTable(FieldSet):
|
||||
def createFields(self):
|
||||
while not self.eof:
|
||||
yield SeekPoint(self, "point[]")
|
||||
|
||||
class MetadataBlock(FieldSet):
|
||||
"Metadata block field: http://flac.sourceforge.net/format.html#metadata_block"
|
||||
|
||||
BLOCK_TYPES = {
|
||||
0: ("stream_info", u"Stream info", StreamInfo),
|
||||
1: ("padding[]", u"Padding", None),
|
||||
2: ("application[]", u"Application", None),
|
||||
3: ("seek_table", u"Seek table", SeekTable),
|
||||
4: ("comment", u"Vorbis comment", VorbisComment),
|
||||
5: ("cue_sheet[]", u"Cue sheet", None),
|
||||
6: ("picture[]", u"Picture", None),
|
||||
}
|
||||
BLOCK_TYPE_DESC = createDict(BLOCK_TYPES, 1)
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
FieldSet.__init__(self, *args, **kw)
|
||||
self._size = 32 + self["metadata_length"].value * 8
|
||||
try:
|
||||
key = self["block_type"].value
|
||||
self._name, self._description, self.handler = self.BLOCK_TYPES[key]
|
||||
except KeyError:
|
||||
self.handler = None
|
||||
|
||||
def createFields(self):
|
||||
yield Bit(self, "last_metadata_block", "True if this is the last metadata block")
|
||||
yield Enum(Bits(self, "block_type", 7, "Metadata block header type"), self.BLOCK_TYPE_DESC)
|
||||
yield UInt24(self, "metadata_length", "Length of following metadata in bytes (doesn't include this header)")
|
||||
|
||||
block_type = self["block_type"].value
|
||||
size = self["metadata_length"].value
|
||||
if not size:
|
||||
return
|
||||
try:
|
||||
handler = self.BLOCK_TYPES[block_type][2]
|
||||
except KeyError:
|
||||
handler = None
|
||||
if handler:
|
||||
yield handler(self, "content", size=size*8)
|
||||
elif self["block_type"].value == 1:
|
||||
yield NullBytes(self, "padding", size)
|
||||
else:
|
||||
yield RawBytes(self, "rawdata", size)
|
||||
|
||||
class Metadata(FieldSet):
|
||||
def createFields(self):
|
||||
while not self.eof:
|
||||
field = MetadataBlock(self,"metadata_block[]")
|
||||
yield field
|
||||
if field["last_metadata_block"].value:
|
||||
break
|
||||
|
||||
class Frame(FieldSet):
|
||||
SAMPLE_RATES = {
|
||||
0: "get from STREAMINFO metadata block",
|
||||
1: "88.2kHz",
|
||||
2: "176.4kHz",
|
||||
3: "192kHz",
|
||||
4: "8kHz",
|
||||
5: "16kHz",
|
||||
6: "22.05kHz",
|
||||
7: "24kHz",
|
||||
8: "32kHz",
|
||||
9: "44.1kHz",
|
||||
10: "48kHz",
|
||||
11: "96kHz",
|
||||
12: "get 8 bit sample rate (in kHz) from end of header",
|
||||
13: "get 16 bit sample rate (in Hz) from end of header",
|
||||
14: "get 16 bit sample rate (in tens of Hz) from end of header",
|
||||
}
|
||||
|
||||
def createFields(self):
|
||||
yield Bits(self, "sync", 14, "Sync code: 11111111111110")
|
||||
yield Bit(self, "reserved[]")
|
||||
yield Bit(self, "blocking_strategy")
|
||||
yield Bits(self, "block_size", 4)
|
||||
yield Enum(Bits(self, "sample_rate", 4), self.SAMPLE_RATES)
|
||||
yield Bits(self, "channel_assign", 4)
|
||||
yield Bits(self, "sample_size", 3)
|
||||
yield Bit(self, "reserved[]")
|
||||
# FIXME: Finish frame header parser
|
||||
|
||||
class Frames(FieldSet):
|
||||
def createFields(self):
|
||||
while not self.eof:
|
||||
yield Frame(self, "frame[]")
|
||||
# FIXME: Parse all frames
|
||||
return
|
||||
|
||||
class FlacParser(Parser):
|
||||
"Parse FLAC audio files: FLAC is a lossless audio codec"
|
||||
MAGIC = "fLaC\x00"
|
||||
PARSER_TAGS = {
|
||||
"id": "flac",
|
||||
"category": "audio",
|
||||
"file_ext": ("flac",),
|
||||
"mime": (u"audio/x-flac",),
|
||||
"magic": ((MAGIC, 0),),
|
||||
"min_size": 4*8,
|
||||
"description": "FLAC audio",
|
||||
}
|
||||
endian = BIG_ENDIAN
|
||||
|
||||
def validate(self):
|
||||
if self.stream.readBytes(0, len(self.MAGIC)) != self.MAGIC:
|
||||
return u"Invalid magic string"
|
||||
return True
|
||||
|
||||
def createFields(self):
|
||||
yield String(self, "signature", 4,charset="ASCII", description="FLAC signature: fLaC string")
|
||||
yield Metadata(self,"metadata")
|
||||
yield Frames(self,"frames")
|
||||
|
||||
@@ -0,0 +1,507 @@
|
||||
"""
|
||||
ID3 metadata parser, supported versions: 1.O, 2.2, 2.3 and 2.4
|
||||
|
||||
Informations: http://www.id3.org/
|
||||
|
||||
Author: Victor Stinner
|
||||
"""
|
||||
|
||||
from hachoir_core.field import (FieldSet, MatchError, ParserError,
|
||||
Enum, UInt8, UInt24, UInt32,
|
||||
CString, String, RawBytes,
|
||||
Bit, Bits, NullBytes, NullBits)
|
||||
from hachoir_core.text_handler import textHandler
|
||||
from hachoir_core.tools import humanDuration
|
||||
from hachoir_core.endian import NETWORK_ENDIAN
|
||||
|
||||
class ID3v1(FieldSet):
|
||||
static_size = 128 * 8
|
||||
GENRE_NAME = {
|
||||
0: u"Blues",
|
||||
1: u"Classic Rock",
|
||||
2: u"Country",
|
||||
3: u"Dance",
|
||||
4: u"Disco",
|
||||
5: u"Funk",
|
||||
6: u"Grunge",
|
||||
7: u"Hip-Hop",
|
||||
8: u"Jazz",
|
||||
9: u"Metal",
|
||||
10: u"New Age",
|
||||
11: u"Oldies",
|
||||
12: u"Other",
|
||||
13: u"Pop",
|
||||
14: u"R&B",
|
||||
15: u"Rap",
|
||||
16: u"Reggae",
|
||||
17: u"Rock",
|
||||
18: u"Techno",
|
||||
19: u"Industrial",
|
||||
20: u"Alternative",
|
||||
21: u"Ska",
|
||||
22: u"Death Metal",
|
||||
23: u"Pranks",
|
||||
24: u"Soundtrack",
|
||||
25: u"Euro-Techno",
|
||||
26: u"Ambient",
|
||||
27: u"Trip-Hop",
|
||||
28: u"Vocal",
|
||||
29: u"Jazz+Funk",
|
||||
30: u"Fusion",
|
||||
31: u"Trance",
|
||||
32: u"Classical",
|
||||
33: u"Instrumental",
|
||||
34: u"Acid",
|
||||
35: u"House",
|
||||
36: u"Game",
|
||||
37: u"Sound Clip",
|
||||
38: u"Gospel",
|
||||
39: u"Noise",
|
||||
40: u"AlternRock",
|
||||
41: u"Bass",
|
||||
42: u"Soul",
|
||||
43: u"Punk",
|
||||
44: u"Space",
|
||||
45: u"Meditative",
|
||||
46: u"Instrumental Pop",
|
||||
47: u"Instrumental Rock",
|
||||
48: u"Ethnic",
|
||||
49: u"Gothic",
|
||||
50: u"Darkwave",
|
||||
51: u"Techno-Industrial",
|
||||
52: u"Electronic",
|
||||
53: u"Pop-Folk",
|
||||
54: u"Eurodance",
|
||||
55: u"Dream",
|
||||
56: u"Southern Rock",
|
||||
57: u"Comedy",
|
||||
58: u"Cult",
|
||||
59: u"Gangsta",
|
||||
60: u"Top 40",
|
||||
61: u"Christian Rap",
|
||||
62: u"Pop/Funk",
|
||||
63: u"Jungle",
|
||||
64: u"Native American",
|
||||
65: u"Cabaret",
|
||||
66: u"New Wave",
|
||||
67: u"Psychadelic",
|
||||
68: u"Rave",
|
||||
69: u"Showtunes",
|
||||
70: u"Trailer",
|
||||
71: u"Lo-Fi",
|
||||
72: u"Tribal",
|
||||
73: u"Acid Punk",
|
||||
74: u"Acid Jazz",
|
||||
75: u"Polka",
|
||||
76: u"Retro",
|
||||
77: u"Musical",
|
||||
78: u"Rock & Roll",
|
||||
79: u"Hard Rock",
|
||||
# Following are winamp extentions
|
||||
80: u"Folk",
|
||||
81: u"Folk-Rock",
|
||||
82: u"National Folk",
|
||||
83: u"Swing",
|
||||
84: u"Fast Fusion",
|
||||
85: u"Bebob",
|
||||
86: u"Latin",
|
||||
87: u"Revival",
|
||||
88: u"Celtic",
|
||||
89: u"Bluegrass",
|
||||
90: u"Avantgarde",
|
||||
91: u"Gothic Rock",
|
||||
92: u"Progressive Rock",
|
||||
93: u"Psychedelic Rock",
|
||||
94: u"Symphonic Rock",
|
||||
95: u"Slow Rock",
|
||||
96: u"Big Band",
|
||||
97: u"Chorus",
|
||||
98: u"Easy Listening",
|
||||
99: u"Acoustic",
|
||||
100: u"Humour",
|
||||
101: u"Speech",
|
||||
102: u"Chanson",
|
||||
103: u"Opera",
|
||||
104: u"Chamber Music",
|
||||
105: u"Sonata",
|
||||
106: u"Symphony",
|
||||
107: u"Booty Bass",
|
||||
108: u"Primus",
|
||||
109: u"Porn Groove",
|
||||
110: u"Satire",
|
||||
111: u"Slow Jam",
|
||||
112: u"Club",
|
||||
113: u"Tango",
|
||||
114: u"Samba",
|
||||
115: u"Folklore",
|
||||
116: u"Ballad",
|
||||
117: u"Power Ballad",
|
||||
118: u"Rhythmic Soul",
|
||||
119: u"Freestyle",
|
||||
120: u"Duet",
|
||||
121: u"Punk Rock",
|
||||
122: u"Drum Solo",
|
||||
123: u"A capella",
|
||||
124: u"Euro-House",
|
||||
125: u"Dance Hall",
|
||||
126: u"Goa",
|
||||
127: u"Drum & Bass",
|
||||
128: u"Club-House",
|
||||
129: u"Hardcore",
|
||||
130: u"Terror",
|
||||
131: u"Indie",
|
||||
132: u"Britpop",
|
||||
133: u"Negerpunk",
|
||||
134: u"Polsk Punk",
|
||||
135: u"Beat",
|
||||
136: u"Christian Gangsta Rap",
|
||||
137: u"Heavy Metal",
|
||||
138: u"Black Metal",
|
||||
139: u"Crossover",
|
||||
140: u"Contemporary Christian",
|
||||
141: u"Christian Rock ",
|
||||
142: u"Merengue",
|
||||
143: u"Salsa",
|
||||
144: u"Trash Metal",
|
||||
145: u"Anime",
|
||||
146: u"JPop",
|
||||
147: u"Synthpop"
|
||||
}
|
||||
|
||||
def createFields(self):
|
||||
yield String(self, "signature", 3, "IDv1 signature (\"TAG\")", charset="ASCII")
|
||||
if self["signature"].value != "TAG":
|
||||
raise MatchError("Stream doesn't look like ID3v1 (wrong signature)!")
|
||||
# TODO: Charset of below strings?
|
||||
yield String(self, "song", 30, "Song title", strip=" \0", charset="ISO-8859-1")
|
||||
yield String(self, "author", 30, "Author", strip=" \0", charset="ISO-8859-1")
|
||||
yield String(self, "album", 30, "Album title", strip=" \0", charset="ISO-8859-1")
|
||||
yield String(self, "year", 4, "Year", strip=" \0", charset="ISO-8859-1")
|
||||
|
||||
# TODO: Write better algorithm to guess ID3v1 version
|
||||
version = self.getVersion()
|
||||
if version in ("v1.1", "v1.1b"):
|
||||
if version == "v1.1b":
|
||||
# ID3 v1.1b
|
||||
yield String(self, "comment", 29, "Comment", strip=" \0", charset="ISO-8859-1")
|
||||
yield UInt8(self, "track_nb", "Track number")
|
||||
else:
|
||||
# ID3 v1.1
|
||||
yield String(self, "comment", 30, "Comment", strip=" \0", charset="ISO-8859-1")
|
||||
yield Enum(UInt8(self, "genre", "Genre"), self.GENRE_NAME)
|
||||
else:
|
||||
# ID3 v1.0
|
||||
yield String(self, "comment", 31, "Comment", strip=" \0", charset="ISO-8859-1")
|
||||
|
||||
def getVersion(self):
|
||||
addr = self.absolute_address + 126*8
|
||||
bytes = self.stream.readBytes(addr, 2)
|
||||
|
||||
# last byte (127) is not space?
|
||||
if bytes[1] != ' ':
|
||||
# byte 126 is nul?
|
||||
if bytes[0] == 0x00:
|
||||
return "v1.1"
|
||||
else:
|
||||
return "v1.1b"
|
||||
else:
|
||||
return "1.0"
|
||||
|
||||
def createDescription(self):
|
||||
version = self.getVersion()
|
||||
return "ID3 %s: author=%s, song=%s" % (
|
||||
version, self["author"].value, self["song"].value)
|
||||
|
||||
def getCharset(field):
|
||||
try:
|
||||
key = field.value
|
||||
return ID3_StringCharset.charset_name[key]
|
||||
except KeyError:
|
||||
raise ParserError("ID3v2: Invalid charset (%s)." % key)
|
||||
|
||||
class ID3_String(FieldSet):
|
||||
STRIP = " \0"
|
||||
def createFields(self):
|
||||
yield String(self, "text", self._size/8, "Text", charset="ISO-8859-1", strip=self.STRIP)
|
||||
|
||||
class ID3_StringCharset(ID3_String):
|
||||
STRIP = " \0"
|
||||
charset_desc = {
|
||||
0: "ISO-8859-1",
|
||||
1: "UTF-16 with BOM",
|
||||
2: "UTF-16 (big endian)",
|
||||
3: "UTF-8"
|
||||
}
|
||||
charset_name = {
|
||||
0: "ISO-8859-1",
|
||||
1: "UTF-16",
|
||||
2: "UTF-16-BE",
|
||||
3: "UTF-8"
|
||||
}
|
||||
def createFields(self):
|
||||
yield Enum(UInt8(self, "charset"), self.charset_desc)
|
||||
size = (self.size - self.current_size)/8
|
||||
if not size:
|
||||
return
|
||||
charset = getCharset(self["charset"])
|
||||
yield String(self, "text", size, "Text", charset=charset, strip=self.STRIP)
|
||||
|
||||
class ID3_GEOB(ID3_StringCharset):
|
||||
def createFields(self):
|
||||
yield Enum(UInt8(self, "charset"), self.charset_desc)
|
||||
charset = getCharset(self["charset"])
|
||||
yield CString(self, "mime", "MIME type", charset=charset)
|
||||
yield CString(self, "filename", "File name", charset=charset)
|
||||
yield CString(self, "description", "Content description", charset=charset)
|
||||
size = (self.size - self.current_size) // 8
|
||||
if not size:
|
||||
return
|
||||
yield String(self, "text", size, "Text", charset=charset)
|
||||
|
||||
class ID3_Comment(ID3_StringCharset):
|
||||
def createFields(self):
|
||||
yield Enum(UInt8(self, "charset"), self.charset_desc)
|
||||
yield String(self, "lang", 3, "Language", charset="ASCII")
|
||||
charset = getCharset(self["charset"])
|
||||
yield CString(self, "title", "Title", charset=charset, strip=self.STRIP)
|
||||
size = (self.size - self.current_size) // 8
|
||||
if not size:
|
||||
return
|
||||
yield String(self, "text", size, "Text", charset=charset, strip=self.STRIP)
|
||||
|
||||
class ID3_StringTitle(ID3_StringCharset):
|
||||
def createFields(self):
|
||||
yield Enum(UInt8(self, "charset"), self.charset_desc)
|
||||
if self.current_size == self.size:
|
||||
return
|
||||
charset = getCharset(self["charset"])
|
||||
yield CString(self, "title", "Title", charset=charset, strip=self.STRIP)
|
||||
size = (self.size - self.current_size)/8
|
||||
if not size:
|
||||
return
|
||||
yield String(self, "text", size, "Text", charset=charset, strip=self.STRIP)
|
||||
|
||||
class ID3_Private(FieldSet):
|
||||
def createFields(self):
|
||||
size = self._size/8
|
||||
# TODO: Strings charset?
|
||||
if self.stream.readBytes(self.absolute_address, 9) == "PeakValue":
|
||||
yield String(self, "text", 9, "Text")
|
||||
size -= 9
|
||||
yield String(self, "content", size, "Content")
|
||||
|
||||
class ID3_TrackLength(FieldSet):
|
||||
def createFields(self):
|
||||
yield NullBytes(self, "zero", 1)
|
||||
yield textHandler(String(self, "length", self._size/8 - 1,
|
||||
"Length in ms", charset="ASCII"), self.computeLength)
|
||||
|
||||
def computeLength(self, field):
|
||||
try:
|
||||
ms = int(field.value)
|
||||
return humanDuration(ms)
|
||||
except:
|
||||
return field.value
|
||||
|
||||
class ID3_Picture23(FieldSet):
|
||||
pict_type_name = {
|
||||
0x00: "Other",
|
||||
0x01: "32x32 pixels 'file icon' (PNG only)",
|
||||
0x02: "Other file icon",
|
||||
0x03: "Cover (front)",
|
||||
0x04: "Cover (back)",
|
||||
0x05: "Leaflet page",
|
||||
0x06: "Media (e.g. lable side of CD)",
|
||||
0x07: "Lead artist/lead performer/soloist",
|
||||
0x08: "Artist/performer",
|
||||
0x09: "Conductor",
|
||||
0x0A: "Band/Orchestra",
|
||||
0x0B: "Composer",
|
||||
0x0C: "Lyricist/text writer",
|
||||
0x0D: "Recording Location",
|
||||
0x0E: "During recording",
|
||||
0x0F: "During performance",
|
||||
0x10: "Movie/video screen capture",
|
||||
0x11: "A bright coloured fish",
|
||||
0x12: "Illustration",
|
||||
0x13: "Band/artist logotype",
|
||||
0x14: "Publisher/Studio logotype"
|
||||
}
|
||||
def createFields(self):
|
||||
yield Enum(UInt8(self, "charset"), ID3_StringCharset.charset_desc)
|
||||
charset = getCharset(self["charset"])
|
||||
yield String(self, "img_fmt", 3, charset="ASCII")
|
||||
yield Enum(UInt8(self, "pict_type"), self.pict_type_name)
|
||||
yield CString(self, "text", "Text", charset=charset, strip=" \0")
|
||||
size = (self._size - self._current_size) / 8
|
||||
if size:
|
||||
yield RawBytes(self, "img_data", size)
|
||||
|
||||
class ID3_Picture24(FieldSet):
|
||||
def createFields(self):
|
||||
yield Enum(UInt8(self, "charset"), ID3_StringCharset.charset_desc)
|
||||
charset = getCharset(self["charset"])
|
||||
yield CString(self, "mime", "MIME type", charset=charset)
|
||||
yield Enum(UInt8(self, "pict_type"), ID3_Picture23.pict_type_name)
|
||||
yield CString(self, "description", charset=charset)
|
||||
size = (self._size - self._current_size) / 8
|
||||
if size:
|
||||
yield RawBytes(self, "img_data", size)
|
||||
|
||||
class ID3_Chunk(FieldSet):
|
||||
endian = NETWORK_ENDIAN
|
||||
tag22_name = {
|
||||
"TT2": "Track title",
|
||||
"TP1": "Artist",
|
||||
"TRK": "Track number",
|
||||
"COM": "Comment",
|
||||
"TCM": "Composer",
|
||||
"TAL": "Album",
|
||||
"TYE": "Year",
|
||||
"TEN": "Encoder",
|
||||
"TCO": "Content type",
|
||||
"PIC": "Picture"
|
||||
}
|
||||
tag23_name = {
|
||||
"COMM": "Comment",
|
||||
"GEOB": "Encapsulated object",
|
||||
"PRIV": "Private",
|
||||
"TPE1": "Artist",
|
||||
"TCOP": "Copyright",
|
||||
"TALB": "Album",
|
||||
"TENC": "Encoder",
|
||||
"TYER": "Year",
|
||||
"TSSE": "Encoder settings",
|
||||
"TCOM": "Composer",
|
||||
"TRCK": "Track number",
|
||||
"PCNT": "Play counter",
|
||||
"TCON": "Content type",
|
||||
"TLEN": "Track length",
|
||||
"TIT2": "Track title",
|
||||
"WXXX": "User defined URL"
|
||||
}
|
||||
handler = {
|
||||
"COMM": ID3_Comment,
|
||||
"COM": ID3_Comment,
|
||||
"GEOB": ID3_GEOB,
|
||||
"PIC": ID3_Picture23,
|
||||
"APIC": ID3_Picture24,
|
||||
"PRIV": ID3_Private,
|
||||
"TXXX": ID3_StringTitle,
|
||||
"WOAR": ID3_String,
|
||||
"WXXX": ID3_StringTitle,
|
||||
}
|
||||
|
||||
def __init__(self, *args):
|
||||
FieldSet.__init__(self, *args)
|
||||
if 3 <= self["../ver_major"].value:
|
||||
self._size = (10 + self["size"].value) * 8
|
||||
else:
|
||||
self._size = (self["size"].value + 6) * 8
|
||||
|
||||
def createFields(self):
|
||||
if 3 <= self["../ver_major"].value:
|
||||
# ID3 v2.3 and 2.4
|
||||
yield Enum(String(self, "tag", 4, "Tag", charset="ASCII", strip="\0"), ID3_Chunk.tag23_name)
|
||||
if 4 <= self["../ver_major"].value:
|
||||
yield ID3_Size(self, "size") # ID3 v2.4
|
||||
else:
|
||||
yield UInt32(self, "size") # ID3 v2.3
|
||||
|
||||
yield Bit(self, "tag_alter", "Tag alter preservation")
|
||||
yield Bit(self, "file_alter", "Tag alter preservation")
|
||||
yield Bit(self, "rd_only", "Read only?")
|
||||
yield NullBits(self, "padding[]", 5)
|
||||
|
||||
yield Bit(self, "compressed", "Frame is compressed?")
|
||||
yield Bit(self, "encrypted", "Frame is encrypted?")
|
||||
yield Bit(self, "group", "Grouping identity")
|
||||
yield NullBits(self, "padding[]", 5)
|
||||
size = self["size"].value
|
||||
is_compressed = self["compressed"].value
|
||||
else:
|
||||
# ID3 v2.2
|
||||
yield Enum(String(self, "tag", 3, "Tag", charset="ASCII", strip="\0"), ID3_Chunk.tag22_name)
|
||||
yield UInt24(self, "size")
|
||||
size = self["size"].value - self.current_size/8 + 6
|
||||
is_compressed = False
|
||||
|
||||
if size:
|
||||
cls = None
|
||||
if not(is_compressed):
|
||||
tag = self["tag"].value
|
||||
if tag in ID3_Chunk.handler:
|
||||
cls = ID3_Chunk.handler[tag]
|
||||
elif tag[0] == "T":
|
||||
cls = ID3_StringCharset
|
||||
if cls:
|
||||
yield cls(self, "content", "Content", size=size*8)
|
||||
else:
|
||||
yield RawBytes(self, "content", size, "Raw data content")
|
||||
|
||||
def createDescription(self):
|
||||
if self["size"].value != 0:
|
||||
return "ID3 Chunk: %s" % self["tag"].display
|
||||
else:
|
||||
return "ID3 Chunk: (terminator)"
|
||||
|
||||
class ID3_Size(Bits):
|
||||
static_size = 32
|
||||
|
||||
def __init__(self, parent, name, description=None):
|
||||
Bits.__init__(self, parent, name, 32, description)
|
||||
|
||||
def createValue(self):
|
||||
data = self.parent.stream.readBytes(self.absolute_address, 4)
|
||||
# TODO: Check that bit #7 of each byte is nul: not(ord(data[i]) & 127)
|
||||
return reduce(lambda x, y: x*128 + y, (ord(item) for item in data ))
|
||||
|
||||
class ID3v2(FieldSet):
|
||||
endian = NETWORK_ENDIAN
|
||||
VALID_MAJOR_VERSIONS = (2, 3, 4)
|
||||
|
||||
def __init__(self, parent, name, size=None):
|
||||
FieldSet.__init__(self, parent, name, size=size)
|
||||
if not self._size:
|
||||
self._size = (self["size"].value + 10) * 8
|
||||
|
||||
def createDescription(self):
|
||||
return "ID3 v2.%s.%s" % \
|
||||
(self["ver_major"].value, self["ver_minor"].value)
|
||||
|
||||
def createFields(self):
|
||||
# Signature + version
|
||||
yield String(self, "header", 3, "Header (ID3)", charset="ASCII")
|
||||
yield UInt8(self, "ver_major", "Version (major)")
|
||||
yield UInt8(self, "ver_minor", "Version (minor)")
|
||||
|
||||
# Check format
|
||||
if self["header"].value != "ID3":
|
||||
raise MatchError("Signature error, should be \"ID3\".")
|
||||
if self["ver_major"].value not in self.VALID_MAJOR_VERSIONS \
|
||||
or self["ver_minor"].value != 0:
|
||||
raise MatchError(
|
||||
"Unknown ID3 metadata version (2.%u.%u)"
|
||||
% (self["ver_major"].value, self["ver_minor"].value))
|
||||
|
||||
# Flags
|
||||
yield Bit(self, "unsync", "Unsynchronisation is used?")
|
||||
yield Bit(self, "ext", "Extended header is used?")
|
||||
yield Bit(self, "exp", "Experimental indicator")
|
||||
yield NullBits(self, "padding[]", 5)
|
||||
|
||||
# Size
|
||||
yield ID3_Size(self, "size")
|
||||
|
||||
# All tags
|
||||
while self.current_size < self._size:
|
||||
field = ID3_Chunk(self, "field[]")
|
||||
yield field
|
||||
if field["size"].value == 0:
|
||||
break
|
||||
|
||||
# Search first byte of the MPEG file
|
||||
padding = self.seekBit(self._size)
|
||||
if padding:
|
||||
yield padding
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
"""
|
||||
iPod iTunesDB parser.
|
||||
|
||||
Documentation:
|
||||
- http://ipodlinux.org/ITunesDB
|
||||
|
||||
Author: Romain HERAULT
|
||||
Creation date: 19 august 2006
|
||||
"""
|
||||
|
||||
from hachoir_parser import Parser
|
||||
from hachoir_core.field import (FieldSet,
|
||||
UInt8, UInt16, UInt32, UInt64, TimestampMac32,
|
||||
String, Float32, NullBytes, Enum)
|
||||
from hachoir_core.endian import LITTLE_ENDIAN
|
||||
from hachoir_core.tools import humanDuration
|
||||
from hachoir_core.text_handler import displayHandler, filesizeHandler
|
||||
|
||||
list_order={
|
||||
1 : "playlist order (manual sort order)",
|
||||
2 : "???",
|
||||
3 : "songtitle",
|
||||
4 : "album",
|
||||
5 : "artist",
|
||||
6 : "bitrate",
|
||||
7 : "genre",
|
||||
8 : "kind",
|
||||
9 : "date modified",
|
||||
10 : "track number",
|
||||
11 : "size",
|
||||
12 : "time",
|
||||
13 : "year",
|
||||
14 : "sample rate",
|
||||
15 : "comment",
|
||||
16 : "date added",
|
||||
17 : "equalizer",
|
||||
18 : "composer",
|
||||
19 : "???",
|
||||
20 : "play count",
|
||||
21 : "last played",
|
||||
22 : "disc number",
|
||||
23 : "my rating",
|
||||
24 : "release date",
|
||||
25 : "BPM",
|
||||
26 : "grouping",
|
||||
27 : "category",
|
||||
28 : "description",
|
||||
29 : "show",
|
||||
30 : "season",
|
||||
31 : "episode number"
|
||||
}
|
||||
|
||||
class DataObject(FieldSet):
|
||||
type_name={
|
||||
1:"Title",
|
||||
2:"Location",
|
||||
3:"Album",
|
||||
4:"Artist",
|
||||
5:"Genre",
|
||||
6:"Filetype",
|
||||
7:"EQ Setting",
|
||||
8:"Comment",
|
||||
9:"Category",
|
||||
12:"Composer",
|
||||
13:"Grouping",
|
||||
14:"Description text",
|
||||
15:"Podcast Enclosure URL",
|
||||
16:"Podcast RSS URL",
|
||||
17:"Chapter data",
|
||||
18:"Subtitle",
|
||||
19:"Show (for TV Shows only)",
|
||||
20:"Episode",
|
||||
21:"TV Network",
|
||||
50:"Smart Playlist Data",
|
||||
51:"Smart Playlist Rules",
|
||||
52:"Library Playlist Index",
|
||||
100:"Column info",
|
||||
}
|
||||
|
||||
mhod52_sort_index_type_name={
|
||||
3:"Title",
|
||||
4:"Album, then Disk/Tracknumber, then Title",
|
||||
5:"Artist, then Album, then Disc/Tracknumber, then Title",
|
||||
7:"Genre, then Artist, then Album, then Disc/Tracknumber, then Title",
|
||||
8:"Composer, then Title"
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
FieldSet.__init__(self, *args, **kw)
|
||||
self._size = self["entry_length"].value *8
|
||||
|
||||
def createFields(self):
|
||||
yield String(self, "header_id", 4, "Data Object Header Markup (\"mhod\")", charset="ISO-8859-1")
|
||||
yield UInt32(self, "header_length", "Header Length")
|
||||
yield UInt32(self, "entry_length", "Entry Length")
|
||||
yield Enum(UInt32(self, "type", "type"),self.type_name)
|
||||
if(self["type"].value<15):
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield UInt32(self, "position", "Position")
|
||||
yield UInt32(self, "length", "String Length in bytes")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield String(self, "string", self["length"].value, "String Data", charset="UTF-16-LE")
|
||||
elif (self["type"].value<17):
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield String(self, "string", self._size/8-self["header_length"].value, "String Data", charset="UTF-8")
|
||||
elif (self["type"].value == 52):
|
||||
yield UInt32(self, "unknown[]", "unk1")
|
||||
yield UInt32(self, "unknown[]", "unk2")
|
||||
yield Enum(UInt32(self, "sort_index_type", "Sort Index Type"),self.mhod52_sort_index_type_name)
|
||||
yield UInt32(self, "entry_count", "Entry Count")
|
||||
indexes_size = self["entry_count"].value*4
|
||||
padding_offset = self["entry_length"].value - indexes_size
|
||||
padding = self.seekByte(padding_offset, "header padding")
|
||||
if padding:
|
||||
yield padding
|
||||
for i in xrange(self["entry_count"].value):
|
||||
yield UInt32(self, "index["+str(i)+"]", "Index of the "+str(i)+"nth mhit")
|
||||
else:
|
||||
padding = self.seekByte(self["header_length"].value, "header padding")
|
||||
if padding:
|
||||
yield padding
|
||||
padding = self.seekBit(self._size, "entry padding")
|
||||
if padding:
|
||||
yield padding
|
||||
|
||||
class TrackItem(FieldSet):
|
||||
x1_type_name={
|
||||
0:"AAC or CBR MP3",
|
||||
1:"VBR MP3"
|
||||
}
|
||||
x2_type_name={
|
||||
0:"AAC",
|
||||
1:"MP3"
|
||||
}
|
||||
media_type_name={
|
||||
0x00:"Audio/Video",
|
||||
0x01:"Audio",
|
||||
0x02:"Video",
|
||||
0x04:"Podcast",
|
||||
0x06:"Video Podcast",
|
||||
0x08:"Audiobook",
|
||||
0x20:"Music Video",
|
||||
0x40:"TV Show",
|
||||
0X60:"TV Show (Music lists)",
|
||||
}
|
||||
def __init__(self, *args, **kw):
|
||||
FieldSet.__init__(self, *args, **kw)
|
||||
self._size = self["entry_length"].value *8
|
||||
|
||||
def createFields(self):
|
||||
yield String(self, "header_id", 4, "Track Item Header Markup (\"mhit\")", charset="ISO-8859-1")
|
||||
yield UInt32(self, "header_length", "Header Length")
|
||||
yield UInt32(self, "entry_length", "Entry Length")
|
||||
yield UInt32(self, "string_number", "Number of Strings")
|
||||
yield UInt32(self, "unique_id", "Unique ID")
|
||||
yield UInt32(self, "visible_tag", "Visible Tag")
|
||||
yield String(self, "file_type", 4, "File Type")
|
||||
yield Enum(UInt8(self, "x1_type", "Extended Type 1"),self.x1_type_name)
|
||||
yield Enum(UInt8(self, "x2_type", "Extended type 2"),self.x2_type_name)
|
||||
yield UInt8(self, "compilation_flag", "Compilation Flag")
|
||||
yield UInt8(self, "rating", "Rating")
|
||||
yield TimestampMac32(self, "added_date", "Date when the item was added")
|
||||
yield filesizeHandler(UInt32(self, "size", "Track size in bytes"))
|
||||
yield displayHandler(UInt32(self, "length", "Track length in milliseconds"), humanDuration)
|
||||
yield UInt32(self, "track_number", "Number of this track")
|
||||
yield UInt32(self, "total_track", "Total number of tracks")
|
||||
yield UInt32(self, "year", "Year of the track")
|
||||
yield UInt32(self, "bitrate", "Bitrate")
|
||||
yield UInt32(self, "samplerate", "Sample Rate")
|
||||
yield UInt32(self, "volume", "volume")
|
||||
yield UInt32(self, "start_time", "Start playing at, in milliseconds")
|
||||
yield UInt32(self, "stop_time", "Stop playing at, in milliseconds")
|
||||
yield UInt32(self, "soundcheck", "SoundCheck preamp")
|
||||
yield UInt32(self, "playcount_1", "Play count of the track")
|
||||
yield UInt32(self, "playcount_2", "Play count of the track (identical to playcount_1)")
|
||||
yield UInt32(self, "last_played_time", "Time the song was last played")
|
||||
yield UInt32(self, "disc_number", "disc number in multi disc sets")
|
||||
yield UInt32(self, "total_discs", "Total number of discs in the disc set")
|
||||
yield UInt32(self, "userid", "User ID in the DRM scheme")
|
||||
yield TimestampMac32(self, "last_modified", "Time of the last modification of the track")
|
||||
yield UInt32(self, "bookmark_time", "Bookmark time for AudioBook")
|
||||
yield UInt64(self, "dbid", "Unique DataBase ID for the song (identical in mhit and in mhii)")
|
||||
yield UInt8(self, "checked", "song is checked")
|
||||
yield UInt8(self, "application_rating", "Last Rating before change")
|
||||
yield UInt16(self, "BPM", "BPM of the track")
|
||||
yield UInt16(self, "artwork_count", "number of artworks fo this item")
|
||||
yield UInt16(self, "unknown[]")
|
||||
yield UInt32(self, "artwork_size", "Total size of artworks in bytes")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield Float32(self, "sample_rate_2", "Sample Rate express in float")
|
||||
yield UInt32(self, "released_date", "Date of release in Music Store or in Podcast")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield UInt8(self, "has_artwork", "0x01 for track with artwork, 0x02 otherwise")
|
||||
yield UInt8(self, "skip_wen_shuffling", "Skip that track when shuffling")
|
||||
yield UInt8(self, "remember_playback_position", "Remember playback position")
|
||||
yield UInt8(self, "flag4", "Flag 4")
|
||||
yield UInt64(self, "dbid2", "Unique DataBase ID for the song (identical as above)")
|
||||
yield UInt8(self, "lyrics_flag", "Lyrics Flag")
|
||||
yield UInt8(self, "movie_file_flag", "Movie File Flag")
|
||||
yield UInt8(self, "played_mark", "Track has been played")
|
||||
yield UInt8(self, "unknown[]")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield UInt32(self, "sample_count", "Number of samples in the song (only for WAV and AAC files)")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield Enum(UInt32(self, "media_type", "Media Type for video iPod"),self.media_type_name)
|
||||
yield UInt32(self, "season_number", "Season Number")
|
||||
yield UInt32(self, "episode_number", "Episode Number")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield UInt32(self, "unknown[]")
|
||||
padding = self.seekByte(self["header_length"].value, "header padding")
|
||||
if padding:
|
||||
yield padding
|
||||
|
||||
#while ((self.stream.readBytes(0, 4) == 'mhod') and ((self.current_size/8) < self["entry_length"].value)):
|
||||
for i in xrange(self["string_number"].value):
|
||||
yield DataObject(self, "data[]")
|
||||
padding = self.seekBit(self._size, "entry padding")
|
||||
if padding:
|
||||
yield padding
|
||||
|
||||
class TrackList(FieldSet):
|
||||
def createFields(self):
|
||||
yield String(self, "header_id", 4, "Track List Header Markup (\"mhlt\")", charset="ISO-8859-1")
|
||||
yield UInt32(self, "header_length", "Header Length")
|
||||
yield UInt32(self, "track_number", "Number of Tracks")
|
||||
|
||||
padding = self.seekByte(self["header_length"].value, "header padding")
|
||||
if padding:
|
||||
yield padding
|
||||
|
||||
for i in xrange(self["track_number"].value):
|
||||
yield TrackItem(self, "track[]")
|
||||
|
||||
class PlaylistItem(FieldSet):
|
||||
def __init__(self, *args, **kw):
|
||||
FieldSet.__init__(self, *args, **kw)
|
||||
self._size = self["entry_length"].value *8
|
||||
|
||||
def createFields(self):
|
||||
yield String(self, "header_id", 4, "Playlist Item Header Markup (\"mhip\")", charset="ISO-8859-1")
|
||||
yield UInt32(self, "header_length", "Header Length")
|
||||
yield UInt32(self, "entry_length", "Entry Length")
|
||||
yield UInt32(self, "data_object_child_count", "Number of Child Data Objects")
|
||||
yield UInt32(self, "podcast_grouping_flag", "Podcast Grouping Flag")
|
||||
yield UInt32(self, "group_id", "Group ID")
|
||||
yield UInt32(self, "track_id", "Track ID")
|
||||
yield TimestampMac32(self, "timestamp", "Song Timestamp")
|
||||
yield UInt32(self, "podcast_grouping_ref", "Podcast Grouping Reference")
|
||||
padding = self.seekByte(self["header_length"].value, "header padding")
|
||||
if padding:
|
||||
yield padding
|
||||
|
||||
for i in xrange(self["data_object_child_count"].value):
|
||||
yield DataObject(self, "mhod[]")
|
||||
|
||||
|
||||
class Playlist(FieldSet):
|
||||
is_master_pl_name={
|
||||
0:"Regular playlist",
|
||||
1:"Master playlist"
|
||||
}
|
||||
|
||||
is_podcast_name={
|
||||
0:"Normal Playlist List",
|
||||
1:"Podcast Playlist List"
|
||||
}
|
||||
|
||||
list_sort_order_name={
|
||||
1:"Manual Sort Order",
|
||||
2:"???",
|
||||
3:"Song Title",
|
||||
4:"Album",
|
||||
5:"Artist",
|
||||
6:"Bitrate",
|
||||
7:"Genre",
|
||||
8:"Kind",
|
||||
9:"Date Modified",
|
||||
10:"Track Number",
|
||||
11:"Size",
|
||||
12:"Time",
|
||||
13:"Year",
|
||||
14:"Sample Rate",
|
||||
15:"Comment",
|
||||
16:"Date Added",
|
||||
17:"Equalizer",
|
||||
18:"Composer",
|
||||
19:"???",
|
||||
20:"Play Count",
|
||||
21:"Last Played",
|
||||
22:"Disc Number",
|
||||
23:"My Rating",
|
||||
24:"Release Date",
|
||||
25:"BPM",
|
||||
26:"Grouping",
|
||||
27:"Category",
|
||||
28:"Description",
|
||||
29:"Show",
|
||||
30:"Season",
|
||||
31:"Episode Number"
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
FieldSet.__init__(self, *args, **kw)
|
||||
self._size = self["entry_length"].value *8
|
||||
|
||||
def createFields(self):
|
||||
yield String(self, "header_id", 4, "Playlist List Header Markup (\"mhyp\")", charset="ISO-8859-1")
|
||||
yield UInt32(self, "header_length", "Header Length")
|
||||
yield UInt32(self, "entry_length", "Entry Length")
|
||||
yield UInt32(self, "data_object_child_count", "Number of Child Data Objects")
|
||||
yield UInt32(self, "playlist_count", "Number of Playlist Items")
|
||||
yield Enum(UInt8(self, "type", "Normal or master playlist?"), self.is_master_pl_name)
|
||||
yield UInt8(self, "XXX1", "XXX1")
|
||||
yield UInt8(self, "XXX2", "XXX2")
|
||||
yield UInt8(self, "XXX3", "XXX3")
|
||||
yield TimestampMac32(self, "creation_date", "Date when the playlist was created")
|
||||
yield UInt64(self, "playlistid", "Persistent Playlist ID")
|
||||
yield UInt32(self, "unk3", "unk3")
|
||||
yield UInt16(self, "string_mhod_count", "Number of string MHODs for this playlist")
|
||||
yield Enum(UInt16(self, "is_podcast", "Playlist or Podcast List?"), self.is_podcast_name)
|
||||
yield Enum(UInt32(self, "sort_order", "Playlist Sort Order"), self.list_sort_order_name)
|
||||
|
||||
padding = self.seekByte(self["header_length"].value, "entry padding")
|
||||
if padding:
|
||||
yield padding
|
||||
|
||||
for i in xrange(self["data_object_child_count"].value):
|
||||
yield DataObject(self, "mhod[]")
|
||||
|
||||
for i in xrange(self["playlist_count"].value):
|
||||
yield PlaylistItem(self, "playlist_item[]")
|
||||
|
||||
|
||||
|
||||
class PlaylistList(FieldSet):
|
||||
def createFields(self):
|
||||
yield String(self, "header_id", 4, "Playlist List Header Markup (\"mhlp\")", charset="ISO-8859-1")
|
||||
yield UInt32(self, "header_length", "Header Length")
|
||||
yield UInt32(self, "playlist_number", "Number of Playlists")
|
||||
|
||||
padding = self.seekByte(self["header_length"].value, "header padding")
|
||||
if padding:
|
||||
yield padding
|
||||
|
||||
for i in xrange(self["playlist_number"].value):
|
||||
yield Playlist(self, "playlist[]")
|
||||
|
||||
class DataSet(FieldSet):
|
||||
type_name={
|
||||
1:"Track List",
|
||||
2:"Play List",
|
||||
3:"Podcast List"
|
||||
}
|
||||
def __init__(self, *args, **kw):
|
||||
FieldSet.__init__(self, *args, **kw)
|
||||
self._size = self["entry_length"].value *8
|
||||
|
||||
def createFields(self):
|
||||
yield String(self, "header_id", 4, "DataSet Header Markup (\"mhsd\")", charset="ISO-8859-1")
|
||||
yield UInt32(self, "header_length", "Header Length")
|
||||
yield UInt32(self, "entry_length", "Entry Length")
|
||||
yield Enum(UInt32(self, "type", "type"),self.type_name)
|
||||
padding = self.seekByte(self["header_length"].value, "header_raw")
|
||||
if padding:
|
||||
yield padding
|
||||
if self["type"].value == 1:
|
||||
yield TrackList(self, "tracklist[]")
|
||||
if self["type"].value == 2:
|
||||
yield PlaylistList(self, "playlist_list[]");
|
||||
if self["type"].value == 3:
|
||||
yield PlaylistList(self, "podcast_list[]");
|
||||
padding = self.seekBit(self._size, "entry padding")
|
||||
if padding:
|
||||
yield padding
|
||||
|
||||
class DataBase(FieldSet):
|
||||
def __init__(self, *args, **kw):
|
||||
FieldSet.__init__(self, *args, **kw)
|
||||
self._size = self["entry_length"].value *8
|
||||
|
||||
# def createFields(self):
|
||||
|
||||
class ITunesDBFile(Parser):
|
||||
PARSER_TAGS = {
|
||||
"id": "itunesdb",
|
||||
"category": "audio",
|
||||
"min_size": 44*8,
|
||||
"magic": (('mhbd',0),),
|
||||
"description": "iPod iTunesDB file"
|
||||
}
|
||||
|
||||
endian = LITTLE_ENDIAN
|
||||
|
||||
def validate(self):
|
||||
return self.stream.readBytes(0, 4) == 'mhbd'
|
||||
|
||||
def createFields(self):
|
||||
yield String(self, "header_id", 4, "DataBase Header Markup (\"mhbd\")", charset="ISO-8859-1")
|
||||
yield UInt32(self, "header_length", "Header Length")
|
||||
yield UInt32(self, "entry_length", "Entry Length")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield UInt32(self, "version_number", "Version Number")
|
||||
yield UInt32(self, "child_number", "Number of Children")
|
||||
yield UInt64(self, "id", "ID for this database")
|
||||
yield UInt32(self, "unknown[]")
|
||||
yield UInt64(self, "initial_dbid", "Initial DBID")
|
||||
size = self["header_length"].value-self.current_size/ 8
|
||||
if size>0:
|
||||
yield NullBytes(self, "padding", size)
|
||||
for i in xrange(self["child_number"].value):
|
||||
yield DataSet(self, "dataset[]")
|
||||
padding = self.seekByte(self["entry_length"].value, "entry padding")
|
||||
if padding:
|
||||
yield padding
|
||||
|
||||
def createContentSize(self):
|
||||
return self["entry_length"].value * 8
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
"""
|
||||
Musical Instrument Digital Interface (MIDI) audio file parser.
|
||||
|
||||
Documentation:
|
||||
- Standard MIDI File Format, Dustin Caldwell (downloaded on wotsit.org)
|
||||
|
||||
Author: Victor Stinner
|
||||
Creation: 27 december 2006
|
||||
"""
|
||||
|
||||
from hachoir_parser import Parser
|
||||
from hachoir_core.field import (FieldSet, Bits, ParserError,
|
||||
String, UInt32, UInt24, UInt16, UInt8, Enum, RawBits, RawBytes)
|
||||
from hachoir_core.endian import BIG_ENDIAN
|
||||
from hachoir_core.text_handler import textHandler, hexadecimal
|
||||
from hachoir_core.tools import createDict, humanDurationNanosec
|
||||
from hachoir_parser.common.tracker import NOTE_NAME
|
||||
|
||||
MAX_FILESIZE = 10 * 1024 * 1024
|
||||
|
||||
class Integer(Bits):
|
||||
def __init__(self, parent, name, description=None):
|
||||
Bits.__init__(self, parent, name, 8, description)
|
||||
stream = parent.stream
|
||||
addr = self.absolute_address
|
||||
value = 0
|
||||
while True:
|
||||
bits = stream.readBits(addr, 8, parent.endian)
|
||||
value = (value << 7) + (bits & 127)
|
||||
if not(bits & 128):
|
||||
break
|
||||
addr += 8
|
||||
self._size += 8
|
||||
if 32 < self._size:
|
||||
raise ParserError("Integer size is bigger than 32-bit")
|
||||
self.createValue = lambda: value
|
||||
|
||||
def parseNote(parser):
|
||||
yield Enum(UInt8(parser, "note", "Note number"), NOTE_NAME)
|
||||
yield UInt8(parser, "velocity")
|
||||
|
||||
def parseControl(parser):
|
||||
yield UInt8(parser, "control", "Controller number")
|
||||
yield UInt8(parser, "value", "New value")
|
||||
|
||||
def parsePatch(parser):
|
||||
yield UInt8(parser, "program", "New program number")
|
||||
|
||||
def parseChannel(parser, size=1):
|
||||
yield UInt8(parser, "channel", "Channel number")
|
||||
|
||||
def parsePitch(parser):
|
||||
yield UInt8(parser, "bottom", "(least sig) 7 bits of value")
|
||||
yield UInt8(parser, "top", "(most sig) 7 bits of value")
|
||||
|
||||
def parseText(parser, size):
|
||||
yield String(parser, "text", size)
|
||||
|
||||
def parseSMPTEOffset(parser, size):
|
||||
yield RawBits(parser, "padding", 1)
|
||||
yield Enum(Bits(parser, "frame_rate", 2),
|
||||
{0:"24 fps", 1:"25 fps", 2:"30 fps (drop frame)", 3:"30 fps"})
|
||||
yield Bits(parser, "hour", 5)
|
||||
yield UInt8(parser, "minute")
|
||||
yield UInt8(parser, "second")
|
||||
yield UInt8(parser, "frame")
|
||||
yield UInt8(parser, "subframe", "100 subframes per frame")
|
||||
|
||||
def formatTempo(field):
|
||||
return humanDurationNanosec(field.value*1000)
|
||||
|
||||
def parseTempo(parser, size):
|
||||
yield textHandler(UInt24(parser, "microsec_quarter", "Microseconds per quarter note"), formatTempo)
|
||||
|
||||
def parseTimeSignature(parser, size):
|
||||
yield UInt8(parser, "numerator", "Numerator of time signature")
|
||||
yield UInt8(parser, "denominator", "denominator of time signature 2=quarter 3=eighth, etc.")
|
||||
yield UInt8(parser, "nb_tick", "Number of ticks in metronome click")
|
||||
yield UInt8(parser, "nb_32nd_note", "Number of 32nd notes to the quarter note")
|
||||
|
||||
class Command(FieldSet):
|
||||
COMMAND = {}
|
||||
for channel in xrange(16):
|
||||
COMMAND[0x80+channel] = ("Note off (channel %u)" % channel, parseNote)
|
||||
COMMAND[0x90+channel] = ("Note on (channel %u)" % channel, parseNote)
|
||||
COMMAND[0xA0+channel] = ("Key after-touch (channel %u)" % channel, parseNote)
|
||||
COMMAND[0xB0+channel] = ("Control change (channel %u)" % channel, parseControl)
|
||||
COMMAND[0xC0+channel] = ("Program (patch) change (channel %u)" % channel, parsePatch)
|
||||
COMMAND[0xD0+channel] = ("Channel after-touch (channel %u)" % channel, parseChannel)
|
||||
COMMAND[0xE0+channel] = ("Pitch wheel change (channel %u)" % channel, parsePitch)
|
||||
COMMAND_DESC = createDict(COMMAND, 0)
|
||||
COMMAND_PARSER = createDict(COMMAND, 1)
|
||||
|
||||
META_COMMAND_TEXT = 1
|
||||
META_COMMAND_NAME = 3
|
||||
META_COMMAND = {
|
||||
0x00: ("Sets the track's sequence number", None),
|
||||
0x01: ("Text event", parseText),
|
||||
0x02: ("Copyright info", parseText),
|
||||
0x03: ("Sequence or Track name", parseText),
|
||||
0x04: ("Track instrument name", parseText),
|
||||
0x05: ("Lyric", parseText),
|
||||
0x06: ("Marker", parseText),
|
||||
0x07: ("Cue point", parseText),
|
||||
0x20: ("MIDI Channel Prefix", parseChannel),
|
||||
0x2F: ("End of the track", None),
|
||||
0x51: ("Set tempo", parseTempo),
|
||||
0x54: ("SMPTE offset", parseSMPTEOffset),
|
||||
0x58: ("Time Signature", parseTimeSignature),
|
||||
0x59: ("Key signature", None),
|
||||
0x7F: ("Sequencer specific information", None),
|
||||
}
|
||||
META_COMMAND_DESC = createDict(META_COMMAND, 0)
|
||||
META_COMMAND_PARSER = createDict(META_COMMAND, 1)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'prev_command' in kwargs:
|
||||
self.prev_command = kwargs['prev_command']
|
||||
del kwargs['prev_command']
|
||||
else:
|
||||
self.prev_command = None
|
||||
self.command = None
|
||||
FieldSet.__init__(self, *args, **kwargs)
|
||||
|
||||
def createFields(self):
|
||||
yield Integer(self, "time", "Delta time in ticks")
|
||||
next = self.stream.readBits(self.absolute_address+self.current_size, 8, self.root.endian)
|
||||
if next & 0x80 == 0:
|
||||
# "Running Status" command
|
||||
if self.prev_command is None:
|
||||
raise ParserError("Running Status command not preceded by another command.")
|
||||
self.command = self.prev_command.command
|
||||
else:
|
||||
yield Enum(textHandler(UInt8(self, "command"), hexadecimal), self.COMMAND_DESC)
|
||||
self.command = self["command"].value
|
||||
if self.command == 0xFF:
|
||||
yield Enum(textHandler(UInt8(self, "meta_command"), hexadecimal), self.META_COMMAND_DESC)
|
||||
yield UInt8(self, "data_len")
|
||||
size = self["data_len"].value
|
||||
if size:
|
||||
command = self["meta_command"].value
|
||||
if command in self.META_COMMAND_PARSER:
|
||||
parser = self.META_COMMAND_PARSER[command]
|
||||
else:
|
||||
parser = None
|
||||
if parser:
|
||||
for field in parser(self, size):
|
||||
yield field
|
||||
else:
|
||||
yield RawBytes(self, "data", size)
|
||||
else:
|
||||
if self.command not in self.COMMAND_PARSER:
|
||||
raise ParserError("Unknown command: %s" % self["command"].display)
|
||||
parser = self.COMMAND_PARSER[self.command]
|
||||
for field in parser(self):
|
||||
yield field
|
||||
|
||||
def createDescription(self):
|
||||
if "meta_command" in self:
|
||||
return self["meta_command"].display
|
||||
else:
|
||||
return self.COMMAND_DESC[self.command]
|
||||
|
||||
class Track(FieldSet):
|
||||
def __init__(self, *args):
|
||||
FieldSet.__init__(self, *args)
|
||||
self._size = (8 + self["size"].value) * 8
|
||||
|
||||
def createFields(self):
|
||||
yield String(self, "marker", 4, "Track marker (MTrk)", charset="ASCII")
|
||||
yield UInt32(self, "size")
|
||||
cur = None
|
||||
if True:
|
||||
while not self.eof:
|
||||
cur = Command(self, "command[]", prev_command=cur)
|
||||
yield cur
|
||||
else:
|
||||
size = self["size"].value
|
||||
if size:
|
||||
yield RawBytes(self, "raw", size)
|
||||
|
||||
def createDescription(self):
|
||||
command = self["command[0]"]
|
||||
if "meta_command" in command \
|
||||
and command["meta_command"].value in (Command.META_COMMAND_TEXT, Command.META_COMMAND_NAME) \
|
||||
and "text" in command:
|
||||
return command["text"].value.strip("\r\n")
|
||||
else:
|
||||
return ""
|
||||
|
||||
class Header(FieldSet):
|
||||
static_size = 10*8
|
||||
FILE_FORMAT = {
|
||||
0: "Single track",
|
||||
1: "Multiple tracks, synchronous",
|
||||
2: "Multiple tracks, asynchronous",
|
||||
}
|
||||
|
||||
def createFields(self):
|
||||
yield UInt32(self, "size")
|
||||
yield Enum(UInt16(self, "file_format"), self.FILE_FORMAT)
|
||||
yield UInt16(self, "nb_track")
|
||||
yield UInt16(self, "delta_time", "Delta-time ticks per quarter note")
|
||||
|
||||
def createDescription(self):
|
||||
return "%s; %s tracks" % (
|
||||
self["file_format"].display, self["nb_track"].value)
|
||||
|
||||
class MidiFile(Parser):
|
||||
MAGIC = "MThd"
|
||||
PARSER_TAGS = {
|
||||
"id": "midi",
|
||||
"category": "audio",
|
||||
"file_ext": ["mid", "midi"],
|
||||
"mime": (u"audio/mime", ),
|
||||
"magic": ((MAGIC, 0),),
|
||||
"min_size": 64,
|
||||
"description": "MIDI audio"
|
||||
}
|
||||
endian = BIG_ENDIAN
|
||||
|
||||
def validate(self):
|
||||
if self.stream.readBytes(0, 4) != self.MAGIC:
|
||||
return "Invalid signature"
|
||||
if self["header/size"].value != 6:
|
||||
return "Invalid header size"
|
||||
return True
|
||||
|
||||
def createFields(self):
|
||||
yield String(self, "signature", 4, r"MIDI signature (MThd)", charset="ASCII")
|
||||
yield Header(self, "header")
|
||||
while not self.eof:
|
||||
yield Track(self, "track[]")
|
||||
|
||||
def createDescription(self):
|
||||
return "MIDI audio: %s" % self["header"].description
|
||||
|
||||
def createContentSize(self):
|
||||
count = self["/header/nb_track"].value - 1
|
||||
start = self["track[%u]" % count].absolute_address
|
||||
# Search "End of track" of last track
|
||||
end = self.stream.searchBytes("\xff\x2f\x00", start, MAX_FILESIZE*8)
|
||||
if end is not None:
|
||||
return end + 3*8
|
||||
return None
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
Parser of FastTrackerII Extended Module (XM) version 1.4
|
||||
|
||||
Documents:
|
||||
- Modplug source code (file modplug/soundlib/Load_mod.cpp)
|
||||
http://sourceforge.net/projects/modplug
|
||||
- Dumb source code (files include/dumb.h and src/it/readmod.c
|
||||
http://dumb.sf.net/
|
||||
- Documents on "MOD" format on Wotsit
|
||||
http://www.wotsit.org
|
||||
|
||||
Compressed formats (i.e. starting with "PP20" or having "PACK" as type
|
||||
are not handled. Also NoiseTracker's NST modules aren't handled, although
|
||||
it might be possible: no file format and 15 samples
|
||||
|
||||
Author: Christophe GISQUET <christophe.gisquet@free.fr>
|
||||
Creation: 18th February 2007
|
||||
"""
|
||||
|
||||
from math import log10
|
||||
from hachoir_parser import Parser
|
||||
from hachoir_core.field import (FieldSet,
|
||||
Bits, UInt16, UInt8,
|
||||
RawBytes, String, GenericVector)
|
||||
from hachoir_core.endian import BIG_ENDIAN
|
||||
from hachoir_core.text_handler import textHandler
|
||||
|
||||
# Old NoiseTracker 15-samples modules can have anything here.
|
||||
MODULE_TYPE = {
|
||||
"M.K.": ("Noise/Pro-Tracker", 4),
|
||||
"M!K!": ("Noise/Pro-Tracker", 4),
|
||||
"M&K&": ("Noise/Pro-Tracker", 4),
|
||||
"RASP": ("StarTrekker", 4),
|
||||
"FLT4": ("StarTrekker", 4),
|
||||
"FLT8": ("StarTrekker", 8),
|
||||
"6CHN": ("FastTracker", 6),
|
||||
"8CHN": ("FastTracker", 8),
|
||||
"CD81": ("Octalyser", 8),
|
||||
"OCTA": ("Octalyser", 8),
|
||||
"FA04": ("Digital Tracker", 4),
|
||||
"FA06": ("Digital Tracker", 6),
|
||||
"FA08": ("Digital Tracker", 8),
|
||||
}
|
||||
|
||||
def getFineTune(val):
|
||||
return ("0", "1", "2", "3", "4", "5", "6", "7", "8",
|
||||
"-8", "-7", "-6", "-5", "-4", "-3", "-2", "-1")[val.value]
|
||||
|
||||
def getVolume(val):
|
||||
return "%.1f dB" % (20.0*log10(val.value/64.0))
|
||||
|
||||
class SampleInfo(FieldSet):
|
||||
static_size = 30*8
|
||||
def createFields(self):
|
||||
yield String(self, "name", 22, strip='\0')
|
||||
yield UInt16(self, "sample_count")
|
||||
yield textHandler(UInt8(self, "fine_tune"), getFineTune)
|
||||
yield textHandler(UInt8(self, "volume"), getVolume)
|
||||
yield UInt16(self, "loop_start", "Loop start offset in samples")
|
||||
yield UInt16(self, "loop_len", "Loop length in samples")
|
||||
|
||||
def createValue(self):
|
||||
return self["name"].value
|
||||
|
||||
class Header(FieldSet):
|
||||
static_size = 1084*8
|
||||
|
||||
def createFields(self):
|
||||
yield String(self, "name", 20, strip='\0')
|
||||
yield GenericVector(self, "samples", 31, SampleInfo, "info")
|
||||
yield UInt8(self, "length")
|
||||
yield UInt8(self, "played_patterns_count")
|
||||
yield GenericVector(self, "patterns", 128, UInt8, "position")
|
||||
yield String(self, "type", 4)
|
||||
|
||||
def getNumChannels(self):
|
||||
return MODULE_TYPE[self["type"].value][1]
|
||||
|
||||
class Note(FieldSet):
|
||||
static_size = 8*4
|
||||
def createFields(self):
|
||||
yield Bits(self, 4, "note_hi_nibble")
|
||||
yield Bits(self, 12, "period")
|
||||
yield Bits(self, 4, "note_low_nibble")
|
||||
yield Bits(self, 4, "effect")
|
||||
yield UInt8(self, "parameter")
|
||||
|
||||
class Row(FieldSet):
|
||||
def __init__(self, parent, name, channels, desc=None):
|
||||
FieldSet.__init__(self, parent, name, description=desc)
|
||||
self.channels = channels
|
||||
self._size = 8*self.channels*4
|
||||
|
||||
def createFields(self):
|
||||
for index in xrange(self.channels):
|
||||
yield Note(self, "note[]")
|
||||
|
||||
class Pattern(FieldSet):
|
||||
def __init__(self, parent, name, channels, desc=None):
|
||||
FieldSet.__init__(self, parent, name, description=desc)
|
||||
self.channels = channels
|
||||
self._size = 64*8*self.channels*4
|
||||
|
||||
def createFields(self):
|
||||
for index in xrange(64):
|
||||
yield Row(self, "row[]", self.channels)
|
||||
|
||||
class AmigaModule(Parser):
|
||||
PARSER_TAGS = {
|
||||
"id": "mod",
|
||||
"category": "audio",
|
||||
"file_ext": ("mod", "nst", "wow", "oct", "sd0" ),
|
||||
"mime": (u'audio/mod', u'audio/x-mod', u'audio/mod', u'audio/x-mod'),
|
||||
"min_size": 1084*8,
|
||||
"description": "Uncompressed amiga module"
|
||||
}
|
||||
endian = BIG_ENDIAN
|
||||
|
||||
def validate(self):
|
||||
t = self.stream.readBytes(1080*8, 4)
|
||||
if t not in MODULE_TYPE:
|
||||
return "Invalid module type '%s'" % t
|
||||
self.createValue = lambda t: "%s module, %u channels" % MODULE_TYPE[t]
|
||||
return True
|
||||
|
||||
def createFields(self):
|
||||
header = Header(self, "header")
|
||||
yield header
|
||||
channels = header.getNumChannels()
|
||||
|
||||
# Number of patterns
|
||||
patterns = 0
|
||||
for index in xrange(128):
|
||||
patterns = max(patterns,
|
||||
header["patterns/position[%u]" % index].value)
|
||||
patterns += 1
|
||||
|
||||
# Yield patterns
|
||||
for index in xrange(patterns):
|
||||
yield Pattern(self, "pattern[]", channels)
|
||||
|
||||
# Yield samples
|
||||
for index in xrange(31):
|
||||
count = header["samples/info[%u]/sample_count" % index].value
|
||||
if count:
|
||||
self.info("Yielding sample %u: %u samples" % (index, count))
|
||||
yield RawBytes(self, "sample_data[]", 2*count, \
|
||||
"Sample %u" % index)
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
"""
|
||||
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_core.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("\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 xrange(16):
|
||||
yield Command(self, "command[]")
|
||||
|
||||
class MidiZXXExt(FieldSet):
|
||||
static_size = 128*32*8
|
||||
def createFields(self):
|
||||
for index in xrange(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 == 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:
|
||||
for field in self.parseBlock():
|
||||
yield field
|
||||
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
"""
|
||||
MPEG audio file parser.
|
||||
|
||||
Creation: 12 decembre 2005
|
||||
Author: Victor Stinner
|
||||
"""
|
||||
|
||||
from hachoir_parser import Parser
|
||||
from hachoir_core.field import (FieldSet,
|
||||
MissingField, ParserError, createOrphanField,
|
||||
Bit, Bits, Enum,
|
||||
PaddingBits, PaddingBytes,
|
||||
RawBytes)
|
||||
from hachoir_parser.audio.id3 import ID3v1, ID3v2
|
||||
from hachoir_core.endian import BIG_ENDIAN
|
||||
from hachoir_core.tools import humanFrequency, humanBitSize
|
||||
from hachoir_core.bits import long2raw
|
||||
from hachoir_core.error import HACHOIR_ERRORS
|
||||
from hachoir_core.stream import InputStreamError
|
||||
|
||||
# Max MP3 filesize: 200 MB
|
||||
MAX_FILESIZE = 200*1024*1024*8
|
||||
|
||||
class Frame(FieldSet):
|
||||
VERSION_NAME = { 0: "2.5", 2: "2", 3: "1" }
|
||||
MPEG_I = 3
|
||||
MPEG_II = 2
|
||||
MPEG_II_5 = 0
|
||||
|
||||
LAYER_NAME = { 1: "III", 2: "II", 3: "I" }
|
||||
LAYER_I = 3
|
||||
LAYER_II = 2
|
||||
LAYER_III = 1
|
||||
|
||||
# Bit rates (bit_rate * 1000 = bits/sec)
|
||||
# key 15 is always invalid
|
||||
BIT_RATES = {
|
||||
1: ( # MPEG1
|
||||
( 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448 ), # layer I
|
||||
( 0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384 ), # layer II
|
||||
( 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320 ), # layer III
|
||||
# - 1 2 3 4 5 6 7 8 9 10 11 12 13 14 -
|
||||
),
|
||||
2: ( # MPEG2 / MPEG2.5
|
||||
( 0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256 ), # layer I
|
||||
( 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160 ), # layer II
|
||||
( 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160 ), # layer III
|
||||
# - 1 2 3 4 5 6 7 8 9 10 11 12 13 14 -
|
||||
)
|
||||
}
|
||||
SAMPLING_RATES = {
|
||||
3: {0: 44100, 1: 48000, 2: 32000}, # MPEG1
|
||||
2: {0: 22050, 1: 24000, 2: 16000}, # MPEG2
|
||||
0: {0: 11025, 1: 12000, 2: 8000} # MPEG2.5
|
||||
}
|
||||
EMPHASIS_NAME = {0: "none", 1: "50/15 ms", 3: "CCIT J.17"}
|
||||
CHANNEL_MODE_NAME = {
|
||||
0: "Stereo",
|
||||
1: "Joint stereo",
|
||||
2: "Dual channel",
|
||||
3: "Single channel"
|
||||
}
|
||||
# Channel mode => number of channels
|
||||
NB_CHANNEL = {
|
||||
0: 2,
|
||||
1: 2,
|
||||
2: 2,
|
||||
3: 1,
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
FieldSet.__init__(self, *args, **kw)
|
||||
if not self._size:
|
||||
frame_size = self.getFrameSize()
|
||||
if not frame_size:
|
||||
raise ParserError("MPEG audio: Invalid frame %s" % self.path)
|
||||
self._size = min(frame_size * 8, self.parent.size - self.address)
|
||||
|
||||
def createFields(self):
|
||||
# Header
|
||||
yield PaddingBits(self, "sync", 11, "Synchronize bits (set to 1)", pattern=1)
|
||||
yield Enum(Bits(self, "version", 2, "MPEG audio version"), self.VERSION_NAME)
|
||||
yield Enum(Bits(self, "layer", 2, "MPEG audio layer"), self.LAYER_NAME)
|
||||
yield Bit(self, "crc16", "No CRC16 protection?")
|
||||
|
||||
# Rates and padding
|
||||
yield Bits(self, "bit_rate", 4, "Bit rate")
|
||||
yield Bits(self, "sampling_rate", 2, "Sampling rate")
|
||||
yield Bit(self, "use_padding", "Stream field use padding?")
|
||||
yield Bit(self, "extension", "Extension")
|
||||
|
||||
# Channel mode, mode extension, copyright, ...
|
||||
yield Enum(Bits(self, "channel_mode", 2, "Channel mode"), self.CHANNEL_MODE_NAME)
|
||||
yield Bits(self, "mode_ext", 2, "Mode extension")
|
||||
yield Bit(self, "copyright", "Is copyrighted?")
|
||||
yield Bit(self, "original", "Is original?")
|
||||
yield Enum(Bits(self, "emphasis", 2, "Emphasis"), self.EMPHASIS_NAME)
|
||||
|
||||
size = (self.size - self.current_size) / 8
|
||||
if size:
|
||||
yield RawBytes(self, "data", size)
|
||||
|
||||
def isValid(self):
|
||||
return (self["layer"].value != 0
|
||||
and self["sync"].value == 2047
|
||||
and self["version"].value != 1
|
||||
and self["sampling_rate"].value != 3
|
||||
and self["bit_rate"].value not in (0, 15)
|
||||
and self["emphasis"].value != 2)
|
||||
|
||||
def getSampleRate(self):
|
||||
"""
|
||||
Read sampling rate. Returns None on error.
|
||||
"""
|
||||
version = self["version"].value
|
||||
rate = self["sampling_rate"].value
|
||||
try:
|
||||
return self.SAMPLING_RATES[version][rate]
|
||||
except (KeyError, IndexError):
|
||||
return None
|
||||
|
||||
def getBitRate(self):
|
||||
"""
|
||||
Read bit rate in bit/sec. Returns None on error.
|
||||
"""
|
||||
layer = 3 - self["layer"].value
|
||||
bit_rate = self["bit_rate"].value
|
||||
if bit_rate in (0, 15):
|
||||
return None
|
||||
if self["version"].value == 3:
|
||||
dataset = self.BIT_RATES[1] # MPEG1
|
||||
else:
|
||||
dataset = self.BIT_RATES[2] # MPEG2 / MPEG2.5
|
||||
try:
|
||||
return dataset[layer][bit_rate] * 1000
|
||||
except (KeyError, IndexError):
|
||||
return None
|
||||
|
||||
def getFrameSize(self):
|
||||
"""
|
||||
Read frame size in bytes. Returns None on error.
|
||||
"""
|
||||
frame_size = self.getBitRate()
|
||||
if not frame_size:
|
||||
return None
|
||||
sample_rate = self.getSampleRate()
|
||||
if not sample_rate:
|
||||
return None
|
||||
padding = int(self["use_padding"].value)
|
||||
|
||||
if self["layer"].value == self.LAYER_III:
|
||||
if self["version"].value == self.MPEG_I:
|
||||
return (frame_size * 144) // sample_rate + padding
|
||||
else:
|
||||
return (frame_size * 72) // sample_rate + padding
|
||||
elif self["layer"].value == self.LAYER_II:
|
||||
return (frame_size * 144) / sample_rate + padding
|
||||
else: # self.LAYER_I:
|
||||
frame_size = (frame_size * 12) / sample_rate
|
||||
return (frame_size + padding) * 4
|
||||
|
||||
def getNbChannel(self):
|
||||
return self.NB_CHANNEL[ self["channel_mode"].value ]
|
||||
|
||||
def createDescription(self):
|
||||
info = ["layer %s" % self["layer"].display]
|
||||
bit_rate = self.getBitRate()
|
||||
if bit_rate:
|
||||
info.append("%s/sec" % humanBitSize(bit_rate))
|
||||
sampling_rate = self.getSampleRate()
|
||||
if sampling_rate:
|
||||
info.append(humanFrequency(sampling_rate))
|
||||
return "MPEG-%s %s" % (self["version"].display, ", ".join(info))
|
||||
|
||||
def findSynchronizeBits(parser, start, max_size):
|
||||
"""
|
||||
Find synchronisation bits (11 bits set to 1)
|
||||
|
||||
Returns None on error, or number of bytes before the synchronization.
|
||||
"""
|
||||
address0 = parser.absolute_address
|
||||
end = start + max_size
|
||||
size = 0
|
||||
while start < end:
|
||||
# Fast search: search 0xFF (first byte of sync frame field)
|
||||
length = parser.stream.searchBytesLength("\xff", False, start, end)
|
||||
if length is None:
|
||||
return None
|
||||
size += length
|
||||
start += length * 8
|
||||
|
||||
# Strong validation of frame: create the frame
|
||||
# and call method isValid()
|
||||
try:
|
||||
frame = createOrphanField(parser, start-address0, Frame, "frame")
|
||||
valid = frame.isValid()
|
||||
except HACHOIR_ERRORS:
|
||||
valid = False
|
||||
if valid:
|
||||
return size
|
||||
|
||||
# Invalid frame: continue
|
||||
start += 8
|
||||
size += 1
|
||||
return None
|
||||
|
||||
class Frames(FieldSet):
|
||||
# Padding bytes allowed before a frame
|
||||
MAX_PADDING = 256
|
||||
|
||||
def synchronize(self):
|
||||
addr = self.absolute_address
|
||||
start = addr + self.current_size
|
||||
end = min(start + self.MAX_PADDING*8, addr + self.size)
|
||||
padding = findSynchronizeBits(self, start, end)
|
||||
if padding is None:
|
||||
raise ParserError("MPEG audio: Unable to find synchronization bits")
|
||||
if padding:
|
||||
return PaddingBytes(self, "padding[]", padding, "Padding before synchronization")
|
||||
else:
|
||||
return None
|
||||
|
||||
def looksConstantBitRate(self, count=10):
|
||||
"""
|
||||
Guess if frames are constant bit rate. If it returns False, you can
|
||||
be sure that frames are variable bit rate. Otherwise, it looks like
|
||||
constant bit rate (on first count fields).
|
||||
"""
|
||||
check_keys = ("version", "layer", "bit_rate")
|
||||
last_field = None
|
||||
for index, field in enumerate(self.array("frame")):
|
||||
if last_field:
|
||||
for key in check_keys:
|
||||
if field[key].value != last_field[key].value:
|
||||
return False
|
||||
last_field = field
|
||||
if index == count:
|
||||
break
|
||||
return True
|
||||
|
||||
def createFields(self):
|
||||
# Find synchronisation bytes
|
||||
padding = self.synchronize()
|
||||
if padding:
|
||||
yield padding
|
||||
|
||||
while self.current_size < self.size:
|
||||
yield Frame(self, "frame[]")
|
||||
# padding = self.synchronize()
|
||||
# if padding:
|
||||
# yield padding
|
||||
|
||||
# Read raw bytes at the end (if any)
|
||||
size = (self.size - self.current_size) / 8
|
||||
if size:
|
||||
yield RawBytes(self, "raw", size)
|
||||
|
||||
def createDescription(self):
|
||||
if self.looksConstantBitRate():
|
||||
text = "(looks like) Constant bit rate (CBR)"
|
||||
else:
|
||||
text = "Variable bit rate (VBR)"
|
||||
return "Frames: %s" % text
|
||||
|
||||
def createMpegAudioMagic():
|
||||
|
||||
# ID3v1 magic
|
||||
magics = [("TAG", 0)]
|
||||
|
||||
# ID3v2 magics
|
||||
for ver_major in ID3v2.VALID_MAJOR_VERSIONS:
|
||||
magic = "ID3%c\x00" % ver_major
|
||||
magics.append( (magic,0) )
|
||||
|
||||
# MPEG frame magic
|
||||
# TODO: Use longer magic: 32 bits instead of 16 bits
|
||||
SYNC_BITS = 2047
|
||||
for version in Frame.VERSION_NAME.iterkeys():
|
||||
for layer in Frame.LAYER_NAME.iterkeys():
|
||||
for crc16 in (0, 1):
|
||||
magic = (SYNC_BITS << 5) | (version << 3) | (layer << 1) | crc16
|
||||
magic = long2raw(magic, BIG_ENDIAN, 2)
|
||||
magics.append( (magic, 0) )
|
||||
return magics
|
||||
|
||||
class MpegAudioFile(Parser):
|
||||
PARSER_TAGS = {
|
||||
"id": "mpeg_audio",
|
||||
"category": "audio",
|
||||
"file_ext": ("mpa", "mp1", "mp2", "mp3"),
|
||||
"mime": (u"audio/mpeg",),
|
||||
"min_size": 4*8,
|
||||
# "magic": createMpegAudioMagic(),
|
||||
"description": "MPEG audio version 1, 2, 2.5",
|
||||
"subfile": "skip",
|
||||
}
|
||||
endian = BIG_ENDIAN
|
||||
|
||||
def validate(self):
|
||||
if self[0].name in ("id3v2", "id3v1"):
|
||||
return True
|
||||
|
||||
if not self.stream.checked: # TODO: is it possible to handle piped input?
|
||||
return False
|
||||
|
||||
# Validate first 5 frames
|
||||
for index in xrange(5):
|
||||
try:
|
||||
frame = self["frames/frame[%u]" % index]
|
||||
except MissingField:
|
||||
# Require a least one valid frame
|
||||
if (1 <= index) \
|
||||
and self["frames"].done:
|
||||
return True
|
||||
return "Unable to get frame #%u" % index
|
||||
except (InputStreamError, ParserError):
|
||||
return "Unable to create frame #%u" % index
|
||||
|
||||
# Check first frame values
|
||||
if not frame.isValid():
|
||||
return "Frame #%u is invalid" % index
|
||||
|
||||
# Check that all frames are similar
|
||||
if not index:
|
||||
frame0 = frame
|
||||
else:
|
||||
if frame0["channel_mode"].value != frame["channel_mode"].value:
|
||||
return "Frame #%u channel mode is different" % index
|
||||
return True
|
||||
|
||||
def createFields(self):
|
||||
# Read ID3v2 (if any)
|
||||
if self.stream.readBytes(0, 3) == "ID3":
|
||||
yield ID3v2(self, "id3v2")
|
||||
|
||||
if self._size is None: # TODO: is it possible to handle piped input?
|
||||
raise NotImplementedError
|
||||
|
||||
# Check if file is ending with ID3v1 or not and compute frames size
|
||||
frames_size = self.size - self.current_size
|
||||
addr = self.size - 128*8
|
||||
if 0 <= addr:
|
||||
has_id3 = (self.stream.readBytes(addr, 3) == "TAG")
|
||||
if has_id3:
|
||||
frames_size -= 128*8
|
||||
else:
|
||||
has_id3 = False
|
||||
|
||||
# Read frames (if any)
|
||||
if frames_size:
|
||||
yield Frames(self, "frames", size=frames_size)
|
||||
|
||||
# Read ID3v1 (if any)
|
||||
if has_id3:
|
||||
yield ID3v1(self, "id3v1")
|
||||
|
||||
def createDescription(self):
|
||||
if "frames" in self:
|
||||
frame = self["frames/frame[0]"]
|
||||
return "%s, %s" % (frame.description, frame["channel_mode"].display)
|
||||
elif "id3v2" in self:
|
||||
return self["id3v2"].description
|
||||
elif "id3v1" in self:
|
||||
return self["id3v1"].description
|
||||
else:
|
||||
return "MPEG audio"
|
||||
|
||||
def createContentSize(self):
|
||||
# Get "frames" field
|
||||
field = self[0]
|
||||
if field.name != "frames":
|
||||
try:
|
||||
field = self[1]
|
||||
except MissingField:
|
||||
# File only contains ID3v1 or ID3v2
|
||||
return field.size
|
||||
|
||||
# Error: second field are not the frames"?
|
||||
if field.name != "frames":
|
||||
return None
|
||||
|
||||
# Go to last frame
|
||||
frames = field
|
||||
frame = frames["frame[0]"]
|
||||
address0 = field.absolute_address
|
||||
size = address0 + frame.size
|
||||
while True:
|
||||
try:
|
||||
# Parse one MPEG audio frame
|
||||
frame = createOrphanField(frames, size - address0, Frame, "frame")
|
||||
|
||||
# Check frame 32 bits header
|
||||
if not frame.isValid():
|
||||
break
|
||||
except HACHOIR_ERRORS:
|
||||
break
|
||||
if MAX_FILESIZE < (size + frame.size):
|
||||
break
|
||||
size += frame.size
|
||||
|
||||
# ID3v1 at the end?
|
||||
try:
|
||||
if self.stream.readBytes(size, 3) == "TAG":
|
||||
size += ID3v1.static_size
|
||||
except InputStreamError:
|
||||
pass
|
||||
return size
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
RealAudio (.ra) parser
|
||||
|
||||
Author: Mike Melanson
|
||||
References:
|
||||
http://wiki.multimedia.cx/index.php?title=RealMedia
|
||||
Samples:
|
||||
http://samples.mplayerhq.hu/real/RA/
|
||||
"""
|
||||
|
||||
from hachoir_parser import Parser
|
||||
from hachoir_core.field import (FieldSet,
|
||||
UInt8, UInt16, UInt32,
|
||||
Bytes, RawBytes, String,
|
||||
PascalString8)
|
||||
from hachoir_core.tools import humanFrequency
|
||||
from hachoir_core.text_handler import displayHandler
|
||||
from hachoir_core.endian import BIG_ENDIAN
|
||||
|
||||
class Metadata(FieldSet):
|
||||
def createFields(self):
|
||||
yield PascalString8(self, "title", charset="ISO-8859-1")
|
||||
yield PascalString8(self, "author", charset="ISO-8859-1")
|
||||
yield PascalString8(self, "copyright", charset="ISO-8859-1")
|
||||
yield PascalString8(self, "comment", charset="ISO-8859-1")
|
||||
|
||||
class RealAudioFile(Parser):
|
||||
MAGIC = ".ra\xFD"
|
||||
PARSER_TAGS = {
|
||||
"id": "real_audio",
|
||||
"category": "audio",
|
||||
"file_ext": ["ra"],
|
||||
"mime": (u"audio/x-realaudio", u"audio/x-pn-realaudio"),
|
||||
"min_size": 6*8,
|
||||
"magic": ((MAGIC, 0),),
|
||||
"description": u"Real audio (.ra)",
|
||||
}
|
||||
endian = BIG_ENDIAN
|
||||
|
||||
def validate(self):
|
||||
if self["signature"].value != self.MAGIC:
|
||||
return "Invalid signature"
|
||||
if self["version"].value not in (3, 4):
|
||||
return "Unknown version"
|
||||
return True
|
||||
|
||||
def createFields(self):
|
||||
yield Bytes(self, "signature", 4, r"RealAudio identifier ('.ra\xFD')")
|
||||
yield UInt16(self, "version", "Version")
|
||||
if self["version"].value == 3:
|
||||
yield UInt16(self, "header_size", "Header size")
|
||||
yield RawBytes(self, "Unknown1", 10)
|
||||
yield UInt32(self, "data_size", "Data size")
|
||||
yield Metadata(self, "metadata")
|
||||
yield UInt8(self, "Unknown2")
|
||||
yield PascalString8(self, "FourCC")
|
||||
audio_size = self["data_size"].value
|
||||
else: # version = 4
|
||||
yield UInt16(self, "reserved1", "Reserved, should be 0")
|
||||
yield String(self, "ra4sig", 4, "'.ra4' signature")
|
||||
yield UInt32(self, "filesize", "File size (minus 40 bytes)")
|
||||
yield UInt16(self, "version2", "Version 2 (always equal to version)")
|
||||
yield UInt32(self, "headersize", "Header size (minus 16)")
|
||||
yield UInt16(self, "codec_flavor", "Codec flavor")
|
||||
yield UInt32(self, "coded_frame_size", "Coded frame size")
|
||||
yield RawBytes(self, "unknown1", 12)
|
||||
yield UInt16(self, "subpacketh", "Subpacket h (?)")
|
||||
yield UInt16(self, "frame_size", "Frame size")
|
||||
yield UInt16(self, "sub_packet_size", "Subpacket size")
|
||||
yield UInt16(self, "unknown2", "Unknown")
|
||||
yield displayHandler(UInt16(self, "sample_rate", "Sample rate"), humanFrequency)
|
||||
yield UInt16(self, "unknown3", "Unknown")
|
||||
yield UInt16(self, "sample_size", "Sample size")
|
||||
yield UInt16(self, "channels", "Channels")
|
||||
yield PascalString8(self, "Interleaving ID String")
|
||||
yield PascalString8(self, "FourCC")
|
||||
yield RawBytes(self, "unknown4", 3)
|
||||
yield Metadata(self, "metadata")
|
||||
audio_size = (self["filesize"].value + 40) - (self["headersize"].value + 16)
|
||||
if 0 < audio_size:
|
||||
yield RawBytes(self, "audio_data", audio_size)
|
||||
|
||||
def createDescription(self):
|
||||
if (self["version"].value == 3):
|
||||
return "RealAudio v3 file, '%s' codec" % self["FourCC"].value
|
||||
elif (self["version"].value == 4):
|
||||
return "RealAudio v4 file, '%s' codec, %s, %u channels" % (
|
||||
self["FourCC"].value, self["sample_rate"].display, self["channels"].value)
|
||||
else:
|
||||
return "Real audio"
|
||||
@@ -0,0 +1,668 @@
|
||||
"""
|
||||
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 hachoir_parser import Parser
|
||||
from hachoir_core.field import (StaticFieldSet, FieldSet, Field,
|
||||
Bit, Bits,
|
||||
UInt32, UInt16, UInt8, Enum,
|
||||
PaddingBytes, RawBytes, NullBytes,
|
||||
String, GenericVector, ParserError)
|
||||
from hachoir_core.endian import LITTLE_ENDIAN
|
||||
from hachoir_core.text_handler import textHandler, hexadecimal
|
||||
from 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
|
||||
@@ -0,0 +1,390 @@
|
||||
"""
|
||||
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_core.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 '%s'" % 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
|
||||
|
||||
Reference in New Issue
Block a user