Skip to content

Instantly share code, notes, and snippets.

@thatdutchguy
Created August 15, 2017 14:59
Show Gist options
  • Save thatdutchguy/2106d68612977099ca8a73cb7caf6455 to your computer and use it in GitHub Desktop.
Save thatdutchguy/2106d68612977099ca8a73cb7caf6455 to your computer and use it in GitHub Desktop.
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