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/io/seqwriter.rb
CHANGED
@@ -1,153 +1,157 @@
|
|
1
1
|
# Writes MIDI files.
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
require_relative '../event'
|
4
|
+
require_relative '../utils'
|
5
5
|
|
6
6
|
module MIDI
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
7
|
+
module IO
|
8
|
+
class SeqWriter
|
9
|
+
def initialize(seq, midi_format = 1, &block) # :yields: num_tracks, index
|
10
|
+
@seq = seq
|
11
|
+
@midi_format = midi_format || 1
|
12
|
+
@update_block = block
|
13
|
+
end
|
14
|
+
|
15
|
+
# Writes a MIDI format 1 file.
|
16
|
+
def write_to(io)
|
17
|
+
if @midi_format == 0
|
18
|
+
# merge tracks before writing
|
19
|
+
merged_seq = Sequence.new
|
20
|
+
merged_track = Track.new(merged_seq)
|
21
|
+
merged_seq.tracks << merged_track
|
22
|
+
@seq.each do |track|
|
23
|
+
merged_track.merge(track.events)
|
24
|
+
end
|
25
|
+
@seq = merged_seq # replace
|
26
|
+
end
|
27
|
+
|
28
|
+
@io = io
|
29
|
+
@bytes_written = 0
|
30
|
+
write_header
|
31
|
+
@update_block.call(nil, @seq.tracks.length, 0) if @update_block
|
32
|
+
@seq.tracks.each_with_index do |track, i|
|
33
|
+
write_track(track)
|
34
|
+
@update_block.call(track, @seq.tracks.length, i) if @update_block
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def write_header
|
39
|
+
@io.print 'MThd'
|
40
|
+
write32(6)
|
41
|
+
write16(@midi_format) # Ignore sequence format; write as format 1 or 0, default 1
|
42
|
+
write16(@seq.tracks.length)
|
43
|
+
write16(@seq.ppqn)
|
44
|
+
end
|
45
|
+
|
46
|
+
def write_track(track)
|
47
|
+
@io.print 'MTrk'
|
48
|
+
track_size_file_pos = @io.tell
|
49
|
+
write32(0) # Dummy byte count; overwritten later
|
50
|
+
@bytes_written = 0 # Reset after previous write
|
51
|
+
|
52
|
+
write_instrument(track.instrument)
|
53
|
+
|
54
|
+
prev_status = 0
|
55
|
+
track.events.each do |event|
|
56
|
+
write_var_len(event.delta_time) unless event.is_a?(Realtime)
|
57
|
+
|
58
|
+
data = event.data_as_bytes
|
59
|
+
status = data[0] # status byte plus channel number, if any
|
60
|
+
|
61
|
+
# running status byte
|
62
|
+
status = possibly_munge_due_to_running_status_byte(data, prev_status)
|
63
|
+
|
64
|
+
@bytes_written += write_bytes(data)
|
65
|
+
|
66
|
+
prev_status = status
|
67
|
+
end
|
68
|
+
|
69
|
+
# Write track end event.
|
70
|
+
event = MetaEvent.new(META_TRACK_END)
|
71
|
+
write_var_len(0)
|
72
|
+
@bytes_written += write_bytes(event.data_as_bytes)
|
73
|
+
|
74
|
+
# Go back to beginning of track data and write number of bytes,
|
75
|
+
# then come back here to end of file.
|
76
|
+
@io.seek(track_size_file_pos)
|
77
|
+
write32(@bytes_written)
|
78
|
+
@io.seek(0, ::IO::SEEK_END)
|
79
|
+
end
|
80
|
+
|
81
|
+
# If we can use a running status byte, delete the status byte from
|
82
|
+
# the given data. Return the status to remember for next time as the
|
83
|
+
# running status byte for this event.
|
84
|
+
def possibly_munge_due_to_running_status_byte(data, prev_status)
|
85
|
+
status = data[0]
|
86
|
+
return status if status >= 0xf0 || prev_status >= 0xf0
|
87
|
+
|
88
|
+
chan = (status & 0x0f)
|
89
|
+
return status if chan != (prev_status & 0x0f)
|
90
|
+
|
91
|
+
status = (status & 0xf0)
|
92
|
+
prev_status = (prev_status & 0xf0)
|
93
|
+
|
94
|
+
# Both events are on the same channel. If the two status bytes are
|
95
|
+
# exactly the same, the rest is trivial. If it's note on/note off,
|
96
|
+
# we can combine those further.
|
97
|
+
if status == prev_status
|
98
|
+
data[0, 1] = [] # delete status byte from data
|
99
|
+
status + chan
|
100
|
+
elsif status == NOTE_OFF && data[2] == 64
|
101
|
+
# If we see a note off and the velocity is 64, we can store
|
102
|
+
# a note on with a velocity of 0. If the velocity isn't 64
|
103
|
+
# then storing a note on would be bad because the would be
|
104
|
+
# changed to 64 when reading the file back in.
|
105
|
+
data[2] = 0 # set vel to 0; do before possible shrinking
|
106
|
+
status = NOTE_ON + chan
|
107
|
+
if prev_status == NOTE_ON
|
108
|
+
data[0, 1] = [] # delete status byte
|
109
|
+
else
|
110
|
+
data[0] = status
|
111
|
+
end
|
112
|
+
status
|
113
|
+
else
|
114
|
+
# Can't compress data
|
115
|
+
status + chan
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def write_instrument(instrument)
|
120
|
+
return if instrument.nil?
|
121
|
+
|
122
|
+
event = MetaEvent.new(META_INSTRUMENT, instrument)
|
123
|
+
write_var_len(0)
|
124
|
+
data = event.data_as_bytes
|
125
|
+
@bytes_written += write_bytes(data)
|
126
|
+
end
|
127
|
+
|
128
|
+
def write_var_len(val)
|
129
|
+
buffer = Utils.as_var_len(val)
|
130
|
+
@bytes_written += write_bytes(buffer)
|
131
|
+
end
|
132
|
+
|
133
|
+
def write16(val)
|
134
|
+
val = (-val | 0x8000) if val < 0
|
135
|
+
|
136
|
+
@io.putc((val >> 8) & 0xff)
|
137
|
+
@io.putc(val & 0xff)
|
138
|
+
@bytes_written += 2
|
139
|
+
end
|
140
|
+
|
141
|
+
def write32(val)
|
142
|
+
val = (-val | 0x80000000) if val < 0
|
143
|
+
|
144
|
+
@io.putc((val >> 24) & 0xff)
|
145
|
+
@io.putc((val >> 16) & 0xff)
|
146
|
+
@io.putc((val >> 8) & 0xff)
|
147
|
+
@io.putc(val & 0xff)
|
148
|
+
@bytes_written += 4
|
149
|
+
end
|
150
|
+
|
151
|
+
def write_bytes(bytes)
|
152
|
+
bytes.each { |b| @io.putc(b) }
|
153
|
+
bytes.length
|
154
|
+
end
|
75
155
|
end
|
76
|
-
|
77
|
-
# If we can use a running status byte, delete the status byte from
|
78
|
-
# the given data. Return the status to remember for next time as the
|
79
|
-
# running status byte for this event.
|
80
|
-
def possibly_munge_due_to_running_status_byte(data, prev_status)
|
81
|
-
status = data[0]
|
82
|
-
return status if status >= 0xf0 || prev_status >= 0xf0
|
83
|
-
|
84
|
-
chan = (status & 0x0f)
|
85
|
-
return status if chan != (prev_status & 0x0f)
|
86
|
-
|
87
|
-
status = (status & 0xf0)
|
88
|
-
prev_status = (prev_status & 0xf0)
|
89
|
-
|
90
|
-
# Both events are on the same channel. If the two status bytes are
|
91
|
-
# exactly the same, the rest is trivial. If it's note on/note off,
|
92
|
-
# we can combine those further.
|
93
|
-
if status == prev_status
|
94
|
-
data[0,1] = [] # delete status byte from data
|
95
|
-
return status + chan
|
96
|
-
elsif status == NOTE_OFF && data[2] == 64
|
97
|
-
# If we see a note off and the velocity is 64, we can store
|
98
|
-
# a note on with a velocity of 0. If the velocity isn't 64
|
99
|
-
# then storing a note on would be bad because the would be
|
100
|
-
# changed to 64 when reading the file back in.
|
101
|
-
data[2] = 0 # set vel to 0; do before possible shrinking
|
102
|
-
status = NOTE_ON + chan
|
103
|
-
if prev_status == NOTE_ON
|
104
|
-
data[0,1] = [] # delete status byte
|
105
|
-
else
|
106
|
-
data[0] = status
|
107
|
-
end
|
108
|
-
return status
|
109
|
-
else
|
110
|
-
# Can't compress data
|
111
|
-
return status + chan
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
def write_instrument(instrument)
|
116
|
-
event = MetaEvent.new(META_INSTRUMENT, instrument)
|
117
|
-
write_var_len(0)
|
118
|
-
data = event.data_as_bytes()
|
119
|
-
@bytes_written += write_bytes(data)
|
120
|
-
end
|
121
|
-
|
122
|
-
def write_var_len(val)
|
123
|
-
buffer = Utils.as_var_len(val)
|
124
|
-
@bytes_written += write_bytes(buffer)
|
125
|
-
end
|
126
|
-
|
127
|
-
def write16(val)
|
128
|
-
val = (-val | 0x8000) if val < 0
|
129
|
-
|
130
|
-
buffer = []
|
131
|
-
@io.putc((val >> 8) & 0xff)
|
132
|
-
@io.putc(val & 0xff)
|
133
|
-
@bytes_written += 2
|
134
|
-
end
|
135
|
-
|
136
|
-
def write32(val)
|
137
|
-
val = (-val | 0x80000000) if val < 0
|
138
|
-
|
139
|
-
@io.putc((val >> 24) & 0xff)
|
140
|
-
@io.putc((val >> 16) & 0xff)
|
141
|
-
@io.putc((val >> 8) & 0xff)
|
142
|
-
@io.putc(val & 0xff)
|
143
|
-
@bytes_written += 4
|
144
|
-
end
|
145
|
-
|
146
|
-
def write_bytes(bytes)
|
147
|
-
bytes.each { |b| @io.putc(b) }
|
148
|
-
bytes.length
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
end
|
156
|
+
end
|
153
157
|
end
|
data/lib/midilib/measure.rb
CHANGED
@@ -1,80 +1,78 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
module MIDI
|
4
|
-
|
5
|
-
# The
|
6
|
-
#
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
end
|
43
|
-
|
44
|
-
|
45
|
-
#
|
46
|
-
#
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
@
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
b
|
75
|
-
b
|
76
|
-
|
77
|
-
|
78
|
-
end
|
79
|
-
|
80
|
-
end
|
1
|
+
require_relative 'consts'
|
2
|
+
|
3
|
+
module MIDI
|
4
|
+
# The Measure class contains information about a measure from the sequence.
|
5
|
+
# The measure data is based on the time signature information from the sequence
|
6
|
+
# and is not stored in the sequence itself
|
7
|
+
class Measure
|
8
|
+
# The numerator (top digit) for the measure's time signature
|
9
|
+
attr_reader :numerator
|
10
|
+
# The denominator for the measure's time signature
|
11
|
+
attr_reader :denominator
|
12
|
+
# Start clock tick for the measure
|
13
|
+
attr_reader :start
|
14
|
+
# End clock tick for the measure (inclusive)
|
15
|
+
attr_reader :end
|
16
|
+
# The measure number (1-based)
|
17
|
+
attr_reader :measure_number
|
18
|
+
# The metronome tick for the measure
|
19
|
+
attr_reader :metronome_ticks
|
20
|
+
|
21
|
+
# Constructor
|
22
|
+
def initialize(meas_no, start_time, duration, numer, denom, met_ticks)
|
23
|
+
@measure_number = meas_no
|
24
|
+
@start = start_time
|
25
|
+
@end = start_time + duration - 1
|
26
|
+
@numerator = numer
|
27
|
+
@denominator = denom
|
28
|
+
@metronome_ticks = met_ticks
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns a detailed string with information about the measure
|
32
|
+
def to_s
|
33
|
+
t = "#{@numerator}/#{2**@denominator}"
|
34
|
+
m = @metronome_ticks.to_f / 24
|
35
|
+
"measure #{@measure_number} #{@start}-#{@end} #{t} #{m} qs metronome"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns +true+ if the event is in the measure
|
39
|
+
def contains_event?(e)
|
40
|
+
(e.time_from_start >= @start) && (e.time_from_start <= @end)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# A specialized container for MIDI::Measure objects, which can be use to map
|
45
|
+
# event times to measure numbers. Please note that this object has to be remade
|
46
|
+
# when events are deleted/added in the sequence.
|
47
|
+
class Measures < Array
|
48
|
+
# The highest event time in the sequence (at the time when the
|
49
|
+
# object was created)
|
50
|
+
attr_reader :max_time
|
51
|
+
|
52
|
+
# The ppqd from the sequence
|
53
|
+
attr_reader :ppqd
|
54
|
+
|
55
|
+
# Constructor
|
56
|
+
def initialize(max_time, ppqd)
|
57
|
+
super(0)
|
58
|
+
@max_time = max_time
|
59
|
+
@ppqd = ppqd
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns the MIDI::Measure object where the event is located.
|
63
|
+
# Returns +nil+ if the event isn't found in the container (should
|
64
|
+
# never happen if the MIDI::Measures object is up to date).
|
65
|
+
def measure_for_event(e)
|
66
|
+
detect { |m| m.contains_event?(e) }
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns the event's time as a formatted MBT string (Measure:Beat:Ticks)
|
70
|
+
# as found in MIDI sequencers.
|
71
|
+
def to_mbt(e)
|
72
|
+
m = measure_for_event(e)
|
73
|
+
b = (e.time_from_start.to_f - m.start.to_f) / @ppqd
|
74
|
+
b *= 24 / m.metronome_ticks
|
75
|
+
format('%d:%02d:%03d', m.measure_number, b.to_i + 1, (b - b.to_i) * @ppqd)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# This code was originally taken from
|
2
|
+
# http://github.com/adamjmurray/cosy/blob/master/lib/cosy/helper/midi_file_renderer_helper.rb
|
3
|
+
# with permission from Adam Murray, who originally suggested this fix.
|
4
|
+
# See http://wiki.github.com/adamjmurray/cosy/midilib-notes for details.
|
5
|
+
|
6
|
+
# A stable sorting algorithm that maintains the relative order of equal
|
7
|
+
# elements.
|
8
|
+
#
|
9
|
+
# This code used to be in a new subclass of Array, but that started causing
|
10
|
+
# problems in Ruby 3.0, apparently due to the return type of the `[]`
|
11
|
+
# operator which was the parent Array class.
|
12
|
+
#
|
13
|
+
# This code borrowed from 'Moser' http://codesnippets.joyent.com/posts/show/1699
|
14
|
+
def mergesort(arr, &cmp)
|
15
|
+
cmp = ->(a, b) { a <=> b } if cmp.nil?
|
16
|
+
if arr.size <= 1
|
17
|
+
arr.dup
|
18
|
+
else
|
19
|
+
halves = mergesort_split(arr).map { |half| mergesort(half, &cmp) }
|
20
|
+
mergesort_merge(*halves, &cmp)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def mergesort_split(arr)
|
25
|
+
n = (arr.length / 2).floor - 1
|
26
|
+
[arr[0..n], arr[n + 1..-1]]
|
27
|
+
end
|
28
|
+
|
29
|
+
def mergesort_merge(first, second, &predicate)
|
30
|
+
result = []
|
31
|
+
until first.empty? || second.empty?
|
32
|
+
result << if predicate.call(first.first, second.first) <= 0
|
33
|
+
first.shift
|
34
|
+
else
|
35
|
+
second.shift
|
36
|
+
end
|
37
|
+
end
|
38
|
+
result.concat(first).concat(second)
|
39
|
+
end
|