mirror of
https://github.com/chocolate-doom/research.git
synced 2024-12-01 08:31:59 +00:00
2b81dd217f
when a voice is programmed. Subversion-branch: /research Subversion-revision: 1698
499 lines
11 KiB
Ruby
Executable file
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
|
|
|