music-performance 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.document +3 -0
  3. data/.gitignore +7 -0
  4. data/.rspec +1 -0
  5. data/.ruby-version +1 -0
  6. data/.yardopts +1 -0
  7. data/ChangeLog.rdoc +5 -0
  8. data/Gemfile +3 -0
  9. data/LICENSE.txt +20 -0
  10. data/README.rdoc +28 -0
  11. data/Rakefile +54 -0
  12. data/bin/midify +61 -0
  13. data/lib/music-performance.rb +25 -0
  14. data/lib/music-performance/arrangement/midi/midi_events.rb +9 -0
  15. data/lib/music-performance/arrangement/midi/midi_util.rb +38 -0
  16. data/lib/music-performance/arrangement/midi/part_sequencer.rb +121 -0
  17. data/lib/music-performance/arrangement/midi/score_sequencer.rb +33 -0
  18. data/lib/music-performance/conversion/glissando_converter.rb +36 -0
  19. data/lib/music-performance/conversion/note_sequence_extractor.rb +100 -0
  20. data/lib/music-performance/conversion/note_time_converter.rb +76 -0
  21. data/lib/music-performance/conversion/portamento_converter.rb +26 -0
  22. data/lib/music-performance/conversion/score_collator.rb +121 -0
  23. data/lib/music-performance/conversion/score_time_converter.rb +112 -0
  24. data/lib/music-performance/model/note_attacks.rb +21 -0
  25. data/lib/music-performance/model/note_sequence.rb +113 -0
  26. data/lib/music-performance/util/interpolation.rb +18 -0
  27. data/lib/music-performance/util/note_linker.rb +30 -0
  28. data/lib/music-performance/util/optimization.rb +33 -0
  29. data/lib/music-performance/util/piecewise_function.rb +124 -0
  30. data/lib/music-performance/util/value_computer.rb +172 -0
  31. data/lib/music-performance/version.rb +7 -0
  32. data/music-performance.gemspec +33 -0
  33. data/spec/conversion/glissando_converter_spec.rb +93 -0
  34. data/spec/conversion/note_sequence_extractor_spec.rb +230 -0
  35. data/spec/conversion/note_time_converter_spec.rb +96 -0
  36. data/spec/conversion/portamento_converter_spec.rb +91 -0
  37. data/spec/conversion/score_collator_spec.rb +136 -0
  38. data/spec/conversion/score_time_converter_spec.rb +73 -0
  39. data/spec/model/note_sequence_spec.rb +147 -0
  40. data/spec/music-performance_spec.rb +7 -0
  41. data/spec/spec_helper.rb +8 -0
  42. data/spec/util/note_linker_spec.rb +68 -0
  43. data/spec/util/optimization_spec.rb +73 -0
  44. data/spec/util/value_computer_spec.rb +146 -0
  45. metadata +242 -0
@@ -0,0 +1,7 @@
1
+ # Prepare transcribed score for computer performance
2
+ module Music
3
+ module Performance
4
+ # music-performance version
5
+ VERSION = "0.2.1"
6
+ end
7
+ end
@@ -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