midilib 2.0.2 → 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,153 +1,157 @@
1
1
  # Writes MIDI files.
2
2
 
3
- require 'midilib/event'
4
- require 'midilib/utils'
3
+ require_relative '../event'
4
+ require_relative '../utils'
5
5
 
6
6
  module MIDI
7
-
8
- module IO
9
-
10
- class SeqWriter
11
-
12
- def initialize(seq, proc = nil) # :yields: num_tracks, index
13
- @seq = seq
14
- @update_block = block_given?() ? Proc.new() : proc
15
- end
16
-
17
- # Writes a MIDI format 1 file.
18
- def write_to(io)
19
- @io = io
20
- @bytes_written = 0
21
- write_header()
22
- @update_block.call(nil, @seq.tracks.length, 0) if @update_block
23
- @seq.tracks.each_with_index { | track, i |
24
- write_track(track)
25
- @update_block.call(track, @seq.tracks.length, i) if @update_block
26
- }
27
- end
28
-
29
- def write_header
30
- @io.print 'MThd'
31
- write32(6)
32
- write16(1) # Ignore sequence format; write as format 1
33
- write16(@seq.tracks.length)
34
- write16(@seq.ppqn)
35
- end
36
-
37
- def write_track(track)
38
- @io.print 'MTrk'
39
- track_size_file_pos = @io.tell()
40
- write32(0) # Dummy byte count; overwritten later
41
- @bytes_written = 0 # Reset after previous write
42
-
43
- write_instrument(track.instrument)
44
-
45
- prev_event = nil
46
- prev_status = 0
47
- track.events.each { | event |
48
- if !event.kind_of?(Realtime)
49
- write_var_len(event.delta_time)
50
- end
51
-
52
- data = event.data_as_bytes()
53
- status = data[0] # status byte plus channel number, if any
54
-
55
- # running status byte
56
- status = possibly_munge_due_to_running_status_byte(data,
57
- prev_status)
58
-
59
- @bytes_written += write_bytes(data)
60
-
61
- prev_event = event
62
- prev_status = status
63
- }
64
-
65
- # Write track end event.
66
- event = MetaEvent.new(META_TRACK_END)
67
- write_var_len(0)
68
- @bytes_written += write_bytes(event.data_as_bytes())
69
-
70
- # Go back to beginning of track data and write number of bytes,
71
- # then come back here to end of file.
72
- @io.seek(track_size_file_pos)
73
- write32(@bytes_written)
74
- @io.seek(0, ::IO::SEEK_END)
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
@@ -1,80 +1,78 @@
1
- require 'midilib/consts'
2
-
3
- module MIDI
4
-
5
- # The Measure class contains information about a measure from the sequence.
6
- # The measure data is based on the time signature information from the sequence
7
- # and is not stored in the sequence itself
8
- class Measure
9
- # The numerator (top digit) for the measure's time signature
10
- attr_reader :numerator
11
- # The denominator for the measure's time signature
12
- attr_reader :denominator
13
- # Start clock tick for the measure
14
- attr_reader :start
15
- # End clock tick for the measure (inclusive)
16
- attr_reader :end
17
- # The measure number (1-based)
18
- attr_reader :measure_number
19
- # The metronome tick for the measure
20
- attr_reader :metronome_ticks
21
-
22
- # Constructor
23
- def initialize(meas_no, start_time, duration, numer, denom, met_ticks)
24
- @measure_number = meas_no
25
- @start = start_time
26
- @end = start_time + duration - 1
27
- @numerator = numer
28
- @denominator = denom
29
- @metronome_ticks = met_ticks
30
- end
31
-
32
- # Returns a detailed string with information about the measure
33
- def to_s
34
- t = "#{@numerator}/#{2**@denominator}"
35
- m = @metronome_ticks.to_f / 24
36
- "measure #{@measure_number} #{@start}-#{@end} #{t} #{m} qs metronome"
37
- end
38
-
39
- # Returns +true+ if the event is in the measure
40
- def contains_event?(e)
41
- (e.time_from_start >= @start) && (e.time_from_start <= @end)
42
- end
43
- end
44
-
45
- # A specialized container for MIDI::Measure objects, which can be use to map
46
- # event times to measure numbers. Please note that this object has to be remade
47
- # when events are deleted/added in the sequence.
48
- class Measures < Array
49
- # The highest event time in the sequence (at the time when the
50
- # object was created)
51
- attr_reader :max_time
52
-
53
- # The ppqd from the sequence
54
- attr_reader :ppqd
55
-
56
- # Constructor
57
- def initialize(max_time, ppqd)
58
- super(0)
59
- @max_time = max_time
60
- @ppqd = ppqd
61
- end
62
-
63
- # Returns the MIDI::Measure object where the event is located.
64
- # Returns +nil+ if the event isn't found in the container (should
65
- # never happen if the MIDI::Measures object is up to date).
66
- def measure_for_event(e)
67
- detect { | m | m.contains_event?(e) }
68
- end
69
-
70
- # Returns the event's time as a formatted MBT string (Measure:Beat:Ticks)
71
- # as found in MIDI sequencers.
72
- def to_mbt(e)
73
- m = measure_for_event(e)
74
- b = (e.time_from_start.to_f - m.start.to_f) / @ppqd
75
- b *= 24 / m.metronome_ticks
76
- sprintf("%d:%02d:%03d", m.measure_number, b.to_i + 1, (b - b.to_i) * @ppqd)
77
- end
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