midilib 2.0.2 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/ChangeLog +2 -1
- data/Credits +44 -2
- data/README.rdoc +42 -33
- data/Rakefile +36 -53
- data/TODO.rdoc +13 -2
- data/examples/from_scratch.rb +4 -6
- data/examples/measures_mbt.rb +11 -11
- data/examples/print_program_changes.rb +11 -11
- data/examples/reader2text.rb +191 -190
- data/examples/seq2text.rb +18 -18
- data/examples/split.rb +21 -20
- data/examples/strings.rb +15 -15
- data/examples/transpose.rb +41 -42
- data/install.rb +53 -34
- data/lib/midilib/consts.rb +406 -408
- data/lib/midilib/event.rb +335 -306
- data/lib/midilib/info.rb +5 -7
- data/lib/midilib/io/midifile.rb +424 -452
- data/lib/midilib/io/seqreader.rb +187 -192
- data/lib/midilib/io/seqwriter.rb +151 -147
- data/lib/midilib/measure.rb +78 -80
- data/lib/midilib/mergesort.rb +39 -0
- data/lib/midilib/sequence.rb +99 -86
- data/lib/midilib/track.rb +71 -118
- data/lib/midilib/utils.rb +17 -20
- data/lib/midilib.rb +5 -5
- data/test/event_equality.rb +50 -52
- data/test/test_event.rb +120 -124
- data/test/test_io.rb +107 -40
- data/test/test_mergesort.rb +37 -0
- data/test/test_midifile.rb +6 -19
- data/test/test_sequence.rb +64 -52
- data/test/test_track.rb +126 -155
- data/test/test_varlen.rb +23 -27
- metadata +20 -22
data/lib/midilib/sequence.rb
CHANGED
@@ -1,12 +1,10 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
require_relative 'io/seqreader'
|
2
|
+
require_relative 'io/seqwriter'
|
3
|
+
require_relative 'measure'
|
4
4
|
|
5
5
|
module MIDI
|
6
|
-
|
7
|
-
|
8
|
-
class Sequence
|
9
|
-
|
6
|
+
# A MIDI::Sequence contains MIDI::Track objects.
|
7
|
+
class Sequence
|
10
8
|
include Enumerable
|
11
9
|
|
12
10
|
UNNAMED = 'Unnamed Sequence'
|
@@ -33,7 +31,7 @@ class Sequence
|
|
33
31
|
# Pulses (i.e. clocks) Per Quarter Note resolution for the sequence
|
34
32
|
attr_accessor :ppqn
|
35
33
|
# The MIDI file format (0, 1, or 2)
|
36
|
-
attr_accessor :format
|
34
|
+
attr_accessor :format
|
37
35
|
attr_accessor :numer, :denom, :clocks, :qnotes
|
38
36
|
# The class to use for reading MIDI from a stream. The default is
|
39
37
|
# MIDI::IO::SeqReader. You can change this at any time.
|
@@ -43,43 +41,49 @@ class Sequence
|
|
43
41
|
attr_accessor :writer_class
|
44
42
|
|
45
43
|
def initialize
|
46
|
-
|
47
|
-
|
44
|
+
@tracks = []
|
45
|
+
@ppqn = 480
|
48
46
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
47
|
+
# Time signature
|
48
|
+
@numer = 4 # Numer + denom = 4/4 time default
|
49
|
+
@denom = 2
|
50
|
+
@clocks = 24 # Bug fix Nov 11, 2007 - this is not the same as ppqn!
|
51
|
+
@qnotes = 8
|
54
52
|
|
55
|
-
|
56
|
-
|
53
|
+
@reader_class = IO::SeqReader
|
54
|
+
@writer_class = IO::SeqWriter
|
57
55
|
end
|
58
56
|
|
59
57
|
# Sets the time signature.
|
60
58
|
def time_signature(numer, denom, clocks, qnotes)
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
59
|
+
@numer = numer
|
60
|
+
@denom = denom
|
61
|
+
@clocks = clocks
|
62
|
+
@qnotes = qnotes
|
65
63
|
end
|
66
64
|
|
67
65
|
# Returns the song tempo in beats per minute.
|
68
66
|
def beats_per_minute
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
67
|
+
return DEFAULT_TEMPO if @tracks.nil? || @tracks.empty?
|
68
|
+
|
69
|
+
event = @tracks.first.events.detect { |e| e.is_a?(MIDI::Tempo) }
|
70
|
+
event ? Tempo.mpq_to_bpm(event.tempo) : DEFAULT_TEMPO
|
71
|
+
end
|
72
|
+
alias bpm beats_per_minute
|
73
|
+
alias tempo beats_per_minute
|
74
|
+
|
75
|
+
# Pulses (also called ticks) are the units of delta times and event
|
76
|
+
# time_from_start values. This method converts a number of pulses to a
|
77
|
+
# float value that is a time in seconds.
|
78
|
+
def pulses_to_seconds(pulses)
|
79
|
+
(pulses.to_f / @ppqn.to_f / beats_per_minute) * 60.0
|
74
80
|
end
|
75
|
-
alias_method :bpm, :beats_per_minute
|
76
|
-
alias_method :tempo, :beats_per_minute
|
77
81
|
|
78
82
|
# Given a note length name like "whole", "dotted quarter", or "8th
|
79
83
|
# triplet", return the length of that note in quarter notes as a delta
|
80
84
|
# time.
|
81
85
|
def note_to_delta(name)
|
82
|
-
|
86
|
+
length_to_delta(note_to_length(name))
|
83
87
|
end
|
84
88
|
|
85
89
|
# Given a note length name like "whole", "dotted quarter", or "8th
|
@@ -94,98 +98,107 @@ class Sequence
|
|
94
98
|
def note_to_length(name)
|
95
99
|
name.strip!
|
96
100
|
name =~ /^(dotted)?(.*?)(triplet)?$/
|
97
|
-
dotted
|
101
|
+
dotted = Regexp.last_match(1)
|
102
|
+
note_name = Regexp.last_match(2)
|
103
|
+
triplet = Regexp.last_match(3)
|
98
104
|
note_name.strip!
|
99
105
|
mult = 1.0
|
100
106
|
mult = 1.5 if dotted
|
101
107
|
mult /= 3.0 if triplet
|
102
108
|
len = NOTE_TO_LENGTH[note_name]
|
103
109
|
raise "Sequence.note_to_length: \"#{note_name}\" not understood in \"#{name}\"" unless len
|
104
|
-
|
110
|
+
|
111
|
+
len * mult
|
105
112
|
end
|
106
113
|
|
107
114
|
# Translates +length+ (a multiple of a quarter note) into a delta time.
|
108
115
|
# For example, 1 is a quarter note, 1.0/32.0 is a 32nd note, 1.5 is a
|
109
116
|
# dotted quarter, etc. Be aware when using division; 1/32 is zero due to
|
110
117
|
# integer mathematics and rounding. Use floating-point numbers like 1.0
|
111
|
-
# and 32.0. This method always returns an integer
|
118
|
+
# and 32.0. This method always returns an integer by calling `.round` on
|
119
|
+
# the floating-point result.
|
112
120
|
#
|
113
121
|
# See also note_to_delta and note_to_length.
|
114
122
|
def length_to_delta(length)
|
115
|
-
|
123
|
+
(@ppqn * length).round
|
116
124
|
end
|
117
125
|
|
118
126
|
# Returns the name of the first track (track zero). If there are no
|
119
127
|
# tracks, returns UNNAMED.
|
120
128
|
def name
|
121
|
-
|
122
|
-
|
129
|
+
return UNNAMED if @tracks.empty?
|
130
|
+
|
131
|
+
@tracks.first.name
|
123
132
|
end
|
124
133
|
|
125
134
|
# Hands the name to the first track. Does nothing if there are no tracks.
|
126
135
|
def name=(name)
|
127
|
-
|
128
|
-
|
136
|
+
return if @tracks.empty?
|
137
|
+
|
138
|
+
@tracks.first.name = name
|
129
139
|
end
|
130
140
|
|
131
141
|
# Reads a MIDI stream.
|
132
|
-
def read(io,
|
133
|
-
|
134
|
-
|
142
|
+
def read(io, &block) # :yields: track, num_tracks, index
|
143
|
+
reader = @reader_class.new(self, &block)
|
144
|
+
reader.read_from(io)
|
135
145
|
end
|
136
146
|
|
137
|
-
# Writes to a MIDI stream.
|
138
|
-
def write(io,
|
139
|
-
|
140
|
-
|
147
|
+
# Writes to a MIDI stream. +midi_format+ defaults to 1.
|
148
|
+
def write(io, midi_format = 1, &block) # :yields: track, num_tracks, index
|
149
|
+
writer = @writer_class.new(self, midi_format, &block)
|
150
|
+
writer.write_to(io)
|
141
151
|
end
|
142
152
|
|
143
153
|
# Iterates over the tracks.
|
144
|
-
def each
|
145
|
-
|
154
|
+
def each(&block) # :yields: track
|
155
|
+
@tracks.each(&block)
|
146
156
|
end
|
147
157
|
|
148
158
|
# Returns a Measures object, which is an array container for all measures
|
149
159
|
# in the sequence
|
150
|
-
def get_measures
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
160
|
+
def get_measures
|
161
|
+
# Collect time sig events and scan for last event time
|
162
|
+
time_sigs = []
|
163
|
+
max_pos = 0
|
164
|
+
@tracks.each do |t|
|
165
|
+
t.each do |e|
|
166
|
+
time_sigs << e if e.is_a?(MIDI::TimeSig)
|
167
|
+
max_pos = e.time_from_start if e.time_from_start > max_pos
|
168
|
+
end
|
169
|
+
end
|
170
|
+
time_sigs.sort { |x, y| x.time_from_start <=> y.time_from_start }
|
171
|
+
|
172
|
+
# Add a "fake" time sig event at the very last position of the sequence,
|
173
|
+
# just to make sure the whole sequence is calculated.
|
174
|
+
t = MIDI::TimeSig.new(4, 2, 24, 8, 0)
|
175
|
+
t.time_from_start = max_pos
|
176
|
+
time_sigs << t
|
177
|
+
|
178
|
+
# Default to 4/4
|
179
|
+
measure_length = @ppqn * 4
|
180
|
+
oldnumer = 4
|
181
|
+
olddenom = 2
|
182
|
+
oldbeats = 24
|
183
|
+
|
184
|
+
measures = MIDI::Measures.new(max_pos, @ppqn)
|
185
|
+
curr_pos = 0
|
186
|
+
curr_meas_no = 1
|
187
|
+
time_sigs.each do |te|
|
188
|
+
meas_count = (te.time_from_start - curr_pos) / measure_length
|
189
|
+
meas_count += 1 if (te.time_from_start - curr_pos) % measure_length > 0
|
190
|
+
1.upto(meas_count) do |i|
|
191
|
+
measures << MIDI::Measure.new(curr_meas_no, curr_pos, measure_length,
|
192
|
+
oldnumer, olddenom, oldbeats)
|
193
|
+
curr_meas_no += 1
|
194
|
+
curr_pos += measure_length
|
195
|
+
end
|
196
|
+
oldnumer = te.numerator
|
197
|
+
olddenom = te.denominator
|
198
|
+
oldbeats = te.metronome_ticks
|
199
|
+
measure_length = te.measure_duration(@ppqn)
|
200
|
+
end
|
201
|
+
measures
|
188
202
|
end
|
189
|
-
|
190
|
-
end
|
203
|
+
end
|
191
204
|
end
|
data/lib/midilib/track.rb
CHANGED
@@ -1,60 +1,17 @@
|
|
1
|
-
|
1
|
+
require_relative 'event'
|
2
|
+
require_relative 'mergesort'
|
2
3
|
|
3
4
|
module MIDI
|
4
|
-
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
|
11
|
-
#
|
12
|
-
|
13
|
-
|
14
|
-
def mergesort(&cmp)
|
15
|
-
if cmp == nil
|
16
|
-
cmp = lambda { |a, b| a <=> b }
|
17
|
-
end
|
18
|
-
if size <= 1
|
19
|
-
self.dup
|
20
|
-
else
|
21
|
-
halves = split.map{ |half|
|
22
|
-
half.mergesort(&cmp)
|
23
|
-
}
|
24
|
-
merge(*halves, &cmp)
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
protected
|
29
|
-
def split
|
30
|
-
n = (length / 2).floor - 1
|
31
|
-
[self[0..n], self[n+1..-1]]
|
32
|
-
end
|
33
|
-
|
34
|
-
def merge(first, second, &predicate)
|
35
|
-
result = []
|
36
|
-
until first.empty? || second.empty?
|
37
|
-
if predicate.call(first.first, second.first) <= 0
|
38
|
-
result << first.shift
|
39
|
-
else
|
40
|
-
result << second.shift
|
41
|
-
end
|
42
|
-
end
|
43
|
-
result.concat(first).concat(second)
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
# A Track is a list of events.
|
48
|
-
#
|
49
|
-
# When you modify the +events+ array, make sure to call recalc_times so
|
50
|
-
# each Event gets its +time_from_start+ recalculated.
|
51
|
-
#
|
52
|
-
# A Track also holds a bitmask that specifies the channels used by the track.
|
53
|
-
# This bitmask is set when the track is read from the MIDI file by an
|
54
|
-
# IO::SeqReader but is _not_ kept up to date by any other methods.
|
55
|
-
|
56
|
-
class Track
|
57
|
-
|
5
|
+
# A Track is a list of events.
|
6
|
+
#
|
7
|
+
# When you modify the +events+ array, make sure to call recalc_times so
|
8
|
+
# each Event gets its +time_from_start+ recalculated.
|
9
|
+
#
|
10
|
+
# A Track also holds a bitmask that specifies the channels used by the track.
|
11
|
+
# This bitmask is set when the track is read from the MIDI file by an
|
12
|
+
# IO::SeqReader but is _not_ kept up to date by any other methods.
|
13
|
+
|
14
|
+
class Track
|
58
15
|
include Enumerable
|
59
16
|
|
60
17
|
UNNAMED = 'Unnamed'
|
@@ -63,62 +20,59 @@ class Track
|
|
63
20
|
attr_reader :sequence
|
64
21
|
|
65
22
|
def initialize(sequence)
|
66
|
-
|
67
|
-
|
23
|
+
@sequence = sequence
|
24
|
+
@events = []
|
68
25
|
|
69
|
-
|
70
|
-
|
71
|
-
|
26
|
+
# Bitmask of all channels used. Set when track is read in from
|
27
|
+
# a MIDI file.
|
28
|
+
@channels_used = 0
|
29
|
+
@instrument = nil
|
72
30
|
end
|
73
31
|
|
74
32
|
# Return track name. If there is no name, return UNNAMED.
|
75
33
|
def name
|
76
|
-
|
77
|
-
|
78
|
-
}
|
79
|
-
return event ? event.data_as_str : UNNAMED
|
34
|
+
event = @events.detect { |e| e.is_a?(MetaEvent) && e.meta_type == META_SEQ_NAME }
|
35
|
+
event ? event.data_as_str : UNNAMED
|
80
36
|
end
|
81
37
|
|
82
38
|
# Set track name. Replaces or creates a name meta-event.
|
83
39
|
def name=(name)
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
@events[0, 0] = event
|
92
|
-
end
|
40
|
+
event = @events.detect { |e| e.is_a?(MetaEvent) && e.meta_type == META_SEQ_NAME }
|
41
|
+
if event
|
42
|
+
event.data = name
|
43
|
+
else
|
44
|
+
event = MetaEvent.new(META_SEQ_NAME, name, 0)
|
45
|
+
@events[0, 0] = event
|
46
|
+
end
|
93
47
|
end
|
94
48
|
|
95
49
|
def instrument
|
96
|
-
|
50
|
+
MetaEvent.bytes_as_str(@instrument)
|
97
51
|
end
|
98
52
|
|
99
53
|
def instrument=(str_or_bytes)
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
54
|
+
@instrument = case str_or_bytes
|
55
|
+
when String
|
56
|
+
MetaEvent.str_as_bytes(str_or_bytes)
|
57
|
+
else
|
58
|
+
str_or_bytes
|
59
|
+
end
|
106
60
|
end
|
107
61
|
|
108
62
|
# Merges an array of events into our event list. After merging, the
|
109
63
|
# events' time_from_start values are correct so you don't need to worry
|
110
64
|
# about calling recalc_times.
|
111
65
|
def merge(event_list)
|
112
|
-
|
66
|
+
@events = merge_event_lists(@events, event_list)
|
113
67
|
end
|
114
68
|
|
115
69
|
# Merges two event arrays together. Does not modify this track.
|
116
70
|
def merge_event_lists(list1, list2)
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
71
|
+
recalc_times(0, list1)
|
72
|
+
recalc_times(0, list2)
|
73
|
+
list = list1 + list2
|
74
|
+
recalc_delta_from_times(0, list)
|
75
|
+
list
|
122
76
|
end
|
123
77
|
|
124
78
|
# Quantize every event. length_or_note is either a length (1 = quarter,
|
@@ -128,48 +82,48 @@ class Track
|
|
128
82
|
# Since each event's time_from_start is modified, we call
|
129
83
|
# recalc_delta_from_times after each event quantizes itself.
|
130
84
|
def quantize(length_or_note)
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
85
|
+
delta = case length_or_note
|
86
|
+
when String
|
87
|
+
@sequence.note_to_delta(length_or_note)
|
88
|
+
else
|
89
|
+
@sequence.length_to_delta(length_or_note.to_i)
|
90
|
+
end
|
91
|
+
@events.each { |event| event.quantize_to(delta) }
|
92
|
+
recalc_delta_from_times
|
139
93
|
end
|
140
94
|
|
141
95
|
# Recalculate start times for all events in +list+ from starting_at to
|
142
96
|
# end.
|
143
|
-
def recalc_times(starting_at=0, list
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
97
|
+
def recalc_times(starting_at = 0, list = @events)
|
98
|
+
t = starting_at == 0 ? 0 : list[starting_at - 1].time_from_start
|
99
|
+
list[starting_at..-1].each do |e|
|
100
|
+
t += e.delta_time
|
101
|
+
e.time_from_start = t
|
102
|
+
end
|
149
103
|
end
|
150
104
|
|
151
105
|
# The opposite of recalc_times: recalculates delta_time for each event
|
152
106
|
# from each event's time_from_start. This is useful, for example, when
|
153
107
|
# merging two event lists. As a side-effect, elements from starting_at
|
154
108
|
# are sorted by time_from_start.
|
155
|
-
def recalc_delta_from_times(starting_at=0, list
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
109
|
+
def recalc_delta_from_times(starting_at = 0, list = @events)
|
110
|
+
prev_time_from_start = 0
|
111
|
+
# We need to sort the sublist. sublist.sort! does not do what we want.
|
112
|
+
# We call mergesort instead of Array.sort because sort is not stable
|
113
|
+
# (it can mix up the order of events that have the same start time).
|
114
|
+
# See http://wiki.github.com/adamjmurray/cosy/midilib-notes for details.
|
115
|
+
list[starting_at..-1] = mergesort(list[starting_at..-1]) do |e1, e2|
|
116
|
+
e1.time_from_start <=> e2.time_from_start
|
117
|
+
end
|
118
|
+
list[starting_at..-1].each do |e|
|
119
|
+
e.delta_time = e.time_from_start - prev_time_from_start
|
120
|
+
prev_time_from_start = e.time_from_start
|
121
|
+
end
|
168
122
|
end
|
169
123
|
|
170
124
|
# Iterate over events.
|
171
|
-
def each
|
172
|
-
|
125
|
+
def each(&block) # :yields: event
|
126
|
+
@events.each(&block)
|
173
127
|
end
|
174
128
|
|
175
129
|
# Sort events by their time_from_start. After sorting,
|
@@ -179,7 +133,6 @@ class Track
|
|
179
133
|
# Note: this method is redundant, since recalc_delta_from_times sorts
|
180
134
|
# the events first. This method may go away in a future release, or at
|
181
135
|
# least be aliased to recalc_delta_from_times.
|
182
|
-
|
183
|
-
end
|
184
|
-
|
136
|
+
alias sort recalc_delta_from_times
|
137
|
+
end
|
185
138
|
end
|
data/lib/midilib/utils.rb
CHANGED
@@ -1,18 +1,16 @@
|
|
1
1
|
module MIDI
|
2
|
-
|
3
|
-
|
4
|
-
class Utils
|
5
|
-
|
2
|
+
# Utility methods.
|
3
|
+
class Utils
|
6
4
|
# MIDI note names. NOTE_NAMES[0] is 'C', NOTE_NAMES[1] is 'C#', etc.
|
7
5
|
NOTE_NAMES = [
|
8
|
-
|
6
|
+
'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'
|
9
7
|
]
|
10
8
|
|
11
9
|
# Given a MIDI note number, return the name and octave as a string.
|
12
|
-
def
|
13
|
-
|
14
|
-
|
15
|
-
|
10
|
+
def self.note_to_s(num)
|
11
|
+
note = num % 12
|
12
|
+
octave = num / 12
|
13
|
+
"#{NOTE_NAMES[note]}#{octave - 1}"
|
16
14
|
end
|
17
15
|
|
18
16
|
# Given an integer, returns it as a variable length array of bytes (the
|
@@ -21,16 +19,15 @@ class Utils
|
|
21
19
|
# The converse operation--converting a var len into a number--requires
|
22
20
|
# input from a stream of bytes. Therefore we don't supply it here. That is
|
23
21
|
# a part of the MIDIFile class.
|
24
|
-
def
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
22
|
+
def self.as_var_len(val)
|
23
|
+
buffer = []
|
24
|
+
buffer << (val & 0x7f)
|
25
|
+
val = (val >> 7)
|
26
|
+
while val > 0
|
27
|
+
buffer << (0x80 + (val & 0x7f))
|
28
|
+
val = (val >> 7)
|
29
|
+
end
|
30
|
+
buffer.reverse!
|
33
31
|
end
|
34
|
-
|
35
|
-
end
|
32
|
+
end
|
36
33
|
end
|
data/lib/midilib.rb
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
#
|
6
6
|
# See the README.rdoc file or http://midilib.rubyforge.org for details.
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
require_relative 'midilib/info'
|
9
|
+
require_relative 'midilib/sequence'
|
10
|
+
require_relative 'midilib/track'
|
11
|
+
require_relative 'midilib/io/seqreader'
|
12
|
+
require_relative 'midilib/io/seqwriter'
|
13
13
|
|
14
14
|
# --
|
15
15
|
# consts.rb, utils.rb, and event.rb are included by these files.
|