freedoom/lumps/genmidi/genmidi.py
Mike Swanson 6eef9be73a use python3 only for building
Python 2 is very near end-of-life, and Python3-compatible changes to a
few scripts introduced compatibility problems with 2.7 again.  It went
unnoticed for me since my system symlinks "python" to "python3", but
it broke the build on systems where that symlink is still python2.  At
this point in time, I feel it is worth targetting modern Python and
forgetting about 2.7.
2019-09-06 14:43:50 -07:00

194 lines
4.7 KiB
Python

#!/usr/bin/env python3
# SPDX-License-Identifier: BSD-3-Clause
#
# Module for interacting with Doom GENMIDI lumps.
#
from instrument import Instrument, NullInstrument
import struct
import sys
GENMIDI_HEADER = "#OPL_II#"
NUM_INSTRUMENTS = 175
INSTR_DATA_LEN = 36
INSTR_NAME_LEN = 32
FLAG_FIXED_PITCH = 0x0001
FLAG_TWO_VOICE = 0x0004
KSL_MASK = 0xC0
VOLUME_MASK = 0x3F
# Order of fields in GENMIDI data structures.
GENMIDI_FIELDS = [
"m_am_vibrato_eg",
"m_attack_decay",
"m_sustain_release",
"m_waveform",
"m_ksl",
"m_volume",
"feedback_fm",
"c_am_vibrato_eg",
"c_attack_decay",
"c_sustain_release",
"c_waveform",
"c_ksl",
"c_volume",
"null",
"note_offset",
]
# Encode a single voice of an instrument to binary.
def encode_voice(data, offset):
result = dict(data)
result["m_ksl"] = data["m_ksl_volume"] & KSL_MASK
result["m_volume"] = data["m_ksl_volume"] & VOLUME_MASK
result["c_ksl"] = data["c_ksl_volume"] & KSL_MASK
result["c_volume"] = data["c_ksl_volume"] & VOLUME_MASK
result["null"] = 0
result["note_offset"] = offset
return struct.pack(
"<BBBBBBBBBBBBBBh", *map(lambda key: result[key], GENMIDI_FIELDS)
)
# Encode an instrument to binary.
def encode_instrument(instrument):
flags = 0
instr1_data = encode_voice(instrument.voice1, instrument.offset1)
if instrument.voice2 is not None:
flags |= FLAG_TWO_VOICE
instr2_data = encode_voice(instrument.voice2, instrument.offset2)
else:
instr2_data = encode_voice(NullInstrument.voice1, 0)
if instrument.fixed_note is not None:
flags |= FLAG_FIXED_PITCH
fixed_note = instrument.fixed_note
else:
fixed_note = 0
header = struct.pack("<hBB", flags, 128, fixed_note)
return header + instr1_data + instr2_data
def encode_instruments(instruments):
result = []
for instrument in instruments:
result.append(encode_instrument(instrument))
return b"".join(result)
def encode_instrument_names(instruments):
result = []
for instrument in instruments:
instr_name = instrument.voice1["name"].encode("ascii")
result.append(struct.pack("32s", instr_name))
return b"".join(result)
def write(filename, instruments):
header = struct.pack(
"%is" % len(GENMIDI_HEADER), GENMIDI_HEADER.encode("ascii")
)
with open(filename, "wb") as f:
f.write(header)
f.write(encode_instruments(instruments))
f.write(encode_instrument_names(instruments))
def decode_voice(data, name):
fields = struct.unpack("<BBBBBBBBBBBBBBh", data)
result = {}
for i in range(len(GENMIDI_FIELDS)):
result[GENMIDI_FIELDS[i]] = fields[i]
result["m_ksl_volume"] = result["m_ksl"] | result["m_volume"]
result["c_ksl_volume"] = result["c_ksl"] | result["c_volume"]
result["name"] = name.decode("ascii").rstrip("\0")
return result
def decode_instrument(data, name):
flags, finetune, fixed_note = struct.unpack("<hBB", data[0:4])
voice1 = decode_voice(data[4:20], name)
offset1 = voice1["note_offset"]
# Second voice?
if (flags & FLAG_TWO_VOICE) != 0:
voice2 = decode_voice(data[20:], name)
offset2 = voice2["note_offset"]
else:
voice2 = None
offset2 = 0
# Null out fixed_note if the fixed pitch flag isn't set:
if (flags & FLAG_FIXED_PITCH) == 0:
fixed_note = None
return Instrument(
voice1, voice2, off1=offset1, off2=offset2, note=fixed_note
)
def read(filename):
with open(filename, "rb") as f:
data = f.read()
# Check header:
header = data[0 : len(GENMIDI_HEADER)]
if header.decode("ascii") != GENMIDI_HEADER:
raise Exception("Incorrect header for GENMIDI lump")
body = data[len(GENMIDI_HEADER) :]
instr_data = body[0 : NUM_INSTRUMENTS * INSTR_DATA_LEN]
instr_names = body[NUM_INSTRUMENTS * INSTR_DATA_LEN :]
result = []
for i in range(NUM_INSTRUMENTS):
data = instr_data[i * INSTR_DATA_LEN : (i + 1) * INSTR_DATA_LEN]
name = instr_names[i * INSTR_NAME_LEN : (i + 1) * INSTR_NAME_LEN]
result.append(decode_instrument(data, name))
return result
if __name__ == "__main__":
for filename in sys.argv[1:]:
instruments = read(filename)
for i in range(len(instruments)):
instrument = instruments[i]
fixed_note = instrument.fixed_note
if fixed_note is not None:
print("%i (fixed note: %i):" % (i, fixed_note))
else:
print("%i:" % i)
print("\tVoice 1: %s" % instrument.voice1)
if instrument.voice2 is not None:
print("\tVoice 2: %s" % instrument.voice2)