mtk 0.0.2 → 0.0.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 (147) hide show
  1. data/.yardopts +3 -2
  2. data/DEVELOPMENT_NOTES.md +114 -0
  3. data/INTRO.md +64 -8
  4. data/LICENSE.txt +1 -1
  5. data/README.md +31 -102
  6. data/Rakefile +56 -18
  7. data/bin/mtk +215 -0
  8. data/examples/crescendo.rb +5 -5
  9. data/examples/drum_pattern1.rb +23 -0
  10. data/examples/dynamic_pattern.rb +8 -11
  11. data/examples/gets_and_play.rb +26 -0
  12. data/examples/notation.rb +22 -0
  13. data/examples/play_midi.rb +8 -10
  14. data/examples/random_tone_row.rb +2 -2
  15. data/examples/syntax_to_midi.rb +28 -0
  16. data/examples/test_output.rb +8 -0
  17. data/examples/tone_row_melody.rb +6 -6
  18. data/lib/mtk.rb +52 -40
  19. data/lib/mtk/chord.rb +55 -0
  20. data/lib/mtk/constants/durations.rb +57 -0
  21. data/lib/mtk/constants/intensities.rb +61 -0
  22. data/lib/mtk/constants/intervals.rb +73 -0
  23. data/lib/mtk/constants/pitch_classes.rb +29 -0
  24. data/lib/mtk/constants/pitches.rb +52 -0
  25. data/lib/mtk/duration.rb +211 -0
  26. data/lib/mtk/events/event.rb +119 -0
  27. data/lib/mtk/events/note.rb +112 -0
  28. data/lib/mtk/events/parameter.rb +54 -0
  29. data/lib/mtk/helpers/collection.rb +164 -0
  30. data/lib/mtk/helpers/convert.rb +36 -0
  31. data/lib/mtk/helpers/lilypond.rb +162 -0
  32. data/lib/mtk/helpers/output_selector.rb +67 -0
  33. data/lib/mtk/helpers/pitch_collection.rb +23 -0
  34. data/lib/mtk/helpers/pseudo_constants.rb +26 -0
  35. data/lib/mtk/intensity.rb +156 -0
  36. data/lib/mtk/interval.rb +155 -0
  37. data/lib/mtk/lang/mtk_grammar.citrus +190 -13
  38. data/lib/mtk/lang/parser.rb +29 -0
  39. data/lib/mtk/melody.rb +94 -0
  40. data/lib/mtk/midi/dls_synth_device.rb +144 -0
  41. data/lib/mtk/midi/dls_synth_output.rb +62 -0
  42. data/lib/mtk/midi/file.rb +67 -32
  43. data/lib/mtk/midi/input.rb +97 -0
  44. data/lib/mtk/midi/jsound_input.rb +36 -17
  45. data/lib/mtk/midi/jsound_output.rb +48 -46
  46. data/lib/mtk/midi/output.rb +195 -0
  47. data/lib/mtk/midi/unimidi_input.rb +117 -0
  48. data/lib/mtk/midi/unimidi_output.rb +121 -0
  49. data/lib/mtk/{_numeric_extensions.rb → numeric_extensions.rb} +12 -0
  50. data/lib/mtk/patterns/chain.rb +49 -0
  51. data/lib/mtk/{pattern → patterns}/choice.rb +14 -8
  52. data/lib/mtk/patterns/cycle.rb +18 -0
  53. data/lib/mtk/patterns/for_each.rb +71 -0
  54. data/lib/mtk/patterns/function.rb +39 -0
  55. data/lib/mtk/{pattern → patterns}/lines.rb +11 -17
  56. data/lib/mtk/{pattern → patterns}/palindrome.rb +11 -8
  57. data/lib/mtk/patterns/pattern.rb +171 -0
  58. data/lib/mtk/patterns/sequence.rb +20 -0
  59. data/lib/mtk/pitch.rb +7 -6
  60. data/lib/mtk/pitch_class.rb +124 -46
  61. data/lib/mtk/pitch_class_set.rb +58 -35
  62. data/lib/mtk/sequencers/event_builder.rb +131 -0
  63. data/lib/mtk/sequencers/legato_sequencer.rb +24 -0
  64. data/lib/mtk/sequencers/rhythmic_sequencer.rb +28 -0
  65. data/lib/mtk/{sequencer/abstract_sequencer.rb → sequencers/sequencer.rb} +37 -11
  66. data/lib/mtk/{sequencer → sequencers}/step_sequencer.rb +4 -4
  67. data/lib/mtk/timeline.rb +39 -22
  68. data/lib/mtk/variable.rb +32 -0
  69. data/spec/mtk/chord_spec.rb +83 -0
  70. data/spec/mtk/{_constants → constants}/durations_spec.rb +12 -41
  71. data/spec/mtk/{_constants → constants}/intensities_spec.rb +13 -37
  72. data/spec/mtk/{_constants → constants}/intervals_spec.rb +14 -32
  73. data/spec/mtk/{_constants → constants}/pitch_classes_spec.rb +8 -4
  74. data/spec/mtk/{_constants → constants}/pitches_spec.rb +5 -1
  75. data/spec/mtk/duration_spec.rb +372 -0
  76. data/spec/mtk/events/event_spec.rb +234 -0
  77. data/spec/mtk/events/note_spec.rb +174 -0
  78. data/spec/mtk/events/parameter_spec.rb +220 -0
  79. data/spec/mtk/{helper → helpers}/collection_spec.rb +86 -3
  80. data/spec/mtk/{helper → helpers}/pseudo_constants_spec.rb +2 -2
  81. data/spec/mtk/intensity_spec.rb +289 -0
  82. data/spec/mtk/interval_spec.rb +265 -0
  83. data/spec/mtk/lang/parser_spec.rb +597 -0
  84. data/spec/mtk/melody_spec.rb +223 -0
  85. data/spec/mtk/midi/file_spec.rb +16 -16
  86. data/spec/mtk/midi/jsound_input_spec.rb +11 -0
  87. data/spec/mtk/midi/jsound_output_spec.rb +11 -0
  88. data/spec/mtk/midi/output_spec.rb +102 -0
  89. data/spec/mtk/midi/unimidi_input_spec.rb +11 -0
  90. data/spec/mtk/midi/unimidi_output_spec.rb +11 -0
  91. data/spec/mtk/{_numeric_extensions_spec.rb → numeric_extensions_spec.rb} +1 -0
  92. data/spec/mtk/patterns/chain_spec.rb +110 -0
  93. data/spec/mtk/{pattern → patterns}/choice_spec.rb +20 -30
  94. data/spec/mtk/{pattern → patterns}/cycle_spec.rb +25 -35
  95. data/spec/mtk/patterns/for_each_spec.rb +136 -0
  96. data/spec/mtk/{pattern → patterns}/function_spec.rb +17 -30
  97. data/spec/mtk/{pattern → patterns}/lines_spec.rb +11 -27
  98. data/spec/mtk/{pattern → patterns}/palindrome_spec.rb +13 -29
  99. data/spec/mtk/patterns/pattern_spec.rb +132 -0
  100. data/spec/mtk/patterns/sequence_spec.rb +203 -0
  101. data/spec/mtk/pitch_class_set_spec.rb +23 -21
  102. data/spec/mtk/pitch_class_spec.rb +151 -39
  103. data/spec/mtk/pitch_spec.rb +22 -1
  104. data/spec/mtk/sequencers/event_builder_spec.rb +245 -0
  105. data/spec/mtk/sequencers/legato_sequencer_spec.rb +45 -0
  106. data/spec/mtk/sequencers/rhythmic_sequencer_spec.rb +84 -0
  107. data/spec/mtk/sequencers/sequencer_spec.rb +215 -0
  108. data/spec/mtk/{sequencer → sequencers}/step_sequencer_spec.rb +35 -13
  109. data/spec/mtk/timeline_spec.rb +109 -16
  110. data/spec/mtk/variable_spec.rb +52 -0
  111. data/spec/spec_coverage.rb +2 -0
  112. data/spec/spec_helper.rb +3 -0
  113. metadata +188 -91
  114. data/lib/mtk/_constants/durations.rb +0 -80
  115. data/lib/mtk/_constants/intensities.rb +0 -81
  116. data/lib/mtk/_constants/intervals.rb +0 -85
  117. data/lib/mtk/_constants/pitch_classes.rb +0 -35
  118. data/lib/mtk/_constants/pitches.rb +0 -49
  119. data/lib/mtk/event.rb +0 -70
  120. data/lib/mtk/helper/collection.rb +0 -114
  121. data/lib/mtk/helper/event_builder.rb +0 -85
  122. data/lib/mtk/helper/pseudo_constants.rb +0 -26
  123. data/lib/mtk/lang/grammar.rb +0 -17
  124. data/lib/mtk/note.rb +0 -63
  125. data/lib/mtk/pattern/abstract_pattern.rb +0 -132
  126. data/lib/mtk/pattern/cycle.rb +0 -51
  127. data/lib/mtk/pattern/enumerator.rb +0 -26
  128. data/lib/mtk/pattern/function.rb +0 -46
  129. data/lib/mtk/pattern/sequence.rb +0 -30
  130. data/lib/mtk/pitch_set.rb +0 -84
  131. data/lib/mtk/sequencer/rhythmic_sequencer.rb +0 -29
  132. data/lib/mtk/transform/invertible.rb +0 -15
  133. data/lib/mtk/transform/mappable.rb +0 -18
  134. data/lib/mtk/transform/set_theory_operations.rb +0 -34
  135. data/lib/mtk/transform/transposable.rb +0 -14
  136. data/spec/mtk/event_spec.rb +0 -139
  137. data/spec/mtk/helper/event_builder_spec.rb +0 -92
  138. data/spec/mtk/lang/grammar_spec.rb +0 -100
  139. data/spec/mtk/note_spec.rb +0 -115
  140. data/spec/mtk/pattern/abstract_pattern_spec.rb +0 -45
  141. data/spec/mtk/pattern/note_cycle_spec.rb.bak +0 -116
  142. data/spec/mtk/pattern/pitch_cycle_spec.rb.bak +0 -47
  143. data/spec/mtk/pattern/pitch_sequence_spec.rb.bak +0 -37
  144. data/spec/mtk/pattern/sequence_spec.rb +0 -151
  145. data/spec/mtk/pitch_set_spec.rb +0 -198
  146. data/spec/mtk/sequencer/abstract_sequencer_spec.rb +0 -159
  147. data/spec/mtk/sequencer/rhythmic_sequencer_spec.rb +0 -49
