class MTK::IO::MIDIOutput

Provides a scheduler and common behavior for realtime MIDI output, using the gamelan gem for scheduling.

@abstract Subclasses must provide {.devices}, {.devices_by_name}, {#note_on}, {#note_off}, {#control}, {#channel_pressure}, {#poly_pressure}, {#bend}, and {#program} to implement a MIDI output.

Attributes

device[R]

The underlying output device implementation wrapped by this class. The device type depends on the platform.

Public Class Methods

available_output_types() click to toggle source
# File lib/mtk/io/midi_output.rb, line 19
def available_output_types
  @available_output_types ||= []
end
devices() click to toggle source

All available output devices.

# File lib/mtk/io/midi_output.rb, line 32
def devices
  @devices ||= available_output_types.map{|output_type| output_type.devices }.flatten
end
devices_by_name() click to toggle source

Maps output device names to the output device.

# File lib/mtk/io/midi_output.rb, line 37
def devices_by_name
  @devices_by_name ||= (
    available_output_types.each_with_object( Hash.new ) do |output_type,hash|
      hash.merge!( output_type.devices_by_name )
    end
  )
end
find_by_name(name) click to toggle source
# File lib/mtk/io/midi_output.rb, line 45
def find_by_name(name)
  if name.is_a? Regexp
    matching_name = devices_by_name.keys.find{|device_name| device_name =~ name }
    device = devices_by_name[matching_name]
  else
    device = devices_by_name[name.to_s]
  end
  open(device) if device
end
inherited(subclass) click to toggle source
# File lib/mtk/io/midi_output.rb, line 15
def inherited(subclass)
  available_output_types << subclass
end
open(device) click to toggle source
# File lib/mtk/io/midi_output.rb, line 55
def open(device)
  output_type = output_types_by_device[device]
  output_type.new(device) if output_type
end
output_types_by_device() click to toggle source
# File lib/mtk/io/midi_output.rb, line 23
def output_types_by_device
  @output_types_by_device ||= (
    available_output_types.each_with_object( Hash.new ) do |output_type,hash|
      output_type.devices.each{|device| hash[device] = output_type }
    end
  )
end

Private Class Methods

new(output_device, options={}) click to toggle source
# File lib/mtk/io/midi_output.rb, line 62
def initialize(output_device, options={})
  @device = output_device
  @device.open
  @options = options
end

Public Instance Methods

name() click to toggle source
# File lib/mtk/io/midi_output.rb, line 77
def name
  @device.name
end
play(anything, options={}) click to toggle source
# File lib/mtk/io/midi_output.rb, line 81
def play(anything, options={})
  timeline = case anything
    when MTK::Events::Timeline then anything
    when Hash then  MTK::Events::Timeline.from_h anything
    when Enumerable,MTK::Events::Event then  MTK::Events::Timeline.from_h(0 => anything)
    else raise "#{self.class}.play() doesn't understand #{anything} (#{anything.class})"
  end
  timeline = timeline.flatten

  scheduler_rate = options.fetch :scheduler_rate, 500 # default: 500 Hz
  trailing_buffer = options.fetch :trailing_buffer, 2 # default: continue playing for 2 beats after the end of the timeline
  in_background = options.fetch :background, false # default: don't run in background Thread
  bpm = options.fetch :bmp, 120 # default: 120 beats per minute

  @scheduler = Gamelan::Scheduler.new :tempo => bpm, :rate => scheduler_rate

  timeline.each do |time,events|
    events.each do |event|
      next if event.rest?

      channel = event.channel || 0

      case event.type
        when :note
          pitch = event.midi_pitch
          velocity = event.velocity
          duration = event.duration.to_f
          # Set a lower priority (via level:1) for note-ons, so legato notes at the same pitch don't
          # prematurely chop off the next note, by ensuring all note-offs at the same timepoint occur first.
          @scheduler.at(time, level: 1) { note_on(pitch,velocity,channel) }
          @scheduler.at(time + duration) { note_on(pitch,0,channel) }
          # TODO: use a proper note off message whenever we support off velocities
          #@scheduler.at(time + duration) { note_off(pitch,velocity,channel) }

        when :control
          @scheduler.at(time) { control(event.number, event.midi_value, channel) }

        when :pressure
          if event.number
            @scheduler.at(time) { poly_pressure(event.number, event.midi_value, channel) }
          else
            @scheduler.at(time) { channel_pressure(event.midi_value, channel) }
          end

        when :bend
          @scheduler.at(time) { bend(event.midi_value, channel) }

        when :program
          @scheduler.at(time) { program(event.number, channel) }
      end
    end
  end

  end_time = timeline.times.last
  final_events = timeline[end_time]
  max_length = final_events.inject(0) {|max,event| len = event.length; max > len ? max : len } || 0
  end_time += max_length + trailing_buffer
  @scheduler.at(end_time) { @scheduler.stop }

  thread = @scheduler.run
  thread.join if not in_background
end

Protected Instance Methods

bend(midi_value, channel) click to toggle source

Send a pitch bend event to the MIDI output.

# File lib/mtk/io/midi_output.rb, line 176
def bend(midi_value, channel)
  [:bend, midi_value, channel]
end
channel_pressure(midi_value, channel) click to toggle source

Send a channel pressure event to the MIDI output.

# File lib/mtk/io/midi_output.rb, line 171
def channel_pressure(midi_value, channel)
  [:channel_pressure, midi_value, channel]
end
control(number, midi_value, channel) click to toggle source

Send a control change event to the MIDI output

# File lib/mtk/io/midi_output.rb, line 161
def control(number, midi_value, channel)
  [:control, number, midi_value, channel]
end
note_off(midi_pitch, velocity, channel) click to toggle source

Send a note off event to the MIDI output

# File lib/mtk/io/midi_output.rb, line 156
def note_off(midi_pitch, velocity, channel)
  [:note_off, midi_pitch, velocity, channel]
end
note_on(midi_pitch, velocity, channel) click to toggle source

Send a note on event to the MIDI output

# File lib/mtk/io/midi_output.rb, line 151
def note_on(midi_pitch, velocity, channel)
  [:note_on, midi_pitch, velocity, channel] # stubbed data for testing purposes
end
poly_pressure(midi_pitch, midi_value, channel) click to toggle source

Send a poly pressure event to the MIDI output.

# File lib/mtk/io/midi_output.rb, line 166
def poly_pressure(midi_pitch, midi_value, channel)
  [:poly_pressure, midi_pitch, midi_value, channel]
end
program(number, channel) click to toggle source

Send a program change event to the MIDI output.

# File lib/mtk/io/midi_output.rb, line 181
def program(number, channel)
  [:program, number, channel]
end