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.
- data/.yardopts +10 -0
- data/DEVELOPMENT_NOTES.md +115 -0
- data/INTRO.md +129 -0
- data/LICENSE.txt +27 -0
- data/README.md +50 -0
- data/Rakefile +102 -0
- data/bin/jmtk +250 -0
- data/bin/mtk +250 -0
- data/examples/crescendo.rb +20 -0
- data/examples/drum_pattern.rb +23 -0
- data/examples/dynamic_pattern.rb +36 -0
- data/examples/gets_and_play.rb +27 -0
- data/examples/notation.rb +22 -0
- data/examples/play_midi.rb +17 -0
- data/examples/print_midi.rb +13 -0
- data/examples/random_tone_row.rb +18 -0
- data/examples/syntax_to_midi.rb +28 -0
- data/examples/test_output.rb +7 -0
- data/examples/tone_row_melody.rb +23 -0
- data/lib/mtk.rb +76 -0
- data/lib/mtk/core/duration.rb +213 -0
- data/lib/mtk/core/intensity.rb +158 -0
- data/lib/mtk/core/interval.rb +157 -0
- data/lib/mtk/core/pitch.rb +154 -0
- data/lib/mtk/core/pitch_class.rb +194 -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/events/timeline.rb +232 -0
- data/lib/mtk/groups/chord.rb +56 -0
- data/lib/mtk/groups/collection.rb +196 -0
- data/lib/mtk/groups/melody.rb +96 -0
- data/lib/mtk/groups/pitch_class_set.rb +163 -0
- data/lib/mtk/groups/pitch_collection.rb +23 -0
- data/lib/mtk/io/dls_synth_device.rb +146 -0
- data/lib/mtk/io/dls_synth_output.rb +62 -0
- data/lib/mtk/io/jsound_input.rb +87 -0
- data/lib/mtk/io/jsound_output.rb +82 -0
- data/lib/mtk/io/midi_file.rb +209 -0
- data/lib/mtk/io/midi_input.rb +97 -0
- data/lib/mtk/io/midi_output.rb +195 -0
- data/lib/mtk/io/notation.rb +162 -0
- data/lib/mtk/io/unimidi_input.rb +117 -0
- data/lib/mtk/io/unimidi_output.rb +140 -0
- data/lib/mtk/lang/durations.rb +57 -0
- data/lib/mtk/lang/intensities.rb +61 -0
- data/lib/mtk/lang/intervals.rb +73 -0
- data/lib/mtk/lang/mtk_grammar.citrus +237 -0
- data/lib/mtk/lang/parser.rb +29 -0
- data/lib/mtk/lang/pitch_classes.rb +29 -0
- data/lib/mtk/lang/pitches.rb +52 -0
- data/lib/mtk/lang/pseudo_constants.rb +26 -0
- data/lib/mtk/lang/variable.rb +32 -0
- data/lib/mtk/numeric_extensions.rb +66 -0
- data/lib/mtk/patterns/chain.rb +49 -0
- data/lib/mtk/patterns/choice.rb +43 -0
- 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/patterns/lines.rb +54 -0
- data/lib/mtk/patterns/palindrome.rb +45 -0
- data/lib/mtk/patterns/pattern.rb +171 -0
- data/lib/mtk/patterns/sequence.rb +20 -0
- data/lib/mtk/sequencers/event_builder.rb +132 -0
- data/lib/mtk/sequencers/legato_sequencer.rb +24 -0
- data/lib/mtk/sequencers/rhythmic_sequencer.rb +28 -0
- data/lib/mtk/sequencers/sequencer.rb +111 -0
- data/lib/mtk/sequencers/step_sequencer.rb +26 -0
- data/spec/mtk/core/duration_spec.rb +372 -0
- data/spec/mtk/core/intensity_spec.rb +289 -0
- data/spec/mtk/core/interval_spec.rb +265 -0
- data/spec/mtk/core/pitch_class_spec.rb +343 -0
- data/spec/mtk/core/pitch_spec.rb +297 -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/events/timeline_spec.rb +430 -0
- data/spec/mtk/groups/chord_spec.rb +85 -0
- data/spec/mtk/groups/collection_spec.rb +374 -0
- data/spec/mtk/groups/melody_spec.rb +225 -0
- data/spec/mtk/groups/pitch_class_set_spec.rb +340 -0
- data/spec/mtk/io/midi_file_spec.rb +243 -0
- data/spec/mtk/io/midi_output_spec.rb +102 -0
- data/spec/mtk/lang/durations_spec.rb +89 -0
- data/spec/mtk/lang/intensities_spec.rb +101 -0
- data/spec/mtk/lang/intervals_spec.rb +143 -0
- data/spec/mtk/lang/parser_spec.rb +603 -0
- data/spec/mtk/lang/pitch_classes_spec.rb +62 -0
- data/spec/mtk/lang/pitches_spec.rb +56 -0
- data/spec/mtk/lang/pseudo_constants_spec.rb +20 -0
- data/spec/mtk/lang/variable_spec.rb +52 -0
- data/spec/mtk/numeric_extensions_spec.rb +83 -0
- data/spec/mtk/patterns/chain_spec.rb +110 -0
- data/spec/mtk/patterns/choice_spec.rb +97 -0
- data/spec/mtk/patterns/cycle_spec.rb +123 -0
- data/spec/mtk/patterns/for_each_spec.rb +136 -0
- data/spec/mtk/patterns/function_spec.rb +120 -0
- data/spec/mtk/patterns/lines_spec.rb +77 -0
- data/spec/mtk/patterns/palindrome_spec.rb +108 -0
- data/spec/mtk/patterns/pattern_spec.rb +132 -0
- data/spec/mtk/patterns/sequence_spec.rb +203 -0
- 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/sequencers/step_sequencer_spec.rb +93 -0
- data/spec/spec_coverage.rb +2 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/test.mid +0 -0
- 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
|