music-performance 0.2.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/.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
|