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,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
|
data/lib/mtk/events/event.rb
CHANGED
@@ -29,7 +29,7 @@ module MTK
|
|
29
29
|
|
30
30
|
def duration= duration
|
31
31
|
@duration = duration
|
32
|
-
@duration = ::MTK::Duration[@duration || 0] unless @duration.is_a? ::MTK::Duration
|
32
|
+
@duration = ::MTK::Core::Duration[@duration || 0] unless @duration.is_a? ::MTK::Core::Duration
|
33
33
|
@duration
|
34
34
|
end
|
35
35
|
|
@@ -42,15 +42,15 @@ module MTK
|
|
42
42
|
@value = options[:value]
|
43
43
|
@number = options[:number]
|
44
44
|
@duration = options.fetch(:duration, 0)
|
45
|
-
@duration = ::MTK::Duration[@duration] unless @duration.is_a? ::MTK::Duration
|
45
|
+
@duration = ::MTK::Core::Duration[@duration] unless @duration.is_a? ::MTK::Core::Duration
|
46
46
|
@channel = options[:channel]
|
47
47
|
end
|
48
48
|
|
49
|
-
def self.
|
49
|
+
def self.from_h(hash)
|
50
50
|
new(hash[:type], hash)
|
51
51
|
end
|
52
52
|
|
53
|
-
def
|
53
|
+
def to_h
|
54
54
|
hash = {:type => @type}
|
55
55
|
hash[:value] = @value unless @value.nil?
|
56
56
|
hash[:duration] = @duration unless @duration.nil?
|
data/lib/mtk/events/note.rb
CHANGED
@@ -5,8 +5,8 @@ module MTK
|
|
5
5
|
# A musical {Event} defined by a {Pitch}, intensity, and duration
|
6
6
|
class Note < Event
|
7
7
|
|
8
|
-
DEFAULT_DURATION = MTK::Duration[1]
|
9
|
-
DEFAULT_INTENSITY = MTK::Intensity[0.75]
|
8
|
+
DEFAULT_DURATION = MTK::Core::Duration[1]
|
9
|
+
DEFAULT_INTENSITY = MTK::Core::Intensity[0.75]
|
10
10
|
|
11
11
|
# Frequency of the note as a {Pitch}.
|
12
12
|
alias :pitch :number
|
@@ -24,16 +24,16 @@ module MTK
|
|
24
24
|
super :note, number:pitch, duration:duration, value:intensity, channel:channel
|
25
25
|
end
|
26
26
|
|
27
|
-
def self.
|
27
|
+
def self.from_h(hash)
|
28
28
|
new(hash[:pitch]||hash[:number], hash[:duration], hash[:intensity]||hash[:value], hash[:channel])
|
29
29
|
end
|
30
30
|
|
31
|
-
def
|
31
|
+
def to_h
|
32
32
|
super.merge({ pitch: @number, intensity: @value })
|
33
33
|
end
|
34
34
|
|
35
35
|
def self.from_midi(pitch, velocity, duration_in_beats, channel=0)
|
36
|
-
new( MTK::
|
36
|
+
new( MTK::Lang::Pitches::PITCHES[pitch.to_i], MTK::Core::Duration[duration_in_beats], MTK::Core::Intensity[velocity/127.0], channel )
|
37
37
|
end
|
38
38
|
|
39
39
|
def midi_pitch
|
@@ -74,7 +74,7 @@ module MTK
|
|
74
74
|
case anything
|
75
75
|
when MTK::Events::Note then anything
|
76
76
|
|
77
|
-
when MTK::Pitch then MTK::Events::Note.new(anything)
|
77
|
+
when MTK::Core::Pitch then MTK::Events::Note.new(anything)
|
78
78
|
|
79
79
|
when Array
|
80
80
|
pitch = nil
|
@@ -84,18 +84,18 @@ module MTK
|
|
84
84
|
unknowns = []
|
85
85
|
anything.each do |item|
|
86
86
|
case item
|
87
|
-
when MTK::Pitch then pitch = item
|
88
|
-
when MTK::Duration then duration = item
|
89
|
-
when MTK::Intensity then intensity = item
|
87
|
+
when MTK::Core::Pitch then pitch = item
|
88
|
+
when MTK::Core::Duration then duration = item
|
89
|
+
when MTK::Core::Intensity then intensity = item
|
90
90
|
else unknowns << item
|
91
91
|
end
|
92
92
|
end
|
93
93
|
|
94
|
-
pitch = MTK
|
94
|
+
pitch = MTK.Pitch(unknowns.shift) if pitch.nil? and not unknowns.empty?
|
95
95
|
raise "MTK::Note() couldn't find a pitch in arguments: #{anything.inspect}" if pitch.nil?
|
96
96
|
|
97
|
-
duration = MTK
|
98
|
-
intensity = MTK
|
97
|
+
duration = MTK.Duration(unknowns.shift) if duration.nil? and not unknowns.empty?
|
98
|
+
intensity = MTK.Intensity(unknowns.shift) if intensity.nil? and not unknowns.empty?
|
99
99
|
channel = unknowns.shift.to_i if channel.nil? and not unknowns.empty?
|
100
100
|
|
101
101
|
duration ||= MTK::Events::Note::DEFAULT_DURATION
|
@@ -0,0 +1,232 @@
|
|
1
|
+
module MTK
|
2
|
+
module Events
|
3
|
+
|
4
|
+
# A collection of timed events. The core data structure used to interface with input and output.
|
5
|
+
#
|
6
|
+
# Maps sorted floating point times to lists of events.
|
7
|
+
#
|
8
|
+
# Enumerable as [time,event_list] pairs.
|
9
|
+
#
|
10
|
+
class Timeline
|
11
|
+
include Enumerable
|
12
|
+
|
13
|
+
def initialize()
|
14
|
+
@timeline = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def from_a(enumerable)
|
19
|
+
new.merge enumerable
|
20
|
+
end
|
21
|
+
alias from_h from_a
|
22
|
+
end
|
23
|
+
|
24
|
+
def merge enumerable
|
25
|
+
enumerable.each do |time,events|
|
26
|
+
add(time,events)
|
27
|
+
end
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def clear
|
32
|
+
@timeline.clear
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_h
|
37
|
+
@timeline
|
38
|
+
end
|
39
|
+
|
40
|
+
def == other
|
41
|
+
other = other.to_h unless other.is_a? Hash
|
42
|
+
@timeline == other
|
43
|
+
end
|
44
|
+
|
45
|
+
def [](time)
|
46
|
+
@timeline[time.to_f]
|
47
|
+
end
|
48
|
+
|
49
|
+
def []=(time, events)
|
50
|
+
time = time.to_f unless time.is_a? Numeric
|
51
|
+
case events
|
52
|
+
when nil?
|
53
|
+
@timeline.delete time.to_f
|
54
|
+
when Array
|
55
|
+
@timeline[time.to_f] = events
|
56
|
+
else
|
57
|
+
@timeline[time.to_f] = [events]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def add(time, event)
|
62
|
+
events = @timeline[time.to_f]
|
63
|
+
if events
|
64
|
+
if event.is_a? Array
|
65
|
+
events.concat event
|
66
|
+
else
|
67
|
+
events << event
|
68
|
+
end
|
69
|
+
else
|
70
|
+
self[time] = event
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def delete(time)
|
75
|
+
@timeline.delete(time.to_f)
|
76
|
+
end
|
77
|
+
|
78
|
+
def has_time? time
|
79
|
+
@timeline.has_key? time.to_f
|
80
|
+
end
|
81
|
+
|
82
|
+
def times
|
83
|
+
@timeline.keys.sort
|
84
|
+
end
|
85
|
+
|
86
|
+
def length
|
87
|
+
last_time = times.last
|
88
|
+
events = @timeline[last_time]
|
89
|
+
last_time + events.map{|event| event.duration }.max
|
90
|
+
end
|
91
|
+
|
92
|
+
def empty?
|
93
|
+
@timeline.empty?
|
94
|
+
end
|
95
|
+
|
96
|
+
def events
|
97
|
+
times.map{|t| @timeline[t] }.flatten
|
98
|
+
end
|
99
|
+
|
100
|
+
def each
|
101
|
+
# this is similar to @timeline.each, but by iterating over #times, we yield the events in chronological order
|
102
|
+
times.each do |time|
|
103
|
+
yield time, @timeline[time]
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# the original Enumerable#map implementation, which returns an Array
|
108
|
+
alias enumerable_map map
|
109
|
+
|
110
|
+
# Constructs a new Timeline by mapping each [time,event_list] pair
|
111
|
+
# @see #map!
|
112
|
+
def map &block
|
113
|
+
self.class.from_a enumerable_map(&block)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Perform #map in place
|
117
|
+
# @see #map
|
118
|
+
def map! &block
|
119
|
+
mapped = enumerable_map(&block)
|
120
|
+
clear
|
121
|
+
merge mapped
|
122
|
+
end
|
123
|
+
|
124
|
+
# Map every individual event, without regard for the time at which is occurs
|
125
|
+
def map_events
|
126
|
+
mapped_timeline = Timeline.new
|
127
|
+
self.each do |time,events|
|
128
|
+
mapped_timeline[time] = events.map{|event| yield event }
|
129
|
+
end
|
130
|
+
mapped_timeline
|
131
|
+
end
|
132
|
+
|
133
|
+
# Map every individual event in place, without regard for the time at which is occurs
|
134
|
+
def map_events!
|
135
|
+
each do |time,events|
|
136
|
+
self[time] = events.map{|event| yield event }
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def clone
|
141
|
+
self.class.from_h(to_h)
|
142
|
+
end
|
143
|
+
|
144
|
+
def compact!
|
145
|
+
@timeline.delete_if {|t,events| events.empty? }
|
146
|
+
end
|
147
|
+
|
148
|
+
def flatten
|
149
|
+
flattened = Timeline.new
|
150
|
+
self.each do |time,events|
|
151
|
+
events.each do |event|
|
152
|
+
if event.is_a? Timeline
|
153
|
+
event.flatten.each do |subtime,subevent|
|
154
|
+
flattened.add(time+subtime, subevent)
|
155
|
+
end
|
156
|
+
else
|
157
|
+
flattened.add(time,event)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
flattened
|
162
|
+
end
|
163
|
+
|
164
|
+
# @return a new Timeline where all times have been quantized to multiples of the given interval
|
165
|
+
# @example timeline.quantize(0.5) # quantize to eight notes (assuming the beat is a quarter note)
|
166
|
+
# @see quantize!
|
167
|
+
def quantize interval
|
168
|
+
map{|time,events| [self.class.quantize_time(time,interval), events] }
|
169
|
+
end
|
170
|
+
|
171
|
+
def quantize! interval
|
172
|
+
map!{|time,events| [self.class.quantize_time(time,interval), events] }
|
173
|
+
end
|
174
|
+
|
175
|
+
# shifts all times by the given amount
|
176
|
+
# @see #shift!
|
177
|
+
# @see #shift_to
|
178
|
+
def shift time_delta
|
179
|
+
map{|time,events| [time+time_delta, events] }
|
180
|
+
end
|
181
|
+
|
182
|
+
# shifts all times in place by the given amount
|
183
|
+
# @see #shift
|
184
|
+
# @see #shift_to!
|
185
|
+
def shift! time_delta
|
186
|
+
map!{|time,events| [time+time_delta, events] }
|
187
|
+
end
|
188
|
+
|
189
|
+
# shifts the times so that the start of the timeline is at the given time
|
190
|
+
# @see #shift_to!
|
191
|
+
# @see #shift
|
192
|
+
def shift_to absolute_time
|
193
|
+
start = times.first
|
194
|
+
if start
|
195
|
+
shift absolute_time - start
|
196
|
+
else
|
197
|
+
clone
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# shifts the times in place so that the start of the timeline is at the given time
|
202
|
+
# @see #shift_to
|
203
|
+
# @see #shift!
|
204
|
+
def shift_to! absolute_time
|
205
|
+
start = times.first
|
206
|
+
if start
|
207
|
+
shift! absolute_time - start
|
208
|
+
end
|
209
|
+
self
|
210
|
+
end
|
211
|
+
|
212
|
+
def to_s
|
213
|
+
times = self.times
|
214
|
+
last = times.last
|
215
|
+
width = sprintf("%d",last).length + 3 # nicely align the '=>' against the longest number
|
216
|
+
times.map{|t| sprintf("%#{width}.2f",t)+" => #{@timeline[t].join ', '}" }.join "\n"
|
217
|
+
end
|
218
|
+
|
219
|
+
def inspect
|
220
|
+
@timeline.inspect
|
221
|
+
end
|
222
|
+
|
223
|
+
def self.quantize_time time, interval
|
224
|
+
upper = interval * (time.to_f/interval).ceil
|
225
|
+
lower = upper - interval
|
226
|
+
(time - lower) < (upper - time) ? lower : upper
|
227
|
+
end
|
228
|
+
|
229
|
+
end
|
230
|
+
|
231
|
+
end
|
232
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module MTK
|
2
|
+
module Groups
|
3
|
+
|
4
|
+
# A sorted collection of distinct {Pitch}es.
|
5
|
+
#
|
6
|
+
# The "vertical" (simultaneous) pitch collection.
|
7
|
+
#
|
8
|
+
# @see Melody
|
9
|
+
# @see Groups::PitchClassSet
|
10
|
+
#
|
11
|
+
class Chord < Melody
|
12
|
+
|
13
|
+
# @param pitches [#to_a] the collection of pitches
|
14
|
+
# @note duplicate pitches will be removed. See #{Melody} if you want to maintain duplicates.
|
15
|
+
#
|
16
|
+
def initialize(pitches)
|
17
|
+
pitches = pitches.to_a.clone
|
18
|
+
pitches.uniq!
|
19
|
+
pitches.sort!
|
20
|
+
@pitches = pitches.freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
# Generate a chord inversion (positive numbers move the lowest notes up an octave, negative moves the highest notes down)
|
24
|
+
def inversion(number)
|
25
|
+
number = number.to_i
|
26
|
+
pitch_set = Array.new(@pitches.uniq.sort)
|
27
|
+
if number > 0
|
28
|
+
number.times do |count|
|
29
|
+
index = count % pitch_set.length
|
30
|
+
pitch_set[index] += 12
|
31
|
+
end
|
32
|
+
else
|
33
|
+
number.abs.times do |count|
|
34
|
+
index = -(count + 1) % pitch_set.length # count from -1 downward to go backwards through the list starting at the end
|
35
|
+
pitch_set[index] -= 12
|
36
|
+
end
|
37
|
+
end
|
38
|
+
self.class.new pitch_set.sort
|
39
|
+
end
|
40
|
+
|
41
|
+
# Transpose the chord so that it's lowest pitch is the given pitch class.
|
42
|
+
def nearest(pitch_class)
|
43
|
+
self.transpose @pitches.first.pitch_class.distance_to(pitch_class)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Construct an ordered {MTK::Groups::Chord} with no duplicates.
|
49
|
+
# @see #MTK::Groups::Chord
|
50
|
+
# @see #MTK::Groups::Melody
|
51
|
+
def Chord(*anything)
|
52
|
+
MTK::Groups::Chord.new MTK::Groups.to_pitches(*anything)
|
53
|
+
end
|
54
|
+
module_function :Chord
|
55
|
+
|
56
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module MTK
|
2
|
-
module
|
2
|
+
module Groups
|
3
3
|
|
4
4
|
# Given a method #elements, which returns an Array of elements in the collection,
|
5
5
|
# including this module will make the class Enumerable and provide various methods you'd expect from an Array.
|
@@ -160,5 +160,37 @@ module MTK
|
|
160
160
|
end
|
161
161
|
|
162
162
|
end
|
163
|
+
|
164
|
+
|
165
|
+
######################################################################
|
166
|
+
# MTK::Groups
|
167
|
+
|
168
|
+
def to_pitch_classes(*anything)
|
169
|
+
anything = anything.first if anything.length == 1
|
170
|
+
if anything.respond_to? :to_pitch_classes
|
171
|
+
anything.to_pitch_classes
|
172
|
+
else
|
173
|
+
case anything
|
174
|
+
when ::Enumerable then anything.map{|item| MTK.PitchClass(item) }
|
175
|
+
else [MTK.PitchClass(anything)]
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
module_function :to_pitch_classes
|
180
|
+
|
181
|
+
|
182
|
+
def to_pitches(*anything)
|
183
|
+
anything = anything.first if anything.length == 1
|
184
|
+
if anything.respond_to? :to_pitches
|
185
|
+
anything.to_pitches
|
186
|
+
else
|
187
|
+
case anything
|
188
|
+
when ::Enumerable then anything.map{|item| MTK.Pitch(item) }
|
189
|
+
else [MTK.Pitch(anything)]
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
module_function :to_pitches
|
194
|
+
|
163
195
|
end
|
164
196
|
end
|