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.
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