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.
Files changed (45) hide show
  1. data/README.md +52 -0
  2. data/Rakefile +31 -0
  3. data/lib/mtk/chord.rb +47 -0
  4. data/lib/mtk/constants/dynamics.rb +56 -0
  5. data/lib/mtk/constants/intervals.rb +76 -0
  6. data/lib/mtk/constants/pitch_classes.rb +18 -0
  7. data/lib/mtk/constants/pitches.rb +24 -0
  8. data/lib/mtk/constants/pseudo_constants.rb +25 -0
  9. data/lib/mtk/event.rb +61 -0
  10. data/lib/mtk/midi/file.rb +179 -0
  11. data/lib/mtk/note.rb +44 -0
  12. data/lib/mtk/numeric_extensions.rb +61 -0
  13. data/lib/mtk/pattern/choice.rb +21 -0
  14. data/lib/mtk/pattern/note_sequence.rb +60 -0
  15. data/lib/mtk/pattern/pitch_sequence.rb +22 -0
  16. data/lib/mtk/pattern/sequence.rb +65 -0
  17. data/lib/mtk/patterns.rb +4 -0
  18. data/lib/mtk/pitch.rb +112 -0
  19. data/lib/mtk/pitch_class.rb +113 -0
  20. data/lib/mtk/pitch_class_set.rb +106 -0
  21. data/lib/mtk/pitch_set.rb +95 -0
  22. data/lib/mtk/timeline.rb +160 -0
  23. data/lib/mtk/util/mappable.rb +14 -0
  24. data/lib/mtk.rb +36 -0
  25. data/spec/mtk/chord_spec.rb +74 -0
  26. data/spec/mtk/constants/dynamics_spec.rb +94 -0
  27. data/spec/mtk/constants/intervals_spec.rb +140 -0
  28. data/spec/mtk/constants/pitch_classes_spec.rb +35 -0
  29. data/spec/mtk/constants/pitches_spec.rb +23 -0
  30. data/spec/mtk/event_spec.rb +120 -0
  31. data/spec/mtk/midi/file_spec.rb +208 -0
  32. data/spec/mtk/note_spec.rb +65 -0
  33. data/spec/mtk/numeric_extensions_spec.rb +102 -0
  34. data/spec/mtk/pattern/choice_spec.rb +21 -0
  35. data/spec/mtk/pattern/note_sequence_spec.rb +121 -0
  36. data/spec/mtk/pattern/pitch_sequence_spec.rb +47 -0
  37. data/spec/mtk/pattern/sequence_spec.rb +54 -0
  38. data/spec/mtk/pitch_class_set_spec.rb +103 -0
  39. data/spec/mtk/pitch_class_spec.rb +165 -0
  40. data/spec/mtk/pitch_set_spec.rb +163 -0
  41. data/spec/mtk/pitch_spec.rb +217 -0
  42. data/spec/mtk/timeline_spec.rb +234 -0
  43. data/spec/spec_helper.rb +7 -0
  44. data/spec/test.mid +0 -0
  45. 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