mtk 0.0.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.
- data/README.md +52 -0
- data/Rakefile +31 -0
- data/lib/mtk/chord.rb +47 -0
- data/lib/mtk/constants/dynamics.rb +56 -0
- data/lib/mtk/constants/intervals.rb +76 -0
- data/lib/mtk/constants/pitch_classes.rb +18 -0
- data/lib/mtk/constants/pitches.rb +24 -0
- data/lib/mtk/constants/pseudo_constants.rb +25 -0
- data/lib/mtk/event.rb +61 -0
- data/lib/mtk/midi/file.rb +179 -0
- data/lib/mtk/note.rb +44 -0
- data/lib/mtk/numeric_extensions.rb +61 -0
- data/lib/mtk/pattern/choice.rb +21 -0
- data/lib/mtk/pattern/note_sequence.rb +60 -0
- data/lib/mtk/pattern/pitch_sequence.rb +22 -0
- data/lib/mtk/pattern/sequence.rb +65 -0
- data/lib/mtk/patterns.rb +4 -0
- data/lib/mtk/pitch.rb +112 -0
- data/lib/mtk/pitch_class.rb +113 -0
- data/lib/mtk/pitch_class_set.rb +106 -0
- data/lib/mtk/pitch_set.rb +95 -0
- data/lib/mtk/timeline.rb +160 -0
- data/lib/mtk/util/mappable.rb +14 -0
- data/lib/mtk.rb +36 -0
- data/spec/mtk/chord_spec.rb +74 -0
- data/spec/mtk/constants/dynamics_spec.rb +94 -0
- data/spec/mtk/constants/intervals_spec.rb +140 -0
- data/spec/mtk/constants/pitch_classes_spec.rb +35 -0
- data/spec/mtk/constants/pitches_spec.rb +23 -0
- data/spec/mtk/event_spec.rb +120 -0
- data/spec/mtk/midi/file_spec.rb +208 -0
- data/spec/mtk/note_spec.rb +65 -0
- data/spec/mtk/numeric_extensions_spec.rb +102 -0
- data/spec/mtk/pattern/choice_spec.rb +21 -0
- data/spec/mtk/pattern/note_sequence_spec.rb +121 -0
- data/spec/mtk/pattern/pitch_sequence_spec.rb +47 -0
- data/spec/mtk/pattern/sequence_spec.rb +54 -0
- data/spec/mtk/pitch_class_set_spec.rb +103 -0
- data/spec/mtk/pitch_class_spec.rb +165 -0
- data/spec/mtk/pitch_set_spec.rb +163 -0
- data/spec/mtk/pitch_spec.rb +217 -0
- data/spec/mtk/timeline_spec.rb +234 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/test.mid +0 -0
- metadata +97 -0
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MTK::PitchClassSet do
|
4
|
+
|
5
|
+
let(:pitch_classes) { [C,E,G] }
|
6
|
+
let(:pitch_class_set) { PitchClassSet.new(pitch_classes) }
|
7
|
+
|
8
|
+
it "is Enumerable" do
|
9
|
+
pitch_class_set.should be_a Enumerable
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "#pitch_classes" do
|
13
|
+
it "is the list of pitch_classes contained in this set" do
|
14
|
+
pitch_class_set.pitch_classes.should == pitch_classes
|
15
|
+
end
|
16
|
+
|
17
|
+
it "is immutable" do
|
18
|
+
lambda { pitch_class_set.pitch_classes << D }.should raise_error
|
19
|
+
end
|
20
|
+
|
21
|
+
it "does not affect the immutabilty of the pitch class list used to construct it" do
|
22
|
+
pitch_classes << D
|
23
|
+
pitch_classes.length.should == 4
|
24
|
+
end
|
25
|
+
|
26
|
+
it "is not affected by changes to the pitch class list used to construct it" do
|
27
|
+
pitch_class_set # force construction before we modify the pitch_classes array
|
28
|
+
pitch_classes << D
|
29
|
+
pitch_class_set.pitch_classes.length.should == 3
|
30
|
+
end
|
31
|
+
|
32
|
+
it "does not include duplicates" do
|
33
|
+
PitchClassSet.new([C, E, G, C]).pitch_classes.should == [C, E, G]
|
34
|
+
end
|
35
|
+
|
36
|
+
it "sorts the pitch_classes (C to B)" do
|
37
|
+
PitchClassSet.new([B, E, C]).pitch_classes.should == [C, E, B]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "#to_a" do
|
42
|
+
it "is equal to #pitch_classes" do
|
43
|
+
pitch_class_set.to_a.should == pitch_class_set.pitch_classes
|
44
|
+
end
|
45
|
+
|
46
|
+
it "is mutable" do
|
47
|
+
(pitch_class_set.to_a << Bb).should == [C, E, G, Bb]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "#each" do
|
52
|
+
it "yields each pitch_class" do
|
53
|
+
pcs = []
|
54
|
+
pitch_class_set.each{|pc| pcs << pc }
|
55
|
+
pcs.should == pitch_classes
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "#map" do
|
60
|
+
it "returns a PitchClassSet with each PitchClass replaced with the results of the block" do
|
61
|
+
pitch_class_set.map{|pc| pc + 2}.should == [D, Gb, A]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "#normal_order" do
|
66
|
+
it "permutes the set so that the first and last pitch classes are as close together as possible" do
|
67
|
+
PitchClassSet.new([E,A,C]).normal_order.should == [A,C,E]
|
68
|
+
end
|
69
|
+
|
70
|
+
it "breaks ties by minimizing the distance between the first and second-to-last pitch class" do
|
71
|
+
# 0,4,8,9,11
|
72
|
+
PitchClassSet.new([C,E,Ab,A,B]).normal_order.should == [Ab,A,B,C,E]
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
describe "#normal_form" do
|
78
|
+
it "is transposes the #normal_order so that the first pitch class set is 0 (C)" do
|
79
|
+
PitchClassSet.new([E,A,C]).normal_form.should == [0,3,7]
|
80
|
+
end
|
81
|
+
|
82
|
+
it "is invariant across reorderings of the pitch classes" do
|
83
|
+
PitchClassSet.new([C,E,G]).normal_form.should == [0,4,7]
|
84
|
+
PitchClassSet.new([E,C,G]).normal_form.should == [0,4,7]
|
85
|
+
PitchClassSet.new([G,E,C]).normal_form.should == [0,4,7]
|
86
|
+
end
|
87
|
+
|
88
|
+
it "is invariant across transpositions" do
|
89
|
+
PitchClassSet.new([C,Eb,G]).normal_form.should == [0,3,7]
|
90
|
+
PitchClassSet.new([Db,E,Ab]).normal_form.should == [0,3,7]
|
91
|
+
PitchClassSet.new([Bb,F,Db]).normal_form.should == [0,3,7]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe ".span_for" do
|
96
|
+
pending
|
97
|
+
end
|
98
|
+
|
99
|
+
describe ".span_between" do
|
100
|
+
pending
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MTK::PitchClass do
|
4
|
+
|
5
|
+
let(:names) {
|
6
|
+
[
|
7
|
+
['B#', 'C', 'Dbb'],
|
8
|
+
['B##', 'C#', 'Db'],
|
9
|
+
['C##', 'D', 'Ebb'],
|
10
|
+
['D#', 'Eb', 'Fbb'],
|
11
|
+
['D##', 'E', 'Fb'],
|
12
|
+
['E#', 'F', 'Gbb'],
|
13
|
+
['E##', 'F#', 'Gb'],
|
14
|
+
['F##', 'G', 'Abb'],
|
15
|
+
['G#', 'Ab'],
|
16
|
+
['G##', 'A', 'Bbb'],
|
17
|
+
['A#', 'Bb', 'Cbb'],
|
18
|
+
['A##', 'B', 'Cb']
|
19
|
+
].flatten
|
20
|
+
}
|
21
|
+
|
22
|
+
describe 'NAMES' do
|
23
|
+
it "is the 12 note names in western chromatic scale" do
|
24
|
+
PitchClass::NAMES =~ ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '.from_s' do
|
29
|
+
context "the argument is a valid name" do
|
30
|
+
it "returns a PitchClass" do
|
31
|
+
names.each { |name| PitchClass.from_s(name).should be_a PitchClass }
|
32
|
+
end
|
33
|
+
it "returns an object with that name" do
|
34
|
+
names.each { |name| PitchClass.from_s(name).name.should == name }
|
35
|
+
end
|
36
|
+
it "ignores case" do
|
37
|
+
for name in names
|
38
|
+
PitchClass.from_s(name.upcase).name.should == name
|
39
|
+
PitchClass.from_s(name.downcase).name.should == name
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
context "the argument is not a valid name" do
|
44
|
+
it "returns nil, if the name doesn't exist" do
|
45
|
+
PitchClass.from_s('z').should be_nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '.from_name' do
|
51
|
+
it "acts like from_s" do
|
52
|
+
for name in ['C', 'bbb', 'z']
|
53
|
+
PitchClass.from_name(name).should == PitchClass.from_s(name)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '.from_i' do
|
59
|
+
it "returns the PitchClass with that value" do
|
60
|
+
PitchClass.from_i(2).should == D
|
61
|
+
end
|
62
|
+
it "returns the PitchClass with that value mod 12" do
|
63
|
+
PitchClass.from_i(14).should == D
|
64
|
+
PitchClass.from_i(-8).should == E
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe '.[]' do
|
69
|
+
it "acts like from_name if the argument is a string" do
|
70
|
+
PitchClass['D'].should == PitchClass.from_name('D')
|
71
|
+
end
|
72
|
+
it "acts like from_i if the argument is a number" do
|
73
|
+
PitchClass[3].should == PitchClass.from_i(3)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe '#name' do
|
78
|
+
it "is the name of the pitch class" do
|
79
|
+
C.name.should == 'C'
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe '#to_i' do
|
84
|
+
it "is the integer value of the pitch class" do
|
85
|
+
C.to_i.should == 0
|
86
|
+
D.to_i.should == 2
|
87
|
+
E.to_i.should == 4
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe '#to_s' do
|
92
|
+
it "returns the name" do
|
93
|
+
C.to_s.should == C.name
|
94
|
+
for name in names
|
95
|
+
PitchClass.from_s(name).to_s.should == name
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe '#==' do
|
101
|
+
it "checks for equality" do
|
102
|
+
C.should == C
|
103
|
+
C.should_not == D
|
104
|
+
end
|
105
|
+
it "treats enharmonic names as equal" do
|
106
|
+
C.should == PitchClass['B#']
|
107
|
+
C.should == PitchClass['Dbb']
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe "#<=>" do
|
112
|
+
it "compares the underlying int value" do
|
113
|
+
(C <=> D).should < 0
|
114
|
+
(B <=> C).should > 0
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe '#+' do
|
119
|
+
it "adds the integer value of the argument and #to_i" do
|
120
|
+
(C + 4).should == E
|
121
|
+
end
|
122
|
+
|
123
|
+
it "'wraps around' the range 0-11" do
|
124
|
+
(D + 10).should == C
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
describe '#-' do
|
129
|
+
it "subtracts the integer value of the argument from #to_i" do
|
130
|
+
(E - 2).should == D
|
131
|
+
end
|
132
|
+
|
133
|
+
it "'wraps around' the range 0-11" do
|
134
|
+
(C - 8).should == E
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
describe "#distance_to" do
|
139
|
+
it "is the distance in semitones between 2 PitchClass objects" do
|
140
|
+
C.distance_to(D).should == 2
|
141
|
+
end
|
142
|
+
|
143
|
+
it "is the shortest distance (accounts from octave 'wrap around')" do
|
144
|
+
B.distance_to(C).should == 1
|
145
|
+
end
|
146
|
+
|
147
|
+
it "is a negative distance in semitones when the cloest given PitchClass is at a higher Pitch" do
|
148
|
+
D.distance_to(C).should == -2
|
149
|
+
C.distance_to(B).should == -1
|
150
|
+
end
|
151
|
+
|
152
|
+
it "is (positive) 6 for tritone distances, when this PitchClass is C-F" do
|
153
|
+
for pc in [C,Db,D,Eb,E,F]
|
154
|
+
pc.distance_to(pc+TT).should == 6
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
it "is -6 for tritone distances, when this PitchClass is Gb-B" do
|
159
|
+
for pc in [Gb,G,Ab,A,Bb,B]
|
160
|
+
pc.distance_to(pc+TT).should == -6
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MTK::PitchSet do
|
4
|
+
|
5
|
+
let(:pitches) { [C4, D4, E4, F4, G4, A4, B4] }
|
6
|
+
let(:pitch_set) { PitchSet.new(pitches) }
|
7
|
+
let(:c_major) { PitchSet.new([C4,E4,G4]) }
|
8
|
+
|
9
|
+
it "is Enumerable" do
|
10
|
+
pitch_set.should be_a Enumerable
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '#pitches' do
|
14
|
+
it 'is the list of pitches used to construct the scale' do
|
15
|
+
pitch_set.pitches.should == pitches
|
16
|
+
end
|
17
|
+
|
18
|
+
it "is immutable" do
|
19
|
+
lambda { pitch_set.pitches << Db4 }.should raise_error
|
20
|
+
end
|
21
|
+
|
22
|
+
it "does not affect the immutabilty of the pitch list used to construct it" do
|
23
|
+
pitches << Db4
|
24
|
+
pitches.length.should == 8
|
25
|
+
end
|
26
|
+
|
27
|
+
it "is not affected by changes to the pitch list used to construct it" do
|
28
|
+
pitch_set # force construction before we modify the pitches array
|
29
|
+
pitches << Db4
|
30
|
+
pitch_set.pitches.length.should == 7
|
31
|
+
end
|
32
|
+
|
33
|
+
it "does not include duplicates" do
|
34
|
+
PitchSet.new([C4, E4, G4, C4]).pitches.should == [C4, E4, G4]
|
35
|
+
end
|
36
|
+
|
37
|
+
it "sorts the pitches" do
|
38
|
+
PitchSet.new([G4, E4, C4]).pitches.should == [C4, E4, G4]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "#to_a" do
|
43
|
+
it "is equal to #pitches" do
|
44
|
+
pitch_set.to_a.should == pitch_set.pitches
|
45
|
+
end
|
46
|
+
|
47
|
+
it "is mutable" do
|
48
|
+
(c_major.to_a << Bb4).should == [C4, E4, G4, Bb4]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#each" do
|
53
|
+
it "yields each pitch" do
|
54
|
+
ps = []
|
55
|
+
pitch_set.each{|p| ps << p }
|
56
|
+
ps.should == pitches
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "#map" do
|
61
|
+
it "returns a PitchSet with each Pitch replaced with the results of the block" do
|
62
|
+
c_major.map{|p| p + 2}.should == [D4, Gb4, A4]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe "#to_pitch_class_set" do
|
67
|
+
it "is a PitchClassSet" do
|
68
|
+
pitch_set.to_pitch_class_set.should be_a PitchClassSet
|
69
|
+
end
|
70
|
+
|
71
|
+
it "contains all the pitch_classes in this PitchSet" do
|
72
|
+
pitch_set.to_pitch_class_set.pitch_classes.should == pitch_set.pitch_classes
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe '#pitch_classes' do
|
77
|
+
it 'is the list of pitch classes' do
|
78
|
+
pitch_set.pitch_classes.should == pitches.map { |p| p.pitch_class }
|
79
|
+
end
|
80
|
+
|
81
|
+
it "doesn't include duplicates" do
|
82
|
+
PitchSet.new([C4, C5, D5, C6, D4]).pitch_classes.should == [C, D]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe '#+' do
|
87
|
+
it 'transposes upward by the given semitones' do
|
88
|
+
(pitch_set + 12).should == PitchSet.new([C5, D5, E5, F5, G5, A5, B5])
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe '#-' do
|
93
|
+
it 'transposes downward by the given semitones' do
|
94
|
+
(pitch_set - 12).should == PitchSet.new([C3, D3, E3, F3, G3, A3, B3])
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe '#invert' do
|
99
|
+
it 'inverts all pitches around the given center pitch' do
|
100
|
+
(pitch_set.invert Gb4).should == PitchSet.new([C5, Bb4, Ab4, G4, F4, Eb4, Db4])
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'inverts all pitches around the first pitch, when no center pitch is given' do
|
104
|
+
pitch_set.invert.should == PitchSet.new([C4, Bb3, Ab3, G3, F3, Eb3, Db3])
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe '#include?' do
|
109
|
+
it 'returns true if the Pitch is in the PitchList' do
|
110
|
+
(pitch_set.include? C4).should be_true
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'returns false if the Pitch is not in the PitchList' do
|
114
|
+
(pitch_set.include? Db4).should be_false
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe '#==' do
|
119
|
+
it "is true when all the pitches are equal" do
|
120
|
+
PitchSet.new([C4, E4, G4]).should == PitchSet.new([Pitch.from_i(60), Pitch.from_i(64), Pitch.from_i(67)])
|
121
|
+
end
|
122
|
+
|
123
|
+
it "doesn't consider duplicates in the comparison" do
|
124
|
+
PitchSet.new([C4, C4]).should == PitchSet.new([C4])
|
125
|
+
end
|
126
|
+
|
127
|
+
it "doesn't consider the order of pitches" do
|
128
|
+
PitchSet.new([G4, E4, C4]).should == PitchSet.new([C4, E4, G4])
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
describe '#inversion' do
|
133
|
+
it "adds an octave to the chord's pitches starting from the lowest, for each whole number in a postive argument" do
|
134
|
+
c_major.inversion(2).should == PitchSet.new([G4,C5,E5])
|
135
|
+
end
|
136
|
+
|
137
|
+
it "subtracts an octave to the chord's pitches starting fromt he highest, for each whole number in a negative argument" do
|
138
|
+
c_major.inversion(-2).should == PitchSet.new([E3,G3,C4])
|
139
|
+
end
|
140
|
+
|
141
|
+
it "wraps around to the lowest pitch when the argument is bigger than the number of pitches in the chord (positive argument)" do
|
142
|
+
c_major.inversion(4).should == PitchSet.new([E5,G5,C6])
|
143
|
+
end
|
144
|
+
|
145
|
+
it "wraps around to the highest pitch when the magnitude of the argument is bigger than the number of pitches in the chord (negative argument)" do
|
146
|
+
c_major.inversion(-4).should == PitchSet.new([G2,C3,E3])
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe "#nearest" do
|
151
|
+
it "returns the nearest PitchSet where the first Pitch has the given PitchClass" do
|
152
|
+
c_major.nearest(F).should == c_major + 5
|
153
|
+
c_major.nearest(G).should == c_major - 5
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
describe "#to_s" do
|
158
|
+
it "looks like an array of pitches" do
|
159
|
+
c_major.to_s.should == "[C4, E4, G4]"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
@@ -0,0 +1,217 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MTK::Pitch do
|
4
|
+
|
5
|
+
let(:middle_c) { Pitch.new(C, 4) }
|
6
|
+
let(:lowest) { Pitch.new(C, -1) }
|
7
|
+
let(:highest) { Pitch.new(G, 9) }
|
8
|
+
let(:middle_c_and_50_cents) { Pitch.new(C, 4, 0.5) }
|
9
|
+
|
10
|
+
describe '.[]' do
|
11
|
+
it "constructs and caches a pitch with the given pitch_class and octave" do
|
12
|
+
Pitch[C,4].should be_equal Pitch[C,4]
|
13
|
+
end
|
14
|
+
it "retains the new() method's ability to construct uncached objects" do
|
15
|
+
Pitch.new(C,4).should_not be_equal Pitch[C,4]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#pitch_class' do
|
20
|
+
it "is the pitch class of the pitch" do
|
21
|
+
middle_c.pitch_class.should == C
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe '#octave' do
|
26
|
+
it "is the octave of the pitch" do
|
27
|
+
middle_c.octave.should == 4
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#offset' do
|
32
|
+
it 'is the third argument of the constructor' do
|
33
|
+
Pitch.new(C, 4, 0.6).offset.should == 0.6
|
34
|
+
end
|
35
|
+
it 'defaults to 0' do
|
36
|
+
Pitch.new(C, 4).offset.should == 0
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#offset_in_cents' do
|
41
|
+
it 'is #offset * 100' do
|
42
|
+
middle_c_and_50_cents.offset_in_cents.should == middle_c_and_50_cents.offset * 100
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '.from_i' do
|
47
|
+
it("converts 60 to middle C") { Pitch.from_i(60).should == middle_c }
|
48
|
+
it("converts 0 to C at octave -1") { Pitch.from_i(0).should == lowest }
|
49
|
+
it("converts 127 to G at octave 9") { Pitch.from_i(127).should == highest }
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '.from_f' do
|
53
|
+
it "converts 60.5 to middle C with a 0.5 offset" do
|
54
|
+
p = Pitch.from_f(60.5)
|
55
|
+
p.pitch_class.should == C
|
56
|
+
p.octave.should == 4
|
57
|
+
p.offset.should == 0.5
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe '.from_s' do
|
62
|
+
it("converts 'C4' to middle c") { Pitch.from_s('C4').should == middle_c }
|
63
|
+
it("converts 'c4' to middle c") { Pitch.from_s('c4').should == middle_c }
|
64
|
+
it("converts 'B#4' to middle c") { Pitch.from_s('B#4').should == middle_c }
|
65
|
+
end
|
66
|
+
|
67
|
+
describe ".from_hash" do
|
68
|
+
it "constructs a Pitch from a hash of pitch attributes" do
|
69
|
+
Pitch.from_hash({:pitch_class => C, :octave => 4, :offset => 0.5}).should == middle_c_and_50_cents
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe '#to_f' do
|
74
|
+
it "is 60.5 for middle C with a 0.5 offset" do
|
75
|
+
middle_c_and_50_cents.to_f.should == 60.5
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe '#to_i' do
|
80
|
+
it("is 60 for middle C") { middle_c.to_i.should == 60 }
|
81
|
+
it("is 0 for the C at octave -1") { lowest.to_i.should == 0 }
|
82
|
+
it("is 127 for the G at octave 9") { highest.to_i.should == 127 }
|
83
|
+
it "rounds to the nearest integer (the nearest semitone value) when there is an offset" do
|
84
|
+
Pitch.new(C, 4, 0.4).to_i.should == 60
|
85
|
+
Pitch.new(C, 4, 0.5).to_i.should == 61
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "#to_hash" do
|
90
|
+
it "converts to a Hash" do
|
91
|
+
middle_c_and_50_cents.to_hash.should == {:pitch_class => C, :octave => 4, :offset => 0.5}
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe '#==' do
|
96
|
+
it "compares the pitch_class and octave for equality" do
|
97
|
+
middle_c.should == Pitch.from_s('C4')
|
98
|
+
middle_c.should_not == Pitch.from_s('C3')
|
99
|
+
middle_c.should_not == Pitch.from_s('G4')
|
100
|
+
middle_c.should_not == Pitch.from_s('G3')
|
101
|
+
highest.should == Pitch.from_s('G9')
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe "#<=>" do
|
106
|
+
it "orders pitches based on their underlying float value" do
|
107
|
+
( Pitch.from_f(60) <=> Pitch.from_f(60.5) ).should < 0
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe '#to_s' do
|
112
|
+
it "should be the pitch class name and the octave" do
|
113
|
+
for pitch in [middle_c, lowest, highest]
|
114
|
+
pitch.to_s.should == pitch.pitch_class.name + pitch.octave.to_s
|
115
|
+
end
|
116
|
+
end
|
117
|
+
it "should include the offset_in_cents when the offset is not 0" do
|
118
|
+
middle_c_and_50_cents.to_s.should == "C4+50cents"
|
119
|
+
end
|
120
|
+
it "rounds to the nearest cent" do
|
121
|
+
Pitch.from_f(60.556).to_s.should == "C4+56cents"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe '#+' do
|
126
|
+
it 'adds the integer value of the argument and #to_i' do
|
127
|
+
(middle_c + 2).should == Pitch.from_i(62)
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'handles offsets' do
|
131
|
+
(middle_c + Pitch.from_f(0.5)).should == Pitch.from_f(60.5)
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'returns a new pitch (Pitch is immutabile)' do
|
135
|
+
original = Pitch.from_i(60)
|
136
|
+
modified = original + 2
|
137
|
+
original.should_not == modified
|
138
|
+
original.should == Pitch.from_i(60)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe '#-' do
|
143
|
+
it 'subtracts the integer value of the argument from #to_i' do
|
144
|
+
(middle_c - 2).should == Pitch.from_i(58)
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'handles offsets' do
|
148
|
+
(middle_c - Pitch.from_f(0.5)).should == Pitch.from_f(59.5)
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'returns a new pitch (Pitch is immutabile)' do
|
152
|
+
original = Pitch.from_i(60)
|
153
|
+
modified = original - 2
|
154
|
+
original.should_not == modified
|
155
|
+
original.should == Pitch.from_i(60)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
describe "#invert" do
|
160
|
+
context 'higher center pitch' do
|
161
|
+
it 'inverts the pitch around the given center pitch' do
|
162
|
+
middle_c.invert(Pitch.from_i 66).should == Pitch.from_i(72)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
context 'lower center pitch' do
|
167
|
+
it 'inverts the pitch around the given center pitch' do
|
168
|
+
middle_c.invert(Pitch.from_i 54).should == Pitch.from_i(48)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
describe "#nearest" do
|
174
|
+
it "is the Pitch with the nearest given PitchClass" do
|
175
|
+
middle_c.nearest(F).should == F4
|
176
|
+
middle_c.nearest(G).should == G3
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
describe '#coerce' do
|
181
|
+
it 'allows a Pitch to be added to a Numeric' do
|
182
|
+
(2 + middle_c).should == Pitch.from_i(62)
|
183
|
+
end
|
184
|
+
|
185
|
+
it 'allows a Pitch to be subtracted from a Numeric' do
|
186
|
+
(62 - middle_c).should == Pitch.from_i(2)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
describe "#clone_with" do
|
191
|
+
it "clones the Pitch when given an empty hash" do
|
192
|
+
middle_c.clone_with({}).should == middle_c
|
193
|
+
end
|
194
|
+
|
195
|
+
it "create a Pitch with the given :pitch_class, and the current Pitch's octave and offset if not provided" do
|
196
|
+
pitch2 = middle_c_and_50_cents.clone_with({:pitch_class => middle_c_and_50_cents.pitch_class+1})
|
197
|
+
pitch2.pitch_class.should == middle_c_and_50_cents.pitch_class + 1
|
198
|
+
pitch2.octave.should == middle_c_and_50_cents.octave
|
199
|
+
pitch2.offset.should == middle_c_and_50_cents.offset
|
200
|
+
end
|
201
|
+
|
202
|
+
it "create a Pitch with the given :octave, and the current Pitch's pitch_class and offset if not provided" do
|
203
|
+
pitch2 = middle_c_and_50_cents.clone_with({:octave => middle_c_and_50_cents.octave+1})
|
204
|
+
pitch2.pitch_class.should == middle_c_and_50_cents.pitch_class
|
205
|
+
pitch2.octave.should == middle_c_and_50_cents.octave + 1
|
206
|
+
pitch2.offset.should == middle_c_and_50_cents.offset
|
207
|
+
end
|
208
|
+
|
209
|
+
it "create a Pitch with the given :offset, and the current Pitch's pitch_class and octave if not provided" do
|
210
|
+
pitch2 = middle_c_and_50_cents.clone_with({:offset => middle_c_and_50_cents.offset+1})
|
211
|
+
pitch2.pitch_class.should == middle_c_and_50_cents.pitch_class
|
212
|
+
pitch2.octave.should == middle_c_and_50_cents.octave
|
213
|
+
pitch2.offset.should == middle_c_and_50_cents.offset + 1
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
end
|