class MTK::IO::MIDIFile
MIDI
file I/O: reads MIDI
files into {Events::Timeline}s and writes {Events::Timeline}s to MIDI
files. @note This class is optional and only available if you require ‘mtk/midi/file’.
It depends on the 'midilib' gem.
Public Class Methods
new(file)
click to toggle source
# File lib/mtk/io/midi_file.rb, line 10 def initialize file if file.respond_to? :path @file = file.path else @file = file.to_s end end
Public Instance Methods
to_timelines()
click to toggle source
Read a MIDI
file into an Array of {Events::Timeline}s
@return [Timeline]
# File lib/mtk/io/midi_file.rb, line 22 def to_timelines timelines = [] ::File.open(@file, 'rb') do |f| sequence = ::MIDI::Sequence.new sequence.read(f) pulses_per_beat = sequence.ppqn.to_f track_idx = -1 sequence.each do |track| track_idx += 1 timeline = MTK::Events::Timeline.new note_ons = {} #puts "TRACK #{track_idx}" track.each do |event| #puts "#{event.class}: #{event} @#{event.time_from_start}" time = (event.time_from_start)/pulses_per_beat case event when ::MIDI::NoteOn note_ons[event.note] = [time,event] # TODO: handle note ons with velocity 0 as a note off (use output from Logic Pro as a test case) # This isn't actually necessary right now as midilibs seqreader#note_on automatically # converts note ons with velocity 0 to note offs. In the future, for full off velocity support, # I'll need to monkey patch midilib and update the code here when ::MIDI::NoteOff on_time,on_event = note_ons.delete(event.note) if on_event duration = time - on_time note = MTK::Events::Note.from_midi(event.note, on_event.velocity, duration, event.channel) timeline.add on_time, note end when ::MIDI::Controller, ::MIDI::PolyPressure, ::MIDI::ChannelPressure, ::MIDI::PitchBend, ::MIDI::ProgramChange timeline.add time, MTK::Events::Parameter.from_midi(*event.data_as_bytes) when ::MIDI::Tempo # Not sure if event.tempo needs to be converted? TODO: test! timeline.add time, MTK::Events::Parameter.new(:tempo, :value => event.tempo) end end timelines << timeline end end timelines end
write(anything)
click to toggle source
# File lib/mtk/io/midi_file.rb, line 71 def write(anything) case anything when MTK::Events::Timeline then write_timeline(anything) when Enumerable then write_timelines(anything) else raise "#{self.class}#write doesn't understand #{anything.class}" end end
write_timeline(timeline, parent_sequence=nil)
click to toggle source
Write the Timeline as a MIDI
file
@param timeline [Timeline]
# File lib/mtk/io/midi_file.rb, line 88 def write_timeline(timeline, parent_sequence=nil) sequence = parent_sequence || ::MIDI::Sequence.new clock_rate = sequence.ppqn track = add_track sequence timeline.each do |time,events| time *= clock_rate events.each do |event| next if event.rest? channel = (event.channel || 1) - 1 # midilib seems to count channels from 0, hence the -1 case event.type when :note pitch, velocity = event.midi_pitch, event.velocity add_event track, time => note_on(channel, pitch, velocity) duration = event.duration_in_pulses(clock_rate) # TODO: use note_off events when supporting off velocities # add_event track, time+duration => note_off(channel, pitch, velocity) # NOTE: cannot test the following line of code properly right now, because midilib automatically converts # note ons with velocity 0 to note offs when reading files. See comments in #to_timelines in this file add_event track, time+duration => note_on(channel, pitch, 0) # we use note ons with velocity 0 to indicate no off velocity when :control add_event track, time => cc(channel, event.number, event.midi_value) when :pressure if event.number add_event track, time => poly_pressure(channel, event.number, event.midi_value) else add_event track, time => channel_pressure(channel, event.midi_value) end when :bend add_event track, time => pitch_bend(channel, event.midi_value) when :program add_event track, time => program(channel, event.midi_value) when :tempo add_event track, time => tempo(event.value) end end end track.recalc_delta_from_times write_to_disk sequence unless parent_sequence end
write_timelines(timelines, parent_sequence=nil)
click to toggle source
# File lib/mtk/io/midi_file.rb, line 79 def write_timelines(timelines, parent_sequence=nil) sequence = parent_sequence || ::MIDI::Sequence.new timelines.each{|timeline| write_timeline(timeline, sequence) } write_to_disk sequence unless parent_sequence end
Private Instance Methods
add_event(track, event_hash)
click to toggle source
# File lib/mtk/io/midi_file.rb, line 200 def add_event track, event_hash for time, event in event_hash event.time_from_start = time.round # MIDI file event times must be in whole number pulses (typically 480 or 960 per quarter note) track.events << event event end end
add_track(sequence, opts={})
click to toggle source
# File lib/mtk/io/midi_file.rb, line 191 def add_track sequence, opts={} track = ::MIDI::Track.new(sequence) track_name = opts[:name] track.name = track_name if track_name sequence.tracks << track sequence.format = if sequence.tracks.length > 1 then 1 else 0 end track end
cc(channel, controller, value)
click to toggle source
# File lib/mtk/io/midi_file.rb, line 175 def cc(channel, controller, value) ::MIDI::Controller.new(channel, controller, value) end
channel_pressure(channel, value)
click to toggle source
# File lib/mtk/io/midi_file.rb, line 183 def channel_pressure(channel, value) ::MIDI::ChannelPressure(channel, value) end
note_off(channel, pitch, velocity)
click to toggle source
# File lib/mtk/io/midi_file.rb, line 171 def note_off(channel, pitch, velocity) ::MIDI::NoteOff.new(channel, pitch.to_i, velocity) end
note_on(channel, pitch, velocity)
click to toggle source
# File lib/mtk/io/midi_file.rb, line 167 def note_on(channel, pitch, velocity) ::MIDI::NoteOn.new(channel, pitch.to_i, velocity) end
pitch_bend(channel, value)
click to toggle source
# File lib/mtk/io/midi_file.rb, line 187 def pitch_bend(channel, value) ::MIDI::PitchBend.new(channel, value) end
poly_pressure(channel, pitch, value)
click to toggle source
# File lib/mtk/io/midi_file.rb, line 179 def poly_pressure(channel, pitch, value) ::MIDI::PolyPressure(channel, pitch.to_i, value) end
print_midi(sequence)
click to toggle source
# File lib/mtk/io/midi_file.rb, line 147 def print_midi sequence sequence.each do |track| puts "\n*** track \"#{track.name}\"" puts "#{track.events.length} events" track.each do |event| puts "#{event.to_s} (#{event.time_from_start})" end end end
program(channel, program_number)
click to toggle source
# File lib/mtk/io/midi_file.rb, line 163 def program(channel, program_number) ::MIDI::ProgramChange.new(channel, program_number) end
tempo(bpm)
click to toggle source
Set tempo in terms of Quarter Notes per Minute (aka BPM)
# File lib/mtk/io/midi_file.rb, line 158 def tempo(bpm) ms_per_quarter_note = ::MIDI::Tempo.bpm_to_mpq(bpm) ::MIDI::Tempo.new(ms_per_quarter_note) end
write_to_disk(sequence)
click to toggle source
# File lib/mtk/io/midi_file.rb, line 142 def write_to_disk(sequence) puts "Writing file #{@file}" unless $__RUNNING_RSPEC_TESTS__ ::File.open(@file, 'wb') { |f| sequence.write f } end