@@ -1,15 +1,17 @@
1
1
  module MTK
2
- module Pattern
2
+ module Patterns
3
3
 
4
4
  # A piecewise linear function (see {http://en.wikipedia.org/wiki/File:PiecewiseLinear.png}) defined in terms
5
5
  # of [value, steps_to_reach_value] pairs.
6
6
  #
7
7
  # The "steps_to_reach_value" for the first element is ignored and may be omitted, since it takes 0 steps to start.
8
- class Lines < AbstractPattern
8
+ class Lines < Pattern
9
9
 
10
- # Reset the sequence to the beginning
11
- def rewind
12
- @index = -1
10
+ ###################
11
+ protected
12
+
13
+ # (see Pattern#rewind_or_cycle)
14
+ def rewind_or_cycle(is_cycling=false)
13
15
  @steps = -1
14
16
  @step_count = -1
15
17
  @prev = nil
@@ -17,13 +19,8 @@ module MTK
17
19
  super
18
20
  end
19
21
 
20
- ###################
21
- protected
22
-
23
- # (see AbstractPattern#advance!)
24
- def advance!
25
- super
26
-
22
+ # (see Pattern#advance)
23
+ def advance
27
24
  while @step_count >= @steps
28
25
  @step_count = 0
29
26
 
@@ -42,15 +39,12 @@ module MTK
42
39
  end
