LMF Packer (Generic .MOD to Lyrica Music Format)
The Packer
# mod2packer.py
# by RocketeerEatingEggs
import sys
import math
ModPeriodTable = [
0, # required by the converter to add no note
3424, 3232, 3048, 2880, 2712, 2560, 2416, 2280, 2152, 2032, 1920, 1812, # octave 2
1712, 1616, 1524, 1440, 1356, 1280, 1208, 1140, 1076, 1016, 960, 906, # octave 3
856, 808, 762, 720, 678, 640, 604, 570, 538, 508, 480, 453, # octave 4, amiga
428, 404, 381, 360, 339, 320, 302, 285, 269, 254, 240, 226, # octave 5, amiga
214, 202, 190, 180, 170, 160, 151, 143, 135, 127, 120, 113, # octave 6, amiga
107, 101, 95, 90, 85, 80, 75, 71, 67, 63, 60, 56, # octave 7
53, 50, 47, 45, 42, 40, 37, 35, 33, 31, 30, 28, # octave 8
0
]
ModChannelsTable = [
"1CHN","2CHN","3CHN","M.K.","5CHN","6CHN","7CHN","8CHN",
"9CHN","10CH","11CH","12CH","13CH","14CH","15CH","16CH",
"17CH","18CH","19CH","20CH","21CH","22CH","23CH","24CH",
"25CH","26CH","27CH","28CH","29CH","30CH","31CH","32CH",
"33CH","34CH","35CH","36CH","37CH","38CH","39CH","40CH",
"41CH","42CH","43CH","44CH","45CH","46CH","47CH","48CH",
"49CH","50CH","51CH","52CH","53CH","54CH","55CH","56CH",
"57CH","58CH","59CH","60CH","61CH","62CH","63CH","64CH",
"65CH","66CH","67CH","68CH","69CH","70CH","71CH","72CH",
"73CH","74CH","75CH","76CH","77CH","78CH","79CH","80CH",
"81CH","82CH","83CH","84CH","85CH","86CH","87CH","88CH",
"89CH","90CH","91CH","92CH","93CH","94CH","95CH","96CH",
"97CH","98CH","99CH"
]
def compareMagic(magic):
for i in range(32):
if magic == ModChannelsTable[i]:
return i + 1
return 0
def periodToNote(lower, upper):
newPeriod = (upper << 8) + lower
for i in range(len(ModPeriodTable)):
if (ModPeriodTable[i] > newPeriod) and (ModPeriodTable[i+1] < newPeriod):
return i
if ModPeriodTable[i] == newPeriod:
return i
return 0 # oops, no note found!
with open(sys.argv[2], "wb+") as PTMfile:
with open(sys.argv[1], "rb") as MODfile:
MODfile.seek(1080)
numChannels = compareMagic(str(MODfile.read(4), encoding="utf-8"))
if numChannels != 0:
PTMfile.seek(0)
MODfile.seek(0)
PTMfile.write(b"LMF1")
PTMfile.write(MODfile.read(20))
for i in range(31):
PTMfile.write(MODfile.read(22))
smpLenB = MODfile.read(2)
smpLen = int.from_bytes(smpLenB, byteorder="big")
smpHead1 = MODfile.read(1)
defVolB = MODfile.read(1)
smpHead2 = MODfile.read(4)
PTMfile.write(smpLenB)
PTMfile.write(smpHead1)
if smpLen == 0:
PTMfile.write((0).to_bytes(1, byteorder="big"))
else:
PTMfile.write(defVolB)
PTMfile.write(smpHead2)
MODfile.seek(950)
numOrdersBytes = MODfile.read(1)
numOrders = int.from_bytes(numOrdersBytes, byteorder="big")
PTMfile.write(numOrdersBytes)
restartPos = MODfile.read(1)
PTMfile.write(restartPos)
numPatterns = 0
for i in range(128):
patternNumber = int.from_bytes(MODfile.read(1), byteorder="big")
PTMfile.write(patternNumber.to_bytes(1, byteorder="big"))
if patternNumber > numPatterns:
numPatterns = patternNumber
PTMfile.write(numChannels.to_bytes(1, byteorder="big"))
MODfile.seek(1084)
for patternNumber in range(numPatterns):
for rowNumber in range(64):
for channelNumber in range(numChannels):
eventPart1 = int.from_bytes(MODfile.read(1), byteorder="little")
lowerPeriod = int.from_bytes(MODfile.read(1), byteorder="little")
eventPart3 = int.from_bytes(MODfile.read(1), byteorder="little")
effectParam = MODfile.read(1)
upperPeriod = eventPart1 & 15
note = periodToNote(lowerPeriod, upperPeriod)
instrument = (((eventPart1 & 240) << 4) + (eventPart3 & 240)) >> 4
effectNumber = eventPart3 & 15
byte1 = (note << 1) + (instrument >> 4)
byte2 = ((instrument & 15) << 4) + effectNumber
PTMfile.write(byte1.to_bytes(1, byteorder="big"))
PTMfile.write(byte2.to_bytes(1, byteorder="big"))
PTMfile.write(effectParam)
PTMfile.write(MODfile.read())
First, we initialize everything (the MOD period table, the channel ID table, etc).
Then, we open the second requested file for writing. Then, we open the first file for reading.
We validate the MOD file simply by checking if the ID is valid. (There are more sophisticated methods of checking this, but I'm pretty much a Cirno when it comes to the MOD format.)
Once that is done, we write the LMF1 magic (LMF0 included no text at all) and copy the first (22 + 130 + (30 * 31)) bytes or so of the file.
We then write the number of channels value.
The four pattern event bytes are converted down to three bytes. The highest seven bits are the note, the next five bits the instrument, the next four the effect number, and the final eight are the effect parameter.
Finally, the samples are written.
The Depacker
# packer2mod.py
# by RocketeerEatingEggs
import sys
import math
ModPeriodTable = [
0, # required by the converter to add no note
3424, 3232, 3048, 2880, 2712, 2560, 2416, 2280, 2152, 2032, 1920, 1812, # octave 2
1712, 1616, 1524, 1440, 1356, 1280, 1208, 1140, 1076, 1016, 960, 906, # octave 3
856, 808, 762, 720, 678, 640, 604, 570, 538, 508, 480, 453, # octave 4, amiga
428, 404, 381, 360, 339, 320, 302, 285, 269, 254, 240, 226, # octave 5, amiga
214, 202, 190, 180, 170, 160, 151, 143, 135, 127, 120, 113, # octave 6, amiga
107, 101, 95, 90, 85, 80, 75, 71, 67, 63, 60, 56, # octave 7
53, 50, 47, 45, 42, 40, 37, 35, 33, 31, 30, 28, # octave 8
0
]
ModChannelsTable = [
b"1CHN",b"2CHN",b"3CHN",b"M.K.",b"5CHN",b"6CHN",b"7CHN",b"8CHN",
b"9CHN",b"10CH",b"11CH",b"12CH",b"13CH",b"14CH",b"15CH",b"16CH",
b"17CH",b"18CH",b"19CH",b"20CH",b"21CH",b"22CH",b"23CH",b"24CH",
b"25CH",b"26CH",b"27CH",b"28CH",b"29CH",b"30CH",b"31CH",b"32CH",
b"33CH",b"34CH",b"35CH",b"36CH",b"37CH",b"38CH",b"39CH",b"40CH",
b"41CH",b"42CH",b"43CH",b"44CH",b"45CH",b"46CH",b"47CH",b"48CH",
b"49CH",b"50CH",b"51CH",b"52CH",b"53CH",b"54CH",b"55CH",b"56CH",
b"57CH",b"58CH",b"59CH",b"60CH",b"61CH",b"62CH",b"63CH",b"64CH",
b"65CH",b"66CH",b"67CH",b"68CH",b"69CH",b"70CH",b"71CH",b"72CH",
b"73CH",b"74CH",b"75CH",b"76CH",b"77CH",b"78CH",b"79CH",b"80CH",
b"81CH",b"82CH",b"83CH",b"84CH",b"85CH",b"86CH",b"87CH",b"88CH",
b"89CH",b"90CH",b"91CH",b"92CH",b"93CH",b"94CH",b"95CH",b"96CH",
b"97CH",b"98CH",b"99CH"
]
def periodToNote(lower, upper):
newPeriod = (upper << 8) + lower
for i in range(len(ModPeriodTable)):
if (ModPeriodTable[i] > newPeriod) and (ModPeriodTable[i+1] < newPeriod):
return i
if ModPeriodTable[i] == newPeriod:
return i
return 0 # oops, no note found!
with open(sys.argv[2], "wb+") as MODfile:
with open(sys.argv[1], "rb") as MKfile:
if str(MKfile.read(4), encoding="utf-8") == "LMF1":
MKfile.seek(4)
MODfile.write(MKfile.read(20))
smpLengths = []
for i in range(31):
MODfile.write(MKfile.read(22))
smpLengthBytes = MKfile.read(2)
smpLength = int.from_bytes(smpLengthBytes, byteorder="big")
smpLengths.append(smpLength*2)
smpHead = MKfile.read(6)
MODfile.write(smpLengthBytes)
MODfile.write(smpHead)
numOrdersBytes = MKfile.read(1)
numOrders = int.from_bytes(numOrdersBytes, byteorder="big")
MODfile.write(numOrdersBytes)
restartPos = MKfile.read(1)
MODfile.write(restartPos)
numPatterns = 0
for i in range(128):
patternNumber = int.from_bytes(MKfile.read(1), byteorder="big")
MODfile.write(patternNumber.to_bytes(1, byteorder="big"))
if patternNumber > numPatterns:
numPatterns = patternNumber
numChannels = int.from_bytes(MKfile.read(1), byteorder="big")
MODfile.write(ModChannelsTable[numChannels-1])
for patternNumber in range(numPatterns):
for rowNumber in range(64):
for channelNumber in range(numChannels):
evP1 = int.from_bytes(MKfile.read(1), byteorder="big")
evP2 = int.from_bytes(MKfile.read(1), byteorder="big")
evP3 = MKfile.read(1)
note = (evP1 & 254) >> 1
inst1 = (evP1 & 1) << 4
inst2 = evP2 & 240
fxTp = evP2 & 15
period = ModPeriodTable[note]
byte1 = inst1 + (period >> 8)
byte2 = period & 255
byte3 = inst2 + fxTp
MODfile.write(byte1.to_bytes(1, byteorder="big"))
MODfile.write(byte2.to_bytes(1, byteorder="big"))
MODfile.write(byte3.to_bytes(1, byteorder="big"))
MODfile.write(evP3)
MODfile.write(MKfile.read())
Here, the opposite happens. The file is validated by way of reading the first four bytes.
The song name, the instrument info, and the order list information are copied.
Then the channel count ID is written. For four-channel MODs this is M.K.
The three byte pattern events are converted to the regular four bytes of the MOD format, and the samples are written after the last pattern.
Check through the code yourself if anything seems off.