require 'patchmaster/consts' module PM # A Connection connects an InputInstrument to an OutputInstrument. Whenever # MIDI data arrives at the InputInstrument it is optionally modified or # filtered, then the remaining modified data is sent to the # OutputInstrument. class Connection attr_accessor :input, :input_chan, :output, :output_chan, :bank, :pc_prog, :zone, :xpose, :filter # If input_chan is nil than all messages from input will be sent to # output. # # All channels (input_chan, output_chan, etc.) are 1-based here but are # turned into 0-based channels for later use. def initialize(input, input_chan, output, output_chan, filter=nil, opts={}) @input, @input_chan, @output, @output_chan, @filter = input, input_chan, output, output_chan, filter @bank, @pc_prog, @zone, @xpose = opts[:bank], opts[:pc_prog], opts[:zone], opts[:xpose] @input_chan -= 1 if @input_chan @output_chan -= 1 if @output_chan end def start(start_bytes=nil) bytes = [] bytes += start_bytes if start_bytes # Bank select uses MSB if we're only sending one byte bytes += [CONTROLLER + @output_chan, CC_BANK_SELECT+32, @bank] if @bank bytes += [PROGRAM_CHANGE + @output_chan, @pc_prog] if @pc_prog midi_out(bytes) unless bytes.empty? @input.add_connection(self) end def stop(stop_bytes=nil) midi_out(stop_bytes) if stop_bytes @input.remove_connection(self) end def accept_from_input?(bytes) return true if @input_chan == nil return true unless bytes.channel? bytes.note? && bytes.channel == @input_chan end # Returns true if the +@zone+ is nil (allowing all notes throught) or if # +@zone+ is a Range and +note+ is inside +@zone+. def inside_zone?(note) @zone == nil || @zone.include?(note) end # The workhorse. Ignore bytes that aren't from our input, or are outside # the zone. Change to output channel. Filter. # # Note that running bytes are not handled, but unimidi doesn't seem to use # them anyway. # # Finally, we go through gyrations to avoid duping bytes unless they are # actually modified in some way. def midi_in(bytes) return unless accept_from_input?(bytes) bytes_duped = false high_nibble = bytes.high_nibble case high_nibble when NOTE_ON, NOTE_OFF, POLY_PRESSURE return unless inside_zone?(bytes[1]) if bytes[0] != high_nibble + @output_chan || (@xpose && @xpose != 0) bytes = bytes.dup bytes_duped = true end bytes[0] = high_nibble + @output_chan bytes[1] = ((bytes[1] + @xpose) & 0xff) if @xpose when CONTROLLER, PROGRAM_CHANGE, CHANNEL_PRESSURE, PITCH_BEND if bytes[0] != high_nibble + @output_chan bytes = bytes.dup bytes_duped = true bytes[0] = high_nibble + @output_chan end end # We can't tell if a filter will modify the bytes, so we have to assume # they will be. If we didn't, we'd have to rely on the filter duping the # bytes and returning the dupe. if @filter if !bytes_duped bytes = bytes.dup bytes_duped = true end bytes = @filter.call(self, bytes) end if bytes && bytes.size > 0 midi_out(bytes) end end def midi_out(bytes) @output.midi_out(bytes) end def pc? @pc_prog != nil end def note_num_to_name(n) oct = (n / 12) - 1 note = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'][n % 12] "#{note}#{oct}" end def to_s str = "#{@input.name} ch #{@input_chan ? @input_chan+1 : 'all'} -> #{@output.name} ch #{@output_chan+1}" str << "; pc #@pc_prog" if pc? str << "; xpose #@xpose" if @xpose str << "; zone #{note_num_to_name(@zone.begin)}..#{note_num_to_name(@zone.end)}" if @zone str end end end # PM