43
40
 
44
41
  @step_count += 1
45
- end
46
42
 
47
- # (see AbstractPattern#current)
48
- def current
49
43
  if @prev and @next
50
44
  # linear interpolation
51
- @prev + (@next - @prev)*@step_count/@steps
45
+ @current = @prev + (@next - @prev)*@step_count/@steps
52
46
  else
53
- @next
47
+ @current = @next
54
48
  end
55
49
  end
56
50
 
@@ -1,15 +1,10 @@
1
1
  module MTK
2
- module Pattern
2
+ module Patterns
3
3
 
4
4
  # An endless enumerator that outputs an element one at a time from a list of elements,
5
5
  # looping back to the beginning when elements run out.
6
6
  class Palindrome < Cycle
7
7
 
8
- def rewind
9
- @direction = 1
10
- super
11
- end
12
-
13
8
  # true if the first/last element are repeated when the ends are reached, else false
14
9
  def repeat_ends?
15
10
  @repeat_ends ||= @options.fetch :repeat_ends, false
@@ -18,8 +13,14 @@ module MTK
18
13
  ##############
19
14
  protected
20
15
 
21
- # (see AbstractPattern#advance!)
22
- def advance!
16
+ # (see Pattern#rewind_or_cycle)
17
+ def rewind_or_cycle(is_cycling=false)
18
+ @direction = 1
19
+ super
20
+ end
21
+
22
+ # (see Pattern#advance)
23
+ def advance
23
24
  raise StopIteration if @elements.nil? or @elements.empty? # prevent infinite loops
