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.
- data/.yardopts +3 -2
- data/DEVELOPMENT_NOTES.md +114 -0
- data/INTRO.md +64 -8
- data/LICENSE.txt +1 -1
- data/README.md +31 -102
- data/Rakefile +56 -18
- data/bin/mtk +215 -0
- data/examples/crescendo.rb +5 -5
- data/examples/drum_pattern1.rb +23 -0
- data/examples/dynamic_pattern.rb +8 -11
- data/examples/gets_and_play.rb +26 -0
- data/examples/notation.rb +22 -0
- data/examples/play_midi.rb +8 -10
- data/examples/random_tone_row.rb +2 -2
- data/examples/syntax_to_midi.rb +28 -0
- data/examples/test_output.rb +8 -0
- data/examples/tone_row_melody.rb +6 -6
- data/lib/mtk.rb +52 -40
- data/lib/mtk/chord.rb +55 -0
- data/lib/mtk/constants/durations.rb +57 -0
- data/lib/mtk/constants/intensities.rb +61 -0
- data/lib/mtk/constants/intervals.rb +73 -0
- data/lib/mtk/constants/pitch_classes.rb +29 -0
- data/lib/mtk/constants/pitches.rb +52 -0
- data/lib/mtk/duration.rb +211 -0
- data/lib/mtk/events/event.rb +119 -0
- data/lib/mtk/events/note.rb +112 -0
- data/lib/mtk/events/parameter.rb +54 -0
- data/lib/mtk/helpers/collection.rb +164 -0
- data/lib/mtk/helpers/convert.rb +36 -0
- data/lib/mtk/helpers/lilypond.rb +162 -0
- data/lib/mtk/helpers/output_selector.rb +67 -0
- data/lib/mtk/helpers/pitch_collection.rb +23 -0
- data/lib/mtk/helpers/pseudo_constants.rb +26 -0
- data/lib/mtk/intensity.rb +156 -0
- data/lib/mtk/interval.rb +155 -0
- data/lib/mtk/lang/mtk_grammar.citrus +190 -13
- data/lib/mtk/lang/parser.rb +29 -0
- data/lib/mtk/melody.rb +94 -0
- data/lib/mtk/midi/dls_synth_device.rb +144 -0
- data/lib/mtk/midi/dls_synth_output.rb +62 -0
- data/lib/mtk/midi/file.rb +67 -32
- data/lib/mtk/midi/input.rb +97 -0
- data/lib/mtk/midi/jsound_input.rb +36 -17
- data/lib/mtk/midi/jsound_output.rb +48 -46
- data/lib/mtk/midi/output.rb +195 -0
- data/lib/mtk/midi/unimidi_input.rb +117 -0
- data/lib/mtk/midi/unimidi_output.rb +121 -0
- data/lib/mtk/{_numeric_extensions.rb → numeric_extensions.rb} +12 -0
- data/lib/mtk/patterns/chain.rb +49 -0
- data/lib/mtk/{pattern → patterns}/choice.rb +14 -8
- data/lib/mtk/patterns/cycle.rb +18 -0
- data/lib/mtk/patterns/for_each.rb +71 -0
- data/lib/mtk/patterns/function.rb +39 -0
- data/lib/mtk/{pattern → patterns}/lines.rb +11 -17
- data/lib/mtk/{pattern → patterns}/palindrome.rb +11 -8
- data/lib/mtk/patterns/pattern.rb +171 -0
- data/lib/mtk/patterns/sequence.rb +20 -0
- data/lib/mtk/pitch.rb +7 -6
- data/lib/mtk/pitch_class.rb +124 -46
- data/lib/mtk/pitch_class_set.rb +58 -35
- data/lib/mtk/sequencers/event_builder.rb +131 -0
- data/lib/mtk/sequencers/legato_sequencer.rb +24 -0
- data/lib/mtk/sequencers/rhythmic_sequencer.rb +28 -0
- data/lib/mtk/{sequencer/abstract_sequencer.rb → sequencers/sequencer.rb} +37 -11
- data/lib/mtk/{sequencer → sequencers}/step_sequencer.rb +4 -4
- data/lib/mtk/timeline.rb +39 -22
- data/lib/mtk/variable.rb +32 -0
- data/spec/mtk/chord_spec.rb +83 -0
- data/spec/mtk/{_constants → constants}/durations_spec.rb +12 -41
- data/spec/mtk/{_constants → constants}/intensities_spec.rb +13 -37
- data/spec/mtk/{_constants → constants}/intervals_spec.rb +14 -32
- data/spec/mtk/{_constants → constants}/pitch_classes_spec.rb +8 -4
- data/spec/mtk/{_constants → constants}/pitches_spec.rb +5 -1
- data/spec/mtk/duration_spec.rb +372 -0
- data/spec/mtk/events/event_spec.rb +234 -0
- data/spec/mtk/events/note_spec.rb +174 -0
- data/spec/mtk/events/parameter_spec.rb +220 -0
- data/spec/mtk/{helper → helpers}/collection_spec.rb +86 -3
- data/spec/mtk/{helper → helpers}/pseudo_constants_spec.rb +2 -2
- data/spec/mtk/intensity_spec.rb +289 -0
- data/spec/mtk/interval_spec.rb +265 -0
- data/spec/mtk/lang/parser_spec.rb +597 -0
- data/spec/mtk/melody_spec.rb +223 -0
- data/spec/mtk/midi/file_spec.rb +16 -16
- data/spec/mtk/midi/jsound_input_spec.rb +11 -0
- data/spec/mtk/midi/jsound_output_spec.rb +11 -0
- data/spec/mtk/midi/output_spec.rb +102 -0
- data/spec/mtk/midi/unimidi_input_spec.rb +11 -0
- data/spec/mtk/midi/unimidi_output_spec.rb +11 -0
- data/spec/mtk/{_numeric_extensions_spec.rb → numeric_extensions_spec.rb} +1 -0
- data/spec/mtk/patterns/chain_spec.rb +110 -0
- data/spec/mtk/{pattern → patterns}/choice_spec.rb +20 -30
- data/spec/mtk/{pattern → patterns}/cycle_spec.rb +25 -35
- data/spec/mtk/patterns/for_each_spec.rb +136 -0
- data/spec/mtk/{pattern → patterns}/function_spec.rb +17 -30
- data/spec/mtk/{pattern → patterns}/lines_spec.rb +11 -27
- data/spec/mtk/{pattern → patterns}/palindrome_spec.rb +13 -29
- data/spec/mtk/patterns/pattern_spec.rb +132 -0
- data/spec/mtk/patterns/sequence_spec.rb +203 -0
- data/spec/mtk/pitch_class_set_spec.rb +23 -21
- data/spec/mtk/pitch_class_spec.rb +151 -39
- data/spec/mtk/pitch_spec.rb +22 -1
- data/spec/mtk/sequencers/event_builder_spec.rb +245 -0
- data/spec/mtk/sequencers/legato_sequencer_spec.rb +45 -0
- data/spec/mtk/sequencers/rhythmic_sequencer_spec.rb +84 -0
- data/spec/mtk/sequencers/sequencer_spec.rb +215 -0
- data/spec/mtk/{sequencer → sequencers}/step_sequencer_spec.rb +35 -13
- data/spec/mtk/timeline_spec.rb +109 -16
- data/spec/mtk/variable_spec.rb +52 -0
- data/spec/spec_coverage.rb +2 -0
- data/spec/spec_helper.rb +3 -0
- metadata +188 -91
- data/lib/mtk/_constants/durations.rb +0 -80
- data/lib/mtk/_constants/intensities.rb +0 -81
- data/lib/mtk/_constants/intervals.rb +0 -85
- data/lib/mtk/_constants/pitch_classes.rb +0 -35
- data/lib/mtk/_constants/pitches.rb +0 -49
- data/lib/mtk/event.rb +0 -70
- data/lib/mtk/helper/collection.rb +0 -114
- data/lib/mtk/helper/event_builder.rb +0 -85
- data/lib/mtk/helper/pseudo_constants.rb +0 -26
- data/lib/mtk/lang/grammar.rb +0 -17
- data/lib/mtk/note.rb +0 -63
- data/lib/mtk/pattern/abstract_pattern.rb +0 -132
- data/lib/mtk/pattern/cycle.rb +0 -51
- data/lib/mtk/pattern/enumerator.rb +0 -26
- data/lib/mtk/pattern/function.rb +0 -46
- data/lib/mtk/pattern/sequence.rb +0 -30
- data/lib/mtk/pitch_set.rb +0 -84
- data/lib/mtk/sequencer/rhythmic_sequencer.rb +0 -29
- data/lib/mtk/transform/invertible.rb +0 -15
- data/lib/mtk/transform/mappable.rb +0 -18
- data/lib/mtk/transform/set_theory_operations.rb +0 -34
- data/lib/mtk/transform/transposable.rb +0 -14
- data/spec/mtk/event_spec.rb +0 -139
- data/spec/mtk/helper/event_builder_spec.rb +0 -92
- data/spec/mtk/lang/grammar_spec.rb +0 -100
- data/spec/mtk/note_spec.rb +0 -115
- data/spec/mtk/pattern/abstract_pattern_spec.rb +0 -45
- data/spec/mtk/pattern/note_cycle_spec.rb.bak +0 -116
- data/spec/mtk/pattern/pitch_cycle_spec.rb.bak +0 -47
- data/spec/mtk/pattern/pitch_sequence_spec.rb.bak +0 -37
- data/spec/mtk/pattern/sequence_spec.rb +0 -151
- data/spec/mtk/pitch_set_spec.rb +0 -198
- data/spec/mtk/sequencer/abstract_sequencer_spec.rb +0 -159
- data/spec/mtk/sequencer/rhythmic_sequencer_spec.rb +0 -49
@@ -1,15 +1,17 @@
|
|
1
1
|
module MTK
|
2
|
-
module
|
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 <
|
8
|
+
class Lines < Pattern
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
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
|
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
|
22
|
-
def
|
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(
|
30
|
-
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
|
-
"
|
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
|
data/lib/mtk/pitch_class.rb
CHANGED
@@ -1,94 +1,171 @@
|
|
1
1
|
module MTK
|
2
2
|
|
3
|
-
# A
|
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
31
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
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
|
52
|
-
alias :from_name :[]
|
103
|
+
alias from_i from_value
|
53
104
|
end
|
54
105
|
|
55
|
-
|
56
|
-
|
57
|
-
|
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.
|
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
|
-
@
|
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
|
-
@
|
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
|
-
|
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
|
-
|
154
|
+
new_value = (value - interval.to_f).round
|
155
|
+
self.class.from_value new_value
|
83
156
|
end
|
84
157
|
|
85
|
-
|
86
|
-
|
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.
|
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.
|
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
|