jmtk 0.0.3.3-java

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 (110) hide show
  1. data/.yardopts +10 -0
  2. data/DEVELOPMENT_NOTES.md +115 -0
  3. data/INTRO.md +129 -0
  4. data/LICENSE.txt +27 -0
  5. data/README.md +50 -0
  6. data/Rakefile +102 -0
  7. data/bin/jmtk +250 -0
  8. data/bin/mtk +250 -0
  9. data/examples/crescendo.rb +20 -0
  10. data/examples/drum_pattern.rb +23 -0
  11. data/examples/dynamic_pattern.rb +36 -0
  12. data/examples/gets_and_play.rb +27 -0
  13. data/examples/notation.rb +22 -0
  14. data/examples/play_midi.rb +17 -0
  15. data/examples/print_midi.rb +13 -0
  16. data/examples/random_tone_row.rb +18 -0
  17. data/examples/syntax_to_midi.rb +28 -0
  18. data/examples/test_output.rb +7 -0
  19. data/examples/tone_row_melody.rb +23 -0
  20. data/lib/mtk.rb +76 -0
  21. data/lib/mtk/core/duration.rb +213 -0
  22. data/lib/mtk/core/intensity.rb +158 -0
  23. data/lib/mtk/core/interval.rb +157 -0
  24. data/lib/mtk/core/pitch.rb +154 -0
  25. data/lib/mtk/core/pitch_class.rb +194 -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/events/timeline.rb +232 -0
  30. data/lib/mtk/groups/chord.rb +56 -0
  31. data/lib/mtk/groups/collection.rb +196 -0
  32. data/lib/mtk/groups/melody.rb +96 -0
  33. data/lib/mtk/groups/pitch_class_set.rb +163 -0
  34. data/lib/mtk/groups/pitch_collection.rb +23 -0
  35. data/lib/mtk/io/dls_synth_device.rb +146 -0
  36. data/lib/mtk/io/dls_synth_output.rb +62 -0
  37. data/lib/mtk/io/jsound_input.rb +87 -0
  38. data/lib/mtk/io/jsound_output.rb +82 -0
  39. data/lib/mtk/io/midi_file.rb +209 -0
  40. data/lib/mtk/io/midi_input.rb +97 -0
  41. data/lib/mtk/io/midi_output.rb +195 -0
  42. data/lib/mtk/io/notation.rb +162 -0
  43. data/lib/mtk/io/unimidi_input.rb +117 -0
  44. data/lib/mtk/io/unimidi_output.rb +140 -0
  45. data/lib/mtk/lang/durations.rb +57 -0
  46. data/lib/mtk/lang/intensities.rb +61 -0
  47. data/lib/mtk/lang/intervals.rb +73 -0
  48. data/lib/mtk/lang/mtk_grammar.citrus +237 -0
  49. data/lib/mtk/lang/parser.rb +29 -0
  50. data/lib/mtk/lang/pitch_classes.rb +29 -0
  51. data/lib/mtk/lang/pitches.rb +52 -0
  52. data/lib/mtk/lang/pseudo_constants.rb +26 -0
  53. data/lib/mtk/lang/variable.rb +32 -0
  54. data/lib/mtk/numeric_extensions.rb +66 -0
  55. data/lib/mtk/patterns/chain.rb +49 -0
  56. data/lib/mtk/patterns/choice.rb +43 -0
  57. data/lib/mtk/patterns/cycle.rb +18 -0
  58. data/lib/mtk/patterns/for_each.rb +71 -0
  59. data/lib/mtk/patterns/function.rb +39 -0
  60. data/lib/mtk/patterns/lines.rb +54 -0
  61. data/lib/mtk/patterns/palindrome.rb +45 -0
  62. data/lib/mtk/patterns/pattern.rb +171 -0
  63. data/lib/mtk/patterns/sequence.rb +20 -0
  64. data/lib/mtk/sequencers/event_builder.rb +132 -0
  65. data/lib/mtk/sequencers/legato_sequencer.rb +24 -0
  66. data/lib/mtk/sequencers/rhythmic_sequencer.rb +28 -0
  67. data/lib/mtk/sequencers/sequencer.rb +111 -0
  68. data/lib/mtk/sequencers/step_sequencer.rb +26 -0
  69. data/spec/mtk/core/duration_spec.rb +372 -0
  70. data/spec/mtk/core/intensity_spec.rb +289 -0
  71. data/spec/mtk/core/interval_spec.rb +265 -0
  72. data/spec/mtk/core/pitch_class_spec.rb +343 -0
  73. data/spec/mtk/core/pitch_spec.rb +297 -0
  74. data/spec/mtk/events/event_spec.rb +234 -0
  75. data/spec/mtk/events/note_spec.rb +174 -0
  76. data/spec/mtk/events/parameter_spec.rb +220 -0
  77. data/spec/mtk/events/timeline_spec.rb +430 -0
  78. data/spec/mtk/groups/chord_spec.rb +85 -0
  79. data/spec/mtk/groups/collection_spec.rb +374 -0
  80. data/spec/mtk/groups/melody_spec.rb +225 -0
  81. data/spec/mtk/groups/pitch_class_set_spec.rb +340 -0
  82. data/spec/mtk/io/midi_file_spec.rb +243 -0
  83. data/spec/mtk/io/midi_output_spec.rb +102 -0
  84. data/spec/mtk/lang/durations_spec.rb +89 -0
  85. data/spec/mtk/lang/intensities_spec.rb +101 -0
  86. data/spec/mtk/lang/intervals_spec.rb +143 -0
  87. data/spec/mtk/lang/parser_spec.rb +603 -0
  88. data/spec/mtk/lang/pitch_classes_spec.rb +62 -0
  89. data/spec/mtk/lang/pitches_spec.rb +56 -0
  90. data/spec/mtk/lang/pseudo_constants_spec.rb +20 -0
  91. data/spec/mtk/lang/variable_spec.rb +52 -0
  92. data/spec/mtk/numeric_extensions_spec.rb +83 -0
  93. data/spec/mtk/patterns/chain_spec.rb +110 -0
  94. data/spec/mtk/patterns/choice_spec.rb +97 -0
  95. data/spec/mtk/patterns/cycle_spec.rb +123 -0
  96. data/spec/mtk/patterns/for_each_spec.rb +136 -0
  97. data/spec/mtk/patterns/function_spec.rb +120 -0
  98. data/spec/mtk/patterns/lines_spec.rb +77 -0
  99. data/spec/mtk/patterns/palindrome_spec.rb +108 -0
  100. data/spec/mtk/patterns/pattern_spec.rb +132 -0
  101. data/spec/mtk/patterns/sequence_spec.rb +203 -0
  102. data/spec/mtk/sequencers/event_builder_spec.rb +245 -0
  103. data/spec/mtk/sequencers/legato_sequencer_spec.rb +45 -0
  104. data/spec/mtk/sequencers/rhythmic_sequencer_spec.rb +84 -0
  105. data/spec/mtk/sequencers/sequencer_spec.rb +215 -0
  106. data/spec/mtk/sequencers/step_sequencer_spec.rb +93 -0
  107. data/spec/spec_coverage.rb +2 -0
  108. data/spec/spec_helper.rb +12 -0
  109. data/spec/test.mid +0 -0
  110. metadata +226 -0