24
25
 
25
26
  @index += @direction
@@ -34,6 +35,8 @@ module MTK
34
35
  @index = 0
35
36
  @index += 1 unless repeat_ends? or @elements.length == 1
36
37
  end
38
+
39
+ @current = @elements[@index]
37
40
  end
38
41
 
39
42
  end
@@ -0,0 +1,171 @@
1
+ module MTK
2
+ module Patterns
3
+
4
+ # A pattern of elements that can be emitted one element at a time via calls to {#next}.
5
+ #
6
+ # Patterns can be reset to the beginning via {#rewind}.
7
+ #
8
+ # @abstract Subclass and override {#advance} and {#current} to implement a Pattern.
9
+ #
10
+ class Pattern
11
+ include MTK::Helpers::Collection
12
+
13
+ # The elements in the pattern
14
+ attr_reader :elements
15
+
16
+ attr_reader :options
17
+
18
+ # The number of elements emitted since the last {#rewind}
19
+ attr_reader :element_count
20
+
21
+ # The minimum number of elements this Pattern will emit before a StopIteration exception
22
+ # This overrides any conflicting max_cycles setting.
23
+ attr_reader :min_elements
24
+
25
+ # The maximum number of elements this Pattern will emit before a StopIteration exception
26
+ # A nil value means infinite elements.
27
+ # @note {max_cycles} may cause this Pattern to end before max_elements are emitted.
28
+ # If this is undesirable then use min_elements to override max_cycles.
29
+ attr_reader :max_elements
30
+
31
+ # The number of cycles emitted (1 cycle == all elements emitted) since the last {#rewind}
32
+ attr_reader :cycle_count
33
+
34
+ # The maximum number of cycles this Pattern will emit before a StopIteration exception.
35
+ # A nil value means inifinite cycles.
36
+ attr_reader :max_cycles
37
+
38
+ # @param elements [Enumerable] the list of elements in the pattern
39
+ # @param options [Hash] the pattern options
40
+ # @option options [Fixnum] :max_elements the {#max_elements} (default is nil, which means unlimited)
41
+ # @option options [Fixnum] :max_cycles the {#max_cycles} (default is 1)
42
+ def initialize(elements, options={})
43
+ elements = elements.to_a if elements.is_a? Enumerable
44
+ @elements = elements
45
+ @options = options
46
+ @min_elements = options[:min_elements]
47
+ @max_elements = options[:max_elements]
48
+ @max_cycles = options.fetch(:max_cycles, 1)
49
+ rewind
50
+ end
51
+
52
+ # Construct a pattern from an Array.
53
+ # @param (see #initialize)
54
+ # @option (see #initialize)
55
+ # @see #initialize
56
+ def self.from_a(elements, options={})
57
+ new(elements, options)
58
+ end
59
+
60
+ # Reset the pattern to the beginning
61
+ def rewind
62
+ rewind_or_cycle
63
+ self
64
+ end
65
+
66
+ # Emit the next element in the pattern
67
+ # @raise StopIteration when the pattern has emitted all values, or has hit the {#max_elements} limit.
68
+ def next
69
+ raise StopIteration if empty?
70
+
71
+ if @current.is_a? Pattern
72
+ begin
73
+ return emit(@current.next)
74
+ rescue StopIteration
75
+ raise if max_elements_exceeded?
76
+ # else fall through and continue with normal behavior
77
+ end
78
+ end
79
+
80
+ begin
81
+ advance
82
+ rescue StopIteration
83
+ rewind_or_cycle(true)
84
+ return self.next
85
+ end
86
+
87
+ if @current.kind_of? Pattern
88
+ @current.rewind # ensure nested patterns start from the beginning each time they are encountered
89
+ return self.next
90
+ end
91
+
92
+ emit(@current)
93
+ end
94
+
95
+
96
+ def min_elements_unmet?
97
+ @min_elements and @element_count < @min_elements
98
+ end
99
+
100
+ def max_elements_exceeded?
101
+ @max_elements and @element_count >= @max_elements
102
+ end
103
+
104
+ def max_cycles_exceeded?
105
+ @max_cycles and @cycle_count >= @max_cycles
106
+ end
107
+
108
+ def empty?
109
+ @elements.nil? or @elements.empty?
110
+ end
111
+
112
+ ##################
113
+ protected
114
+
115
+ # Reset the pattern to the beginning
116
+ # @param is_cycling [Boolean] true when #next is performing a cycle back to the beginning of the Pattern. false for a normal #rewind
117
+ def rewind_or_cycle(is_cycling=false)
118
+ @current = nil
119
+ @index = -1
120
+
121
+ # and rewind child patterns
122
+ @elements.each{|element| element.rewind if element.is_a? Pattern }
123
+
124
+ if is_cycling
125
+ @cycle_count += 1
126
+ raise StopIteration if max_cycles_exceeded? and not min_elements_unmet?
127
+ else
128
+ @element_count = 0
129
+ @cycle_count = 0
130
+ end
131
+ end
132
+
133
+ # Update internal state (index, etc) and set @current to the next element.
134
+ def advance
135
+ @current = elements[0]
136
+ end
137
+
138
+
139
+ ##################
140
+ private
141
+
142
+ def emit element
143
+ raise StopIteration if (max_elements_exceeded? or max_cycles_exceeded?) and not min_elements_unmet?
144
+ @element_count += 1
145
+ element
146
+ end
147
+
148
+ def self.inherited(subclass)
149
+ # Define a convenience method like MTK::Patterns.Sequence()
150
+ # that can handle varargs or a single array argument, plus any Hash options
151
+ classname = subclass.name.sub /.*::/, '' # Strip off module prefixes
152
+ MTK::Patterns.define_singleton_method classname do |*args|
153
+ options = (args[-1].is_a? Hash) ? args.pop : {}
154
+ args = args[0] if args.length == 1 and args[0].is_a? Array
155
+ subclass.new(args,options)
156
+ end
157
+
158
+ %w(Pitch PitchClass Intensity Duration Interval Rhythm).each do |type|
159
+ MTK::Patterns.define_singleton_method "#{type}#{classname}" do |*args|
160
+ options = (args[-1].is_a? Hash) ? args.pop : {}
161
+ args = args[0] if args.length == 1 and args[0].is_a? Array
162
+ constructor_for_type = (type == 'Rhythm') ? 'Duration' : type
163
+ args = args.map{|arg| (arg.nil? or arg.is_a? Proc) ? arg : MTK.send(constructor_for_type, arg) } # coerce to the given type (or Duration for rhythm type)
164
+ subclass.new(args,options)
165
+ end
166
+ end
167
+ end
168
+ end
169
+
170
+ end
171
+ end
@@ -0,0 +1,20 @@
1
+ module MTK
2
+ module Patterns
3
+
4
+ # A finite list of elements, which can be enumerated one at a time.
5
+ class Sequence < Pattern
6
+
7
+ ###################
8
+ protected
9
+
10
+ # (see Pattern#advance)
11
+ def advance
12
+ @index += 1
13
+ raise StopIteration if @index >= @elements.length
14
+ @current = @elements[@index]
15
+ end
16
+
17
+ end
18
+
19
+ end
20
+ end
data/lib/mtk/pitch.rb CHANGED
@@ -26,8 +26,8 @@ module MTK
26
26
  # Pitch.from_s('C4')
