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
The underlying output device implementation wrapped by this class. The device type depends on the platform.
Public Class Methods
# File lib/mtk/io/midi_output.rb, line 19 def available_output_types @available_output_types ||= [] end
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
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
# 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
# File lib/mtk/io/midi_output.rb, line 15 def inherited(subclass) available_output_types << subclass end
# 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
# 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
# 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
# File lib/mtk/io/midi_output.rb, line 77 def name @device.name end
# 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
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
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
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
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
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
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
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