@@ -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
@@ -0,0 +1,194 @@
1
+ module MTK
2
+ module Core
3
+
4
+ # A set of all pitches that are an integer number of octaves apart.
5
+ # A {Pitch} has the same PitchClass as the pitches one or more octaves away.
6
+ # @see https://en.wikipedia.org/wiki/Pitch_class
7
+ #
8
+ class PitchClass
9
+
10
+ # The normalized names of the 12 pitch classes in the chromatic scale.
11
+ # The index of each {#name} is the pitch class's numeric {#value}.
12
+ NAMES = %w( C Db D Eb E F Gb G Ab A Bb B ).freeze
13
+
14
+ # All enharmonic names of the 12 pitch classes, including sharps, flats, double-sharps, and double-flats,
15
+ # organized such that each index contains the allowed names of the pitch class with a {#value} equal to that index.
16
+ # @see VALID_NAMES
17
+ VALID_NAMES_BY_VALUE =
18
+ [ # (valid names ), # value # normalized name
19
+ %w( B# C Dbb ), # 0 # C
20
+ %w( B## C# Db ), # 1 # Db
21
+ %w( C## D Ebb ), # 2 # D
22
+ %w( D# Eb Fbb ), # 3 # Eb
23
+ %w( D## E Fb ), # 4 # E
24
+ %w( E# F Gbb ), # 5 # F
25
+ %w( E## F# Gb ), # 6 # Gb
26
+ %w( F## G Abb ), # 7 # G
27
+ %w( G# Ab ), # 8 # Ab
28
+ %w( G## A Bbb ), # 9 # A
29
+ %w( A# Bb Cbb ), # 10 # Bb
30
+ %w( A## B Cb ) # 11 # B
31
+ ].freeze
32
+
33
+ # All valid enharmonic pitch class names in a flat list.
34
+ # @see VALID_NAMES_BY_VALUE
35
+ VALID_NAMES = VALID_NAMES_BY_VALUE.flatten.freeze
36
+
37
+ # A mapping from valid names to the value of the pitch class with that name
38
+ VALUES_BY_NAME = Hash[ # a map from a list of name,value pairs
39
+ VALID_NAMES_BY_VALUE.map.with_index do |valid_names,value|
40
+ valid_names.map{|name| [name,value] }
41
+ end.flatten(1)
42
+ ].freeze
43
+
44
+
45
+ # The name of this pitch class.
46
+ # One of the {NAMES} defined by this class.
47
+ attr_reader :name
48
+
49
+ # The value of this pitch class.
50
+ # An integer from 0..11 that indexes this pitch class in {PITCH_CLASSES} and the {#name} in {NAMES}.
51
+ attr_reader :value
52
+
53
+
54
+ private ######
55
+ # Even though new is a private_class_method, YARD gets confused so we temporarily go private
56
+
57
+ def initialize(name, value)
58
+ @name, @value = name, value
59
+ end
60
+ private_class_method :new
61
+
62
+ @flyweight = {}
63
+
64
+ public ######
65
+
66
+
67
+ # Lookup a PitchClass by name or value.
68
+ # @param name_or_value [String,Symbol,Numeric] one of {VALID_NAMES} or 0..12
69
+ # @return the PitchClass representing the argument
70
+ # @raise ArgumentError for arguments that cannot be converted to a PitchClass
71
+ def self.[] name_or_value
72
+ @flyweight[name_or_value] ||= case name_or_value
73
+ when String,Symbol then from_name(name_or_value)
74
+ when Numeric then from_value(name_or_value.round)
75
+ else raise ArgumentError.new("PitchClass.[] doesn't understand #{name_or_value.class}")
76
+ end
77
+ end
78
+
79
+ # Lookup a PitchClass by name.
80
+ # @param name [String,#to_s] one of {VALID_NAMES} (case-insensitive)
81
+ def self.from_name(name)
82
+ @flyweight[name] ||= (
83
+ valid_name = name.to_s.capitalize
84
+ value = VALUES_BY_NAME[valid_name] or raise ArgumentError.new("Invalid PitchClass name: #{name}")
85
+ new(valid_name,value)
86
+ )
87
+ end
88
+
89
+ class << self
90
+ alias from_s from_name
91
+ end
92
+
93
+ # All 12 pitch classes in the chromatic scale.
94
+ # The index of each pitch class is the pitch class's numeric {#value}.
95
+ PITCH_CLASSES = NAMES.map{|name| from_name name }.freeze
96
+
97
+ # return the pitch class with the given integer value mod 12
98
+ # @param value [Integer,#to_i]
99
+ def self.from_value(value)
100
+ PITCH_CLASSES[value.to_i % 12]
101
+ end
102
+
103
+ class << self
104
+ alias from_i from_value
105
+ end
106
+
107
+ # return the pitch class with the given float rounded to the nearest integer, mod 12
108
+ # @param value [Float,#to_f]
109
+ def self.from_f(value)
110
+ from_i value.to_f.round
111
+ end
112
+
113
+ # Compare 2 pitch classes for equal values.
114
+ # @param other [PitchClass]
115
+ # @return true if this pitch class's value is equal to the other pitch class's value
116
+ def == other
117
+ other.is_a? PitchClass and other.value == @value
118
+ end
119
+
120
+ # Compare a pitch class with another pitch class or integer value
121
+ # @param other [PitchClass,#to_i]
122
+ # @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
123
+ # @see http://ruby-doc.org/core-1.9.3/Comparable.html
124
+ def <=> other
125
+ @value <=> other.to_i
126
+ end
127
+
128
+ # This pitch class's normalized {#name}.
129
+ # @see NAMES
130
+ def to_s
131
+ @name.to_s
132
+ end
133
+
134
+ # This pitch class's integer {#value}
135
+ def to_i
136
+ @value.to_i
137
+ end
138
+
139
+ # This pitch class's {#value} as a floating point number
140
+ def to_f
141
+ @value.to_f
142
+ end
143
+
144
+ # Transpose this pitch class by adding it's value to the value given (mod 12)
145
+ # @param interval [PitchClass,Float,#to_f]
146
+ def + interval
147
+ new_value = (value + interval.to_f).round
148
+ self.class.from_value new_value
149
+ end
150
+ alias transpose +
151
+
152
+ # Transpose this pitch class by subtracing the given value from this value (mod 12)
153
+ # @param interval [PitchClass,Float,#to_f]
154
+ def - interval
155
+ new_value = (value - interval.to_f).round
156
+ self.class.from_value new_value
157
+ end
158
+
159
+ # Inverts (mirrors) the pitch class around the given center
160
+ # @param center [PitchClass,Pitch,Float,#to_f] the value to "mirror" this pitch class around
161
+ def invert(center)
162
+ delta = (2*(center.to_f - value)).round
163
+ self + delta
164
+ end
165
+
166
+ # the smallest interval in semitones that needs to be added to this PitchClass to reach the given PitchClass
167
+ # @param pitch_class [PitchClass,#value]
168
+ def distance_to(pitch_class)
169
+ delta = (pitch_class.value - value) % 12
170
+ if delta > 6
171
+ delta -= 12
172
+ elsif delta == 6 and to_i >= 6
173
+ # this is a special edge case to prevent endlessly ascending pitch sequences when alternating between two pitch classes a tritone apart
174
+ delta = -6
175
+ end
176
+ delta
177
+ end
178
+
179
+ end
180
+ end
181
+
182
+ # Construct a {PitchClass} from any supported type
183
+ # @param anything [PitchClass,String,Symbol,Numeric]
184
+ def PitchClass(anything)
185
+ case anything
186
+ when Numeric then MTK::Core::PitchClass.from_f(anything)
187
+ when String, Symbol then MTK::Core::PitchClass.from_s(anything)
188
+ when MTK::Core::PitchClass then anything
189
+ else raise ArgumentError.new("PitchClass doesn't understand #{anything.class}")
190
+ end
191
+ end
192
+ module_function :PitchClass
193
+
194
+ end