research/opl/dosbox-trace/opl-parser
Simon Howard 2b81dd217f Add command line option to specify genmidi lump, identify instrument
when a voice is programmed.

Subversion-branch: /research
Subversion-revision: 1698
2009-09-30 17:59:35 +00:00

499 lines
11 KiB
Ruby
Executable file

#!/usr/bin/env ruby
require "scanf"
FREQ_MULT_OFFSET = 0x20
SCALE_LEVEL_OFFSET = 0x40
ATTACK_DECAY_OFFSET = 0x60
SUSTAIN_RELEASE_OFFSET = 0x80
OCTAVE_KEY_LSB_OFFSET = 0xa0
OCTAVE_KEY_MSB_OFFSET = 0xb0
FEEDBACK_OFFSET = 0xc0
WAVEFORM_SELECT_OFFSET = 0xe0
CHANNEL_OPERATORS = [ [ 0x00, 0x03 ],
[ 0x01, 0x04 ],
[ 0x02, 0x05 ],
[ 0x08, 0x0b ],
[ 0x09, 0x0c ],
[ 0x0a, 0x0d ],
[ 0x10, 0x13 ],
[ 0x11, 0x14 ],
[ 0x12, 0x15 ] ]
def reverse_channels_list
result = [ -1 ] * 21
for channel in 0...CHANNEL_OPERATORS.length
result[CHANNEL_OPERATORS[channel][0]] = channel
result[CHANNEL_OPERATORS[channel][1]] = channel
end
result
end
OPERATOR_TO_CHANNEL = reverse_channels_list
class RegisterWrite
attr_reader :register, :value
def initialize(register, value)
@register = register
@value = value
end
def to_s
sprintf("%02x: %02x", @register, @value)
end
end
class MatchPattern
def initialize(pattern_class, pattern)
@pattern_class = pattern_class
@pattern = pattern
end
def matches(array, offset)
# Sanity check
if offset + length > array.length
return nil
end
# Check that all values in this pattern match
for i in 0...@pattern.length
reg_pattern = @pattern[i]
reg_write = array[offset + i]
# Is this a write to the expected register?
expected = reg_pattern[0]
mask = reg_pattern[1]
if (reg_write.register & mask) != (expected & mask)
return nil
end
# Optional checking of the value:
if reg_pattern.length > 2
expected = reg_pattern[2]
mask = reg_pattern[3]
if (reg_write.value & mask) != (expected & mask)
return nil
end
end
end
@pattern_class.new(array[offset,length])
end
def length
@pattern.length
end
end
class Event
attr_reader :reg_writes
def initialize(reg_writes)
@reg_writes = reg_writes
end
# Get a particular register value set in this event:
def find_value(register)
for r in @reg_writes
if register == r.register
return r.value
end
end
nil
end
end
# Event to configure a channel:
class ChannelEvent < Event
def channel
first_reg = @reg_writes[0].register
first_reg & 0xf
end
def operator_1
CHANNEL_OPERATORS[channel][0]
end
def operator_2
CHANNEL_OPERATORS[channel][1]
end
end
# Event to configure a channel, but we are actually only configuring
# a single operator
class OperatorEvent < ChannelEvent
def channel
first_reg = @reg_writes[0].register
operator = first_reg & 0x1f
OPERATOR_TO_CHANNEL[operator]
end
# Operator number - 0 or 1
def operator_num
first_reg = @reg_writes[0].register
operator = first_reg & 0x1f
if operator == operator_1
0
else
1
end
end
end
class InitChannel < OperatorEvent
GENMIDI_REGS = [ FREQ_MULT_OFFSET, ATTACK_DECAY_OFFSET,
SUSTAIN_RELEASE_OFFSET, WAVEFORM_SELECT_OFFSET,
SCALE_LEVEL_OFFSET, SCALE_LEVEL_OFFSET ]
def operator_values(op)
result = []
for base in GENMIDI_REGS
result.push(find_value(base + op))
end
result[4] &= 0xc0 # Key scale level
result[5] &= 0x3f # Output level
result
end
# Get values in the order that they are stored in the GENMIDI
# lump.
def values
operator_values(operator_1) +
[ find_value(FEEDBACK_OFFSET + channel) ] +
operator_values(operator_2)
end
# Description of the instrument being loaded:
def instr_desc
instr_data = $instruments[values]
if instr_data == nil
return "- Unknown instrument"
end
result = ""
for possible in instr_data
result += "- Instrument ##{possible[:instrument]} " \
+ "(#{possible[:name]}) " \
+ "v#{possible[:voice]}\n"
end
result
end
def to_s
stringified = values.map { |val| sprintf("%02x", val) }
stringified = stringified.join(",")
"Initializing channel #{channel}: #{stringified}\n" + instr_desc
end
end
class DetectionSequence < Event
def to_s
"Adlib detection sequence"
end
end
class ScaleLevelChange < OperatorEvent
SCALE_LEVELS = [
"no change",
"3dB/8ve",
"1.5dB/8ve",
"6dB/8ve"
]
def to_s
value = @reg_writes[0].value
scale_level = (value >> 6) & 0x3
total_level = (value & 0x3f) * 0.75
"Scale level change on channel #{channel}, op #{operator_num}: " +
"#{total_level}dB, #{SCALE_LEVELS[scale_level]}"
end
end
class KeyOn < ChannelEvent
def to_s
value = @reg_writes[0].value | (@reg_writes[1].value << 8)
octave = (value >> 10) & 0x7
f_number = value & 0x3ff
sprintf("Key on, channel %i: octave %i, freq 0x%x",
channel, octave, f_number)
end
end
class KeyOff < ChannelEvent
OPERATOR_TYPE = true
def to_s
"Key off, channel #{channel}"
end
end
class InitialInit < Event
def to_s
"Initial initialisation of registers..."
end
end
# Fallback for if we can't match a pattern:
class BasicRegisterWrite < Event
def to_s
"Basic register write: " + @reg_writes[0].to_s
end
end
# GENMIDI parsing code:
NUM_INSTRUMENTS = 175
def read_instrument(file)
result = []
36.times do
c = file.getc
result.push(c)
end
result
end
# "Flatten" voice data in the way that the Doom code will:
def flatten_voice(voice_data)
modulating = (voice_data[6] & 0x01) == 0;
# Voices 1 and 2 (for OPL3)
voice_data[6] |= 0x30;
# 2nd op always has level set to max
voice_data[12] |= 0x3f;
# 1st op has level set to max if not modulating
if !modulating
voice_data[5] |= 0x3f;
end
end
# Add a voice to the instruments lookup table.
def add_voice(instruments, voice_data, instr_num, voice_num, name)
if instruments[voice_data] == nil
instruments[voice_data] = []
end
instruments[voice_data].push({
:instrument => instr_num,
:voice => voice_num,
:name => name
})
end
def read_genmidi(filename)
instr_data = []
instr_names = []
File.open(filename) do |file|
header = file.read(8)
if header != "#OPL_II#"
raise "Header not found!"
end
NUM_INSTRUMENTS.times do
data = read_instrument(file)
instr_data.push(data)
end
NUM_INSTRUMENTS.times do
name = file.read(32).strip
instr_names.push(name)
end
end
instruments = {}
for i in 0...NUM_INSTRUMENTS
dual_voice = (instr_data[i][0] & 0x04) != 0
voice1 = instr_data[i][4, 13]
voice2 = instr_data[i][20, 13]
flatten_voice(voice1)
flatten_voice(voice2)
add_voice(instruments, voice1, i, 1, instr_names[i])
if dual_voice
add_voice(instruments, voice2, i, 2, instr_names[i])
end
end
instruments
end
def parse_cmdline
i = 0
$instruments = {}
while i < ARGV.length
if ARGV[i] == "-genmidi"
$instruments = read_genmidi(ARGV[i + 1])
i += 1
end
i += 1
end
end
def parse_file(stream)
register = nil
result = []
stream.each_line do |s|
if s =~ /OPL_write: (\d+), ([0-9a-fA-F]+)/
reg = $1.to_i
value = $2.scanf("%x")[0]
if reg == 0
register = value
else
result.push(RegisterWrite.new(register, value))
end
end
end
result
end
MATCH_PATTERNS = [
# Adlib detection sequence:
MatchPattern.new(DetectionSequence,
[ [ 4, 0xff, 0x60, 0xff ],
[ 4, 0xff, 0x80, 0xff ],
[ 2, 0xff, 0xff, 0xff ],
[ 4, 0xff, 0x21, 0xff ],
[ 4, 0xff, 0x60, 0xff ],
[ 4, 0xff, 0x80, 0xff ] ]),
# Startup initialisation values:
MatchPattern.new(InitialInit,
[ [ 0x40, 0xe3, 0x3f, 0xff ],
[ 0x41, 0xe3, 0x3f, 0xff ],
[ 0x42, 0xe3, 0x3f, 0xff ],
[ 0x43, 0xe3, 0x3f, 0xff ] ]),
MatchPattern.new(InitialInit,
[ [ 0x00, 0x03, 0x00, 0xff ],
[ 0x01, 0x03, 0x00, 0xff ],
[ 0x02, 0x03, 0x00, 0xff ],
[ 0x03, 0x03, 0x00, 0xff ] ]),
# Key on
MatchPattern.new(KeyOn,
[ [ OCTAVE_KEY_LSB_OFFSET, 0xf0 ],
[ OCTAVE_KEY_MSB_OFFSET, 0xf0, 0x20, 0x20 ]]),
# Key off
MatchPattern.new(KeyOff,
[ [ OCTAVE_KEY_MSB_OFFSET, 0xf0, 0x20, 0x00 ]]),
# This pattern occurs when a channel has the registers for its
# operators initialised.
MatchPattern.new(InitChannel,
[ [SCALE_LEVEL_OFFSET, 0xe0], # First op
[FREQ_MULT_OFFSET, 0xe0],
[ATTACK_DECAY_OFFSET, 0xe0],
[SUSTAIN_RELEASE_OFFSET, 0xe0],
[WAVEFORM_SELECT_OFFSET, 0xe0],
[SCALE_LEVEL_OFFSET, 0xe0], # Second op
[FREQ_MULT_OFFSET, 0xe0],
[ATTACK_DECAY_OFFSET, 0xe0],
[SUSTAIN_RELEASE_OFFSET, 0xe0],
[WAVEFORM_SELECT_OFFSET, 0xe0],
[FEEDBACK_OFFSET, 0xf0] ]),
# Scale level change
MatchPattern.new(ScaleLevelChange,
[ [ SCALE_LEVEL_OFFSET, 0xe0 ] ]),
# Fallback basic register write:
MatchPattern.new(BasicRegisterWrite, [[0, 0]])
]
parse_cmdline
writes = parse_file($stdin)
offset = 0
parsed = []
channel_in_use = [ false ] * 9
channel_allocate_queue = [ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
while offset < writes.length
for pattern in MATCH_PATTERNS
match = pattern.matches(writes, offset)
if match != nil
parsed.push(match)
offset += pattern.length
if match.is_a?(KeyOn) and (match.channel >= 0 and match.channel < 9)
if !channel_in_use[match.channel]
expected = channel_allocate_queue[0]
channel_allocate_queue = (channel_allocate_queue[1,9] or [])
channel_in_use[match.channel] = true
puts "expect: #{expected}"
end
end
if match.is_a?(KeyOff) and (match.channel >= 0 and match.channel < 9)
channel_allocate_queue.push(match.channel)
channel_in_use[match.channel] = false
end
puts match
for r in match.reg_writes
puts "\t#{r}"
end
break
end
end
end