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,91 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe PortamentoConverter do
4
+ describe '.portamento_pitches' do
5
+ context 'start pitch <= target_pitch' do
6
+ [
7
+ [C4,E4,10],
8
+ [D2,G3,11],
9
+ [C4.transpose(0.5), A4.transpose(-0.3), 12],
10
+ [D5,D5,7],
11
+ [D5,D5.transpose(0.06),7],
12
+ [D5,D5.transpose(0.07),7],
13
+ [D5,D5.transpose(0.08),7],
14
+ ].each do |start,finish,step_size|
15
+ context "start at #{start.to_s}, end at #{finish.to_s}" do
16
+ pitches = PortamentoConverter.portamento_pitches(start,finish,step_size)
17
+
18
+ it 'should begin at start pitch' do
19
+ pitches.first.should eq(start)
20
+ end
21
+
22
+ it 'should space pitches using given cent step size' do
23
+ (1...pitches.size).each do |i|
24
+ diff = pitches[i].total_cents - pitches[i-1].total_cents
25
+ diff.should eq(step_size)
26
+ end
27
+ end
28
+
29
+ it 'should end where one more step would be >= target pitch' do
30
+ diff = finish.total_cents - pitches.last.total_cents
31
+ diff.should be <= step_size
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ context 'start pitch > target_pitch' do
38
+ [
39
+ [E4,C4,10],
40
+ [G3,D2,11],
41
+ [A4,Bb3,12],
42
+ [A4.transpose(-0.33),Bb3.transpose(0.11),13],
43
+ [Bb3.transpose(0.64),Bb3.transpose(0.54),14],
44
+ ].each do |start,finish,step_size|
45
+ context "start at #{start.to_s}, end at #{finish.to_s}" do
46
+ pitches = PortamentoConverter.portamento_pitches(start,finish,step_size)
47
+
48
+ it 'should begin at start pitch' do
49
+ pitches.first.should eq(start)
50
+ end
51
+
52
+ it 'should space pitches using negative of given cent step size' do
53
+ (1...pitches.size).each do |i|
54
+ diff = pitches[i-1].total_cents - pitches[i].total_cents
55
+ diff.should eq(step_size)
56
+ end
57
+ end
58
+
59
+ it 'should end where one more step would be <= target pitch' do
60
+ diff = pitches.last.total_cents - finish.total_cents
61
+ diff.should be <= step_size
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ describe '.portamento_elements' do
69
+ before :all do
70
+ @dur = Rational(3,2)
71
+ @acc = false
72
+ @els = PortamentoConverter.portamento_elements(C4,F4,25,@dur,@acc)
73
+ end
74
+
75
+ it 'should return an array of SlurredElement objects' do
76
+ @els.each {|el| el.should be_a SlurredElement }
77
+ end
78
+
79
+ it 'should split up duration among elements' do
80
+ sum = @els.map {|el| el.duration }.inject(0,:+)
81
+ sum.should eq(@dur)
82
+ end
83
+
84
+ it 'should set accented as given' do
85
+ els = PortamentoConverter.portamento_elements(C4,D4,10,1,false)
86
+ els.each {|el| el.accented.should eq(false) }
87
+ els = PortamentoConverter.portamento_elements(C4,D4,10,1,true)
88
+ els.each {|el| el.accented.should eq(true) }
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,136 @@
1
+ require 'spec_helper'
2
+
3
+ describe ScoreCollator do
4
+ describe '#collate_parts' do
5
+ before :all do
6
+ @part = Part.new(Dynamics::FF,
7
+ notes: [ Note.quarter([C2]),
8
+ Note.half([D2]),
9
+ Note.half([E2])
10
+ ])
11
+ end
12
+
13
+ context 'first note starts before the segment start' do
14
+ context 'first note ends right at segment start' do
15
+ it 'should not be included in the part' do
16
+ score = Score.new(FOUR_FOUR, 120,
17
+ parts: {1 => @part},
18
+ program: Program.new(["1/4".to_r..."5/4".to_r]))
19
+ collator = ScoreCollator.new(score)
20
+ parts = collator.collate_parts
21
+ notes = parts[1].notes
22
+ notes.size.should eq(@part.notes.size - 1)
23
+ notes[0].pitches[0].should eq D2
24
+ notes[1].pitches[0].should eq E2
25
+ end
26
+ end
27
+
28
+ context 'first note ends after segment start' do
29
+ it 'should not be included in the part, and a rest is inserted' do
30
+ score = Score.new(FOUR_FOUR, 120,
31
+ parts: {1 => @part},
32
+ program: Program.new(["1/8".to_r..."5/4".to_r]))
33
+ collator = ScoreCollator.new(score)
34
+ parts = collator.collate_parts
35
+ notes = parts[1].notes
36
+ notes.size.should eq(@part.notes.size)
37
+ notes[0].pitches.should be_empty
38
+ notes[0].duration.should eq "1/8".to_r
39
+ notes[1].pitches[0].should eq D2
40
+ notes[2].pitches[0].should eq E2
41
+ end
42
+ end
43
+ end
44
+
45
+ context 'first note starts at segment start' do
46
+ context 'last note starts at program end' do
47
+ it 'should not be included in the part' do
48
+ score = Score.new(FOUR_FOUR, 120,
49
+ parts: {1 => @part},
50
+ program: Program.new([0.to_r..."3/4".to_r]))
51
+ collator = ScoreCollator.new(score)
52
+ parts = collator.collate_parts
53
+ notes = parts[1].notes
54
+ notes.size.should eq(@part.notes.size - 1)
55
+ end
56
+ end
57
+
58
+ context 'last note start before program end, but lasts until after' do
59
+ it 'should be included in the part, but truncated' do
60
+ score = Score.new(FOUR_FOUR, 120,
61
+ parts: {1 => @part},
62
+ program: Program.new([0.to_r...1.to_r]))
63
+ collator = ScoreCollator.new(score)
64
+ parts = collator.collate_parts
65
+ notes = parts[1].notes
66
+ notes.size.should eq(@part.notes.size)
67
+ notes[-1].duration.should eq("1/4".to_r)
68
+ end
69
+ end
70
+
71
+ context 'last note ends before program segment end' do
72
+ it 'should insert a rest between last note end and segment end' do
73
+ score = Score.new(FOUR_FOUR, 120,
74
+ parts: {1 => @part},
75
+ program: Program.new([0.to_r..."6/4".to_r]))
76
+ collator = ScoreCollator.new(score)
77
+ parts = collator.collate_parts
78
+ notes = parts[1].notes
79
+ notes.size.should eq(@part.notes.size + 1)
80
+ notes[-1].pitches.should be_empty
81
+ notes[-1].duration.should eq("1/4".to_r)
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ describe '#collate_tempo_changes' do
88
+ before :all do
89
+ @change0 = Change::Immediate.new(120)
90
+ @change1 = Change::Immediate.new(200)
91
+ @change2 = Change::Gradual.new(100,1)
92
+ end
93
+
94
+ context 'tempo change at end of program segment' do
95
+ it 'should not be included in the tempo changes' do
96
+ score = Score.new(FOUR_FOUR, 120, tempo_changes: {
97
+ 1 => @change1, 2 => @change2 }, program: Program.new([0..2]))
98
+ collator = ScoreCollator.new(score)
99
+ tcs = collator.collate_tempo_changes
100
+ tcs.size.should eq 2
101
+ tcs[0.to_r].should eq @change0
102
+ tcs[1.to_r].should eq @change1
103
+ end
104
+ end
105
+
106
+ context 'tempo change starts before segment end, lasts until after' do
107
+ it 'should be included in the tempo changes, but truncated' do
108
+ score = Score.new(FOUR_FOUR, 120, tempo_changes: {
109
+ 1 => @change1, 2 => @change2 }, program: Program.new([0..2.5]))
110
+ collator = ScoreCollator.new(score)
111
+ tcs = collator.collate_tempo_changes
112
+ tcs.size.should eq 3
113
+ tcs[0.to_r].should eq @change0
114
+ tcs[1.to_r].should eq @change1
115
+ tcs[2.to_r].should eq @change2
116
+ tcs[2.to_r].duration.should eq(0.5)
117
+ tcs[2.to_r].value.should eq(150.0)
118
+ end
119
+ end
120
+ end
121
+
122
+ describe '#collate_meter_changes' do
123
+ it 'should behave just as #collate_tempo_changes' do
124
+ change0 = Change::Immediate.new(FOUR_FOUR)
125
+ change1 = Change::Immediate.new(THREE_FOUR)
126
+ change2 = Change::Immediate.new(SIX_EIGHT)
127
+ score = Score.new(FOUR_FOUR, 120, meter_changes: {
128
+ 1 => change1, 2 => change2 }, program: Program.new([0...2]))
129
+ collator = ScoreCollator.new(score)
130
+ tcs = collator.collate_meter_changes
131
+ tcs.size.should eq 2
132
+ tcs[0.to_r].should eq change0
133
+ tcs[1.to_r].should eq change1
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,73 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe ScoreTimeConverter do
4
+ before :all do
5
+ @score = Score.new(
6
+ Meter.new(4,"1/4".to_r),
7
+ 120,
8
+ program: Program.new([(1.to_r)...(2.to_r)]),
9
+ parts: {
10
+ "abc" => Part.new(
11
+ Dynamics::MF,
12
+ notes: [
13
+ Note.new(0.1,[C5]),
14
+ Note.new(0.2,[D5]),
15
+ Note.new(0.3,[C5]),
16
+ Note.new(0.4,[D5]),
17
+ ]
18
+ )
19
+ },
20
+ )
21
+ @converter = ScoreTimeConverter.new(@score,1000)
22
+ end
23
+
24
+ describe '#convert_parts' do
25
+ before :all do
26
+ @timeparts = @converter.convert_parts
27
+ end
28
+
29
+ it 'should return a hash of Part objects' do
30
+ @timeparts.values.count {|v| !v.is_a?(Part)}.should eq 0
31
+ end
32
+
33
+ it "should produce notes with duration appropriate to the tempo" do
34
+ part = @timeparts.values.first
35
+ part.notes[0].duration.should be_within(0.01).of(0.2)
36
+ part.notes[1].duration.should be_within(0.01).of(0.4)
37
+ part.notes[2].duration.should be_within(0.01).of(0.6)
38
+ part.notes[3].duration.should be_within(0.01).of(0.8)
39
+ end
40
+
41
+ it "should produce notes twice as long when tempo is half" do
42
+ score = @score.clone
43
+ score.parts.values.first.notes.concat [
44
+ Note.new(0.2,[C5]), Note.new(0.4,[D5]), Note.new(0.3,[C5]), Note.new(0.1,[D5]) ]
45
+ score.tempo_changes[1.to_r] = Change::Immediate.new(60)
46
+ converter = ScoreTimeConverter.new(score,1000)
47
+ timeparts = converter.convert_parts
48
+
49
+ part = timeparts.values.first
50
+ part.notes[0].duration.should be_within(0.01).of(0.2)
51
+ part.notes[1].duration.should be_within(0.01).of(0.4)
52
+ part.notes[2].duration.should be_within(0.01).of(0.6)
53
+ part.notes[3].duration.should be_within(0.01).of(0.8)
54
+
55
+ part.notes[4].duration.should be_within(0.01).of(0.8)
56
+ part.notes[5].duration.should be_within(0.01).of(1.6)
57
+ part.notes[6].duration.should be_within(0.01).of(1.2)
58
+ part.notes[7].duration.should be_within(0.01).of(0.4)
59
+ end
60
+ end
61
+
62
+ describe '#convert_program' do
63
+ before :all do
64
+ @timeprogram = @converter.convert_program
65
+ end
66
+
67
+ it 'should convert note offsets to time offsets' do
68
+ timeseg = @timeprogram.segments[0]
69
+ timeseg.first.should be_within(0.01).of(2)
70
+ timeseg.last.should be_within(0.01).of(4)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,147 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe NoteSequence do
4
+ describe '#initialize' do
5
+ it 'should assign given start, stop, pitches and attacks' do
6
+ start, stop = 15, 22
7
+ pitches = { 15 => F2, 16 => G2, 16.1 => Ab2, 21.99 => C2 }
8
+ attacks = { 15 => ACCENTED, 17 => UNACCENTED, 18 => ACCENTED }
9
+ seq = NoteSequence.new(start,stop,pitches,attacks)
10
+ seq.start.should eq(start)
11
+ seq.stop.should eq(stop)
12
+ seq.pitches.should eq(pitches)
13
+ seq.attacks.should eq(attacks)
14
+ end
15
+
16
+ it 'should raise ArgumentError if start offset >= stop offset' do
17
+ expect do
18
+ NoteSequence.new(20,19, { 20 => C4 }, { 20 => UNACCENTED })
19
+ end.to raise_error(ArgumentError)
20
+ end
21
+
22
+ it 'should raise ArgumentError if no pitches are given' do
23
+ expect do
24
+ NoteSequence.new(20,21, {}, { 20 => UNACCENTED })
25
+ end.to raise_error(ArgumentError)
26
+ end
27
+
28
+ it 'should raise ArgumentError if no attacks are given' do
29
+ expect do
30
+ NoteSequence.new(20,21, { 20 => C4 }, {})
31
+ end.to raise_error(ArgumentError)
32
+ end
33
+
34
+ it 'should raise ArgumentError if no start pitch is given' do
35
+ expect do
36
+ NoteSequence.new(20,21, { 20.1 => C4 }, { 20 => UNACCENTED })
37
+ end.to raise_error(ArgumentError)
38
+ end
39
+
40
+ it 'should raise ArgumentError if no start attack is given' do
41
+ expect do
42
+ NoteSequence.new(20,21, { 20 => C4 }, { 20.1 => UNACCENTED })
43
+ end.to raise_error(ArgumentError)
44
+ end
45
+
46
+ it 'should raise ArgumentError if any pitch offset is not between start..stop' do
47
+ expect do
48
+ NoteSequence.new(20,21, { 20 => C4, 21.01 => D4 }, { 20 => UNACCENTED })
49
+ end.to raise_error(ArgumentError)
50
+
51
+ expect do
52
+ NoteSequence.new(20,21, { 20 => C4, 19.99 => D4 }, { 20 => UNACCENTED })
53
+ end.to raise_error(ArgumentError)
54
+ end
55
+
56
+ it 'should raise ArgumentError if any attack offset is not between start..stop' do
57
+ expect do
58
+ NoteSequence.new(20,21, { 20 => C4 }, { 20 => UNACCENTED, 21.01 => ACCENTED })
59
+ end.to raise_error(ArgumentError)
60
+
61
+ expect do
62
+ NoteSequence.new(20,21, { 20 => C4 }, { 20 => UNACCENTED, 19.99 => ACCENTED })
63
+ end.to raise_error(ArgumentError)
64
+ end
65
+ end
66
+
67
+ describe '.from_elements' do
68
+ it 'should raise ArgumentError if no elements are given' do
69
+ expect { NoteSequence.from_elements(2,[]) }.to raise_error(ArgumentError)
70
+ end
71
+
72
+ context 'single element' do
73
+ before :all do
74
+ @offset = 0
75
+ @el = FinalElement.new(2, C2, true, NORMAL)
76
+ @seq = NoteSequence.from_elements(@offset, [ @el ])
77
+
78
+ end
79
+
80
+ it 'should return a NoteSequence' do
81
+ @seq.should be_a NoteSequence
82
+ end
83
+
84
+ it 'should set start offset to given offset' do
85
+ @seq.start.should eq(@offset)
86
+ end
87
+
88
+ it 'should set stop offset no more than "duration of first element" away from start' do
89
+ (@seq.stop - @seq.start).should be <= @el.duration
90
+ end
91
+
92
+ it 'should set start pitch according to element pitch' do
93
+ @seq.pitches[@seq.start].should eq(@el.pitch)
94
+ end
95
+
96
+ it 'should set start attack according to element.accented' do
97
+ @seq.attacks[@seq.start].accented?.should eq(@el.accented)
98
+ end
99
+ end
100
+
101
+ context 'multi-element' do
102
+ before :all do
103
+ @offset = 1.5
104
+ @els = [
105
+ SlurredElement.new(1.0, A2, false),
106
+ LegatoElement.new(1.1, B2, false),
107
+ SlurredElement.new(1.2, C2, false),
108
+ LegatoElement.new(1.3, B2, false),
109
+ FinalElement.new(1.4, A2, false, NORMAL)
110
+ ]
111
+ @seq = NoteSequence.from_elements(@offset, @els)
112
+ end
113
+
114
+ it 'should place pitches according to element duration' do
115
+ offset = @offset
116
+ @els.each do |el|
117
+ @seq.pitches.should have_key(offset)
118
+ @seq.pitches[offset].should eq(el.pitch)
119
+ offset += el.duration
120
+ end
121
+ end
122
+
123
+ it 'should place attacks at beginning and following non-slur elements' do
124
+ @seq.attacks.should have_key(@offset)
125
+
126
+ offset = @offset + @els.first.duration
127
+ (1...@els.size).each do |i|
128
+ unless @els[i-1].slurred?
129
+ @seq.attacks.should have_key(offset)
130
+ end
131
+ offset += @els[i].duration
132
+ end
133
+ end
134
+ end
135
+
136
+ context 'elements contain slur to same pitch' do
137
+ it 'should not add same pitch nor attack for second element' do
138
+ els = [ SlurredElement.new(1, C4, false), FinalElement.new(1, C4, false, NORMAL) ]
139
+ seq = NoteSequence.from_elements(0, els)
140
+ seq.pitches.should have_key(0)
141
+ seq.pitches.should_not have_key(1)
142
+ seq.attacks.should have_key(0)
143
+ seq.attacks.should_not have_key(1)
144
+ end
145
+ end
146
+ end
147
+ end