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