mirror of
https://github.com/freedoom/freedoom.git
synced 2025-09-05 16:25:45 -04:00
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...).
184 lines
4.3 KiB
Python
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)
|
|
|