mtk 0.0.3.2 → 0.0.3.3

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