music_player 0.9.0-universal-darwin-9
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +22 -0
- data/Rakefile +36 -0
- data/examples/drum_machine.rb +64 -0
- data/ext/music_player/extconf.rb +9 -0
- data/ext/music_player/music_player.c +1440 -0
- data/ext/music_player/util.h +25 -0
- data/lib/music_player.rb +216 -0
- data/test/example.mid +0 -0
- data/test/extended_tempo_event_test.rb +15 -0
- data/test/midi_channel_message_test.rb +93 -0
- data/test/midi_note_message_test.rb +50 -0
- data/test/music_event_iterator_test.rb +160 -0
- data/test/music_player_test.rb +40 -0
- data/test/music_sequence_test.rb +52 -0
- data/test/music_track_collection_test.rb +53 -0
- data/test/music_track_test.rb +92 -0
- data/test/test_helper.rb +4 -0
- metadata +77 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
#include <ruby.h>
|
2
|
+
#include <CoreFoundation/CoreFoundation.h>
|
3
|
+
|
4
|
+
/* Test whether a VALUE is a primitive number type. */
|
5
|
+
#define PRIM_NUM_P(num) (T_FIXNUM == TYPE(num) || \
|
6
|
+
T_FLOAT == TYPE(num) || \
|
7
|
+
T_BIGNUM == TYPE(num))
|
8
|
+
|
9
|
+
/* Convert a C string to a Ruby Symbol. */
|
10
|
+
#define CSTR2SYM(str) (ID2SYM(rb_intern(str)))
|
11
|
+
|
12
|
+
/* Call ruby's === operator on the given lhs and rhs. */
|
13
|
+
#define THRQL(lhs, rhs) (rb_funcall(lhs, rb_intern("==="), 1, rhs))
|
14
|
+
|
15
|
+
/* Convert a Ruby String to a CFURLRef. */
|
16
|
+
#define PATH2CFURL(path) (rb_path_to_cfurl(path))
|
17
|
+
|
18
|
+
static CFURLRef
|
19
|
+
rb_path_to_cfurl (VALUE rb_path)
|
20
|
+
{
|
21
|
+
VALUE rb_abs_path = rb_file_expand_path(rb_path, Qnil);
|
22
|
+
char *path = StringValueCStr(rb_abs_path);
|
23
|
+
CFURLRef url = CFURLCreateFromFileSystemRepresentation(NULL, (const UInt8 *) path, strlen(path), false);
|
24
|
+
return url;
|
25
|
+
}
|
data/lib/music_player.rb
ADDED
@@ -0,0 +1,216 @@
|
|
1
|
+
$:.unshift File.join(File.dirname(__FILE__), '../ext/music_player')
|
2
|
+
require 'thread'
|
3
|
+
require 'music_player.bundle'
|
4
|
+
|
5
|
+
module AudioToolbox
|
6
|
+
class MusicSequence
|
7
|
+
attr :tracks
|
8
|
+
|
9
|
+
def load(path)
|
10
|
+
@tracks.lock.synchronize do
|
11
|
+
load_internal(path)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class MusicTrackCollection
|
17
|
+
include Enumerable
|
18
|
+
|
19
|
+
attr :lock # :nodoc:
|
20
|
+
|
21
|
+
def initialize(sequence)
|
22
|
+
@sequence = sequence
|
23
|
+
@tracks = []
|
24
|
+
@lock = Mutex.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def each
|
28
|
+
0.upto(size-1) { |i| yield self[i] }
|
29
|
+
end
|
30
|
+
|
31
|
+
def new(options=nil)
|
32
|
+
@lock.synchronize do
|
33
|
+
track = MusicTrack.send(:new, @sequence, options)
|
34
|
+
@tracks << track
|
35
|
+
track
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def [](index)
|
40
|
+
@lock.synchronize do
|
41
|
+
@tracks[index] ||= ind_internal(index)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def delete(track)
|
46
|
+
@lock.synchronize do
|
47
|
+
delete_internal(track)
|
48
|
+
@tracks.delete(track)
|
49
|
+
track.freeze
|
50
|
+
end
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
|
54
|
+
def tempo
|
55
|
+
@tempo ||= tempo_internal
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class MusicTrack
|
60
|
+
class << self
|
61
|
+
private :new
|
62
|
+
end
|
63
|
+
|
64
|
+
def add(time, message)
|
65
|
+
message.add(time, self)
|
66
|
+
end
|
67
|
+
|
68
|
+
def iterator
|
69
|
+
MusicEventIterator.new(self)
|
70
|
+
end
|
71
|
+
|
72
|
+
include Enumerable
|
73
|
+
|
74
|
+
def each
|
75
|
+
i = iterator
|
76
|
+
while i.current?
|
77
|
+
yield i.event
|
78
|
+
i.next
|
79
|
+
end
|
80
|
+
nil
|
81
|
+
end
|
82
|
+
|
83
|
+
def each_with_time
|
84
|
+
i = iterator
|
85
|
+
while i.current?
|
86
|
+
yield [i.event, i.time]
|
87
|
+
i.next
|
88
|
+
end
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class MIDINoteMessage
|
94
|
+
def ==(msg)
|
95
|
+
self.class == msg.class &&
|
96
|
+
channel == msg.channel &&
|
97
|
+
note == msg.note &&
|
98
|
+
velocity == msg.velocity &&
|
99
|
+
release_velocity == msg.release_velocity &&
|
100
|
+
duration == msg.duration
|
101
|
+
end
|
102
|
+
|
103
|
+
def add(time, track)
|
104
|
+
track.add_midi_note_message(time, self)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class MIDIChannelMessage
|
109
|
+
def ==(msg)
|
110
|
+
self.class == msg.class &&
|
111
|
+
status == msg.status &&
|
112
|
+
data1 == msg.data1 &&
|
113
|
+
data2 == msg.data2
|
114
|
+
end
|
115
|
+
|
116
|
+
def channel
|
117
|
+
status ^ mask
|
118
|
+
end
|
119
|
+
|
120
|
+
def mask
|
121
|
+
raise NotImplementedError, "Subclass responsibility."
|
122
|
+
end
|
123
|
+
|
124
|
+
def add(time, track)
|
125
|
+
track.add_midi_channel_message(time, self)
|
126
|
+
end
|
127
|
+
|
128
|
+
protected
|
129
|
+
def required_opts(opts, *keys)
|
130
|
+
keys.map do |key|
|
131
|
+
opts[key] or raise ArgumentError, "%s is required." % key.inspect
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class MIDIKeyPressureMessage < MIDIChannelMessage
|
137
|
+
alias :note :data1
|
138
|
+
alias :pressure :data2
|
139
|
+
|
140
|
+
def mask; 0xA0 end
|
141
|
+
|
142
|
+
def initialize(opts)
|
143
|
+
channel, note, pressure = required_opts(opts, :channel, :note, :pressure)
|
144
|
+
super(:status => mask | channel,
|
145
|
+
:data1 => note,
|
146
|
+
:data2 => pressure)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
class MIDIControlChangeMessage < MIDIChannelMessage
|
151
|
+
alias :number :data1
|
152
|
+
alias :value :data2
|
153
|
+
|
154
|
+
def mask; 0xB0 end
|
155
|
+
|
156
|
+
def initialize(opts)
|
157
|
+
channel, number, value = required_opts(opts, :channel, :number, :value)
|
158
|
+
super(:status => mask | channel,
|
159
|
+
:data1 => number,
|
160
|
+
:data2 => value)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
class MIDIProgramChangeMessage < MIDIChannelMessage
|
165
|
+
alias :program :data1
|
166
|
+
|
167
|
+
def mask; 0xC0 end
|
168
|
+
|
169
|
+
def initialize(opts)
|
170
|
+
channel, program = required_opts(opts, :channel, :program)
|
171
|
+
super(:status => mask | channel,
|
172
|
+
:data1 => program)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
class MIDIChannelPressureMessage < MIDIChannelMessage
|
177
|
+
alias :pressure :data1
|
178
|
+
|
179
|
+
def mask; 0xD0 end
|
180
|
+
|
181
|
+
def initialize(opts)
|
182
|
+
channel, pressure = required_opts(opts, :channel, :pressure)
|
183
|
+
super(:status => mask | channel,
|
184
|
+
:data1 => pressure)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
class MIDIPitchBendMessage < MIDIChannelMessage
|
189
|
+
alias :value :data1
|
190
|
+
|
191
|
+
def mask; 0xE0 end
|
192
|
+
|
193
|
+
def initialize(opts)
|
194
|
+
channel, value = required_opts(opts, :channel, :value)
|
195
|
+
super(:status => mask | channel,
|
196
|
+
:data1 => value)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
class ExtendedTempoEvent
|
201
|
+
attr :bpm
|
202
|
+
|
203
|
+
def add(time, track)
|
204
|
+
track.add_extended_tempo_event(time, @bpm)
|
205
|
+
end
|
206
|
+
|
207
|
+
def initialize(opts)
|
208
|
+
@bpm = opts[:bpm]
|
209
|
+
end
|
210
|
+
|
211
|
+
def ==(other)
|
212
|
+
self.class == other.class &&
|
213
|
+
bpm == other.bpm
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
data/test/example.mid
ADDED
Binary file
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper.rb')
|
2
|
+
|
3
|
+
class ExtendedTempoEventTest < Test::Unit::TestCase
|
4
|
+
def test_initialization
|
5
|
+
event = ExtendedTempoEvent.new(:bpm => 120)
|
6
|
+
assert_equal 120, event.bpm
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_add
|
10
|
+
sequence = MusicSequence.new
|
11
|
+
tempo = sequence.tracks.tempo
|
12
|
+
event = ExtendedTempoEvent.new(:bpm => 120)
|
13
|
+
assert_nothing_raised { event.add(0.0, tempo) }
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper.rb')
|
2
|
+
|
3
|
+
class MIDIChannelMessageTest < Test::Unit::TestCase
|
4
|
+
def test_initialization
|
5
|
+
assert_raise(ArgumentError) { MIDIChannelMessage.new }
|
6
|
+
assert_raise(ArgumentError) { MIDIChannelMessage.new({}) }
|
7
|
+
assert_nothing_raised { MIDIChannelMessage.new :status => 42 }
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_eq
|
11
|
+
msg1 = make_msg
|
12
|
+
msg2 = make_msg
|
13
|
+
# Demonstrates that separate Ruby MIDIChannelMessage objects may be
|
14
|
+
# compared by value.
|
15
|
+
assert msg1.object_id != msg2.object_id
|
16
|
+
assert msg1 == msg2
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_accessors
|
20
|
+
msg = make_msg
|
21
|
+
assert_equal 42, msg.status
|
22
|
+
assert_equal 43, msg.data1
|
23
|
+
assert_equal 44, msg.data2
|
24
|
+
end
|
25
|
+
|
26
|
+
def make_msg
|
27
|
+
MIDIChannelMessage.new(:status => 42, :data1 => 43, :data2 => 44)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class MIDIKeyPressureMessageTest < Test::Unit::TestCase
|
32
|
+
def test_initialization
|
33
|
+
msg = MIDIKeyPressureMessage.new(
|
34
|
+
:channel => 1,
|
35
|
+
:note => 60,
|
36
|
+
:pressure => 64)
|
37
|
+
|
38
|
+
assert_equal 1, msg.channel
|
39
|
+
assert_equal 60, msg.note
|
40
|
+
assert_equal 64, msg.pressure
|
41
|
+
assert_equal msg.mask | msg.channel, msg.status
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class MIDIControlChangeMessageTest < Test::Unit::TestCase
|
46
|
+
def test_initialization
|
47
|
+
msg = MIDIControlChangeMessage.new(
|
48
|
+
:channel => 1,
|
49
|
+
:number => 8,
|
50
|
+
:value => 64)
|
51
|
+
|
52
|
+
assert_equal 1, msg.channel
|
53
|
+
assert_equal 8, msg.number
|
54
|
+
assert_equal 64, msg.value
|
55
|
+
assert_equal msg.mask | msg.channel, msg.status
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class MIDIProgramChangeMessageTest < Test::Unit::TestCase
|
60
|
+
def test_initialization
|
61
|
+
msg = MIDIProgramChangeMessage.new(
|
62
|
+
:channel => 1,
|
63
|
+
:program => 2)
|
64
|
+
|
65
|
+
assert_equal 1, msg.channel
|
66
|
+
assert_equal 2, msg.program
|
67
|
+
assert_equal msg.mask | msg.channel, msg.status
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class MIDIChannelPressureMessageTest < Test::Unit::TestCase
|
72
|
+
def test_initialization
|
73
|
+
msg = MIDIChannelPressureMessage.new(
|
74
|
+
:channel => 1,
|
75
|
+
:pressure => 64)
|
76
|
+
|
77
|
+
assert_equal 1, msg.channel
|
78
|
+
assert_equal 64, msg.pressure
|
79
|
+
assert_equal msg.mask | msg.channel, msg.status
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class MIDIPitchBendMessageTest < Test::Unit::TestCase
|
84
|
+
def test_initialization
|
85
|
+
msg = MIDIPitchBendMessage.new(
|
86
|
+
:channel => 1,
|
87
|
+
:value => 64)
|
88
|
+
|
89
|
+
assert_equal 1, msg.channel
|
90
|
+
assert_equal 64, msg.value
|
91
|
+
assert_equal msg.mask | msg.channel, msg.status
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper.rb')
|
2
|
+
|
3
|
+
class MIDINoteMessageTest < Test::Unit::TestCase
|
4
|
+
include AudioToolbox
|
5
|
+
|
6
|
+
def test_initialization
|
7
|
+
assert_raise(ArgumentError) { MIDINoteMessage.new }
|
8
|
+
assert_raise(ArgumentError) { MIDINoteMessage.new({}) }
|
9
|
+
assert_nothing_raised { MIDINoteMessage.new :note => 60 }
|
10
|
+
assert_nothing_raised do
|
11
|
+
# Do your own range-checking. This is harmless.
|
12
|
+
oob = rand(10000)
|
13
|
+
msg = MIDINoteMessage.new :note => oob
|
14
|
+
assert_equal (oob%256), msg.note
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_defaults
|
19
|
+
msg = MIDINoteMessage.new :note => 60 # No default for :note.
|
20
|
+
assert_equal 1, msg.channel
|
21
|
+
assert_equal 64, msg.velocity
|
22
|
+
assert_equal 0, msg.release_velocity
|
23
|
+
assert_equal 1.0, msg.duration
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_accessors
|
27
|
+
msg = make_msg
|
28
|
+
assert_equal 50, msg.note
|
29
|
+
assert_equal 40, msg.velocity
|
30
|
+
assert_equal 30, msg.release_velocity
|
31
|
+
assert_equal 2.0, msg.duration
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_eq
|
35
|
+
msg1 = make_msg
|
36
|
+
msg2 = make_msg
|
37
|
+
# Demonstrates that separate Ruby MIDIChannelMessage objects may be
|
38
|
+
# compared by value.
|
39
|
+
assert msg1.object_id != msg2.object_id
|
40
|
+
assert msg1 == msg2
|
41
|
+
end
|
42
|
+
|
43
|
+
def make_msg
|
44
|
+
MIDINoteMessage.new(
|
45
|
+
:note => 50,
|
46
|
+
:velocity => 40,
|
47
|
+
:release_velocity => 30,
|
48
|
+
:duration => 2.0)
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper.rb')
|
2
|
+
|
3
|
+
class MusicEventIteratorTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@sequence = MusicSequence.new
|
6
|
+
@track = @sequence.tracks.new
|
7
|
+
@track.add 0, @ev1=MIDINoteMessage.new(:note => 60)
|
8
|
+
@track.add 1, @ev2=MIDINoteMessage.new(:note => 67)
|
9
|
+
@iter = @track.iterator
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_seek
|
13
|
+
assert_nothing_raised { @iter.seek(0) }
|
14
|
+
assert_equal @ev1, @iter.event
|
15
|
+
# Seeking to a point between event onsets causes
|
16
|
+
# the iterator to advance to the next onset.
|
17
|
+
@iter.seek(0.1)
|
18
|
+
assert_equal @ev2, @iter.event
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_current?
|
22
|
+
assert @iter.current?
|
23
|
+
@iter.next
|
24
|
+
assert @iter.current?
|
25
|
+
@iter.next
|
26
|
+
assert !@iter.current?
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_next
|
30
|
+
assert_nothing_raised do
|
31
|
+
@iter.next; @iter.next
|
32
|
+
end
|
33
|
+
assert_raise(EndOfTrack) { @iter.next }
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_next?
|
37
|
+
assert @iter.next?
|
38
|
+
@iter.next
|
39
|
+
assert !@iter.next?
|
40
|
+
@iter.next # We may advance once beyond the last event.
|
41
|
+
assert_raise(EndOfTrack) { @iter.next }
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_prev
|
45
|
+
assert_nothing_raised do
|
46
|
+
@iter.next; @iter.prev
|
47
|
+
end
|
48
|
+
assert_raise(StartOfTrack) { @iter.prev }
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_prev?
|
52
|
+
assert !@iter.prev?
|
53
|
+
@iter.next
|
54
|
+
assert @iter.prev?
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_time
|
58
|
+
assert_equal 0.0, @iter.time
|
59
|
+
@iter.next
|
60
|
+
assert_equal 1.0, @iter.time
|
61
|
+
@iter.next
|
62
|
+
assert_raise(EndOfTrack) { @iter.time }
|
63
|
+
@iter.seek(0.0)
|
64
|
+
assert_equal 0.0, @iter.time
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_set_time
|
68
|
+
# swap note onsets
|
69
|
+
assert_equal 0, @iter.time
|
70
|
+
@iter.time = 1
|
71
|
+
assert_equal 1, @iter.time
|
72
|
+
@iter.next
|
73
|
+
assert_equal 1, @iter.time
|
74
|
+
@iter.time = 0
|
75
|
+
|
76
|
+
@iter.seek 0
|
77
|
+
assert_equal @ev2, @iter.event
|
78
|
+
@iter.next
|
79
|
+
assert_equal @ev1, @iter.event
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_event__note
|
83
|
+
assert_nothing_raised do
|
84
|
+
assert_equal @ev1, @iter.event
|
85
|
+
@iter.next
|
86
|
+
assert_equal @ev2, @iter.event
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_event__channel
|
91
|
+
track = @sequence.tracks.new
|
92
|
+
track.add 0, ev1=MIDIKeyPressureMessage.new(:channel => 1, :note => 60, :pressure => 64)
|
93
|
+
track.add 0, ev2=MIDIControlChangeMessage.new(:channel => 1, :number => 1, :value => 127)
|
94
|
+
track.add 0, ev3=MIDIProgramChangeMessage.new(:channel => 1, :program => 42)
|
95
|
+
track.add 0, ev4=MIDIChannelPressureMessage.new(:channel => 1, :pressure => 37)
|
96
|
+
track.add 0, ev5=MIDIPitchBendMessage.new(:channel => 1, :value => 84)
|
97
|
+
iter = track.iterator
|
98
|
+
|
99
|
+
assert_equal ev1, iter.event
|
100
|
+
iter.next
|
101
|
+
assert_equal ev2, iter.event
|
102
|
+
iter.next
|
103
|
+
assert_equal ev3, iter.event
|
104
|
+
iter.next
|
105
|
+
assert_equal ev4, iter.event
|
106
|
+
iter.next
|
107
|
+
assert_equal ev5, iter.event
|
108
|
+
end
|
109
|
+
|
110
|
+
def test_event__tempo
|
111
|
+
track = @sequence.tracks.tempo
|
112
|
+
track.add 0, ev=ExtendedTempoEvent.new(:bpm => 120)
|
113
|
+
iter = track.iterator
|
114
|
+
assert_equal ev, iter.event
|
115
|
+
end
|
116
|
+
|
117
|
+
def test_set_event
|
118
|
+
# swapping notes
|
119
|
+
@iter.seek(0)
|
120
|
+
@iter.event = @ev2
|
121
|
+
@iter.next
|
122
|
+
@iter.event = @ev1
|
123
|
+
|
124
|
+
@iter.seek(0)
|
125
|
+
assert_equal @ev2, @iter.event
|
126
|
+
@iter.next
|
127
|
+
assert_equal @ev1, @iter.event
|
128
|
+
|
129
|
+
# Event types may be altered.
|
130
|
+
@iter.seek(0)
|
131
|
+
@iter.event = cc=MIDIControlChangeMessage.new(:channel => 1, :number => 2, :value => 3)
|
132
|
+
assert_equal cc, @iter.event
|
133
|
+
|
134
|
+
# Assigning when there is no current event raises an exception.
|
135
|
+
# Use MusicTrack#add to add new events.
|
136
|
+
@iter.next until !@iter.current?
|
137
|
+
assert_raise(EndOfTrack) do
|
138
|
+
@iter.event = MIDINoteMessage.new(:note => 60)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Tempo tracks are iterable as well.
|
142
|
+
tempo = @sequence.tracks.tempo
|
143
|
+
tempo.add 0, ExtendedTempoEvent.new(:bpm => 120)
|
144
|
+
|
145
|
+
iter = @sequence.tracks.tempo.iterator
|
146
|
+
assert_equal 120, iter.event.bpm
|
147
|
+
iter.event = ExtendedTempoEvent.new(:bpm => 60)
|
148
|
+
assert_equal 60, iter.event.bpm
|
149
|
+
end
|
150
|
+
|
151
|
+
def test_delete
|
152
|
+
assert_equal @ev1, @iter.event
|
153
|
+
@iter.delete
|
154
|
+
assert_equal @ev2, @iter.event
|
155
|
+
@iter.delete
|
156
|
+
|
157
|
+
assert !@iter.current?
|
158
|
+
assert_nothing_raised { @iter.delete }
|
159
|
+
end
|
160
|
+
end
|