mtk 0.0.3.2 → 0.0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +2 -2
- data/DEVELOPMENT_NOTES.md +20 -0
- data/README.md +9 -3
- data/Rakefile +47 -13
- data/bin/mtk +55 -20
- data/examples/crescendo.rb +4 -4
- data/examples/{drum_pattern1.rb → drum_pattern.rb} +8 -8
- data/examples/dynamic_pattern.rb +5 -5
- data/examples/gets_and_play.rb +3 -2
- data/examples/notation.rb +3 -3
- data/examples/play_midi.rb +4 -4
- data/examples/print_midi.rb +2 -2
- data/examples/random_tone_row.rb +3 -3
- data/examples/syntax_to_midi.rb +2 -2
- data/examples/test_output.rb +4 -5
- data/examples/tone_row_melody.rb +7 -5
- 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 +4 -4
- data/lib/mtk/events/note.rb +12 -12
- data/lib/mtk/events/timeline.rb +232 -0
- data/lib/mtk/groups/chord.rb +56 -0
- data/lib/mtk/{helpers → groups}/collection.rb +33 -1
- data/lib/mtk/groups/melody.rb +96 -0
- data/lib/mtk/groups/pitch_class_set.rb +163 -0
- data/lib/mtk/{helpers → groups}/pitch_collection.rb +1 -1
- data/lib/mtk/{midi → io}/dls_synth_device.rb +3 -1
- data/lib/mtk/{midi → io}/dls_synth_output.rb +10 -10
- data/lib/mtk/{midi → io}/jsound_input.rb +2 -2
- data/lib/mtk/{midi → io}/jsound_output.rb +9 -9
- data/lib/mtk/{midi/file.rb → io/midi_file.rb} +13 -13
- data/lib/mtk/{midi/input.rb → io/midi_input.rb} +4 -4
- data/lib/mtk/{midi/output.rb → io/midi_output.rb} +8 -8
- data/lib/mtk/{helpers/lilypond.rb → io/notation.rb} +5 -5
- data/lib/mtk/{midi → io}/unimidi_input.rb +2 -2
- data/lib/mtk/{midi → io}/unimidi_output.rb +14 -9
- data/lib/mtk/{constants → lang}/durations.rb +11 -11
- data/lib/mtk/{constants → lang}/intensities.rb +11 -11
- data/lib/mtk/{constants → lang}/intervals.rb +17 -17
- data/lib/mtk/lang/mtk_grammar.citrus +9 -9
- data/lib/mtk/{constants → lang}/pitch_classes.rb +5 -5
- data/lib/mtk/{constants → lang}/pitches.rb +7 -7
- data/lib/mtk/{helpers → lang}/pseudo_constants.rb +1 -1
- data/lib/mtk/{variable.rb → lang/variable.rb} +1 -1
- data/lib/mtk/numeric_extensions.rb +40 -47
- data/lib/mtk/patterns/for_each.rb +1 -1
- data/lib/mtk/patterns/pattern.rb +3 -3
- data/lib/mtk/sequencers/event_builder.rb +16 -15
- data/lib/mtk/sequencers/legato_sequencer.rb +1 -1
- data/lib/mtk/sequencers/rhythmic_sequencer.rb +1 -1
- data/lib/mtk/sequencers/sequencer.rb +8 -8
- data/lib/mtk/sequencers/step_sequencer.rb +2 -2
- data/lib/mtk.rb +33 -39
- data/spec/mtk/{duration_spec.rb → core/duration_spec.rb} +3 -3
- data/spec/mtk/{intensity_spec.rb → core/intensity_spec.rb} +3 -3
- data/spec/mtk/{interval_spec.rb → core/interval_spec.rb} +1 -1
- data/spec/mtk/{pitch_class_spec.rb → core/pitch_class_spec.rb} +1 -1
- data/spec/mtk/{pitch_spec.rb → core/pitch_spec.rb} +8 -8
- data/spec/mtk/events/event_spec.rb +4 -4
- data/spec/mtk/events/note_spec.rb +8 -8
- data/spec/mtk/{timeline_spec.rb → events/timeline_spec.rb} +47 -47
- data/spec/mtk/{chord_spec.rb → groups/chord_spec.rb} +18 -16
- data/spec/mtk/{helpers → groups}/collection_spec.rb +3 -3
- data/spec/mtk/{melody_spec.rb → groups/melody_spec.rb} +36 -34
- data/spec/mtk/{pitch_class_set_spec.rb → groups/pitch_class_set_spec.rb} +57 -55
- data/spec/mtk/{midi/file_spec.rb → io/midi_file_spec.rb} +17 -17
- data/spec/mtk/{midi/output_spec.rb → io/midi_output_spec.rb} +6 -6
- data/spec/mtk/{constants → lang}/durations_spec.rb +1 -1
- data/spec/mtk/{constants → lang}/intensities_spec.rb +1 -1
- data/spec/mtk/{constants → lang}/intervals_spec.rb +1 -1
- data/spec/mtk/lang/parser_spec.rb +12 -6
- data/spec/mtk/{constants → lang}/pitch_classes_spec.rb +1 -1
- data/spec/mtk/{constants → lang}/pitches_spec.rb +1 -1
- data/spec/mtk/{helpers → lang}/pseudo_constants_spec.rb +2 -2
- data/spec/mtk/{variable_spec.rb → lang/variable_spec.rb} +4 -4
- data/spec/mtk/numeric_extensions_spec.rb +35 -55
- data/spec/mtk/patterns/for_each_spec.rb +1 -1
- data/spec/mtk/patterns/sequence_spec.rb +1 -1
- data/spec/mtk/sequencers/legato_sequencer_spec.rb +2 -2
- data/spec/mtk/sequencers/rhythmic_sequencer_spec.rb +4 -4
- data/spec/mtk/sequencers/step_sequencer_spec.rb +5 -5
- data/spec/spec_helper.rb +7 -6
- metadata +75 -61
- data/ext/mkrf_conf.rb +0 -25
- data/lib/mtk/chord.rb +0 -55
- data/lib/mtk/duration.rb +0 -211
- data/lib/mtk/helpers/convert.rb +0 -36
- data/lib/mtk/helpers/output_selector.rb +0 -67
- data/lib/mtk/intensity.rb +0 -156
- data/lib/mtk/interval.rb +0 -155
- data/lib/mtk/melody.rb +0 -94
- data/lib/mtk/pitch.rb +0 -152
- data/lib/mtk/pitch_class.rb +0 -192
- data/lib/mtk/pitch_class_set.rb +0 -161
- data/lib/mtk/timeline.rb +0 -230
- data/spec/mtk/midi/jsound_input_spec.rb +0 -11
- data/spec/mtk/midi/jsound_output_spec.rb +0 -11
- data/spec/mtk/midi/unimidi_input_spec.rb +0 -11
- data/spec/mtk/midi/unimidi_output_spec.rb +0 -11
data/lib/mtk/intensity.rb
DELETED
@@ -1,156 +0,0 @@
|
|
1
|
-
module MTK
|
2
|
-
|
3
|
-
# A measure of intensity, using an underlying value in the range 0.0-1.0
|
4
|
-
class Intensity
|
5
|
-
|
6
|
-
include Comparable
|
7
|
-
|
8
|
-
# The names of the base intensities. See {}MTK::Constants::Intensities} for more info.
|
9
|
-
NAMES = %w[ppp pp p mp mf o ff fff].freeze
|
10
|
-
|
11
|
-
VALUES_BY_NAME = {
|
12
|
-
'ppp' => 0.125,
|
13
|
-
'pp' => 0.25,
|
14
|
-
'p' => 0.375,
|
15
|
-
'mp' => 0.5,
|
16
|
-
'mf' => 0.625,
|
17
|
-
'o' => 0.75,
|
18
|
-
'ff' => 0.875,
|
19
|
-
'fff' => 1.0
|
20
|
-
}
|
21
|
-
|
22
|
-
@flyweight = {}
|
23
|
-
|
24
|
-
# The number of beats, typically representation as a Rational
|
25
|
-
attr_reader :value
|
26
|
-
|
27
|
-
def initialize(value)
|
28
|
-
@value = value
|
29
|
-
end
|
30
|
-
|
31
|
-
# Return an Intensity, only constructing a new instance when not already in the flyweight cache
|
32
|
-
def self.[](value)
|
33
|
-
value = value.to_f
|
34
|
-
@flyweight[value] ||= new(value)
|
35
|
-
end
|
36
|
-
|
37
|
-
class << self
|
38
|
-
alias :from_f :[]
|
39
|
-
alias :from_i :[]
|
40
|
-
end
|
41
|
-
|
42
|
-
# Lookup an intensity by name.
|
43
|
-
# This method supports appending '-' or '+' for more fine-grained values.
|
44
|
-
def self.from_s(s)
|
45
|
-
return self[1.0] if s == 'fff+' # special case because "fff" is already the maximum
|
46
|
-
|
47
|
-
name = nil
|
48
|
-
modifier = nil
|
49
|
-
if s =~ /^(\w+)([+-])?$/
|
50
|
-
name = $1
|
51
|
-
modifier = $2
|
52
|
-
end
|
53
|
-
|
54
|
-
value = VALUES_BY_NAME[name]
|
55
|
-
raise ArgumentError.new("Invalid Intensity string '#{s}'") unless value
|
56
|
-
|
57
|
-
value += 1.0/24 if modifier == '+'
|
58
|
-
value -= 1.0/24 if modifier == '-'
|
59
|
-
|
60
|
-
self[value]
|
61
|
-
end
|
62
|
-
|
63
|
-
class << self
|
64
|
-
alias :from_name :from_s
|
65
|
-
end
|
66
|
-
|
67
|
-
# The number of beats as a floating point number
|
68
|
-
def to_f
|
69
|
-
@value.to_f
|
70
|
-
end
|
71
|
-
|
72
|
-
# The numerical value for the nearest whole number of beats
|
73
|
-
def to_i
|
74
|
-
@value.round
|
75
|
-
end
|
76
|
-
|
77
|
-
def to_midi
|
78
|
-
(to_f * 127).round
|
79
|
-
end
|
80
|
-
|
81
|
-
def to_percent
|
82
|
-
(@value * 100).round
|
83
|
-
end
|
84
|
-
|
85
|
-
def to_s
|
86
|
-
"#{to_percent}% intensity"
|
87
|
-
end
|
88
|
-
|
89
|
-
def inspect
|
90
|
-
"#<#{self.class}:#{object_id} @value=#{@value}>"
|
91
|
-
end
|
92
|
-
|
93
|
-
def ==( other )
|
94
|
-
other.is_a? MTK::Intensity and other.value == @value
|
95
|
-
end
|
96
|
-
|
97
|
-
def <=> other
|
98
|
-
if other.respond_to? :value
|
99
|
-
@value <=> other.value
|
100
|
-
else
|
101
|
-
@value <=> other
|
102
|
-
end
|
103
|
-
|
104
|
-
end
|
105
|
-
|
106
|
-
def + intensity
|
107
|
-
if intensity.is_a? MTK::Intensity
|
108
|
-
MTK::Intensity[@value + intensity.value]
|
109
|
-
else
|
110
|
-
MTK::Intensity[@value + intensity]
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
def -intensity
|
115
|
-
if intensity.is_a? MTK::Intensity
|
116
|
-
MTK::Intensity[@value - intensity.value]
|
117
|
-
else
|
118
|
-
MTK::Intensity[@value - intensity]
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
def * intensity
|
123
|
-
if intensity.is_a? MTK::Intensity
|
124
|
-
MTK::Intensity[@value * intensity.value]
|
125
|
-
else
|
126
|
-
MTK::Intensity[@value * intensity]
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
def / intensity
|
131
|
-
if intensity.is_a? MTK::Intensity
|
132
|
-
MTK::Intensity[to_f / intensity.value]
|
133
|
-
else
|
134
|
-
MTK::Intensity[to_f / intensity]
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
def coerce(other)
|
139
|
-
return MTK::Intensity[other], self
|
140
|
-
end
|
141
|
-
|
142
|
-
end
|
143
|
-
|
144
|
-
# Construct a {Duration} from any supported type
|
145
|
-
def Intensity(*anything)
|
146
|
-
anything = anything.first if anything.length == 1
|
147
|
-
case anything
|
148
|
-
when Numeric then MTK::Intensity[anything]
|
149
|
-
when String, Symbol then MTK::Intensity.from_s(anything)
|
150
|
-
when Intensity then anything
|
151
|
-
else raise "Intensity doesn't understand #{anything.class}"
|
152
|
-
end
|
153
|
-
end
|
154
|
-
module_function :Intensity
|
155
|
-
|
156
|
-
end
|
data/lib/mtk/interval.rb
DELETED
@@ -1,155 +0,0 @@
|
|
1
|
-
module MTK
|
2
|
-
|
3
|
-
# A measure of intensity, using an underlying value in the range 0.0-1.0
|
4
|
-
class Interval
|
5
|
-
|
6
|
-
include Comparable
|
7
|
-
|
8
|
-
# The preferred names of all pre-defined intervals
|
9
|
-
NAMES = %w[P1 m2 M2 m3 M3 P4 TT P5 m6 M6 m7 M7 P8].freeze
|
10
|
-
|
11
|
-
# All valid names of pre-defined intervals, indexed by their value.
|
12
|
-
NAMES_BY_VALUE =
|
13
|
-
[ # names # value # description
|
14
|
-
%w( P1 p1 ), # 0 # unison
|
15
|
-
%w( m2 min2 ), # 1 # minor second
|
16
|
-
%w( M2 maj2 ), # 2 # major second
|
17
|
-
%w( m3 min3 ), # 3 # minor third
|
18
|
-
%w( M3 maj3 ), # 4 # major third
|
19
|
-
%w( P4 p4 ), # 5 # perfect fourth
|
20
|
-
%w( TT tt ), # 6 # tritone (AKA augmented fourth, diminished fifth)
|
21
|
-
%w( P5 p5 ), # 7 # perfect fifth
|
22
|
-
%w( m6 min6 ), # 8 # minor sixth
|
23
|
-
%w( M6 maj6 ), # 9 # major sixth
|
24
|
-
%w( m7 min7 ), # 10 # minor seventh
|
25
|
-
%w( M7 maj7 ), # 11 # major seventh
|
26
|
-
%w( P8 p8 ) # 12 # octave
|
27
|
-
].freeze
|
28
|
-
|
29
|
-
# A mapping from intervals names to their value
|
30
|
-
VALUES_BY_NAME = Hash[ # a map from a list of name,value pairs
|
31
|
-
NAMES_BY_VALUE.map.with_index do |names,value|
|
32
|
-
names.map{|name| [name,value] }
|
33
|
-
end.flatten(1)
|
34
|
-
].freeze
|
35
|
-
|
36
|
-
# All valid interval names
|
37
|
-
ALL_NAMES = NAMES_BY_VALUE.flatten.freeze
|
38
|
-
|
39
|
-
|
40
|
-
@flyweight = {}
|
41
|
-
|
42
|
-
# The number of semitones represented by this interval
|
43
|
-
attr_reader :value
|
44
|
-
|
45
|
-
def initialize(value)
|
46
|
-
@value = value
|
47
|
-
end
|
48
|
-
|
49
|
-
# Return an {Interval}, only constructing a new instance when not already in the flyweight cache
|
50
|
-
def self.[](value)
|
51
|
-
value = value.to_f unless value.is_a? Fixnum
|
52
|
-
@flyweight[value] ||= new(value)
|
53
|
-
end
|
54
|
-
|
55
|
-
class << self
|
56
|
-
alias :from_f :[]
|
57
|
-
alias :from_i :[]
|
58
|
-
end
|
59
|
-
|
60
|
-
# Lookup an interval duration by name.
|
61
|
-
def self.from_s(s)
|
62
|
-
value = VALUES_BY_NAME[s.to_s]
|
63
|
-
raise ArgumentError.new("Invalid Interval string '#{s}'") unless value
|
64
|
-
self[value]
|
65
|
-
end
|
66
|
-
|
67
|
-
class << self
|
68
|
-
alias :from_name :from_s
|
69
|
-
end
|
70
|
-
|
71
|
-
# The number of semitones as a floating point number
|
72
|
-
def to_f
|
73
|
-
@value.to_f
|
74
|
-
end
|
75
|
-
|
76
|
-
# The numerical value for the nearest whole number of semitones in this interval
|
77
|
-
def to_i
|
78
|
-
@value.round
|
79
|
-
end
|
80
|
-
|
81
|
-
def to_s
|
82
|
-
@value.to_s
|
83
|
-
end
|
84
|
-
|
85
|
-
def inspect
|
86
|
-
"#{self.class}<#{to_s} semitones>"
|
87
|
-
end
|
88
|
-
|
89
|
-
def ==( other )
|
90
|
-
other.is_a? MTK::Interval and other.value == @value
|
91
|
-
end
|
92
|
-
|
93
|
-
def <=> other
|
94
|
-
if other.respond_to? :value
|
95
|
-
@value <=> other.value
|
96
|
-
else
|
97
|
-
@value <=> other
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
def + interval
|
102
|
-
if interval.is_a? MTK::Interval
|
103
|
-
MTK::Interval[@value + interval.value]
|
104
|
-
else
|
105
|
-
MTK::Interval[@value + interval]
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
def -interval
|
110
|
-
if interval.is_a? MTK::Interval
|
111
|
-
MTK::Interval[@value - interval.value]
|
112
|
-
else
|
113
|
-
MTK::Interval[@value - interval]
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
def * interval
|
118
|
-
if interval.is_a? MTK::Interval
|
119
|
-
MTK::Interval[@value * interval.value]
|
120
|
-
else
|
121
|
-
MTK::Interval[@value * interval]
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
def / interval
|
126
|
-
if interval.is_a? MTK::Interval
|
127
|
-
MTK::Interval[to_f / interval.value]
|
128
|
-
else
|
129
|
-
MTK::Interval[to_f / interval]
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
def -@
|
134
|
-
MTK::Interval[@value * -1]
|
135
|
-
end
|
136
|
-
|
137
|
-
def coerce(other)
|
138
|
-
return MTK::Interval[other], self
|
139
|
-
end
|
140
|
-
|
141
|
-
end
|
142
|
-
|
143
|
-
# Construct a {Duration} from any supported type
|
144
|
-
def Interval(*anything)
|
145
|
-
anything = anything.first if anything.length == 1
|
146
|
-
case anything
|
147
|
-
when Numeric then MTK::Interval[anything]
|
148
|
-
when String, Symbol then MTK::Interval.from_s(anything)
|
149
|
-
when Interval then anything
|
150
|
-
else raise "Interval doesn't understand #{anything.class}"
|
151
|
-
end
|
152
|
-
end
|
153
|
-
module_function :Interval
|
154
|
-
|
155
|
-
end
|
data/lib/mtk/melody.rb
DELETED
@@ -1,94 +0,0 @@
|
|
1
|
-
module MTK
|
2
|
-
|
3
|
-
# An ordered collection of {Pitch}es.
|
4
|
-
#
|
5
|
-
# The "horizontal" (sequential) pitch collection.
|
6
|
-
#
|
7
|
-
# Unlike the strict definition of melody, this class is fairly abstract and only models a succession of pitches.
|
8
|
-
# To create a true, playable melody one must combine an MTK::Melody and rhythms into a {Timeline}.
|
9
|
-
#
|
10
|
-
# @see Chord
|
11
|
-
#
|
12
|
-
class Melody
|
13
|
-
include Helpers::PitchCollection
|
14
|
-
|
15
|
-
attr_reader :pitches
|
16
|
-
|
17
|
-
# @param pitches [#to_a] the collection of pitches
|
18
|
-
# @see MTK#Melody
|
19
|
-
#
|
20
|
-
def initialize(pitches)
|
21
|
-
@pitches = pitches.to_a.clone.freeze
|
22
|
-
end
|
23
|
-
|
24
|
-
def self.from_pitch_classes(pitch_classes, start=Constants::Pitches::C4, max_distance=12)
|
25
|
-
pitch = start
|
26
|
-
pitches = []
|
27
|
-
pitch_classes.each do |pitch_class|
|
28
|
-
pitch = pitch.nearest(pitch_class)
|
29
|
-
pitch -= 12 if pitch > start+max_distance # keep within max_distance of start (default is one octave)
|
30
|
-
pitch += 12 if pitch < start-max_distance
|
31
|
-
pitches << pitch
|
32
|
-
end
|
33
|
-
new pitches
|
34
|
-
end
|
35
|
-
|
36
|
-
# @see Helper::Collection
|
37
|
-
def elements
|
38
|
-
@pitches
|
39
|
-
end
|
40
|
-
|
41
|
-
# Convert to an Array of pitches.
|
42
|
-
# @note this returns a mutable copy the underlying @pitches attribute, which is otherwise unmutable
|
43
|
-
alias :to_pitches :to_a
|
44
|
-
|
45
|
-
def self.from_a enumerable
|
46
|
-
new enumerable
|
47
|
-
end
|
48
|
-
|
49
|
-
def to_pitch_class_set(remove_duplicates=true)
|
50
|
-
PitchClassSet.new(remove_duplicates ? pitch_classes.uniq : pitch_classes)
|
51
|
-
end
|
52
|
-
|
53
|
-
def pitch_classes
|
54
|
-
@pitch_classes ||= @pitches.map{|p| p.pitch_class }
|
55
|
-
end
|
56
|
-
|
57
|
-
# @param other [#pitches, Enumerable]
|
58
|
-
def == other
|
59
|
-
if other.respond_to? :pitches
|
60
|
-
@pitches == other.pitches
|
61
|
-
elsif other.is_a? Enumerable
|
62
|
-
@pitches == other.to_a
|
63
|
-
else
|
64
|
-
@pitches == other
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
# Compare for equality, ignoring order and duplicates
|
69
|
-
# @param other [#pitches, Array, #to_a]
|
70
|
-
def =~ other
|
71
|
-
@normalized_pitches ||= @pitches.uniq.sort
|
72
|
-
@normalized_pitches == case
|
73
|
-
when other.respond_to?(:pitches) then other.pitches.uniq.sort
|
74
|
-
when (other.is_a? Array and other.frozen?) then other
|
75
|
-
when other.respond_to?(:to_a) then other.to_a.uniq.sort
|
76
|
-
else other
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
def to_s
|
81
|
-
'[' + @pitches.map{|pitch| pitch.to_s}.join(', ') + ']'
|
82
|
-
end
|
83
|
-
|
84
|
-
end
|
85
|
-
|
86
|
-
# Construct an ordered {Melody} that allows duplicates
|
87
|
-
# @see #Melody
|
88
|
-
# @see #Chord
|
89
|
-
def Melody(*anything)
|
90
|
-
Melody.new Helpers::Convert.to_pitches(*anything)
|
91
|
-
end
|
92
|
-
module_function :Melody
|
93
|
-
|
94
|
-
end
|
data/lib/mtk/pitch.rb
DELETED
@@ -1,152 +0,0 @@
|
|
1
|
-
module MTK
|
2
|
-
|
3
|
-
# A frequency represented by a {PitchClass}, an integer octave, and an offset in semitones.
|
4
|
-
class Pitch
|
5
|
-
|
6
|
-
include Comparable
|
7
|
-
|
8
|
-
attr_reader :pitch_class, :octave, :offset
|
9
|
-
|
10
|
-
def initialize( pitch_class, octave, offset=0 )
|
11
|
-
@pitch_class, @octave, @offset = pitch_class, octave, offset
|
12
|
-
@value = @pitch_class.to_i + 12*(@octave+1) + @offset
|
13
|
-
end
|
14
|
-
|
15
|
-
@flyweight = {}
|
16
|
-
|
17
|
-
# Return a pitch with no offset, only constructing a new instance when not already in the flyweight cache
|
18
|
-
def self.[](pitch_class, octave)
|
19
|
-
pitch_class = MTK.PitchClass(pitch_class)
|
20
|
-
@flyweight[[pitch_class,octave]] ||= new(pitch_class, octave)
|
21
|
-
end
|
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')
|
29
|
-
def self.from_s( name )
|
30
|
-
s = name.to_s
|
31
|
-
s = s[0..0].upcase + s[1..-1].downcase # normalize name
|
32
|
-
if s =~ /^([A-G](#|##|b|bb)?)(-?\d+)(\+(\d+(\.\d+)?)cents)?$/
|
33
|
-
pitch_class = PitchClass.from_s($1)
|
34
|
-
if pitch_class
|
35
|
-
octave = $3.to_i
|
36
|
-
offset_in_cents = $5.to_f
|
37
|
-
if offset_in_cents.nil? or offset_in_cents.zero?
|
38
|
-
return self[pitch_class, octave]
|
39
|
-
else
|
40
|
-
return new( pitch_class, octave, offset_in_cents/100.0 )
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
raise ArgumentError.new("Invalid pitch name: #{name.inspect}")
|
45
|
-
end
|
46
|
-
|
47
|
-
class << self
|
48
|
-
alias :from_name :from_s
|
49
|
-
end
|
50
|
-
|
51
|
-
# Convert a Numeric semitones value into a Pitch
|
52
|
-
def self.from_f( f )
|
53
|
-
i, offset = f.floor, f%1 # split into int and fractional part
|
54
|
-
pitch_class = PitchClass.from_i(i)
|
55
|
-
octave = i/12 - 1
|
56
|
-
if offset == 0
|
57
|
-
self[pitch_class, octave]
|
58
|
-
else
|
59
|
-
new( pitch_class, octave, offset )
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
def self.from_hash(hash)
|
64
|
-
new hash[:pitch_class], hash[:octave], hash.fetch(:offset,0)
|
65
|
-
end
|
66
|
-
|
67
|
-
# Convert a Numeric semitones value into a Pitch
|
68
|
-
def self.from_i( i )
|
69
|
-
from_f( i )
|
70
|
-
end
|
71
|
-
|
72
|
-
# The numerical value of this pitch
|
73
|
-
def to_f
|
74
|
-
@value
|
75
|
-
end
|
76
|
-
|
77
|
-
# The numerical value for the nearest semitone
|
78
|
-
def to_i
|
79
|
-
@value.round
|
80
|
-
end
|
81
|
-
|
82
|
-
def offset_in_cents
|
83
|
-
@offset * 100
|
84
|
-
end
|
85
|
-
|
86
|
-
def to_hash
|
87
|
-
{:pitch_class => @pitch_class, :octave => @octave, :offset => @offset}
|
88
|
-
end
|
89
|
-
|
90
|
-
def to_s
|
91
|
-
"#{@pitch_class}#{@octave}" + (@offset.zero? ? '' : "+#{offset_in_cents.round}cents")
|
92
|
-
end
|
93
|
-
|
94
|
-
def inspect
|
95
|
-
"#<#{self.class}:#{object_id} @value=#{@value}>"
|
96
|
-
end
|
97
|
-
|
98
|
-
def ==( other )
|
99
|
-
other.respond_to? :pitch_class and other.respond_to? :octave and other.respond_to? :offset and
|
100
|
-
other.pitch_class == @pitch_class and other.octave == @octave and other.offset == @offset
|
101
|
-
end
|
102
|
-
|
103
|
-
def <=> other
|
104
|
-
@value <=> other.to_f
|
105
|
-
end
|
106
|
-
|
107
|
-
def + interval_in_semitones
|
108
|
-
self.class.from_f( @value + interval_in_semitones.to_f )
|
109
|
-
end
|
110
|
-
alias transpose +
|
111
|
-
|
112
|
-
def - interval_in_semitones
|
113
|
-
self.class.from_f( @value - interval_in_semitones.to_f )
|
114
|
-
end
|
115
|
-
|
116
|
-
def invert(center_pitch)
|
117
|
-
self + 2*(center_pitch.to_f - to_f)
|
118
|
-
end
|
119
|
-
|
120
|
-
def nearest(pitch_class)
|
121
|
-
self + self.pitch_class.distance_to(pitch_class)
|
122
|
-
end
|
123
|
-
|
124
|
-
def coerce(other)
|
125
|
-
return self.class.from_f(other.to_f), self
|
126
|
-
end
|
127
|
-
|
128
|
-
def clone_with(hash)
|
129
|
-
self.class.from_hash(to_hash.merge hash)
|
130
|
-
end
|
131
|
-
|
132
|
-
end
|
133
|
-
|
134
|
-
# Construct a {Pitch} from any supported type
|
135
|
-
def Pitch(*anything)
|
136
|
-
anything = anything.first if anything.length == 1
|
137
|
-
case anything
|
138
|
-
when Numeric then Pitch.from_f(anything)
|
139
|
-
when String, Symbol then Pitch.from_s(anything)
|
140
|
-
when Pitch then anything
|
141
|
-
when Array
|
142
|
-
if anything.length == 2
|
143
|
-
Pitch[*anything]
|
144
|
-
else
|
145
|
-
Pitch.new(*anything)
|
146
|
-
end
|
147
|
-
else raise ArgumentError.new("Pitch doesn't understand #{anything.class}")
|
148
|
-
end
|
149
|
-
end
|
150
|
-
module_function :Pitch
|
151
|
-
|
152
|
-
end
|