music-performance 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +3 -0
- data/.gitignore +7 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -0
- data/.yardopts +1 -0
- data/ChangeLog.rdoc +5 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +28 -0
- data/Rakefile +54 -0
- data/bin/midify +61 -0
- data/lib/music-performance.rb +25 -0
- data/lib/music-performance/arrangement/midi/midi_events.rb +9 -0
- data/lib/music-performance/arrangement/midi/midi_util.rb +38 -0
- data/lib/music-performance/arrangement/midi/part_sequencer.rb +121 -0
- data/lib/music-performance/arrangement/midi/score_sequencer.rb +33 -0
- data/lib/music-performance/conversion/glissando_converter.rb +36 -0
- data/lib/music-performance/conversion/note_sequence_extractor.rb +100 -0
- data/lib/music-performance/conversion/note_time_converter.rb +76 -0
- data/lib/music-performance/conversion/portamento_converter.rb +26 -0
- data/lib/music-performance/conversion/score_collator.rb +121 -0
- data/lib/music-performance/conversion/score_time_converter.rb +112 -0
- data/lib/music-performance/model/note_attacks.rb +21 -0
- data/lib/music-performance/model/note_sequence.rb +113 -0
- data/lib/music-performance/util/interpolation.rb +18 -0
- data/lib/music-performance/util/note_linker.rb +30 -0
- data/lib/music-performance/util/optimization.rb +33 -0
- data/lib/music-performance/util/piecewise_function.rb +124 -0
- data/lib/music-performance/util/value_computer.rb +172 -0
- data/lib/music-performance/version.rb +7 -0
- data/music-performance.gemspec +33 -0
- data/spec/conversion/glissando_converter_spec.rb +93 -0
- data/spec/conversion/note_sequence_extractor_spec.rb +230 -0
- data/spec/conversion/note_time_converter_spec.rb +96 -0
- data/spec/conversion/portamento_converter_spec.rb +91 -0
- data/spec/conversion/score_collator_spec.rb +136 -0
- data/spec/conversion/score_time_converter_spec.rb +73 -0
- data/spec/model/note_sequence_spec.rb +147 -0
- data/spec/music-performance_spec.rb +7 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/util/note_linker_spec.rb +68 -0
- data/spec/util/optimization_spec.rb +73 -0
- data/spec/util/value_computer_spec.rb +146 -0
- metadata +242 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require File.expand_path('../lib/music-performance/version', __FILE__)
|
4
|
+
|
5
|
+
Gem::Specification.new do |gem|
|
6
|
+
gem.name = "music-performance"
|
7
|
+
gem.version = Music::Performance::VERSION
|
8
|
+
gem.summary = %q{Classes for representing music notational features like pitch, note, loudness, tempo, etc.}
|
9
|
+
gem.description = <<DESCRIPTION
|
10
|
+
Prepare a transcribed musical score for performance by a computer.
|
11
|
+
DESCRIPTION
|
12
|
+
gem.license = "MIT"
|
13
|
+
gem.authors = ["James Tunnell"]
|
14
|
+
gem.email = "jamestunnell@gmail.com"
|
15
|
+
gem.homepage = "https://github.com/jamestunnell/music-performance"
|
16
|
+
|
17
|
+
gem.files = `git ls-files`.split($/)
|
18
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
19
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
20
|
+
gem.require_paths = ['lib']
|
21
|
+
|
22
|
+
gem.add_development_dependency 'bundler', '~> 1.5'
|
23
|
+
gem.add_development_dependency 'rubygems-bundler', '~> 1.4'
|
24
|
+
gem.add_development_dependency 'rake', '~> 10.1'
|
25
|
+
gem.add_development_dependency 'rspec', '~> 2.14'
|
26
|
+
gem.add_development_dependency 'yard', '~> 0.8'
|
27
|
+
gem.add_development_dependency 'pry'
|
28
|
+
gem.add_development_dependency 'pry-nav'
|
29
|
+
|
30
|
+
gem.add_dependency 'music-transcription', '~> 0.17.0'
|
31
|
+
gem.add_dependency 'midilib', '~> 2.0'
|
32
|
+
gem.add_dependency 'docopt'
|
33
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe GlissandoConverter do
|
4
|
+
describe '.glissando_pitches' do
|
5
|
+
context 'start pitch <= target pitch' do
|
6
|
+
[
|
7
|
+
[F3,B3],
|
8
|
+
[C4,Gb5],
|
9
|
+
[D2,C5],
|
10
|
+
[F2,F2],
|
11
|
+
[C4.transpose(0.5),D4],
|
12
|
+
[C4.transpose(0.5),D4.transpose(0.5)],
|
13
|
+
[C4.transpose(0.01),F5],
|
14
|
+
[C4.transpose(-0.01),F5.transpose(-0.01)],
|
15
|
+
].each do |start,finish|
|
16
|
+
context "start at #{start.to_s}, target #{finish.to_s}" do
|
17
|
+
pitches = GlissandoConverter.glissando_pitches(start,finish)
|
18
|
+
|
19
|
+
it 'should begin with start pitch' do
|
20
|
+
pitches.first.should eq(start)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should move up to next whole (zero-cent) pitches' do
|
24
|
+
(1...pitches.size).each do |i|
|
25
|
+
pitches[i].cent.should eq(0)
|
26
|
+
pitches[i].diff(pitches[i-1]).should be <= 1
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should end on the whole (zero-cent) pitch below target pitch' do
|
31
|
+
pitches.last.cent.should eq(0)
|
32
|
+
diff = finish.total_cents - pitches.last.total_cents
|
33
|
+
diff.should be <= 100
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'start pitch > target pitch' do
|
40
|
+
[
|
41
|
+
[B3,F3],
|
42
|
+
[Gb5,C4],
|
43
|
+
[C5,D2],
|
44
|
+
[D4,C4.transpose(0.5)],
|
45
|
+
[D4.transpose(0.5),C4.transpose(0.5)],
|
46
|
+
[F5,C4.transpose(0.01)],
|
47
|
+
[F5.transpose(-0.01),C4.transpose(-0.01)],
|
48
|
+
].each do |start,finish|
|
49
|
+
context "start at #{start.to_s}, target #{finish.to_s}" do
|
50
|
+
pitches = GlissandoConverter.glissando_pitches(start,finish)
|
51
|
+
|
52
|
+
it 'should move down to next whole (zero-cent) pitches' do
|
53
|
+
(1...pitches.size).each do |i|
|
54
|
+
pitches[i].cent.should eq(0)
|
55
|
+
pitches[i-1].diff(pitches[i]).should be <= 1
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should end on the whole (zero-cent) pitch above target pitch' do
|
60
|
+
pitches.last.cent.should eq(0)
|
61
|
+
diff = pitches.last.total_cents - finish.total_cents
|
62
|
+
diff.should be <= 100
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe '.glissando_elements' do
|
71
|
+
before :all do
|
72
|
+
@dur = Rational(3,2)
|
73
|
+
@acc = false
|
74
|
+
@els = GlissandoConverter.glissando_elements(C4,A4,@dur,@acc)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should return an array of LegatoElement objects' do
|
78
|
+
@els.each {|el| el.should be_a LegatoElement }
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'should split up duration among elements' do
|
82
|
+
sum = @els.map {|el| el.duration }.inject(0,:+)
|
83
|
+
sum.should eq(@dur)
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'should set accented as given' do
|
87
|
+
els = GlissandoConverter.glissando_elements(C4,A4,1,false)
|
88
|
+
els.each {|el| el.accented.should eq(false) }
|
89
|
+
els = GlissandoConverter.glissando_elements(C4,A4,1,true)
|
90
|
+
els.each {|el| el.accented.should eq(true) }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,230 @@
|
|
1
|
+
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
3
|
+
|
4
|
+
describe NoteSequenceExtractor do
|
5
|
+
describe '#initialize' do
|
6
|
+
it 'should clone original notes' do
|
7
|
+
notes = [ Note.quarter([C2]), Note.half, Note.half ]
|
8
|
+
extr = NoteSequenceExtractor.new(notes)
|
9
|
+
extr.notes[0].should eq(notes[0])
|
10
|
+
notes[0].transpose!(1)
|
11
|
+
extr.notes[0].should_not eq(notes[0])
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should maintain the same number of notes' do
|
15
|
+
extr = NoteSequenceExtractor.new(
|
16
|
+
[ Note.quarter, Note.half, Note.half ])
|
17
|
+
extr.notes.size.should eq 3
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should remove any bad ties (tying pitch does not exist in next note' do
|
21
|
+
extr = NoteSequenceExtractor.new(
|
22
|
+
[ Note.quarter([C4,E4], links: {C4 => Link::Tie.new}),
|
23
|
+
Note.quarter([E4]) ]
|
24
|
+
)
|
25
|
+
extr.notes[0].links.should_not have_key(C4)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should replace any good ties with slurs' do
|
29
|
+
extr = NoteSequenceExtractor.new(
|
30
|
+
[ Note.quarter([C4,E4], links: {C4 => Link::Tie.new, E4 => Link::Tie.new}),
|
31
|
+
Note.quarter([C4,E4]) ]
|
32
|
+
)
|
33
|
+
extr.notes[0].links[C4].should be_a Link::Slur
|
34
|
+
extr.notes[0].links[E4].should be_a Link::Slur
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should remove dead slur/legato (where target pitch is non-existent)' do
|
38
|
+
extr = NoteSequenceExtractor.new(
|
39
|
+
[ Note.quarter([C4,E4], links: { C4 => Link::Slur.new(D4), E4 => Link::Legato.new(F4) }),
|
40
|
+
Note.quarter([C4]) ]
|
41
|
+
)
|
42
|
+
extr.notes[0].links.should be_empty
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should remove any link where the source pitch is missing' do
|
46
|
+
extr = NoteSequenceExtractor.new(
|
47
|
+
[ Note.quarter([C4,D4,E4,F4,G4], links: {
|
48
|
+
Bb4 => Link::Tie.new, Db4 => Link::Slur.new(C4),
|
49
|
+
Eb4 => Link::Legato.new(D4), Gb4 => Link::Glissando.new(E4),
|
50
|
+
Ab5 => Link::Portamento.new(F4)
|
51
|
+
}),
|
52
|
+
Note.quarter([C4,D4,E4,F4,G4])
|
53
|
+
])
|
54
|
+
extr.notes[0].links.should be_empty
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should not remove portamento and glissando with non-existent target pitches' do
|
58
|
+
extr = NoteSequenceExtractor.new(
|
59
|
+
[ Note.quarter([C4,D4]),
|
60
|
+
Note.quarter([C4,D4,E4,F4,G4], links: {
|
61
|
+
C4 => Link::Tie.new, D4 => Link::Slur.new(Eb4),
|
62
|
+
E4 => Link::Legato.new(Gb4), F4 => Link::Glissando.new(A5),
|
63
|
+
G4 => Link::Portamento.new(Bb5)}) ]
|
64
|
+
)
|
65
|
+
extr.notes[-1].links.size.should eq 2
|
66
|
+
extr.notes[-1].links.should have_key(F4)
|
67
|
+
extr.notes[-1].links.should have_key(G4)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe '#extract_sequences' do
|
72
|
+
context 'empty note array' do
|
73
|
+
it 'should return empty' do
|
74
|
+
seqs = NoteSequenceExtractor.new([]).extract_sequences
|
75
|
+
seqs.should be_empty
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
context 'array of only rest notes' do
|
80
|
+
it 'should return empty' do
|
81
|
+
notes = [ Note::quarter, Note::quarter ]
|
82
|
+
seqs = NoteSequenceExtractor.new(notes).extract_sequences
|
83
|
+
seqs.should be_empty
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context 'array with only one note, single pitch' do
|
88
|
+
before :all do
|
89
|
+
@note = Note::quarter([C5])
|
90
|
+
@seqs = NoteSequenceExtractor.new([@note]).extract_sequences
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'should return array with one sequence' do
|
94
|
+
@seqs.size.should eq 1
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'should start offset 0' do
|
98
|
+
@seqs[0].start.should eq 0
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'should stop offset <= note duration' do
|
102
|
+
@seqs[0].stop.should be <= @note.duration
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context 'array with two slurred notes, single pitch' do
|
107
|
+
before :all do
|
108
|
+
@notes = [ Note.quarter([C5], articulation: SLUR), Note.quarter([D5]) ]
|
109
|
+
@seqs = NoteSequenceExtractor.new(@notes).extract_sequences
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'should return array with one sequence' do
|
113
|
+
@seqs.size.should eq 1
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'should start offset 0' do
|
117
|
+
@seqs[0].start.should eq 0
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'should stop offset <= combined duration of the two notes' do
|
121
|
+
@seqs[0].stop.should be <= (@notes[0].duration + @notes[1].duration)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
context 'array with one note, multiple pitches' do
|
126
|
+
before :all do
|
127
|
+
@note = Note.quarter([C5,D5,E5])
|
128
|
+
@seqs = NoteSequenceExtractor.new([@note]).extract_sequences
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'should return array with as many sequences as pitches' do
|
132
|
+
@seqs.size.should eq @note.pitches.size
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'should start the sequences at 0' do
|
136
|
+
@seqs.each {|s| s.start.should eq(0) }
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'should end each sequence at or before note duration' do
|
140
|
+
@seqs.each {|s| s.stop.should be <= @note.duration }
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'should put one pitch in each seq' do
|
144
|
+
@seqs.each {|s| s.pitches.size.should eq(1) }
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'should assign a different pitch to each' do
|
148
|
+
@seqs.map {|seq| seq.pitches[0] }.sort.should eq @note.pitches.sort
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
context 'array with multiple notes and links' do
|
153
|
+
before :all do
|
154
|
+
@notes = [ Note.quarter([C3,E3], links: {
|
155
|
+
C3 => Link::Slur.new(D3), E3 => Link::Legato.new(F3)}),
|
156
|
+
Note.eighth([D3,F3]) ]
|
157
|
+
@seqs = NoteSequenceExtractor.new(@notes).extract_sequences
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'should create a sequence for linked notes' do
|
161
|
+
@seqs.size.should eq(2)
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'should add pitch at 0 from first note' do
|
165
|
+
@seqs[0].pitches.should have_key(0)
|
166
|
+
@notes[0].pitches.should include(@seqs[0].pitches[0])
|
167
|
+
@seqs[1].pitches.should have_key(0)
|
168
|
+
@notes[0].pitches.should include(@seqs[1].pitches[0])
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
context 'single note with single pitch, and glissando up' do
|
173
|
+
before :all do
|
174
|
+
@note = Note.whole([D3], links: { D3 => Link::Glissando.new(G3) })
|
175
|
+
@seqs = NoteSequenceExtractor.new([@note]).extract_sequences
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'should produce one sequence' do
|
179
|
+
@seqs.size.should eq(1)
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'should include pitches up to (not including) target pitch' do
|
183
|
+
@seqs[0].pitches.values.should include(D3,Eb3,E3,F3,Gb3)
|
184
|
+
end
|
185
|
+
|
186
|
+
it 'should produce sequence with duration <= note duration' do
|
187
|
+
@seqs[0].duration.should be <= @note.duration
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
context 'single note with single pitch, and glissando down' do
|
192
|
+
before :all do
|
193
|
+
@note = Note.whole([D3], links: { D3 => Link::Glissando.new(A2) })
|
194
|
+
@seqs = NoteSequenceExtractor.new([@note]).extract_sequences
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'should produce one sequence' do
|
198
|
+
@seqs.size.should eq(1)
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'should include pitches down to (not including) target pitch' do
|
202
|
+
@seqs[0].pitches.values.should include(D3,Db3,C3,B2,Bb2)
|
203
|
+
end
|
204
|
+
|
205
|
+
it 'should produce sequence with duration <= note duration' do
|
206
|
+
@seqs[0].duration.should be <= @note.duration
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
context 'two notes with single pitch, glissando up to pitch in second note' do
|
211
|
+
before :all do
|
212
|
+
@notes = [Note.whole([D3], links: { D3 => Link::Glissando.new(G3) }),
|
213
|
+
Note.quarter([G3]) ]
|
214
|
+
@seqs = NoteSequenceExtractor.new(@notes).extract_sequences
|
215
|
+
end
|
216
|
+
|
217
|
+
it 'should produce a single sequence' do
|
218
|
+
@seqs.size.should eq(1)
|
219
|
+
end
|
220
|
+
|
221
|
+
it 'should includes pitches up through target pitch' do
|
222
|
+
@seqs[0].pitches.values.should include(D3,Eb3,E3,F3,Gb3,G3)
|
223
|
+
end
|
224
|
+
|
225
|
+
it 'should produce sequence with duration <= note1dur + note2dur' do
|
226
|
+
@seqs[0].duration.should be <= (@notes[0].duration + @notes[1].duration)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe NoteTimeConverter do
|
4
|
+
describe '.notes_per_second' do
|
5
|
+
it 'should multiply tempo and beat duration, then divide by 60' do
|
6
|
+
nps = NoteTimeConverter.notes_per_second(120,"1/4".to_r)
|
7
|
+
nps.should eq("1/2".to_r)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '#notes_per_second_at' do
|
12
|
+
it 'should invoke .notes_per_second using current tempo and beat duration values' do
|
13
|
+
tc = ValueComputer.new(
|
14
|
+
120, 1 => Change::Gradual.new(100,1), 2 => Change::Gradual.new(150,1))
|
15
|
+
bdc = ValueComputer.new Rational(1,4)
|
16
|
+
converter = NoteTimeConverter.new(tc,bdc,200)
|
17
|
+
(0..2).step(0.2).each do |offset|
|
18
|
+
tempo = tc.value_at(offset)
|
19
|
+
beat_duration = bdc.value_at(offset)
|
20
|
+
nps = NoteTimeConverter.notes_per_second(tempo,beat_duration)
|
21
|
+
converter.notes_per_second_at(offset).should eq nps
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "#time_elapsed" do
|
27
|
+
context "constant tempo" do
|
28
|
+
before :each do
|
29
|
+
@tempo_computer = ValueComputer.new 120
|
30
|
+
@beat_duration_computer = ValueComputer.new Rational(1,4)
|
31
|
+
sample_rate = 48
|
32
|
+
@converter = NoteTimeConverter.new(
|
33
|
+
@tempo_computer, @beat_duration_computer, sample_rate)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should return a time of zero when note end is zero." do
|
37
|
+
@converter.time_elapsed(0, 0).should eq(0)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should return a time of 1 second when note end is equal to the initial notes-per-second" do
|
41
|
+
note_end = @converter.notes_per_second_at(0)
|
42
|
+
@converter.time_elapsed(0, note_end).should eq(1)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
context "linear tempo-change" do
|
47
|
+
before :each do
|
48
|
+
@tempo_computer = ValueComputer.new(
|
49
|
+
120, 1 => Change::Gradual.new(60, 1))
|
50
|
+
@beat_duration_computer = ValueComputer.new(Rational(1,4))
|
51
|
+
sample_rate = 200
|
52
|
+
@converter = NoteTimeConverter.new(
|
53
|
+
@tempo_computer, @beat_duration_computer, sample_rate)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should return a time of zero when note end is zero." do
|
57
|
+
@converter.time_elapsed(0.0, 0.0).should eq(0.0)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should return a time of 3 sec during a 1-note long transition from 120bpm to 60bpm" do
|
61
|
+
@converter.notes_per_second_at(1.0).should eq(0.5)
|
62
|
+
@converter.notes_per_second_at(2.0).should eq(0.25)
|
63
|
+
|
64
|
+
@converter.time_elapsed(1.0, 2.0).should be_within(0.05).of(2.77)
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "#map_note_offsets_to_time_offsets" do
|
71
|
+
context "constant tempo" do
|
72
|
+
before :each do
|
73
|
+
@tempo_computer = ValueComputer.new 120
|
74
|
+
@beat_duration_computer = ValueComputer.new Rational(1,4)
|
75
|
+
sample_rate = 4800
|
76
|
+
@converter = NoteTimeConverter.new(
|
77
|
+
@tempo_computer, @beat_duration_computer, sample_rate)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should map offset 0.0 to time 0.0" do
|
81
|
+
map = @converter.map_note_offsets_to_time_offsets [0.0]
|
82
|
+
map[0.0].should eq(0.0)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should map offset 0.25 to time 0.5" do
|
86
|
+
map = @converter.map_note_offsets_to_time_offsets [0.0, 0.25]
|
87
|
+
map[0.25].should be_within(0.01).of(0.5)
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should map offset 1.0 to time 2.0" do
|
91
|
+
map = @converter.map_note_offsets_to_time_offsets [0.0, 1.0]
|
92
|
+
map[1.0].should be_within(0.01).of(2.0)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|