-
-
Save thatdutchguy/2106d68612977099ca8a73cb7caf6455 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class CommandProcessor | |
attr_reader :handler, :output | |
attr_accessor :instruments | |
def initialize | |
@handler = EventHandler.new | |
end | |
def tick(commands) | |
if commands.empty? | |
handler.slide | |
else | |
commands.each do |command| | |
type = command[:command] | |
public_send(type, command) | |
end | |
end | |
end | |
# ------------------------------------------------------------ | |
# | |
# Command handlers | |
# 0x80 | |
def note_off(params) | |
handler.key_off(params[:note]) | |
end | |
# 0x90 | |
def note_on(params) | |
if handler.keymap? | |
program = handler.program | |
keymap = instruments[program] | |
mappings = keymap[:instrument_map] | |
offset = params[:note] - keymap[:offset] - 0x14 | |
offset -= 4 | |
offset -= 1 while mappings.at(offset).eql?(program) | |
no = mappings.at(offset) | |
handler.load_instrument(instruments[no]) | |
end | |
handler.key_on(params[:note], params[:velocity]) | |
end | |
# 0xC0 | |
def program_change(params) | |
program = params[:program] | |
return if handler.program?(program) | |
instrument = instruments[program] | |
if instrument[:mode].eql? 0xFF | |
handler.enable_keymap | |
else | |
handler.disable_keymap | |
handler.load_instrument(instrument) | |
end | |
handler.program = program | |
end | |
# 0xE0 | |
def pitch_bend(params) | |
handler.bend_pitch(params[:bend]) | |
end | |
# 0xD0 | |
def channel_pressure(params) | |
handler.apply_aftertouch(params[:pressure]) | |
end | |
# 0xF0 | |
def track_end(params) | |
# do nothing | |
end | |
# delegators | |
def state=(state) | |
handler.state = state | |
end | |
def output=(bus) | |
handler.output = bus | |
end | |
end | |
# ------------------------------------------------------------ | |
# | |
# main HERAD logic | |
# only used by command processor | |
# | |
class EventHandler | |
include Constants | |
attr_accessor :state, :output | |
# :state - a simple hash (see Channel::State) holding the current channel state | |
# :output - where events are emitted to; needs to implement "<<(list_of_event_hashes)" | |
def slide | |
return unless state[:slide_remaining] > 0 | |
state[:slide_remaining] -= 1 | |
state[:bend] += state[:slide_amount] | |
note = state[:note] & 0x7F | |
return if note.zero? | |
note = (note - 0x18) & 0xFF | |
play(note % 12, note / 12, state[:bend]) | |
end | |
def key_off(midi_note) | |
note = transpose_note(midi_note) | |
return unless note.eql? state[:note] | |
state[:note] = 0 | |
emit(key_on: 0) | |
end | |
def key_on(midi_note, velocity) | |
update_levels(velocity) | |
kill_note unless state[:note].zero? | |
note = transpose_note(midi_note) | |
state[:note] = note | |
state[:bend] = BEND_CENTER | |
state[:slide_remaining] = state[:slide_duration] | |
note = (note - 0x18) & 0xFF | |
note = 0 if note > 0x60 | |
write(note % 12, note / 12, 0) | |
end | |
def bend_pitch(bend) | |
return if state[:note].zero? | |
note = (state[:note] - 0x18) & 0xFF | |
play(note % 12, note / 12, bend) | |
end | |
def apply_aftertouch(pressure) | |
return unless version? 1 | |
update_aftertouch(pressure) | |
end | |
def load_instrument(instrument) | |
state[:mod_base_level] = instrument[:mod_output_level] | |
state[:mod_output_level] = instrument[:mod_output_level] | |
state[:mod_output_scale] = instrument[:macro_mod_output_level_scaling] | |
state[:mod_aftertouch_scale] = instrument[:macro_mod_output_level_aftertouch] | |
state[:car_base_level] = instrument[:car_output_level] | |
state[:car_output_level] = instrument[:car_output_level] | |
state[:car_output_scale] = instrument[:macro_car_output_level_scaling] | |
state[:car_aftertouch_scale] = instrument[:macro_car_output_level_aftertouch] | |
state[:fb_base_level] = instrument[:feedback_modulation_factor] | |
state[:fb_output_level] = instrument[:feedback_modulation_factor] | |
state[:fb_output_scale] = instrument[:macro_feedback_scaling] | |
state[:fb_aftertouch_scale] = instrument[:macro_feedback_aftertouch] | |
state[:slide_duration] = instrument[:macro_slide_duration] | |
state[:slide_amount] = instrument[:macro_slide_amount] | |
state[:slide_type] = instrument[:macro_slide_type] | |
state[:transpose] = instrument[:macro_transpose] | |
writes = INSTRUMENT_REGISTERS.map { |reg| [reg, instrument[reg]] }.to_h | |
emit(writes) | |
end | |
# external access to state | |
def program=(no) | |
state[:program] = no | |
end | |
def program | |
state[:program] | |
end | |
def program?(no) | |
no.eql? program | |
end | |
def keymap? | |
!!state[:keymap] | |
end | |
def enable_keymap | |
state[:keymap] = true | |
end | |
def disable_keymap | |
state[:keymap] = false | |
end | |
def version?(v) | |
v.eql? version | |
end | |
def version | |
state[:version] | |
end | |
private | |
# FIXME: this method does some re-ordering and fixing of events, and it | |
# doesn't feel like the right place for it | |
def emit(events) | |
# invert synthesis mode | |
if events.has_key?(:synthesis_mode) | |
events[:synthesis_mode] = 1 - events[:synthesis_mode] | |
end | |
# ensure freq lsb is written first (not sure if this is needed) | |
lsb = events.delete :frequency_number_lsb | |
list = [] | |
list << { frequency_number_lsb: lsb } if lsb | |
list << events | |
output << list | |
end | |
def write(note, octave, detune) | |
# occasionally a coarse bend pushes a note out of range | |
return if (octave < 0) || (octave > 7) | |
f_num = FNUM_TABLE[note % 12] | |
f_num_msb = f_num >> 8 | |
f_num_lsb = f_num & 0xFF | |
f_num_lsb += detune | |
f_num_msb -= 1 if f_num_lsb < 0 | |
f_num_msb += 1 if f_num_lsb > 0xFF | |
emit( | |
frequency_number_lsb: f_num_lsb & 0xFF, | |
frequency_number_msb: f_num_msb & 0xFF, | |
block_number: octave, | |
key_on: 1 | |
) | |
end | |
def kill_note | |
emit( | |
frequency_number_msb: 0, | |
frequency_number_lsb: 0, | |
block_number: 0, | |
key_on: 0 | |
) | |
end | |
def transpose_note(note) | |
transpose = state[:transpose] | |
case version | |
when 1 | |
note += transpose | |
when 2 | |
tmp = (transpose - 0x31) & 0xFF | |
if tmp < 0x60 | |
note = tmp + 0x18 # fixed pitch | |
else | |
note += transpose | |
end | |
end | |
note & 0xFF | |
end | |
# called by slide and bend_pitch | |
def play(note, octave, bend) | |
return if state[:note].zero? | |
case state[:slide_type] | |
when 0 | |
play_with_fine_bend(note, octave, bend - BEND_CENTER) | |
when 1 | |
play_with_coarse_bend(note, octave, bend - BEND_CENTER) | |
end | |
end | |
def play_with_fine_bend(note, octave, bend) | |
if bend < 0 | |
offset = 0 | |
amount = -bend | |
note -= (amount >> 5) | |
else | |
offset = 1 | |
amount = bend + 1 | |
note += (amount >> 5) | |
end | |
if note < 0 | |
note += 12 | |
octave -= 1 | |
elsif note >= 12 | |
note -= 12 | |
octave += 1 | |
end | |
offset += note | |
value = FINE_BEND_LOOKUP[offset] | |
detune = value * ((amount << 3) & 0xFF) >> 8 | |
detune = -detune if bend < 0 | |
write(note, octave, detune) | |
end | |
def play_with_coarse_bend(note, octave, bend) | |
if bend < 0 | |
amount = -bend | |
note -= amount / 5 | |
else | |
amount = bend | |
note += amount / 5 | |
end | |
if note < 0 | |
note += 12 | |
octave -= 1 | |
elsif note >= 12 | |
note -= 12 | |
octave += 1 | |
end | |
offset = amount % 5 | |
offset += 5 if note >= 6 | |
detune = COARSE_BEND_LOOKUP[offset] | |
detune = -detune if bend < 0 | |
write(note, octave, detune) | |
end | |
# note-on output/feedback levels | |
def update_levels(velocity) | |
update_mod_level(velocity) | |
update_car_level(velocity) | |
update_feedback_level(velocity) | |
end | |
def update_mod_level(velocity) | |
base, scale = state.values_at(:mod_base_level, :mod_output_scale) | |
return if scale.zero? | |
if scale < 0 | |
amount = velocity | |
rshift = -(-scale - 4) | |
else | |
amount = 0x80 - velocity | |
rshift = -(scale - 4) | |
end | |
level = (base & 0x3F) + (amount >> rshift) | |
level = 0x3F if level > 0x3F | |
state[:mod_output_level] = level # needed for v1 aftertouch | |
emit(mod_output_level: level) | |
end | |
def update_car_level(velocity) | |
base, scale = state.values_at(:car_base_level, :car_output_scale) | |
return if scale.zero? | |
if scale < 0 | |
amount = velocity | |
rshift = 4 + scale | |
else | |
amount = 0x80 - velocity | |
rshift = 4 - scale | |
end | |
level = (base & 0x3F) + (amount >> rshift) | |
level = 0x3F if level > 0x3F | |
state[:car_output_level] = level # needed for v1 aftertouch | |
emit(car_output_level: level) | |
end | |
def update_feedback_level(velocity) | |
base, scale = state.values_at(:fb_base_level, :fb_output_scale) | |
if scale.zero? | |
state[:fb_output_level] = base | |
return | |
end | |
if scale < 0 | |
amount = velocity | |
rshift = -(-scale - 6) | |
else | |
amount = 0x80 - velocity | |
rshift = -(scale - 6) | |
end | |
# one extra shift here, in original driver things are offset | |
# by 1 bit (the synthesis mode or "connector" bit) | |
rshift += 1 | |
level = (base & 7) + (amount >> rshift) | |
level = 7 if level > 7 | |
level | |
state[:fb_output_level] = level | |
emit(feedback_modulation_factor: level) | |
end | |
# aftertouch output/feedback levels | |
def update_aftertouch(pressure) | |
update_mod_aftertouch(pressure) | |
update_car_aftertouch(pressure) | |
update_feedback_aftertouch(pressure) | |
end | |
def update_mod_aftertouch(pressure) | |
base, scale = state.values_at(:mod_output_level, :mod_aftertouch_scale) | |
return if scale.zero? | |
if scale < 0 | |
amount = 0x80 - pressure | |
rshift = -(-scale - 4) | |
else | |
amount = pressure | |
rshift = -(scale - 4) | |
end | |
level = (base & 0x3F) - (amount >> rshift) | |
level = 0 if level < 0 | |
emit(mod_output_level: level) | |
end | |
def update_car_aftertouch(pressure) | |
base, scale = state.values_at(:car_output_level, :car_aftertouch_scale) | |
return if scale.zero? | |
if scale < 0 | |
amount = 0x80 - pressure | |
rshift = 4 + scale | |
else | |
amount = pressure | |
rshift = 4 - scale | |
end | |
level = (base & 0x3F) - (amount >> rshift) | |
level = 0 if level < 0 | |
emit(car_output_level: level) | |
end | |
def update_feedback_aftertouch(pressure) | |
base, scale = state.values_at(:fb_output_level, :fb_aftertouch_scale) | |
return if scale.zero? | |
if scale < 0 | |
amount = pressure | |
rshift = -(-scale - 6) | |
else | |
amount = 0x80 - pressure | |
rshift = -(scale - 6) | |
end | |
# one extra shift here, in original driver things are offset | |
# by 1 bit (the synthesis mode or "connector" bit) | |
rshift += 1 | |
level = (base & 7) + (amount >> rshift) | |
level = 7 if level > 7 | |
level | |
emit(feedback_modulation_factor: level) | |
end | |
end | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment