mtk 0.0.3.2 → 0.0.3.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. data/.yardopts +2 -2
  2. data/DEVELOPMENT_NOTES.md +20 -0
  3. data/README.md +9 -3
  4. data/Rakefile +47 -13
  5. data/bin/mtk +55 -20
  6. data/examples/crescendo.rb +4 -4
  7. data/examples/{drum_pattern1.rb → drum_pattern.rb} +8 -8
  8. data/examples/dynamic_pattern.rb +5 -5
  9. data/examples/gets_and_play.rb +3 -2
  10. data/examples/notation.rb +3 -3
  11. data/examples/play_midi.rb +4 -4
  12. data/examples/print_midi.rb +2 -2
  13. data/examples/random_tone_row.rb +3 -3
  14. data/examples/syntax_to_midi.rb +2 -2
  15. data/examples/test_output.rb +4 -5
  16. data/examples/tone_row_melody.rb +7 -5
  17. data/lib/mtk/core/duration.rb +213 -0
  18. data/lib/mtk/core/intensity.rb +158 -0
  19. data/lib/mtk/core/interval.rb +157 -0
  20. data/lib/mtk/core/pitch.rb +154 -0
  21. data/lib/mtk/core/pitch_class.rb +194 -0
  22. data/lib/mtk/events/event.rb +4 -4
  23. data/lib/mtk/events/note.rb +12 -12
  24. data/lib/mtk/events/timeline.rb +232 -0
  25. data/lib/mtk/groups/chord.rb +56 -0
  26. data/lib/mtk/{helpers → groups}/collection.rb +33 -1
  27. data/lib/mtk/groups/melody.rb +96 -0
  28. data/lib/mtk/groups/pitch_class_set.rb +163 -0
  29. data/lib/mtk/{helpers → groups}/pitch_collection.rb +1 -1
  30. data/lib/mtk/{midi → io}/dls_synth_device.rb +3 -1
  31. data/lib/mtk/{midi → io}/dls_synth_output.rb +10 -10
  32. data/lib/mtk/{midi → io}/jsound_input.rb +2 -2
  33. data/lib/mtk/{midi → io}/jsound_output.rb +9 -9
  34. data/lib/mtk/{midi/file.rb → io/midi_file.rb} +13 -13
  35. data/lib/mtk/{midi/input.rb → io/midi_input.rb} +4 -4
  36. data/lib/mtk/{midi/output.rb → io/midi_output.rb} +8 -8
  37. data/lib/mtk/{helpers/lilypond.rb → io/notation.rb} +5 -5
  38. data/lib/mtk/{midi → io}/unimidi_input.rb +2 -2
  39. data/lib/mtk/{midi → io}/unimidi_output.rb +14 -9
  40. data/lib/mtk/{constants → lang}/durations.rb +11 -11
  41. data/lib/mtk/{constants → lang}/intensities.rb +11 -11
  42. data/lib/mtk/{constants → lang}/intervals.rb +17 -17
  43. data/lib/mtk/lang/mtk_grammar.citrus +9 -9
  44. data/lib/mtk/{constants → lang}/pitch_classes.rb +5 -5
  45. data/lib/mtk/{constants → lang}/pitches.rb +7 -7
  46. data/lib/mtk/{helpers → lang}/pseudo_constants.rb +1 -1
  47. data/lib/mtk/{variable.rb → lang/variable.rb} +1 -1
  48. data/lib/mtk/numeric_extensions.rb +40 -47
  49. data/lib/mtk/patterns/for_each.rb +1 -1
  50. data/lib/mtk/patterns/pattern.rb +3 -3
  51. data/lib/mtk/sequencers/event_builder.rb +16 -15
  52. data/lib/mtk/sequencers/legato_sequencer.rb +1 -1
  53. data/lib/mtk/sequencers/rhythmic_sequencer.rb +1 -1
  54. data/lib/mtk/sequencers/sequencer.rb +8 -8
  55. data/lib/mtk/sequencers/step_sequencer.rb +2 -2
  56. data/lib/mtk.rb +33 -39
  57. data/spec/mtk/{duration_spec.rb → core/duration_spec.rb} +3 -3
  58. data/spec/mtk/{intensity_spec.rb → core/intensity_spec.rb} +3 -3
  59. data/spec/mtk/{interval_spec.rb → core/interval_spec.rb} +1 -1
  60. data/spec/mtk/{pitch_class_spec.rb → core/pitch_class_spec.rb} +1 -1
  61. data/spec/mtk/{pitch_spec.rb → core/pitch_spec.rb} +8 -8
  62. data/spec/mtk/events/event_spec.rb +4 -4
  63. data/spec/mtk/events/note_spec.rb +8 -8
  64. data/spec/mtk/{timeline_spec.rb → events/timeline_spec.rb} +47 -47
  65. data/spec/mtk/{chord_spec.rb → groups/chord_spec.rb} +18 -16
  66. data/spec/mtk/{helpers → groups}/collection_spec.rb +3 -3
  67. data/spec/mtk/{melody_spec.rb → groups/melody_spec.rb} +36 -34
  68. data/spec/mtk/{pitch_class_set_spec.rb → groups/pitch_class_set_spec.rb} +57 -55
  69. data/spec/mtk/{midi/file_spec.rb → io/midi_file_spec.rb} +17 -17
  70. data/spec/mtk/{midi/output_spec.rb → io/midi_output_spec.rb} +6 -6
  71. data/spec/mtk/{constants → lang}/durations_spec.rb +1 -1
  72. data/spec/mtk/{constants → lang}/intensities_spec.rb +1 -1
  73. data/spec/mtk/{constants → lang}/intervals_spec.rb +1 -1
  74. data/spec/mtk/lang/parser_spec.rb +12 -6
  75. data/spec/mtk/{constants → lang}/pitch_classes_spec.rb +1 -1
  76. data/spec/mtk/{constants → lang}/pitches_spec.rb +1 -1
  77. data/spec/mtk/{helpers → lang}/pseudo_constants_spec.rb +2 -2
  78. data/spec/mtk/{variable_spec.rb → lang/variable_spec.rb} +4 -4
  79. data/spec/mtk/numeric_extensions_spec.rb +35 -55
  80. data/spec/mtk/patterns/for_each_spec.rb +1 -1
  81. data/spec/mtk/patterns/sequence_spec.rb +1 -1
  82. data/spec/mtk/sequencers/legato_sequencer_spec.rb +2 -2
  83. data/spec/mtk/sequencers/rhythmic_sequencer_spec.rb +4 -4
  84. data/spec/mtk/sequencers/step_sequencer_spec.rb +5 -5
  85. data/spec/spec_helper.rb +7 -6
  86. metadata +75 -61
  87. data/ext/mkrf_conf.rb +0 -25
  88. data/lib/mtk/chord.rb +0 -55
  89. data/lib/mtk/duration.rb +0 -211
  90. data/lib/mtk/helpers/convert.rb +0 -36
  91. data/lib/mtk/helpers/output_selector.rb +0 -67
  92. data/lib/mtk/intensity.rb +0 -156
  93. data/lib/mtk/interval.rb +0 -155
  94. data/lib/mtk/melody.rb +0 -94
  95. data/lib/mtk/pitch.rb +0 -152
  96. data/lib/mtk/pitch_class.rb +0 -192
  97. data/lib/mtk/pitch_class_set.rb +0 -161
  98. data/lib/mtk/timeline.rb +0 -230
  99. data/spec/mtk/midi/jsound_input_spec.rb +0 -11
  100. data/spec/mtk/midi/jsound_output_spec.rb +0 -11
  101. data/spec/mtk/midi/unimidi_input_spec.rb +0 -11
  102. data/spec/mtk/midi/unimidi_output_spec.rb +0 -11
