mtk 0.0.3.2 → 0.0.3.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 +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
|