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
@@ -0,0 +1,213 @@
|
|
1
|
+
module MTK
|
2
|
+
module Core
|
3
|
+
|
4
|
+
# A measure of time in musical beats.
|
5
|
+
# May be negative to indicate a rest, which uses the absolute value for the effective duration.
|
6
|
+
class Duration
|
7
|
+
|
8
|
+
include Comparable
|
9
|
+
|
10
|
+
# The names of the base durations. See {MTK::Lang::Durations} for more info.
|
11
|
+
NAMES = %w[w h q i s r x].freeze
|
12
|
+
|
13
|
+
VALUES_BY_NAME = {
|
14
|
+
'w' => 4,
|
15
|
+
'h' => 2,
|
16
|
+
'q' => 1,
|
17
|
+
'i' => Rational(1,2),
|
18
|
+
's' => Rational(1,4),
|
19
|
+
'r' => Rational(1,8),
|
20
|
+
'x' => Rational(1,16)
|
21
|
+
}
|
22
|
+
|
23
|
+
@flyweight = {}
|
24
|
+
|
25
|
+
# The number of beats, typically represented as a Rational
|
26
|
+
attr_reader :value
|
27
|
+
|
28
|
+
def initialize( length_in_beats )
|
29
|
+
@value = length_in_beats
|
30
|
+
end
|
31
|
+
|
32
|
+
# Return a duration, only constructing a new instance when not already in the flyweight cache
|
33
|
+
def self.[](length_in_beats)
|
34
|
+
if length_in_beats.is_a? Fixnum
|
35
|
+
value = length_in_beats
|
36
|
+
else
|
37
|
+
value = Rational(length_in_beats)
|
38
|
+
end
|
39
|
+
@flyweight[value] ||= new(value)
|
40
|
+
end
|
41
|
+
|
42
|
+
class << self
|
43
|
+
alias :from_f :[]
|
44
|
+
alias :from_i :[]
|
45
|
+
end
|
46
|
+
|
47
|
+
# Lookup a duration by name.
|
48
|
+
# This method supports appending any combination of '.' and 't' for more fine-grained values.
|
49
|
+
# each '.' multiplies by 3/2, and each 't' multiplies by 2/3.
|
50
|
+
# You may use the prefix '-' to negate the duration (which turns it into a rest of the same length).
|
51
|
+
# You may also prefix (after the '-' if present) the base duration name with an integer, float, or rational number
|
52
|
+
# to multiply the base duration value. Rationals are in the form "#!{numerator_integer}/#!{denominator_integer}".
|
53
|
+
# @example lookup value of 'q.', which is 1.5 times a quarter note (1.5 beats):
|
54
|
+
# MTK::Core::Duration.from_s('q.')
|
55
|
+
# @example lookup the value of 3/4w, which three-quarters of a whole note (3 beats):
|
56
|
+
# MTK::Core::Duration.from_s('3/4w')
|
57
|
+
def self.from_s(s)
|
58
|
+
if s =~ /^(-)?(\d+([\.\/]\d+)?)?([whqisrx])((\.|t)*)$/i
|
59
|
+
name = $4.downcase
|
60
|
+
modifier = $5.downcase
|
61
|
+
modifier << $1 if $1 # negation
|
62
|
+
multiplier = $2
|
63
|
+
else
|
64
|
+
raise ArgumentError.new("Invalid Duration string '#{s}'")
|
65
|
+
end
|
66
|
+
|
67
|
+
value = VALUES_BY_NAME[name]
|
68
|
+
modifier.each_char do |mod|
|
69
|
+
case mod
|
70
|
+
when '-' then value *= -1
|
71
|
+
when '.' then value *= Rational(3,2)
|
72
|
+
when 't' then value *= Rational(2,3)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
if multiplier
|
77
|
+
case multiplier
|
78
|
+
when /\./
|
79
|
+
value *= multiplier.to_f
|
80
|
+
when /\//
|
81
|
+
numerator, denominator = multiplier.split('/')
|
82
|
+
value *= Rational(numerator.to_i, denominator.to_i)
|
83
|
+
else
|
84
|
+
value *= multiplier.to_i
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
self[value]
|
89
|
+
end
|
90
|
+
|
91
|
+
class << self
|
92
|
+
alias :from_name :from_s
|
93
|
+
end
|
94
|
+
|
95
|
+
# The magnitude (absolute value) of the duration.
|
96
|
+
# This is the actual duration for rests.
|
97
|
+
# @see #rest?
|
98
|
+
def length
|
99
|
+
@value < 0 ? -@value : @value
|
100
|
+
end
|
101
|
+
|
102
|
+
# Durations with negative values are rests.
|
103
|
+
# @see #length
|
104
|
+
# @see #-@
|
105
|
+
def rest?
|
106
|
+
@value < 0
|
107
|
+
end
|
108
|
+
|
109
|
+
# The number of beats as a floating point number
|
110
|
+
def to_f
|
111
|
+
@value.to_f
|
112
|
+
end
|
113
|
+
|
114
|
+
# The numerical value for the nearest whole number of beats
|
115
|
+
def to_i
|
116
|
+
@value.round
|
117
|
+
end
|
118
|
+
|
119
|
+
def to_s
|
120
|
+
value = @value.to_s
|
121
|
+
value = sprintf '%.2f', @value if value.length > 6 # threshold is 6 for no particular reason...
|
122
|
+
"#{value} #{@value.abs > 1 || @value==0 ? 'beats' : 'beat'}"
|
123
|
+
end
|
124
|
+
|
125
|
+
def inspect
|
126
|
+
"#<#{self.class}:#{object_id} @value=#{@value}>"
|
127
|
+
end
|
128
|
+
|
129
|
+
def ==( other )
|
130
|
+
if other.is_a? MTK::Core::Duration
|
131
|
+
other.value == @value
|
132
|
+
else
|
133
|
+
other == @value
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def <=> other
|
138
|
+
if other.respond_to? :value
|
139
|
+
@value <=> other.value
|
140
|
+
else
|
141
|
+
@value <=> other
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Add this duration to another.
|
146
|
+
# @return a new Duration that has a value of the sum of the arguments.
|
147
|
+
def + duration
|
148
|
+
if duration.is_a? MTK::Core::Duration
|
149
|
+
MTK::Core::Duration[@value + duration.value]
|
150
|
+
else
|
151
|
+
MTK::Core::Duration[@value + duration]
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Subtract another duration from this one.
|
156
|
+
# @return a new Duration that has a value of the difference of the arguments.
|
157
|
+
def - duration
|
158
|
+
if duration.is_a? MTK::Core::Duration
|
159
|
+
MTK::Core::Duration[@value - duration.value]
|
160
|
+
else
|
161
|
+
MTK::Core::Duration[@value - duration]
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Multiply this duration with another.
|
166
|
+
# @return a new Duration that has a value of the product of the arguments.
|
167
|
+
def * duration
|
168
|
+
if duration.is_a? MTK::Core::Duration
|
169
|
+
MTK::Core::Duration[@value * duration.value]
|
170
|
+
else
|
171
|
+
MTK::Core::Duration[@value * duration]
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Divide this duration with another.
|
176
|
+
# @return a new Duration that has a value of the division of the arguments.
|
177
|
+
def / duration
|
178
|
+
if duration.is_a? MTK::Core::Duration
|
179
|
+
MTK::Core::Duration[to_f / duration.value]
|
180
|
+
else
|
181
|
+
MTK::Core::Duration[to_f / duration]
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Negate the duration value.
|
186
|
+
# Turns normal durations into rests and vice versa.
|
187
|
+
# @return a new Duration that has a negated value.
|
188
|
+
# @see #rest?
|
189
|
+
def -@
|
190
|
+
MTK::Core::Duration[@value * -1]
|
191
|
+
end
|
192
|
+
|
193
|
+
# Allow basic math operations with Numeric objects.
|
194
|
+
def coerce(other)
|
195
|
+
return MTK::Core::Duration[other], self
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Construct a {Duration} from any supported type
|
202
|
+
def Duration(*anything)
|
203
|
+
anything = anything.first if anything.length == 1
|
204
|
+
case anything
|
205
|
+
when Numeric then MTK::Core::Duration[anything]
|
206
|
+
when String, Symbol then MTK::Core::Duration.from_s(anything)
|
207
|
+
when Duration then anything
|
208
|
+
else raise "Duration doesn't understand #{anything.class}"
|
209
|
+
end
|
210
|
+
end
|
211
|
+
module_function :Duration
|
212
|
+
|
213
|
+
end
|
@@ -0,0 +1,158 @@
|
|
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 Intensity
|
6
|
+
|
7
|
+
include Comparable
|
8
|
+
|
9
|
+
# The names of the base intensities. See {}MTK::Lang::Intensities} for more info.
|
10
|
+
NAMES = %w[ppp pp p mp mf o ff fff].freeze
|
11
|
+
|
12
|
+
VALUES_BY_NAME = {
|
13
|
+
'ppp' => 0.125,
|
14
|
+
'pp' => 0.25,
|
15
|
+
'p' => 0.375,
|
16
|
+
'mp' => 0.5,
|
17
|
+
'mf' => 0.625,
|
18
|
+
'o' => 0.75,
|
19
|
+
'ff' => 0.875,
|
20
|
+
'fff' => 1.0
|
21
|
+
}
|
22
|
+
|
23
|
+
@flyweight = {}
|
24
|
+
|
25
|
+
# The number of beats, typically representation as a Rational
|
26
|
+
attr_reader :value
|
27
|
+
|
28
|
+
def initialize(value)
|
29
|
+
@value = value
|
30
|
+
end
|
31
|
+
|
32
|
+
# Return an Intensity, only constructing a new instance when not already in the flyweight cache
|
33
|
+
def self.[](value)
|
34
|
+
value = value.to_f
|
35
|
+
@flyweight[value] ||= new(value)
|
36
|
+
end
|
37
|
+
|
38
|
+
class << self
|
39
|
+
alias :from_f :[]
|
40
|
+
alias :from_i :[]
|
41
|
+
end
|
42
|
+
|
43
|
+
# Lookup an intensity by name.
|
44
|
+
# This method supports appending '-' or '+' for more fine-grained values.
|
45
|
+
def self.from_s(s)
|
46
|
+
return self[1.0] if s == 'fff+' # special case because "fff" is already the maximum
|
47
|
+
|
48
|
+
name = nil
|
49
|
+
modifier = nil
|
50
|
+
if s =~ /^(\w+)([+-])?$/
|
51
|
+
name = $1
|
52
|
+
modifier = $2
|
53
|
+
end
|
54
|
+
|
55
|
+
value = VALUES_BY_NAME[name]
|
56
|
+
raise ArgumentError.new("Invalid Intensity string '#{s}'") unless value
|
57
|
+
|
58
|
+
value += 1.0/24 if modifier == '+'
|
59
|
+
value -= 1.0/24 if modifier == '-'
|
60
|
+
|
61
|
+
self[value]
|
62
|
+
end
|
63
|
+
|
64
|
+
class << self
|
65
|
+
alias :from_name :from_s
|
66
|
+
end
|
67
|
+
|
68
|
+
# The number of beats as a floating point number
|
69
|
+
def to_f
|
70
|
+
@value.to_f
|
71
|
+
end
|
72
|
+
|
73
|
+
# The numerical value for the nearest whole number of beats
|
74
|
+
def to_i
|
75
|
+
@value.round
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_midi
|
79
|
+
(to_f * 127).round
|
80
|
+
end
|
81
|
+
|
82
|
+
def to_percent
|
83
|
+
(@value * 100).round
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_s
|
87
|
+
"#{to_percent}% intensity"
|
88
|
+
end
|
89
|
+
|
90
|
+
def inspect
|
91
|
+
"#<#{self.class}:#{object_id} @value=#{@value}>"
|
92
|
+
end
|
93
|
+
|
94
|
+
def ==( other )
|
95
|
+
other.is_a? MTK::Core::Intensity and other.value == @value
|
96
|
+
end
|
97
|
+
|
98
|
+
def <=> other
|
99
|
+
if other.respond_to? :value
|
100
|
+
@value <=> other.value
|
101
|
+
else
|
102
|
+
@value <=> other
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
def + intensity
|
108
|
+
if intensity.is_a? MTK::Core::Intensity
|
109
|
+
MTK::Core::Intensity[@value + intensity.value]
|
110
|
+
else
|
111
|
+
MTK::Core::Intensity[@value + intensity]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def -intensity
|
116
|
+
if intensity.is_a? MTK::Core::Intensity
|
117
|
+
MTK::Core::Intensity[@value - intensity.value]
|
118
|
+
else
|
119
|
+
MTK::Core::Intensity[@value - intensity]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def * intensity
|
124
|
+
if intensity.is_a? MTK::Core::Intensity
|
125
|
+
MTK::Core::Intensity[@value * intensity.value]
|
126
|
+
else
|
127
|
+
MTK::Core::Intensity[@value * intensity]
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def / intensity
|
132
|
+
if intensity.is_a? MTK::Core::Intensity
|
133
|
+
MTK::Core::Intensity[to_f / intensity.value]
|
134
|
+
else
|
135
|
+
MTK::Core::Intensity[to_f / intensity]
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def coerce(other)
|
140
|
+
return MTK::Core::Intensity[other], self
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Construct a {Duration} from any supported type
|
147
|
+
def Intensity(*anything)
|
148
|
+
anything = anything.first if anything.length == 1
|
149
|
+
case anything
|
150
|
+
when Numeric then MTK::Core::Intensity[anything]
|
151
|
+
when String, Symbol then MTK::Core::Intensity.from_s(anything)
|
152
|
+
when Intensity then anything
|
153
|
+
else raise "Intensity doesn't understand #{anything.class}"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
module_function :Intensity
|
157
|
+
|
158
|
+
end
|
@@ -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
|