@@ -0,0 +1,213 @@
1
+ module MTK
2
+ module Core
3
+
4
+ # A measure of time in musical beats.
5
+ # May be negative to indicate a rest, which uses the absolute value for the effective duration.
6
+ class Duration
7
+
8
+ include Comparable
9
+
10
+ # The names of the base durations. See {MTK::Lang::Durations} for more info.
11
+ NAMES = %w[w h q i s r x].freeze
12
+
13
+ VALUES_BY_NAME = {
14
+ 'w' => 4,
15
+ 'h' => 2,
16
+ 'q' => 1,
17
+ 'i' => Rational(1,2),
18
+ 's' => Rational(1,4),
19
+ 'r' => Rational(1,8),
20
+ 'x' => Rational(1,16)
21
+ }
22
+
23
+ @flyweight = {}
24
+
25
+ # The number of beats, typically represented as a Rational
26
+ attr_reader :value
27
+
28
+ def initialize( length_in_beats )
29
+ @value = length_in_beats
30
+ end
31
+
32
+ # Return a duration, only constructing a new instance when not already in the flyweight cache
33
+ def self.[](length_in_beats)
34
+ if length_in_beats.is_a? Fixnum
35
+ value = length_in_beats
36
+ else
37
+ value = Rational(length_in_beats)
38
+ end
39
+ @flyweight[value] ||= new(value)
40
+ end
41
+
42
+ class << self
43
+ alias :from_f :[]
44
+ alias :from_i :[]
45
+ end
46
+
47
+ # Lookup a duration by name.
48
+ # This method supports appending any combination of '.' and 't' for more fine-grained values.
49
+ # each '.' multiplies by 3/2, and each 't' multiplies by 2/3.
50
+ # You may use the prefix '-' to negate the duration (which turns it into a rest of the same length).
51
+ # You may also prefix (after the '-' if present) the base duration name with an integer, float, or rational number
52
+ # to multiply the base duration value. Rationals are in the form "#!{numerator_integer}/#!{denominator_integer}".
53
+ # @example lookup value of 'q.', which is 1.5 times a quarter note (1.5 beats):
54
+ # MTK::Core::Duration.from_s('q.')
55
+ # @example lookup the value of 3/4w, which three-quarters of a whole note (3 beats):
56
+ # MTK::Core::Duration.from_s('3/4w')
57
+ def self.from_s(s)
58
+ if s =~ /^(-)?(\d+([\.\/]\d+)?)?([whqisrx])((\.|t)*)$/i
59
+ name = $4.downcase
60
+ modifier = $5.downcase
61
+ modifier << $1 if $1 # negation
62
+ multiplier = $2
63
+ else
64
+ raise ArgumentError.new("Invalid Duration string '#{s}'")
65
+ end
66
+
67
+ value = VALUES_BY_NAME[name]
68
+ modifier.each_char do |mod|
69
+ case mod
70
+ when '-' then value *= -1
71
+ when '.' then value *= Rational(3,2)
72
+ when 't' then value *= Rational(2,3)
73
+ end
74
+ end
75
+
76
+ if multiplier
77
+ case multiplier
78
+ when /\./
79
+ value *= multiplier.to_f
80
+ when /\//
81
+ numerator, denominator = multiplier.split('/')
82
+ value *= Rational(numerator.to_i, denominator.to_i)
83
+ else
84
+ value *= multiplier.to_i
85
+ end
86
+ end
87
+
88
+ self[value]
89
+ end
90
+
91
+ class << self
92
+ alias :from_name :from_s
93
+ end
94
+
95
+ # The magnitude (absolute value) of the duration.
96
+ # This is the actual duration for rests.
97
+ # @see #rest?
98
+ def length
99
+ @value < 0 ? -@value : @value
100
+ end
101
+
102
+ # Durations with negative values are rests.
103
+ # @see #length
104
+ # @see #-@
105
+ def rest?
106
+ @value < 0
107
+ end
108
+
109
+ # The number of beats as a floating point number
110
+ def to_f
111
+ @value.to_f
112
+ end
113
+
114
+ # The numerical value for the nearest whole number of beats
115
+ def to_i
116
+ @value.round
117
+ end
118
+
119
+ def to_s
120
+ value = @value.to_s
121
+ value = sprintf '%.2f', @value if value.length > 6 # threshold is 6 for no particular reason...
122
+ "#{value} #{@value.abs > 1 || @value==0 ? 'beats' : 'beat'}"
123
+ end
124
+
125
+ def inspect
126
+ "#<#{self.class}:#{object_id} @value=#{@value}>"
127
+ end
128
+
129
+ def ==( other )
130
+ if other.is_a? MTK::Core::Duration
131
+ other.value == @value
132
+ else
133
+ other == @value
134
+ end
135
+ end
136
+
137
+ def <=> other
138
+ if other.respond_to? :value
139
+ @value <=> other.value
140
+ else
141
+ @value <=> other
142
+ end
143
+ end
144
+
145
+ # Add this duration to another.
146
+ # @return a new Duration that has a value of the sum of the arguments.
147
+ def + duration
148
+ if duration.is_a? MTK::Core::Duration
149
+ MTK::Core::Duration[@value + duration.value]
150
+ else
151
+ MTK::Core::Duration[@value + duration]
152
+ end
153
+ end
154
+
155
+ # Subtract another duration from this one.
156
+ # @return a new Duration that has a value of the difference of the arguments.
157
+ def - duration
158
+ if duration.is_a? MTK::Core::Duration
159
+ MTK::Core::Duration[@value - duration.value]
160
+ else
161
+ MTK::Core::Duration[@value - duration]
162
+ end
163
+ end
164
+
165
+ # Multiply this duration with another.
166
+ # @return a new Duration that has a value of the product of the arguments.
167
+ def * duration
168
+ if duration.is_a? MTK::Core::Duration
169
+ MTK::Core::Duration[@value * duration.value]
170
+ else
171
+ MTK::Core::Duration[@value * duration]
172
+ end
173
+ end
174
+
175
+ # Divide this duration with another.
176
+ # @return a new Duration that has a value of the division of the arguments.
177
+ def / duration
178
+ if duration.is_a? MTK::Core::Duration
179
+ MTK::Core::Duration[to_f / duration.value]
180
+ else
181
+ MTK::Core::Duration[to_f / duration]
182
+ end
183
+ end
184
+
185
+ # Negate the duration value.
186
+ # Turns normal durations into rests and vice versa.
187
+ # @return a new Duration that has a negated value.
188
+ # @see #rest?
189
+ def -@
190
+ MTK::Core::Duration[@value * -1]
191
+ end
192
+
193
+ # Allow basic math operations with Numeric objects.
194
+ def coerce(other)
195
+ return MTK::Core::Duration[other], self
196
+ end
197
+
198
+ end
199
+ end
200
+
201
+ # Construct a {Duration} from any supported type
202
+ def Duration(*anything)
203
+ anything = anything.first if anything.length == 1
204
+ case anything
205
+ when Numeric then MTK::Core::Duration[anything]
206
+ when String, Symbol then MTK::Core::Duration.from_s(anything)
207
+ when Duration then anything
208
+ else raise "Duration doesn't understand #{anything.class}"
209
+ end
210
+ end
211
+ module_function :Duration
212
+
213
+ end
@@ -0,0 +1,158 @@
1
+ module MTK
2
+ module Core
3
+
4
+ # A measure of intensity, using an underlying value in the range 0.0-1.0
5
+ class Intensity
6
+
7
+ include Comparable
8
+
9
+ # The names of the base intensities. See {}MTK::Lang::Intensities} for more info.
10
+ NAMES = %w[ppp pp p mp mf o ff fff].freeze
11
+
12
+ VALUES_BY_NAME = {
13
+ 'ppp' => 0.125,
14
+ 'pp' => 0.25,
15
+ 'p' => 0.375,
16
+ 'mp' => 0.5,
17
+ 'mf' => 0.625,
18
+ 'o' => 0.75,
19
+ 'ff' => 0.875,
20
+ 'fff' => 1.0
21
+ }
22
+
23
+ @flyweight = {}
24
+
25
+ # The number of beats, typically representation as a Rational
26
+ attr_reader :value
27
+
28
+ def initialize(value)
29
+ @value = value
30
+ end
31
+
32
+ # Return an Intensity, only constructing a new instance when not already in the flyweight cache
33
+ def self.[](value)
34
+ value = value.to_f
35
+ @flyweight[value] ||= new(value)
36
+ end
37
+
38
+ class << self
39
+ alias :from_f :[]
40
+ alias :from_i :[]
41
+ end
42
+
43
+ # Lookup an intensity by name.
44
+ # This method supports appending '-' or '+' for more fine-grained values.
45
+ def self.from_s(s)
46
+ return self[1.0] if s == 'fff+' # special case because "fff" is already the maximum
47
+
48
+ name = nil
49
+ modifier = nil
50
+ if s =~ /^(\w+)([+-])?$/
51
+ name = $1
52
+ modifier = $2
53
+ end
54
+
55
+ value = VALUES_BY_NAME[name]
56
+ raise ArgumentError.new("Invalid Intensity string '#{s}'") unless value
57
+
58
+ value += 1.0/24 if modifier == '+'
59
+ value -= 1.0/24 if modifier == '-'
60
+
61
+ self[value]
62
+ end
63
+
64
+ class << self
65
+ alias :from_name :from_s
66
+ end
67
+
68
+ # The number of beats as a floating point number
69
+ def to_f
70
+ @value.to_f
71
+ end
72
+
73
+ # The numerical value for the nearest whole number of beats
74
+ def to_i
75
+ @value.round
76
+ end
77
+
78
+ def to_midi
79
+ (to_f * 127).round
80
+ end
81
+
82
+ def to_percent
83
+ (@value * 100).round
84
+ end
85
+
86
+ def to_s
87
+ "#{to_percent}% intensity"
88
+ end
89
+
90
+ def inspect
91
+ "#<#{self.class}:#{object_id} @value=#{@value}>"
92
+ end
93
+
94
+ def ==( other )
95
+ other.is_a? MTK::Core::Intensity and other.value == @value
96
+ end
97
+
98
+ def <=> other
99
+ if other.respond_to? :value
100
+ @value <=> other.value
101
+ else
102
+ @value <=> other
103
+ end
104
+
105
+ end
106
+
107
+ def + intensity
108
+ if intensity.is_a? MTK::Core::Intensity
109
+ MTK::Core::Intensity[@value + intensity.value]
110
+ else
111
+ MTK::Core::Intensity[@value + intensity]
112
+ end
113
+ end
114
+
115
+ def -intensity
116
+ if intensity.is_a? MTK::Core::Intensity
117
+ MTK::Core::Intensity[@value - intensity.value]
118
+ else
119
+ MTK::Core::Intensity[@value - intensity]
120
+ end
121
+ end
122
+
123
+ def * intensity
124
+ if intensity.is_a? MTK::Core::Intensity
125
+ MTK::Core::Intensity[@value * intensity.value]
126
+ else
127
+ MTK::Core::Intensity[@value * intensity]
128
+ end
129
+ end
130
+
131
+ def / intensity
132
+ if intensity.is_a? MTK::Core::Intensity
133
+ MTK::Core::Intensity[to_f / intensity.value]
134
+ else
135
+ MTK::Core::Intensity[to_f / intensity]
136
+ end
137
+ end
138
+
139
+ def coerce(other)
140
+ return MTK::Core::Intensity[other], self
141
+ end
142
+
143
+ end
144
+ end
145
+
146
+ # Construct a {Duration} from any supported type
147
+ def Intensity(*anything)
148
+ anything = anything.first if anything.length == 1
149
+ case anything
150
+ when Numeric then MTK::Core::Intensity[anything]
151
+ when String, Symbol then MTK::Core::Intensity.from_s(anything)
152
+ when Intensity then anything
153
+ else raise "Intensity doesn't understand #{anything.class}"
154
+ end
155
+ end
156
+ module_function :Intensity
157
+
158
+ end
@@ -0,0 +1,157 @@
1
+ module MTK
2
+ module Core
3
+
4
+ # A measure of intensity, using an underlying value in the range 0.0-1.0
5
+ class Interval
6
+
7
+ include Comparable
8
+
9
+ # The preferred names of all pre-defined intervals
10
+ NAMES = %w[P1 m2 M2 m3 M3 P4 TT P5 m6 M6 m7 M7 P8].freeze
11
+
12
+ # All valid names of pre-defined intervals, indexed by their value.
13
+ NAMES_BY_VALUE =
14
+ [ # names # value # description
15
+ %w( P1 p1 ), # 0 # unison
16
+ %w( m2 min2 ), # 1 # minor second
17
+ %w( M2 maj2 ), # 2 # major second
18
+ %w( m3 min3 ), # 3 # minor third
19
+ %w( M3 maj3 ), # 4 # major third
20
+ %w( P4 p4 ), # 5 # perfect fourth
21
+ %w( TT tt ), # 6 # tritone (AKA augmented fourth, diminished fifth)
22
+ %w( P5 p5 ), # 7 # perfect fifth
23
+ %w( m6 min6 ), # 8 # minor sixth
24
+ %w( M6 maj6 ), # 9 # major sixth
25
+ %w( m7 min7 ), # 10 # minor seventh
26
+ %w( M7 maj7 ), # 11 # major seventh
27
+ %w( P8 p8 ) # 12 # octave
28
+ ].freeze
29
+
30
+ # A mapping from intervals names to their value
31
+ VALUES_BY_NAME = Hash[ # a map from a list of name,value pairs
32
+ NAMES_BY_VALUE.map.with_index do |names,value|
33
+ names.map{|name| [name,value] }
34
+ end.flatten(1)
35
+ ].freeze
36
+
37
+ # All valid interval names
38
+ ALL_NAMES = NAMES_BY_VALUE.flatten.freeze
39
+
40
+
41
+ @flyweight = {}
42
+
43
+ # The number of semitones represented by this interval
44
+ attr_reader :value
45
+
46
+ def initialize(value)
47
+ @value = value
48
+ end
49
+
50
+ # Return an {Interval}, only constructing a new instance when not already in the flyweight cache
51
+ def self.[](value)
52
+ value = value.to_f unless value.is_a? Fixnum
53
+ @flyweight[value] ||= new(value)
54
+ end
55
+
56
+ class << self
57
+ alias :from_f :[]
58
+ alias :from_i :[]
59
+ end
60
+
61
+ # Lookup an interval duration by name.
62
+ def self.from_s(s)
63
+ value = VALUES_BY_NAME[s.to_s]
64
+ raise ArgumentError.new("Invalid Interval string '#{s}'") unless value
65
+ self[value]
66
+ end
67
+
68
+ class << self
69
+ alias :from_name :from_s
70
+ end
71
+
72
+ # The number of semitones as a floating point number
73
+ def to_f
74
+ @value.to_f
75
+ end
76
+
77
+ # The numerical value for the nearest whole number of semitones in this interval
78
+ def to_i
79
+ @value.round
80
+ end
81
+
82
+ def to_s
83
+ @value.to_s
84
+ end
85
+
86
+ def inspect
87
+ "#{self.class}<#{to_s} semitones>"
88
+ end
89
+
90
+ def ==( other )
91
+ other.is_a? MTK::Core::Interval and other.value == @value
92
+ end
93
+
94
+ def <=> other
95
+ if other.respond_to? :value
96
+ @value <=> other.value
97
+ else
98
+ @value <=> other
99
+ end
100
+ end
101
+
102
+ def + interval
103
+ if interval.is_a? MTK::Core::Interval
104
+ MTK::Core::Interval[@value + interval.value]
105
+ else
106
+ MTK::Core::Interval[@value + interval]
107
+ end
108
+ end
109
+
110
+ def -interval
111
+ if interval.is_a? MTK::Core::Interval
112
+ MTK::Core::Interval[@value - interval.value]
113
+ else
114
+ MTK::Core::Interval[@value - interval]
115
+ end
116
+ end
117
+
118
+ def * interval
119
+ if interval.is_a? MTK::Core::Interval
120
+ MTK::Core::Interval[@value * interval.value]
121
+ else
122
+ MTK::Core::Interval[@value * interval]
123
+ end
124
+ end
125
+
126
+ def / interval
127
+ if interval.is_a? MTK::Core::Interval
128
+ MTK::Core::Interval[to_f / interval.value]
129
+ else
130
+ MTK::Core::Interval[to_f / interval]
131
+ end
132
+ end
133
+
134
+ def -@
135
+ MTK::Core::Interval[@value * -1]
136
+ end
137
+
138
+ def coerce(other)
139
+ return MTK::Core::Interval[other], self
140
+ end
141
+
142
+ end
143
+ end
144
+
145
+ # Construct a {Duration} from any supported type
146
+ def Interval(*anything)
147
+ anything = anything.first if anything.length == 1
148
+ case anything
149
+ when Numeric then MTK::Core::Interval[anything]
150
+ when String, Symbol then MTK::Core::Interval.from_s(anything)
151
+ when Interval then anything
152
+ else raise "Interval doesn't understand #{anything.class}"
153
+ end
154
+ end
155
+ module_function :Interval
156
+
157
+ end
@@ -0,0 +1,154 @@
1
+ module MTK
2
+ module Core
3
+
4
+ # A frequency represented by a {PitchClass}, an integer octave, and an offset in semitones.
5
+ class Pitch
6
+
7
+ include Comparable
8
+
9
+ attr_reader :pitch_class, :octave, :offset
10
+
11
+ def initialize( pitch_class, octave, offset=0 )
12
+ @pitch_class, @octave, @offset = pitch_class, octave, offset
13
+ @value = @pitch_class.to_i + 12*(@octave+1) + @offset
14
+ end
15
+
16
+ @flyweight = {}
17
+
18
+ # Return a pitch with no offset, only constructing a new instance when not already in the flyweight cache
19
+ def self.[](pitch_class, octave)
20
+ pitch_class = MTK.PitchClass(pitch_class)
21
+ @flyweight[[pitch_class,octave]] ||= new(pitch_class, octave)
22
+ end
23
+
24
+ # Lookup a pitch by name, which consists of any {PitchClass::VALID_NAMES} and an octave number.
25
+ # The name may also be optionally suffixed by +/-###cents (where ### is any number).
26
+ # @example get the Pitch for middle C :
27
+ # Pitch.from_s('C4')
28
+ # @example get the Pitch for middle C + 50 cents:
29
+ # Pitch.from_s('C4+50cents')
30
+ def self.from_s( name )
31
+ s = name.to_s
32
+ s = s[0..0].upcase + s[1..-1].downcase # normalize name
33
+ if s =~ /^([A-G](#|##|b|bb)?)(-?\d+)(\+(\d+(\.\d+)?)cents)?$/
34
+ pitch_class = PitchClass.from_s($1)
35
+ if pitch_class
36
+ octave = $3.to_i
37
+ offset_in_cents = $5.to_f
38
+ if offset_in_cents.nil? or offset_in_cents.zero?
39
+ return self[pitch_class, octave]
40
+ else
41
+ return new( pitch_class, octave, offset_in_cents/100.0 )
42
+ end
43
+ end
44
+ end
45
+ raise ArgumentError.new("Invalid pitch name: #{name.inspect}")
46
+ end
47
+
48
+ class << self
49
+ alias :from_name :from_s
50
+ end
51
+
52
+ # Convert a Numeric semitones value into a Pitch
53
+ def self.from_f( f )
54
+ i, offset = f.floor, f%1 # split into int and fractional part
55
+ pitch_class = PitchClass.from_i(i)
56
+ octave = i/12 - 1
57
+ if offset == 0
58
+ self[pitch_class, octave]
59
+ else
60
+ new( pitch_class, octave, offset )
61
+ end
62
+ end
63
+
64
+ def self.from_h(hash)
65
+ new hash[:pitch_class], hash[:octave], hash.fetch(:offset,0)
66
+ end
67
+
68
+ # Convert a Numeric semitones value into a Pitch
69
+ def self.from_i( i )
70
+ from_f( i )
71
+ end
72
+
73
+ # The numerical value of this pitch
74
+ def to_f
75
+ @value
76
+ end
77
+
78
+ # The numerical value for the nearest semitone
79
+ def to_i
80
+ @value.round
81
+ end
82
+
83
+ def offset_in_cents
84
+ @offset * 100
85
+ end
86
+
87
+ def to_h
88
+ {:pitch_class => @pitch_class, :octave => @octave, :offset => @offset}
89
+ end
90
+
91
+ def to_s
92
+ "#{@pitch_class}#{@octave}" + (@offset.zero? ? '' : "+#{offset_in_cents.round}cents")
93
+ end
94
+
95
+ def inspect
96
+ "#<#{self.class}:#{object_id} @value=#{@value}>"
97
+ end
98
+
99
+ def ==( other )
100
+ other.respond_to? :pitch_class and other.respond_to? :octave and other.respond_to? :offset and
101
+ other.pitch_class == @pitch_class and other.octave == @octave and other.offset == @offset
102
+ end
103
+
104
+ def <=> other
105
+ @value <=> other.to_f
106
+ end
107
+
108
+ def + interval_in_semitones
109
+ self.class.from_f( @value + interval_in_semitones.to_f )
110
+ end
111
+ alias transpose +
112
+
113
+ def - interval_in_semitones
114
+ self.class.from_f( @value - interval_in_semitones.to_f )
115
+ end
116
+
117
+ def invert(center_pitch)
118
+ self + 2*(center_pitch.to_f - to_f)
119
+ end
120
+
121
+ def nearest(pitch_class)
122
+ self + self.pitch_class.distance_to(pitch_class)
123
+ end
124
+
125
+ def coerce(other)
126
+ return self.class.from_f(other.to_f), self
127
+ end
128
+
129
+ def clone_with(hash)
130
+ self.class.from_h(to_h.merge hash)
131
+ end
132
+
133
+ end
134
+ end
135
+
136
+ # Construct a {Pitch} from any supported type
137
+ def Pitch(*anything)
138
+ anything = anything.first if anything.length == 1
139
+ case anything
140
+ when Numeric then MTK::Core::Pitch.from_f(anything)
141
+ when String, Symbol then MTK::Core::Pitch.from_s(anything)
142
+ when MTK::Core::Pitch then anything
143
+ when Array
144
+ if anything.length == 2
145
+ MTK::Core::Pitch[*anything]
146
+ else
147
+ MTK::Core::Pitch.new(*anything)
148
+ end
149
+ else raise ArgumentError.new("Pitch doesn't understand #{anything.class}")
150
+ end
151
+ end
152
+ module_function :Pitch
153
+
154
+ end