mirror of
https://github.com/freedoom/freedoom.git
synced 2025-09-04 13:25:47 -04:00
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.
194 lines
4.7 KiB
Python
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)
|