Skip to content

Instantly share code, notes, and snippets.

@msikma
Last active September 22, 2022 23:55
Show Gist options
  • Save msikma/7062b33b25854e030ff61c80cbdd83e1 to your computer and use it in GitHub Desktop.
Save msikma/7062b33b25854e030ff61c80cbdd83e1 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python2
'''
Short script to make OPL music (in DOSBox DRO files) less loud.
'''
from struct import *
from collections import namedtuple
import sys
import argparse
def namedunpack(data, names, format):
'''
Unpacks into a named tuple.
'''
str = namedtuple('struct', names)
data = str(*unpack(format, data))
return data
CARRIERS = (0x43, 0x44, 0x45, 0x4B, 0x4C, 0x4D, 0x53, 0x54, 0x55)
LEVEL_TO_ALGORITHM = {
0x40: 0xC0, 0x41: 0xC0, 0x42: 0xC1, 0x43: 0xC1, 0x44: 0xC2, 0x45: 0xC2,
0x48: 0xC3, 0x49: 0xC3, 0x4A: 0xC4, 0x4B: 0xC4, 0x4C: 0xC5, 0x4D: 0xC5,
0x50: 0xC6, 0x51: 0xC6, 0x52: 0xC7, 0x53: 0xC7, 0x54: 0xC8, 0x55: 0xC8
}
class OPL2Quieter(object):
def __init__(self, quiet_function):
self.quiet_function = quiet_function
self.registers = [0x00 for i in xrange(0x100)]
def write(self, register, value):
if 0x40 <= register <= 0x55: # if it is a level register
# algorithm bit is the LSB of algorithm/feedback reg
algorithm = self.registers[LEVEL_TO_ALGORITHM[register]] & 0x01
if algorithm == 0x00:
# FM synthesis, only modify the level if it's a carrier
modify_level = True if register in CARRIERS else False
else:
# AM synthesis, modify level for both operators
modify_level = True
if modify_level:
# keep the top two bits (the KSL value) separate
# for purposes of calculation
ksl = value & 0xC0
# perform the volume modification on the lower six bits
level = self.quiet_function(value & 0x3F)
value = ksl | (level & 0x3F)
self.registers[register] = value
return register, value
parser = argparse.ArgumentParser(
description='Reduces the volume of a DOSBox DRO file.'
)
parser.add_argument('infile', help='input file', type=str)
parser.add_argument('outfile', help='output file', type=str)
args = parser.parse_args()
infile = open(args.infile, 'rb')
outfile = open(args.outfile, 'wb')
chunk = infile.read(26)
data = namedunpack(chunk, ['cSignature', 'iVersionMajor', 'iVersionMinor', 'iLengthPairs', 'iLengthMS', 'iHardwareType', 'iFormat', 'iCompression', 'iShortDelayCode', 'iLongDelayCode', 'iCodemapLength'], '8sHHIIBBBBBB')
outfile.write(chunk)
if data.cSignature != 'DBRAWOPL':
parser.error('input file is not a DOSBox DRO file.')
sys.exit(1)
# Copy over the codemap table, which has a variable length.
chunk = infile.read(data.iCodemapLength)
outfile.write(chunk)
pairsTotal = data.iLengthPairs
pairsRead = 0
quieter = OPL2Quieter(lambda level: max(level - 5, 0))
while pairsRead < pairsTotal:
# Read the next OPL bytes from the file
chunk = infile.read(2)
pairsRead += 1
if not chunk: break
reg, val = unpack('BB', chunk)
reg, val = quieter.write(reg, val)
outfile.write(pack('BB', reg, val))
infile.close()
outfile.close()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment