mtk 0.0.1 → 0.0.2

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 (93) hide show
  1. data/.yardopts +9 -0
  2. data/INTRO.md +73 -0
  3. data/LICENSE.txt +27 -0
  4. data/README.md +93 -18
  5. data/Rakefile +13 -1
  6. data/examples/crescendo.rb +20 -0
  7. data/examples/dynamic_pattern.rb +39 -0
  8. data/examples/play_midi.rb +19 -0
  9. data/examples/print_midi.rb +13 -0
  10. data/examples/random_tone_row.rb +18 -0
  11. data/examples/tone_row_melody.rb +21 -0
  12. data/lib/mtk/_constants/durations.rb +80 -0
  13. data/lib/mtk/_constants/intensities.rb +81 -0
  14. data/lib/mtk/{constants → _constants}/intervals.rb +10 -1
  15. data/lib/mtk/_constants/pitch_classes.rb +35 -0
  16. data/lib/mtk/_constants/pitches.rb +49 -0
  17. data/lib/mtk/{numeric_extensions.rb → _numeric_extensions.rb} +0 -0
  18. data/lib/mtk/event.rb +14 -5
  19. data/lib/mtk/helper/collection.rb +114 -0
  20. data/lib/mtk/helper/event_builder.rb +85 -0
  21. data/lib/mtk/{constants → helper}/pseudo_constants.rb +7 -6
  22. data/lib/mtk/lang/grammar.rb +17 -0
  23. data/lib/mtk/lang/mtk_grammar.citrus +60 -0
  24. data/lib/mtk/midi/file.rb +10 -15
  25. data/lib/mtk/midi/jsound_input.rb +68 -0
  26. data/lib/mtk/midi/jsound_output.rb +80 -0
  27. data/lib/mtk/note.rb +22 -3
  28. data/lib/mtk/pattern/abstract_pattern.rb +132 -0
  29. data/lib/mtk/pattern/choice.rb +25 -9
  30. data/lib/mtk/pattern/cycle.rb +51 -0
  31. data/lib/mtk/pattern/enumerator.rb +26 -0
  32. data/lib/mtk/pattern/function.rb +46 -0
  33. data/lib/mtk/pattern/lines.rb +60 -0
  34. data/lib/mtk/pattern/palindrome.rb +42 -0
  35. data/lib/mtk/pattern/sequence.rb +15 -50
  36. data/lib/mtk/pitch.rb +45 -6
  37. data/lib/mtk/pitch_class.rb +36 -35
  38. data/lib/mtk/pitch_class_set.rb +46 -14
  39. data/lib/mtk/pitch_set.rb +20 -31
  40. data/lib/mtk/sequencer/abstract_sequencer.rb +85 -0
  41. data/lib/mtk/sequencer/rhythmic_sequencer.rb +29 -0
  42. data/lib/mtk/sequencer/step_sequencer.rb +26 -0
  43. data/lib/mtk/timeline.rb +75 -22
  44. data/lib/mtk/transform/invertible.rb +15 -0
  45. data/lib/mtk/{util → transform}/mappable.rb +6 -2
  46. data/lib/mtk/transform/set_theory_operations.rb +34 -0
  47. data/lib/mtk/transform/transposable.rb +14 -0
  48. data/lib/mtk.rb +56 -22
  49. data/spec/mtk/_constants/durations_spec.rb +118 -0
  50. data/spec/mtk/{constants/dynamics_spec.rb → _constants/intensities_spec.rb} +48 -17
  51. data/spec/mtk/{constants → _constants}/intervals_spec.rb +21 -0
  52. data/spec/mtk/_constants/pitch_classes_spec.rb +58 -0
  53. data/spec/mtk/_constants/pitches_spec.rb +52 -0
  54. data/spec/mtk/{numeric_extensions_spec.rb → _numeric_extensions_spec.rb} +0 -0
  55. data/spec/mtk/event_spec.rb +19 -0
  56. data/spec/mtk/helper/collection_spec.rb +291 -0
  57. data/spec/mtk/helper/event_builder_spec.rb +92 -0
  58. data/spec/mtk/helper/pseudo_constants_spec.rb +20 -0
  59. data/spec/mtk/lang/grammar_spec.rb +100 -0
  60. data/spec/mtk/midi/file_spec.rb +41 -6
  61. data/spec/mtk/note_spec.rb +53 -3
  62. data/spec/mtk/pattern/abstract_pattern_spec.rb +45 -0
  63. data/spec/mtk/pattern/choice_spec.rb +89 -3
  64. data/spec/mtk/pattern/cycle_spec.rb +133 -0
  65. data/spec/mtk/pattern/function_spec.rb +133 -0
  66. data/spec/mtk/pattern/lines_spec.rb +93 -0
  67. data/spec/mtk/pattern/note_cycle_spec.rb.bak +116 -0
  68. data/spec/mtk/pattern/palindrome_spec.rb +124 -0
  69. data/spec/mtk/pattern/pitch_cycle_spec.rb.bak +47 -0
  70. data/spec/mtk/pattern/pitch_sequence_spec.rb.bak +37 -0
  71. data/spec/mtk/pattern/sequence_spec.rb +128 -31
  72. data/spec/mtk/pitch_class_set_spec.rb +240 -7
  73. data/spec/mtk/pitch_class_spec.rb +84 -18
  74. data/spec/mtk/pitch_set_spec.rb +45 -10
  75. data/spec/mtk/pitch_spec.rb +59 -0
  76. data/spec/mtk/sequencer/abstract_sequencer_spec.rb +159 -0
  77. data/spec/mtk/sequencer/rhythmic_sequencer_spec.rb +49 -0
  78. data/spec/mtk/sequencer/step_sequencer_spec.rb +71 -0
  79. data/spec/mtk/timeline_spec.rb +118 -15
  80. data/spec/spec_helper.rb +4 -3
  81. metadata +59 -22
  82. data/lib/mtk/chord.rb +0 -47
  83. data/lib/mtk/constants/dynamics.rb +0 -56
  84. data/lib/mtk/constants/pitch_classes.rb +0 -18
  85. data/lib/mtk/constants/pitches.rb +0 -24
  86. data/lib/mtk/pattern/note_sequence.rb +0 -60
  87. data/lib/mtk/pattern/pitch_sequence.rb +0 -22
  88. data/lib/mtk/patterns.rb +0 -4
  89. data/spec/mtk/chord_spec.rb +0 -74
  90. data/spec/mtk/constants/pitch_classes_spec.rb +0 -35
  91. data/spec/mtk/constants/pitches_spec.rb +0 -23
  92. data/spec/mtk/pattern/note_sequence_spec.rb +0 -121
  93. data/spec/mtk/pattern/pitch_sequence_spec.rb +0 -47
@@ -0,0 +1,132 @@
1
+ module MTK
2
+ module Pattern
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 AbstractPattern
11
+ include Helper::Collection
12
+ include Enumerator
13
+
14
+ # The elements in the pattern
15
+ attr_reader :elements
16
+
17
+ attr_reader :options
18
+
19
+ # The type of elements in the pattern, such as :pitch, :intensity, or :duration
20
+ #
21
+ # This is often needed by {Sequencer} classes to interpret the pattern elements.
22
+ attr_reader :type
23
+
24
+ # The number of elements emitted since the last {#rewind}
25
+ attr_reader :element_count
26
+
27
+ # The maximum number of elements this Pattern will emit before a StopIteration exception
28
+ attr_reader :max_elements
29
+
30
+ # @param elements [Enumerable, #to_a] the list of elements in the pattern
31
+ # @param options [Hash] the pattern options
32
+ # @option options [String] :type the pattern {#type}
33
+ # @option options [Fixnum] :max_elements the {#max_elements}
34
+ def initialize(elements, options={})
35
+ elements = elements.to_a if elements.respond_to? :to_a and not elements.is_a? Proc # Proc check prevents warnings in Ruby 1.8
36
+ @elements = elements
37
+ @options = options
38
+ @type = options[:type]
39
+ @max_elements = options[:max_elements]
40
+ rewind
41
+ end
42
+
43
+ # Construct a pattern from an Array.
44
+ # @param (see #initialize)
45
+ # @option (see #initialize)
46
+ # @see #initialize
47
+ def self.from_a(elements, options={})
48
+ new(elements, options)
49
+ end
50
+
51
+ # Reset the pattern to the beginning
52
+ def rewind
53
+ @current = nil
54
+ @element_count = 0
55
+ self
56
+ end
57
+
58
+ # Emit the next element in the pattern
59
+ # @raise StopIteration when the pattern has emitted all values, or has hit the {#max_elements} limit.
60
+ def next
61
+ if @current.is_a? Enumerator
62
+ begin
63
+ subpattern_next = @current.next
64
+ subpattern_has_next = true
65
+ rescue StopIteration
66
+ subpattern_has_next = false
67
+ end
68
+
69
+ return emit subpattern_next if subpattern_has_next
70
+ # else fall through and continue with normal behavior
71
+ end
72
+
73
+ begin
74
+ advance!
75
+ rescue StopIteration
76
+ @current = nil
77
+ raise
78
+ end
79
+
80
+ @current = current
81
+ if @current.is_a? Enumerator
82
+ @current.rewind # start over, in case we already enumerated this element and then did a rewind
83
+ return self.next
84
+ end
85
+
86
+ emit @current
87
+ end
88
+
89
+
90
+ ##################
91
+ protected
92
+
93
+ # Update internal state (index, etc) so that {#current} will refer to the next element.
94
+ # @note Override this method in a subclass to define a custom Pattern.
95
+ # @raise StopIteration if there are no more elements
96
+ def advance!
97
+ raise StopIteration if @elements.nil? or @elements.empty?
98
+ end
99
+
100
+ # The current element in the pattern, which will be returned by {#next} (after a call to {#advance!}).
101
+ # @note Override this method in a subclass to define a custom Pattern.
102
+ def current
103
+ @elements[0]
104
+ end
105
+
106
+
107
+ ##################
108
+ private
109
+
110
+ def emit element
111
+ @element_count += 1
112
+ raise StopIteration if @max_elements and @element_count > @max_elements
113
+ element
114
+ end
115
+ end
116
+
117
+ # Build any "TypedPattern" (like PitchCycle or DurationPalindrome) or even just Pattern
118
+ def method_missing(method, *args, &block)
119
+ # Assuming we get something like PitchCycle, split into 'Pitch' and 'Cycle'
120
+ camel_case_words = method.to_s.gsub(/([a-z])([A-Z])/,'\1 \2').split(' ')
121
+ pattern = MTK::Pattern.const_get camel_case_words.last
122
+ if camel_case_words.length > 1
123
+ type = camel_case_words.first.downcase.to_sym
124
+ pattern.new(args, :type => type)
125
+ else
126
+ pattern.new(args)
127
+ end
128
+ end
129
+ module_function :method_missing
130
+
131
+ end
132
+ end
@@ -1,18 +1,34 @@
1
1
  module MTK
2
2
  module Pattern
3
3
 
4
- # An element enumerator that randomly choices from a list of elements
5
- class Choice
4
+ # Randomly choose from a list of elements.
5
+ #
6
+ # Supports giving different weights to different choices.
7
+ # Default is to weight all choices equally.
8
+ #
9
+ class Choice < AbstractPattern
6
10
 
7
- # The element choices
8
- attr_reader :elements
9
-
10
- def initialize(elements)
11
- @elements = elements
11
+ # @param (see AbstractPattern#initialize)
12
+ # @option (see AbstractPattern#initialize)
13
+ # @option options [Array] :weights a list of chances that each corresponding element will be selected (normalized against the total weight)
14
+ # @example choose the second element twice as often as the first or third:
15
+ # MTK::Pattern::Choice.new [:first,:second,:third], :weights => [1,2,1]
16
+ def initialize(elements, options={})
17
+ super
18
+ @weights = options.fetch :weights, Array.new(@elements.length, 1)
19
+ @total_weight = @weights.inject(:+).to_f
12
20
  end
13
21
 
14
- def next
15
- @elements[rand(@elements.length)] if @elements and not @elements.empty?
22
+ #####################
23
+ protected
24
+
25
+ # (see AbstractPattern#current)
26
+ def current
27
+ target = rand * @total_weight
28
+ @weights.each_with_index do |weight,index|
29
+ return @elements[index] if target < weight
30
+ target -= weight
31
+ end
16
32
  end
17
33
 
18
34
  end
@@ -0,0 +1,51 @@
1
+ module MTK
2
+ module Pattern
3
+
4
+ # An endless enumerator that outputs an element one at a time from a list of elements,
5
+ # looping back to the beginning when elements run out.
6
+ class Cycle < AbstractPattern
7
+
8
+ # The number of cycles emitted (1 cycle == all elements emitted) since the last {#rewind}
9
+ attr_reader :cycle_count
10
+
11
+ # The maximum number of cycles this Pattern will emit before a StopIteration exception
12
+ attr_reader :max_cycles
13
+
14
+ def initialize(elements, options={})
15
+ super
16
+ @max_cycles = options[:max_cycles]
17
+ end
18
+
19
+ # Reset the sequence to the beginning
20
+ def rewind
21
+ @index = -1
22
+ @cycle_count = 0
23
+ super
24
+ end
25
+
26
+ ###################
27
+ protected
28
+
29
+ # (see AbstractPattern#advance!)
30
+ def advance!
31
+ super # base advance!() implementation prevents infinite loops with empty patterns
32
+ @index += 1
33
+ if @index >= @elements.length
34
+ @cycle_count += 1
35
+ if @max_cycles and @cycle_count >= @max_cycles
36
+ raise StopIteration
37
+ end
38
+ @index = -1
39
+ advance!
40
+ end
41
+ end
42
+
43
+ # (see AbstractPattern#current)
44
+ def current
45
+ @elements[@index]
46
+ end
47
+
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,26 @@
1
+ module MTK
2
+ module Pattern
3
+
4
+ # The core interface for all Patterns.
5
+ #
6
+ # This module doesn't provide any useful default functionality.
7
+ # It only indicates that a class is compatible with MTK's pattern enumerator interface.
8
+ #
9
+ module Enumerator
10
+
11
+ # Return the next element in the enumerator
12
+ # @raise StopIteration when no more elements are available
13
+ def next
14
+ raise StopIteration
15
+ end
16
+
17
+ # Reset the enumerator to the beginning
18
+ # @return self
19
+ def rewind
20
+ self
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,46 @@
1
+ module MTK
2
+ module Pattern
3
+
4
+ # An arbitrary function that dynamically generates elements.
5
+ #
6
+ class Function < AbstractPattern
7
+
8
+ attr_reader :function
9
+
10
+ def initialize(elements, options={})
11
+ super
12
+ @function = @elements
13
+ # unpack from the varargs Array that may be passed in from the "convenience constructor methods" defined in MTK::Pattern \
14
+ @function = @function.first if @function.is_a? Enumerable
15
+ end
16
+
17
+ # Reset the sequence to the beginning
18
+ def rewind
19
+ @prev = nil
20
+ @function_call_count = -1
21
+ super
22
+ end
23
+
24
+ ###################
25
+ protected
26
+
27
+ # (see AbstractPattern#advance!)
28
+ def advance!
29
+ raise StopIteration if @elements.nil?
30
+ end
31
+
32
+ # (see AbstractPattern#current)
33
+ def current
34
+ @function_call_count += 1
35
+ @prev = case @function.arity
36
+ when 0 then @function.call
37
+ when 1 then @function.call(@prev)
38
+ when 2 then @function.call(@prev, @function_call_count)
39
+ else @function.call(@prev, @function_call_count, @element_count)
40
+ end
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,60 @@
1
+ module MTK
2
+ module Pattern
3
+
4
+ # A piecewise linear function (see {http://en.wikipedia.org/wiki/File:PiecewiseLinear.png}) defined in terms
5
+ # of [value, steps_to_reach_value] pairs.
6
+ #
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
9
+
10
+ # Reset the sequence to the beginning
11
+ def rewind
12
+ @index = -1
13
+ @steps = -1
14
+ @step_count = -1
15
+ @prev = nil
16
+ @next = nil
17
+ super
18
+ end
19
+
20
+ ###################
21
+ protected
22
+
23
+ # (see AbstractPattern#advance!)
24
+ def advance!
25
+ super
26
+
27
+ while @step_count >= @steps
28
+ @step_count = 0
29
+
30
+ @index += 1
31
+ raise StopIteration if @index >= @elements.length
32
+
33
+ @prev = @next
34
+ next_elem = @elements[@index]
35
+ if next_elem.is_a? Array
36
+ @next = next_elem.first
37
+ @steps = next_elem.last.to_f
38
+ else
39
+ @next = next_elem
40
+ @steps = 1.0
41
+ end
42
+ end
43
+
44
+ @step_count += 1
45
+ end
46
+
47
+ # (see AbstractPattern#current)
48
+ def current
49
+ if @prev and @next
50
+ # linear interpolation
51
+ @prev + (@next - @prev)*@step_count/@steps
52
+ else
53
+ @next
54
+ end
55
+ end
56
+
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,42 @@
1
+ module MTK
2
+ module Pattern
3
+
4
+ # An endless enumerator that outputs an element one at a time from a list of elements,
5
+ # looping back to the beginning when elements run out.
6
+ class Palindrome < Cycle
7
+
8
+ def rewind
9
+ @direction = 1
10
+ super
11
+ end
12
+
13
+ # true if the first/last element are repeated when the ends are reached, else false
14
+ def repeat_ends?
15
+ @repeat_ends ||= @options.fetch :repeat_ends, false
16
+ end
17
+
18
+ ##############
19
+ protected
20
+
21
+ # (see AbstractPattern#advance!)
22
+ def advance!
23
+ raise StopIteration if @elements.nil? or @elements.empty? # prevent infinite loops
24
+
25
+ @index += @direction
26
+
27
+ if @index >= @elements.length
28
+ @direction = -1
29
+ @index = @elements.length - 1
30
+ @index -= 1 unless repeat_ends? or @elements.length == 1
31
+
32
+ elsif @index < 0
33
+ @direction = 1
34
+ @index = 0
35
+ @index += 1 unless repeat_ends? or @elements.length == 1
36
+ end
37
+ end
38
+
39
+ end
40
+
41
+ end
42
+ end
@@ -1,64 +1,29 @@
1
1
  module MTK
2
2
  module Pattern
3
3
 
4
- # An endless enumerator that outputs an element one at a time from a list of elements,
5
- # looping back to the beginning when elements run out.
6
- class Sequence
4
+ # A finite list of elements, which can be enumerated one at a time.
5
+ class Sequence < AbstractPattern
7
6
 
8
- # The list of elements enumerated by this Sequence
9
- attr_reader :elements
10
-
11
- # @param elements [Array] the list of {#elements}
12
- # @param default [Object] the default value returned by {#next} in place of nil
13
- def initialize(elements)
14
- if elements.respond_to? :elements
15
- @elements = elements.elements
16
- else
17
- @elements = elements.to_a
18
- end
19
- reset
20
- end
21
-
22
- # reset the Sequence to its initial state
23
- def reset
7
+ # Reset the sequence to the beginning
8
+ def rewind
24
9
  @index = -1
25
- @element = nil
26
- @value = nil
10
+ super
27
11
  end
28
12
 
29
- # The value of the next element in the Sequence.
30
- # The sequence goes back to the first element if there are no more elements.
31
- #
32
- # @return if an element is a Proc, it is called (depending on the arity of the Proc: with either no arguments,
33
- # the last value of #next, or the last value and last element) and its return value is returned.
34
- # @return if an element is nil, previous value of #next is returned. If there is no previous value the @default is returned.
35
- # @return otherwise the element itself is returned
36
- #
37
- def next
38
- if @elements and not @elements.empty?
39
- @index = (@index + 1) % @elements.length
40
- element = @elements[@index]
41
- value = value_of(element)
42
- @element, @value = element, value
43
- end
44
- @value
45
- end
46
-
47
- ####################
13
+ ###################
48
14
  protected
49
15
 
50
- def value_of element
51
- case element
52
- when Proc
53
- case element.arity
54
- when 0 then element.call
55
- when 1 then element.call(@value)
56
- else element.call(@value, @element)
57
- end
58
- else element
59
- end
16
+ # (see AbstractPattern#advance!)
17
+ def advance!
18
+ super
19
+ @index += 1
20
+ raise StopIteration if @index >= @elements.length
60
21
  end
61
22
 
23
+ # (see AbstractPattern#current)
24
+ def current
25
+ @elements[@index]
26
+ end
62
27
  end
63
28
 
64
29
  end
data/lib/mtk/pitch.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  module MTK
2
2
 
3
3
  # A frequency represented by a {PitchClass}, an integer octave, and an offset in semitones.
4
-
5
4
  class Pitch
6
5
 
7
6
  include Comparable
@@ -15,28 +14,49 @@ module MTK
15
14
 
16
15
  @flyweight = {}
17
16
 
17
+ # Return a pitch with no offset, only constructing a new instance when not already in the flyweight cache
18
18
  def self.[](pitch_class, octave)
19
+ pitch_class = MTK.PitchClass(pitch_class)
19
20
  @flyweight[[pitch_class,octave]] ||= new(pitch_class, octave)
20
21
  end
21
-
22
+
23
+ # Lookup a pitch by name, which consists of any {PitchClass::VALID_NAMES} and an octave number.
24
+ # The name may also be optionally suffixed by +/-###cents (where ### is any number).
25
+ # @example get the Pitch for middle C :
26
+ # Pitch.from_s('C4')
27
+ # @example get the Pitch for middle C + 50 cents:
28
+ # Pitch.from_s('C4+50cents')
22
29
  def self.from_s( s )
23
- # TODO: update to handle offset
30
+ s = s.to_s
24
31
  s = s[0..0].upcase + s[1..-1].downcase # normalize name
25
- if s =~ /^([A-G](#|##|b|bb)?)(-?\d+)$/
32
+ if s =~ /^([A-G](#|##|b|bb)?)(-?\d+)(\+(\d+(\.\d+)?)cents)?$/
26
33
  pitch_class = PitchClass.from_s($1)
27
34
  if pitch_class
28
35
  octave = $3.to_i
29
- new( pitch_class, octave )
36
+ offset_in_cents = $5.to_f
37
+ if offset_in_cents.nil? or offset_in_cents.zero?
38
+ self[pitch_class, octave]
39
+ else
40
+ new( pitch_class, octave, offset_in_cents/100.0 )
41
+ end
30
42
  end
31
43
  end
32
44
  end
45
+
46
+ class << self
47
+ alias :from_name :from_s
48
+ end
33
49
 
34
50
  # Convert a Numeric semitones value into a Pitch
35
51
  def self.from_f( f )
36
52
  i, offset = f.floor, f%1 # split into int and fractional part
37
53
  pitch_class = PitchClass.from_i(i)
38
54
  octave = i/12 - 1
39
- new( pitch_class, octave, offset )
55
+ if offset == 0
56
+ self[pitch_class, octave]
57
+ else
58
+ new( pitch_class, octave, offset )
59
+ end
40
60
  end
41
61
 
42
62
  def self.from_hash(hash)
@@ -86,6 +106,7 @@ module MTK
86
106
  def + interval_in_semitones
87
107
  self.class.from_f( @value + interval_in_semitones.to_f )
88
108
  end
109
+ alias transpose +
89
110
 
90
111
  def - interval_in_semitones
91
112
  self.class.from_f( @value - interval_in_semitones.to_f )
@@ -109,4 +130,22 @@ module MTK
109
130
 
110
131
  end
111
132
 
133
+ # Construct a {Pitch} from any supported type
134
+ def Pitch(*anything)
135
+ anything = anything.first if anything.length == 1
136
+ case anything
137
+ when Numeric then Pitch.from_f(anything)
138
+ when String, Symbol then Pitch.from_s(anything)
139
+ when Pitch then anything
140
+ when Array
141
+ if anything.length == 2
142
+ Pitch[*anything]
143
+ else
144
+ Pitch.new(*anything)
145
+ end
146
+ else raise "Pitch doesn't understand #{anything.class}"
147
+ end
148
+ end
149
+ module_function :Pitch
150
+
112
151
  end
@@ -6,9 +6,9 @@ module MTK
6
6
 
7
7
  class PitchClass
8
8
 
9
- NAMES = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B']
9
+ NAMES = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'].freeze
10
10
 
11
- VALID_NAMES = [
11
+ VALID_NAMES_BY_VALUE = [
12
12
  ['B#', 'C', 'Dbb'],
13
13
  ['B##', 'C#', 'Db'],
14
14
  ['C##', 'D', 'Ebb'],
@@ -21,55 +21,40 @@ module MTK
21
21
  ['G##', 'A', 'Bbb'],
22
22
  ['A#', 'Bb', 'Cbb'],
23
23
  ['A##', 'B', 'Cb']
24
- ]
24
+ ].freeze
25
25
 
26
- attr_reader :name
26
+ VALID_NAMES = VALID_NAMES_BY_VALUE.flatten.freeze
27
27
 
28
- ##########################################
29
- private
28
+ attr_reader :name
30
29
 
31
30
  def initialize(name, int_value)
32
31
  @name, @int_value = name, int_value
33
32
  end
34
-
35
33
  private_class_method :new
36
34
 
37
35
  @flyweight = {}
38
36
 
39
- def self.get(name, int_value)
40
- @flyweight[name] ||= new(name, int_value)
41
- end
42
-
43
- private_class_method :get
44
-
45
- ##########################################
46
- public
47
-
48
- def self.from_s(s)
49
- s = s.to_s
50
- s = s[0..0].upcase + s[1..-1].downcase # normalize the name
51
- VALID_NAMES.each_with_index do |names, index|
52
- return get(s, index) if names.include? s
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
53
46
  end
54
47
  nil
55
48
  end
56
49
 
57
50
  class << self
58
- alias from_name from_s # alias self.from_name to self.from_s
51
+ alias :from_s :[]
52
+ alias :from_name :[]
59
53
  end
60
54
 
61
55
  def self.from_i(value)
62
- value = value.to_i % 12
63
- name = NAMES[value]
64
- get(name, value)
65
- end
66
-
67
- def self.[](name_or_value)
68
- if name_or_value.kind_of? Numeric
69
- from_i(name_or_value)
70
- else
71
- from_s(name_or_value)
72
- end
56
+ name = NAMES[value.to_i % 12]
57
+ self[name]
73
58
  end
74
59
 
75
60
  def == other
@@ -88,14 +73,19 @@ module MTK
88
73
  @int_value
89
74
  end
90
75
 
91
- def +(interval)
76
+ def + interval
92
77
  self.class.from_i(to_i + interval.to_i)
93
78
  end
79
+ alias transpose +
94
80
 
95
- def -(interval)
81
+ def - interval
96
82
  self.class.from_i(to_i - interval.to_i)
97
83
  end
98
84
 
85
+ def invert(center_pitch_class)
86
+ self + 2*(center_pitch_class.to_i - to_i)
87
+ end
88
+
99
89
  # the smallest interval in semitones that needs to be added to this PitchClass to reach the given PitchClass
100
90
  def distance_to(pitch_class)
101
91
  delta = (pitch_class.to_i - to_i) % 12
@@ -110,4 +100,15 @@ module MTK
110
100
 
111
101
  end
112
102
 
103
+ # Construct a {PitchClass} from any supported type
104
+ def PitchClass(anything)
105
+ case anything
106
+ when Numeric then PitchClass.from_i(anything)
107
+ when String, Symbol then PitchClass.from_s(anything)
108
+ when PitchClass then anything
109
+ else raise "PitchClass doesn't understand #{anything.class}"
110
+ end
111
+ end
112
+ module_function :PitchClass
113
+
113
114
  end