freedoom/lumps/genmidi/genmidi.py
Mike Swanson 8835afef5f Copyright date bump and apply SPDX tags.
The tags are shorthand for the license of each file and avoid
copying the full license text into each one (and avoids having
to manually update the dates in each one...).
2017-02-15 16:41:53 -08:00

184 lines
4.3 KiB
Python

#!/usr/bin/env python
# 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)