27
27
  # @example get the Pitch for middle C + 50 cents:
28
28
  # Pitch.from_s('C4+50cents')
29
- def self.from_s( s )
30
- s = s.to_s
29
+ def self.from_s( name )
30
+ s = name.to_s
31
31
  s = s[0..0].upcase + s[1..-1].downcase # normalize name
32
32
  if s =~ /^([A-G](#|##|b|bb)?)(-?\d+)(\+(\d+(\.\d+)?)cents)?$/
33
33
  pitch_class = PitchClass.from_s($1)
@@ -35,12 +35,13 @@ module MTK
35
35
  octave = $3.to_i
36
36
  offset_in_cents = $5.to_f
37
37
  if offset_in_cents.nil? or offset_in_cents.zero?
38
- self[pitch_class, octave]
38
+ return self[pitch_class, octave]
39
39
  else
40
- new( pitch_class, octave, offset_in_cents/100.0 )
40
+ return new( pitch_class, octave, offset_in_cents/100.0 )
41
41
  end
42
42
  end
43
43
  end
44
+ raise ArgumentError.new("Invalid pitch name: #{name.inspect}")
44
45
  end
45
46
 
46
47
  class << self
@@ -91,7 +92,7 @@ module MTK
91
92
  end
92
93
 
93
94
  def inspect
94
- "#{@pitch_class}#{@octave}" + (@offset.zero? ? '' : "+#{offset_in_cents}cents")
95
+ "#<#{self.class}:#{object_id} @value=#{@value}>"
95
96
  end
96
97
 
97
98
  def ==( other )
@@ -143,7 +144,7 @@ module MTK
143
144
  else
144
145
  Pitch.new(*anything)
145
146
  end
146
- else raise "Pitch doesn't understand #{anything.class}"
147
+ else raise ArgumentError.new("Pitch doesn't understand #{anything.class}")
147
148
  end
148
149
  end
149
150
  module_function :Pitch
@@ -1,94 +1,171 @@
1
1
  module MTK
2
2
 
3
- # A class of pitches under octave equivalence.
3
+ # A set of all pitches that are an integer number of octaves apart.
4
+ # A {Pitch} has the same PitchClass as the pitches one or more octaves away.
5
+ # @see https://en.wikipedia.org/wiki/Pitch_class
4
6
  #
5
- # A {Pitch} has the same PitchClass as the {Pitches} one or more octaves away.
6
-
7
7
  class PitchClass
8
8
 
9
- NAMES = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'].freeze
10
-
11
- VALID_NAMES_BY_VALUE = [
12
- ['B#', 'C', 'Dbb'],
13
- ['B##', 'C#', 'Db'],
14
- ['C##', 'D', 'Ebb'],
15
- ['D#', 'Eb', 'Fbb'],
16
- ['D##', 'E', 'Fb'],
17
- ['E#', 'F', 'Gbb'],
18
- ['E##', 'F#', 'Gb'],
19
- ['F##', 'G', 'Abb'],
20
- ['G#', 'Ab'],
21
- ['G##', 'A', 'Bbb'],
22
- ['A#', 'Bb', 'Cbb'],
23
- ['A##', 'B', 'Cb']
9
+ # The normalized names of the 12 pitch classes in the chromatic scale.
10
+ # The index of each {#name} is the pitch class's numeric {#value}.
11
+ NAMES = %w( C Db D Eb E F Gb G Ab A Bb B ).freeze
12
+
13
+ # All enharmonic names of the 12 pitch classes, including sharps, flats, double-sharps, and double-flats,
14
+ # organized such that each index contains the allowed names of the pitch class with a {#value} equal to that index.
15
+ # @see VALID_NAMES
16
+ VALID_NAMES_BY_VALUE =
17
+ [ # (valid names ), # value # normalized name
18
+ %w( B# C Dbb ), # 0 # C
19
+ %w( B## C# Db ), # 1 # Db
20
+ %w( C## D Ebb ), # 2 # D
21
+ %w( D# Eb Fbb ), # 3 # Eb
22
+ %w( D## E Fb ), # 4 # E
23
+ %w( E# F Gbb ), # 5 # F
24
+ %w( E## F# Gb ), # 6 # Gb
25
+ %w( F## G Abb ), # 7 # G
26
+ %w( G# Ab ), # 8 # Ab
27
+ %w( G## A Bbb ), # 9 # A
28
+ %w( A# Bb Cbb ), # 10 # Bb
29
+ %w( A## B Cb ) # 11 # B
24
30
  ].freeze
25
31
 
32
+ # All valid enharmonic pitch class names in a flat list.
33
+ # @see VALID_NAMES_BY_VALUE
26
34
  VALID_NAMES = VALID_NAMES_BY_VALUE.flatten.freeze
27
35
 
36
+ # A mapping from valid names to the value of the pitch class with that name
37
+ VALUES_BY_NAME = Hash[ # a map from a list of name,value pairs
38
+ VALID_NAMES_BY_VALUE.map.with_index do |valid_names,value|
39
+ valid_names.map{|name| [name,value] }
40
+ end.flatten(1)
41
+ ].freeze
42
+
43
+
44
+ # The name of this pitch class.
45
+ # One of the {NAMES} defined by this class.
28
46
  attr_reader :name
29
47
 
30
- def initialize(name, int_value)
31
- @name, @int_value = name, int_value
48
+ # The value of this pitch class.
49
+ # An integer from 0..11 that indexes this pitch class in {PITCH_CLASSES} and the {#name} in {NAMES}.
50
+ attr_reader :value
51
+
52
+
53
+ private ######
54
+ # Even though new is a private_class_method, YARD gets confused so we temporarily go private
55
+
56
+ def initialize(name, value)
57
+ @name, @value = name, value
32
58
  end
33
59
  private_class_method :new
34
60
 
35
61
  @flyweight = {}
36
62
 
37
- # Lookup a PitchClass by name.
38
- # @param name [String, #to_s] one of the values in {VALID_NAMES}
39
- def self.[](name)
40
- name = name.to_s
41
- name = name[0..0].upcase + name[1..-1].downcase # normalize the name
42
- VALID_NAMES_BY_VALUE.each_with_index do |names, value|
43
- if names.include? name
44
- return @flyweight[name] ||= new(name, value)
45
- end
63
+ public ######
64
+
65
+
66
+ # Lookup a PitchClass by name or value.
67
+ # @param name_or_value [String,Symbol,Numeric] one of {VALID_NAMES} or 0..12
68
+ # @return the PitchClass representing the argument
69
+ # @raise ArgumentError for arguments that cannot be converted to a PitchClass
70
+ def self.[] name_or_value
71
+ @flyweight[name_or_value] ||= case name_or_value
72
+ when String,Symbol then from_name(name_or_value)
73
+ when Numeric then from_value(name_or_value.round)
74
+ else raise ArgumentError.new("PitchClass.[] doesn't understand #{name_or_value.class}")
46
75
  end
47
- nil
76
+ end
77
+
78
+ # Lookup a PitchClass by name.
79
+ # @param name [String,#to_s] one of {VALID_NAMES} (case-insensitive)
80
+ def self.from_name(name)
81
+ @flyweight[name] ||= (
82
+ valid_name = name.to_s.capitalize
83
+ value = VALUES_BY_NAME[valid_name] or raise ArgumentError.new("Invalid PitchClass name: #{name}")
84
+ new(valid_name,value)
85
+ )
86
+ end
87
+
88
+ class << self
89
+ alias from_s from_name
90
+ end
91
+
92
+ # All 12 pitch classes in the chromatic scale.
93
+ # The index of each pitch class is the pitch class's numeric {#value}.
94
+ PITCH_CLASSES = NAMES.map{|name| from_name name }.freeze
95
+
96
+ # return the pitch class with the given integer value mod 12
97
+ # @param value [Integer,#to_i]
98
+ def self.from_value(value)
99
+ PITCH_CLASSES[value.to_i % 12]
48
100
  end
49
101
 
50
102
  class << self
51
- alias :from_s :[]
52
- alias :from_name :[]
103
+ alias from_i from_value
53
104
  end
54
105
 
55
- def self.from_i(value)
56
- name = NAMES[value.to_i % 12]
57
- self[name]
106
+ # return the pitch class with the given float rounded to the nearest integer, mod 12
107
+ # @param value [Float,#to_f]
108
+ def self.from_f(value)
109
+ from_i value.to_f.round
58
110
  end
59
111
 
112
+ # Compare 2 pitch classes for equal values.
113
+ # @param other [PitchClass]
114
+ # @return true if this pitch class's value is equal to the other pitch class's value
60
115
  def == other
61
- other.is_a? PitchClass and other.to_i == @int_value
116
+ other.is_a? PitchClass and other.value == @value
62
117
  end
63
118
 
119
+ # Compare a pitch class with another pitch class or integer value
120
+ # @param other [PitchClass,#to_i]
121
+ # @return -1, 0, or +1 depending on whether this pitch class's value is less than, equal to, or greater than the other object's integer value
122
+ # @see http://ruby-doc.org/core-1.9.3/Comparable.html
64
123
  def <=> other
65
- @int_value <=> other.to_i
124
+ @value <=> other.to_i
66
125
  end
67
126
 
127
+ # This pitch class's normalized {#name}.
128
+ # @see NAMES
68
129
  def to_s
69
- @name
130
+ @name.to_s
70
131
  end
71
132
 
133
+ # This pitch class's integer {#value}
72
134
  def to_i
73
- @int_value
135
+ @value.to_i
136
+ end
137
+
138
+ # This pitch class's {#value} as a floating point number
139
+ def to_f
140
+ @value.to_f
74
141
  end
75
142
 
143
+ # Transpose this pitch class by adding it's value to the value given (mod 12)
144
+ # @param interval [PitchClass,Float,#to_f]
76
145
  def + interval
77
- self.class.from_i(to_i + interval.to_i)
146
+ new_value = (value + interval.to_f).round
147
+ self.class.from_value new_value
78
148
  end
79
149
  alias transpose +
80
150
 
151
+ # Transpose this pitch class by subtracing the given value from this value (mod 12)
152
+ # @param interval [PitchClass,Float,#to_f]
81
153
  def - interval
82
- self.class.from_i(to_i - interval.to_i)
154
+ new_value = (value - interval.to_f).round
155
+ self.class.from_value new_value
83
156
  end
84
157
 
85
- def invert(center_pitch_class)
86
- self + 2*(center_pitch_class.to_i - to_i)
158
+ # Inverts (mirrors) the pitch class around the given center
159
+ # @param center [PitchClass,Pitch,Float,#to_f] the value to "mirror" this pitch class around
160
+ def invert(center)
161
+ delta = (2*(center.to_f - value)).round
162
+ self + delta
87
163
  end
88
164
 
89
165
  # the smallest interval in semitones that needs to be added to this PitchClass to reach the given PitchClass
166
+ # @param pitch_class [PitchClass,#value]
90
167
  def distance_to(pitch_class)
91
- delta = (pitch_class.to_i - to_i) % 12
168
+ delta = (pitch_class.value - value) % 12
92
169
  if delta > 6
93
170
  delta -= 12
94
171
  elsif delta == 6 and to_i >= 6
@@ -101,12 +178,13 @@ module MTK
101
178
  end
102
179
 
103
180
  # Construct a {PitchClass} from any supported type
181
+ # @param anything [PitchClass,String,Symbol,Numeric]
104
182
  def PitchClass(anything)
105
183
  case anything
106
- when Numeric then PitchClass.from_i(anything)
184
+ when Numeric then PitchClass.from_f(anything)
107
185
  when String, Symbol then PitchClass.from_s(anything)
108
186
  when PitchClass then anything
109
- else raise "PitchClass doesn't understand #{anything.class}"
187
+ else raise ArgumentError.new("PitchClass doesn't understand #{anything.class}")
110
188
  end
111
189
  end
112
190
  module_function :PitchClass