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/pitch_class.rb
DELETED
@@ -1,192 +0,0 @@
|
|
1
|
-
module MTK
|
2
|
-
|
3
|
-
# A set of all pitches that are an integer number of octaves apart.
|
4
|
-
# A {Pitch} has the same PitchClass as the pitches one or more octaves away.
|
5
|
-
# @see https://en.wikipedia.org/wiki/Pitch_class
|
6
|
-
#
|
7
|
-
class PitchClass
|
8
|
-
|
9
|
-
# The normalized names of the 12 pitch classes in the chromatic scale.
|
10
|
-
# The index of each {#name} is the pitch class's numeric {#value}.
|
11
|
-
NAMES = %w( C Db D Eb E F Gb G Ab A Bb B ).freeze
|
12
|
-
|
13
|
-
# All enharmonic names of the 12 pitch classes, including sharps, flats, double-sharps, and double-flats,
|
14
|
-
# organized such that each index contains the allowed names of the pitch class with a {#value} equal to that index.
|
15
|
-
# @see VALID_NAMES
|
16
|
-
VALID_NAMES_BY_VALUE =
|
17
|
-
[ # (valid names ), # value # normalized name
|
18
|
-
%w( B# C Dbb ), # 0 # C
|
19
|
-
%w( B## C# Db ), # 1 # Db
|
20
|
-
%w( C## D Ebb ), # 2 # D
|
21
|
-
%w( D# Eb Fbb ), # 3 # Eb
|
22
|
-
%w( D## E Fb ), # 4 # E
|
23
|
-
%w( E# F Gbb ), # 5 # F
|
24
|
-
%w( E## F# Gb ), # 6 # Gb
|
25
|
-
%w( F## G Abb ), # 7 # G
|
26
|
-
%w( G# Ab ), # 8 # Ab
|
27
|
-
%w( G## A Bbb ), # 9 # A
|
28
|
-
%w( A# Bb Cbb ), # 10 # Bb
|
29
|
-
%w( A## B Cb ) # 11 # B
|
30
|
-
].freeze
|
31
|
-
|
32
|
-
# All valid enharmonic pitch class names in a flat list.
|
33
|
-
# @see VALID_NAMES_BY_VALUE
|
34
|
-
VALID_NAMES = VALID_NAMES_BY_VALUE.flatten.freeze
|
35
|
-
|
36
|
-
# A mapping from valid names to the value of the pitch class with that name
|
37
|
-
VALUES_BY_NAME = Hash[ # a map from a list of name,value pairs
|
38
|
-
VALID_NAMES_BY_VALUE.map.with_index do |valid_names,value|
|
39
|
-
valid_names.map{|name| [name,value] }
|
40
|
-
end.flatten(1)
|
41
|
-
].freeze
|
42
|
-
|
43
|
-
|
44
|
-
# The name of this pitch class.
|
45
|
-
# One of the {NAMES} defined by this class.
|
46
|
-
attr_reader :name
|
47
|
-
|
48
|
-
# The value of this pitch class.
|
49
|
-
# An integer from 0..11 that indexes this pitch class in {PITCH_CLASSES} and the {#name} in {NAMES}.
|
50
|
-
attr_reader :value
|
51
|
-
|
52
|
-
|
53
|
-
private ######
|
54
|
-
# Even though new is a private_class_method, YARD gets confused so we temporarily go private
|
55
|
-
|
56
|
-
def initialize(name, value)
|
57
|
-
@name, @value = name, value
|
58
|
-
end
|
59
|
-
private_class_method :new
|
60
|
-
|
61
|
-
@flyweight = {}
|
62
|
-
|
63
|
-
public ######
|
64
|
-
|
65
|
-
|
66
|
-
# Lookup a PitchClass by name or value.
|
67
|
-
# @param name_or_value [String,Symbol,Numeric] one of {VALID_NAMES} or 0..12
|
68
|
-
# @return the PitchClass representing the argument
|
69
|
-
# @raise ArgumentError for arguments that cannot be converted to a PitchClass
|
70
|
-
def self.[] name_or_value
|
71
|
-
@flyweight[name_or_value] ||= case name_or_value
|
72
|
-
when String,Symbol then from_name(name_or_value)
|
73
|
-
when Numeric then from_value(name_or_value.round)
|
74
|
-
else raise ArgumentError.new("PitchClass.[] doesn't understand #{name_or_value.class}")
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
# Lookup a PitchClass by name.
|
79
|
-
# @param name [String,#to_s] one of {VALID_NAMES} (case-insensitive)
|
80
|
-
def self.from_name(name)
|
81
|
-
@flyweight[name] ||= (
|
82
|
-
valid_name = name.to_s.capitalize
|
83
|
-
value = VALUES_BY_NAME[valid_name] or raise ArgumentError.new("Invalid PitchClass name: #{name}")
|
84
|
-
new(valid_name,value)
|
85
|
-
)
|
86
|
-
end
|
87
|
-
|
88
|
-
class << self
|
89
|
-
alias from_s from_name
|
90
|
-
end
|
91
|
-
|
92
|
-
# All 12 pitch classes in the chromatic scale.
|
93
|
-
# The index of each pitch class is the pitch class's numeric {#value}.
|
94
|
-
PITCH_CLASSES = NAMES.map{|name| from_name name }.freeze
|
95
|
-
|
96
|
-
# return the pitch class with the given integer value mod 12
|
97
|
-
# @param value [Integer,#to_i]
|
98
|
-
def self.from_value(value)
|
99
|
-
PITCH_CLASSES[value.to_i % 12]
|
100
|
-
end
|
101
|
-
|
102
|
-
class << self
|
103
|
-
alias from_i from_value
|
104
|
-
end
|
105
|
-
|
106
|
-
# return the pitch class with the given float rounded to the nearest integer, mod 12
|
107
|
-
# @param value [Float,#to_f]
|
108
|
-
def self.from_f(value)
|
109
|
-
from_i value.to_f.round
|
110
|
-
end
|
111
|
-
|
112
|
-
# Compare 2 pitch classes for equal values.
|
113
|
-
# @param other [PitchClass]
|
114
|
-
# @return true if this pitch class's value is equal to the other pitch class's value
|
115
|
-
def == other
|
116
|
-
other.is_a? PitchClass and other.value == @value
|
117
|
-
end
|
118
|
-
|
119
|
-
# Compare a pitch class with another pitch class or integer value
|
120
|
-
# @param other [PitchClass,#to_i]
|
121
|
-
# @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
|
122
|
-
# @see http://ruby-doc.org/core-1.9.3/Comparable.html
|
123
|
-
def <=> other
|
124
|
-
@value <=> other.to_i
|
125
|
-
end
|
126
|
-
|
127
|
-
# This pitch class's normalized {#name}.
|
128
|
-
# @see NAMES
|
129
|
-
def to_s
|
130
|
-
@name.to_s
|
131
|
-
end
|
132
|
-
|
133
|
-
# This pitch class's integer {#value}
|
134
|
-
def to_i
|
135
|
-
@value.to_i
|
136
|
-
end
|
137
|
-
|
138
|
-
# This pitch class's {#value} as a floating point number
|
139
|
-
def to_f
|
140
|
-
@value.to_f
|
141
|
-
end
|
142
|
-
|
143
|
-
# Transpose this pitch class by adding it's value to the value given (mod 12)
|
144
|
-
# @param interval [PitchClass,Float,#to_f]
|
145
|
-
def + interval
|
146
|
-
new_value = (value + interval.to_f).round
|
147
|
-
self.class.from_value new_value
|
148
|
-
end
|
149
|
-
alias transpose +
|
150
|
-
|
151
|
-
# Transpose this pitch class by subtracing the given value from this value (mod 12)
|
152
|
-
# @param interval [PitchClass,Float,#to_f]
|
153
|
-
def - interval
|
154
|
-
new_value = (value - interval.to_f).round
|
155
|
-
self.class.from_value new_value
|
156
|
-
end
|
157
|
-
|
158
|
-
# Inverts (mirrors) the pitch class around the given center
|
159
|
-
# @param center [PitchClass,Pitch,Float,#to_f] the value to "mirror" this pitch class around
|
160
|
-
def invert(center)
|
161
|
-
delta = (2*(center.to_f - value)).round
|
162
|
-
self + delta
|
163
|
-
end
|
164
|
-
|
165
|
-
# the smallest interval in semitones that needs to be added to this PitchClass to reach the given PitchClass
|
166
|
-
# @param pitch_class [PitchClass,#value]
|
167
|
-
def distance_to(pitch_class)
|
168
|
-
delta = (pitch_class.value - value) % 12
|
169
|
-
if delta > 6
|
170
|
-
delta -= 12
|
171
|
-
elsif delta == 6 and to_i >= 6
|
172
|
-
# this is a special edge case to prevent endlessly ascending pitch sequences when alternating between two pitch classes a tritone apart
|
173
|
-
delta = -6
|
174
|
-
end
|
175
|
-
delta
|
176
|
-
end
|
177
|
-
|
178
|
-
end
|
179
|
-
|
180
|
-
# Construct a {PitchClass} from any supported type
|
181
|
-
# @param anything [PitchClass,String,Symbol,Numeric]
|
182
|
-
def PitchClass(anything)
|
183
|
-
case anything
|
184
|
-
when Numeric then PitchClass.from_f(anything)
|
185
|
-
when String, Symbol then PitchClass.from_s(anything)
|
186
|
-
when PitchClass then anything
|
187
|
-
else raise ArgumentError.new("PitchClass doesn't understand #{anything.class}")
|
188
|
-
end
|
189
|
-
end
|
190
|
-
module_function :PitchClass
|
191
|
-
|
192
|
-
end
|
data/lib/mtk/pitch_class_set.rb
DELETED
@@ -1,161 +0,0 @@
|
|
1
|
-
module MTK
|
2
|
-
|
3
|
-
# An ordered collection of {PitchClass}es.
|
4
|
-
#
|
5
|
-
# Unlike a mathematical Set, a PitchClassSet is ordered and may contain duplicates.
|
6
|
-
#
|
7
|
-
# @see Melody
|
8
|
-
# @see Chord
|
9
|
-
#
|
10
|
-
class PitchClassSet
|
11
|
-
include Helpers::PitchCollection
|
12
|
-
|
13
|
-
attr_reader :pitch_classes
|
14
|
-
|
15
|
-
def self.random_row
|
16
|
-
new(Constants::PitchClasses::PITCH_CLASSES.shuffle)
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.all
|
20
|
-
@all ||= new(Constants::PitchClasses::PITCH_CLASSES)
|
21
|
-
end
|
22
|
-
|
23
|
-
# @param pitch_classes [#to_a] the collection of pitch classes
|
24
|
-
#
|
25
|
-
# @see MTK#PitchClassSet
|
26
|
-
#
|
27
|
-
def initialize(pitch_classes)
|
28
|
-
@pitch_classes = pitch_classes.to_a.clone.freeze
|
29
|
-
end
|
30
|
-
|
31
|
-
# @see Helper::Collection
|
32
|
-
def elements
|
33
|
-
@pitch_classes
|
34
|
-
end
|
35
|
-
|
36
|
-
# Convert to an Array of pitch_classes.
|
37
|
-
# @note this returns a mutable copy the underlying @pitch_classes attribute, which is otherwise unmutable
|
38
|
-
alias :to_pitch_classes :to_a
|
39
|
-
|
40
|
-
def self.from_a enumerable
|
41
|
-
new enumerable
|
42
|
-
end
|
43
|
-
|
44
|
-
def normal_order
|
45
|
-
ordering = Array.new(@pitch_classes.uniq.sort)
|
46
|
-
min_span, start_index_for_normal_order = nil, nil
|
47
|
-
|
48
|
-
# check every rotation for the minimal span:
|
49
|
-
size.times do |index|
|
50
|
-
span = self.class.span_between ordering.first, ordering.last
|
51
|
-
|
52
|
-
if min_span.nil? or span < min_span
|
53
|
-
# best so far
|
54
|
-
min_span = span
|
55
|
-
start_index_for_normal_order = index
|
56
|
-
|
57
|
-
elsif span == min_span
|
58
|
-
# handle ties, minimize distance between first and second-to-last note, then first and third-to-last, etc
|
59
|
-
span1, span2 = nil, nil
|
60
|
-
tie_breaker = 1
|
61
|
-
while span1 == span2 and tie_breaker < size
|
62
|
-
span1 = self.class.span_between( ordering[0], ordering[-1 - tie_breaker] )
|
63
|
-
span2 = self.class.span_between( ordering[start_index_for_normal_order], ordering[start_index_for_normal_order - tie_breaker] )
|
64
|
-
tie_breaker -= 1
|
65
|
-
end
|
66
|
-
if span1 != span2
|
67
|
-
# tie cannot be broken, pick the one starting with the lowest pitch class
|
68
|
-
if ordering[0].to_i < ordering[start_index_for_normal_order].to_i
|
69
|
-
start_index_for_normal_order = index
|
70
|
-
end
|
71
|
-
elsif span1 < span2
|
72
|
-
start_index_for_normal_order = index
|
73
|
-
end
|
74
|
-
|
75
|
-
end
|
76
|
-
ordering << ordering.shift # rotate
|
77
|
-
end
|
78
|
-
|
79
|
-
# we've rotated all the way around, so we now need to rotate back to the start index we just found:
|
80
|
-
start_index_for_normal_order.times{ ordering << ordering.shift }
|
81
|
-
|
82
|
-
ordering
|
83
|
-
end
|
84
|
-
|
85
|
-
def normal_form
|
86
|
-
norder = normal_order
|
87
|
-
first_pc_val = norder.first.to_i
|
88
|
-
norder.map{|pitch_class| (pitch_class.to_i - first_pc_val) % 12 }
|
89
|
-
end
|
90
|
-
|
91
|
-
# the collection of elements present in both sets
|
92
|
-
def intersection(other)
|
93
|
-
self.class.from_a(to_a & other.to_a)
|
94
|
-
end
|
95
|
-
|
96
|
-
# the collection of all elements present in either set
|
97
|
-
def union(other)
|
98
|
-
self.class.from_a(to_a | other.to_a)
|
99
|
-
end
|
100
|
-
|
101
|
-
# the collection of elements from this set with any elements from the other set removed
|
102
|
-
def difference(other)
|
103
|
-
self.class.from_a(to_a - other.to_a)
|
104
|
-
end
|
105
|
-
|
106
|
-
# the collection of elements that are members of exactly one of the sets
|
107
|
-
def symmetric_difference(other)
|
108
|
-
union(other).difference( intersection(other) )
|
109
|
-
end
|
110
|
-
|
111
|
-
# the collection of elements that are not members of this set
|
112
|
-
def complement
|
113
|
-
self.class.all.difference(self)
|
114
|
-
end
|
115
|
-
|
116
|
-
# @param other [#pitch_classes, #to_a, Array]
|
117
|
-
def == other
|
118
|
-
if other.respond_to? :pitch_classes
|
119
|
-
@pitch_classes == other.pitch_classes
|
120
|
-
elsif other.respond_to? :to_a
|
121
|
-
@pitch_classes == other.to_a
|
122
|
-
else
|
123
|
-
@pitch_classes == other
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
# Compare for equality, ignoring order and duplicates
|
128
|
-
# @param other [#pitch_classes, Array, #to_a]
|
129
|
-
def =~ other
|
130
|
-
@normalized_pitch_classes ||= @pitch_classes.uniq.sort
|
131
|
-
@normalized_pitch_classes == case
|
132
|
-
when other.respond_to?(:pitch_classes) then other.pitch_classes.uniq.sort
|
133
|
-
when (other.is_a? Array and other.frozen?) then other
|
134
|
-
when other.respond_to?(:to_a) then other.to_a.uniq.sort
|
135
|
-
else other
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
|
-
def to_s
|
140
|
-
@pitch_classes.join(' ')
|
141
|
-
end
|
142
|
-
|
143
|
-
def inspect
|
144
|
-
@pitch_classes.inspect
|
145
|
-
end
|
146
|
-
|
147
|
-
def self.span_between(pc1, pc2)
|
148
|
-
(pc2.to_i - pc1.to_i) % 12
|
149
|
-
end
|
150
|
-
|
151
|
-
end
|
152
|
-
|
153
|
-
|
154
|
-
# Construct a {PitchClassSet}
|
155
|
-
# @see PitchClassSet#initialize
|
156
|
-
def PitchClassSet(*anything)
|
157
|
-
PitchClassSet.new Helpers::Convert.to_pitch_classes(*anything)
|
158
|
-
end
|
159
|
-
module_function :PitchClassSet
|
160
|
-
|
161
|
-
end
|
data/lib/mtk/timeline.rb
DELETED
@@ -1,230 +0,0 @@
|
|
1
|
-
module MTK
|
2
|
-
|
3
|
-
# A collection of timed events. The core data structure used to interface with input and output.
|
4
|
-
#
|
5
|
-
# Maps sorted floating point times to lists of events.
|
6
|
-
#
|
7
|
-
# Enumerable as [time,event_list] pairs.
|
8
|
-
#
|
9
|
-
class Timeline
|
10
|
-
include Enumerable
|
11
|
-
|
12
|
-
def initialize()
|
13
|
-
@timeline = {}
|
14
|
-
end
|
15
|
-
|
16
|
-
class << self
|
17
|
-
def from_a(enumerable)
|
18
|
-
new.merge enumerable
|
19
|
-
end
|
20
|
-
alias from_hash from_a
|
21
|
-
end
|
22
|
-
|
23
|
-
def merge enumerable
|
24
|
-
enumerable.each do |time,events|
|
25
|
-
add(time,events)
|
26
|
-
end
|
27
|
-
self
|
28
|
-
end
|
29
|
-
|
30
|
-
def clear
|
31
|
-
@timeline.clear
|
32
|
-
self
|
33
|
-
end
|
34
|
-
|
35
|
-
def to_hash
|
36
|
-
@timeline
|
37
|
-
end
|
38
|
-
|
39
|
-
def == other
|
40
|
-
other = other.to_hash unless other.is_a? Hash
|
41
|
-
@timeline == other
|
42
|
-
end
|
43
|
-
|
44
|
-
def [](time)
|
45
|
-
@timeline[time.to_f]
|
46
|
-
end
|
47
|
-
|
48
|
-
def []=(time, events)
|
49
|
-
time = time.to_f unless time.is_a? Numeric
|
50
|
-
case events
|
51
|
-
when nil?
|
52
|
-
@timeline.delete time.to_f
|
53
|
-
when Array
|
54
|
-
@timeline[time.to_f] = events
|
55
|
-
else
|
56
|
-
@timeline[time.to_f] = [events]
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
def add(time, event)
|
61
|
-
events = @timeline[time.to_f]
|
62
|
-
if events
|
63
|
-
if event.is_a? Array
|
64
|
-
events.concat event
|
65
|
-
else
|
66
|
-
events << event
|
67
|
-
end
|
68
|
-
else
|
69
|
-
self[time] = event
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
def delete(time)
|
74
|
-
@timeline.delete(time.to_f)
|
75
|
-
end
|
76
|
-
|
77
|
-
def has_time? time
|
78
|
-
@timeline.has_key? time.to_f
|
79
|
-
end
|
80
|
-
|
81
|
-
def times
|
82
|
-
@timeline.keys.sort
|
83
|
-
end
|
84
|
-
|
85
|
-
def length
|
86
|
-
last_time = times.last
|
87
|
-
events = @timeline[last_time]
|
88
|
-
last_time + events.map{|event| event.duration }.max
|
89
|
-
end
|
90
|
-
|
91
|
-
def empty?
|
92
|
-
@timeline.empty?
|
93
|
-
end
|
94
|
-
|
95
|
-
def events
|
96
|
-
times.map{|t| @timeline[t] }.flatten
|
97
|
-
end
|
98
|
-
|
99
|
-
def each
|
100
|
-
# this is similar to @timeline.each, but by iterating over #times, we yield the events in chronological order
|
101
|
-
times.each do |time|
|
102
|
-
yield time, @timeline[time]
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
# the original Enumerable#map implementation, which returns an Array
|
107
|
-
alias enumerable_map map
|
108
|
-
|
109
|
-
# Constructs a new Timeline by mapping each [time,event_list] pair
|
110
|
-
# @see #map!
|
111
|
-
def map &block
|
112
|
-
self.class.from_a enumerable_map(&block)
|
113
|
-
end
|
114
|
-
|
115
|
-
# Perform #map in place
|
116
|
-
# @see #map
|
117
|
-
def map! &block
|
118
|
-
mapped = enumerable_map(&block)
|
119
|
-
clear
|
120
|
-
merge mapped
|
121
|
-
end
|
122
|
-
|
123
|
-
# Map every individual event, without regard for the time at which is occurs
|
124
|
-
def map_events
|
125
|
-
mapped_timeline = Timeline.new
|
126
|
-
self.each do |time,events|
|
127
|
-
mapped_timeline[time] = events.map{|event| yield event }
|
128
|
-
end
|
129
|
-
mapped_timeline
|
130
|
-
end
|
131
|
-
|
132
|
-
# Map every individual event in place, without regard for the time at which is occurs
|
133
|
-
def map_events!
|
134
|
-
each do |time,events|
|
135
|
-
self[time] = events.map{|event| yield event }
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
|
-
def clone
|
140
|
-
self.class.from_hash(to_hash)
|
141
|
-
end
|
142
|
-
|
143
|
-
def compact!
|
144
|
-
@timeline.delete_if {|t,events| events.empty? }
|
145
|
-
end
|
146
|
-
|
147
|
-
def flatten
|
148
|
-
flattened = Timeline.new
|
149
|
-
self.each do |time,events|
|
150
|
-
events.each do |event|
|
151
|
-
if event.is_a? Timeline
|
152
|
-
event.flatten.each do |subtime,subevent|
|
153
|
-
flattened.add(time+subtime, subevent)
|
154
|
-
end
|
155
|
-
else
|
156
|
-
flattened.add(time,event)
|
157
|
-
end
|
158
|
-
end
|
159
|
-
end
|
160
|
-
flattened
|
161
|
-
end
|
162
|
-
|
163
|
-
# @return a new Timeline where all times have been quantized to multiples of the given interval
|
164
|
-
# @example timeline.quantize(0.5) # quantize to eight notes (assuming the beat is a quarter note)
|
165
|
-
# @see quantize!
|
166
|
-
def quantize interval
|
167
|
-
map{|time,events| [self.class.quantize_time(time,interval), events] }
|
168
|
-
end
|
169
|
-
|
170
|
-
def quantize! interval
|
171
|
-
map!{|time,events| [self.class.quantize_time(time,interval), events] }
|
172
|
-
end
|
173
|
-
|
174
|
-
# shifts all times by the given amount
|
175
|
-
# @see #shift!
|
176
|
-
# @see #shift_to
|
177
|
-
def shift time_delta
|
178
|
-
map{|time,events| [time+time_delta, events] }
|
179
|
-
end
|
180
|
-
|
181
|
-
# shifts all times in place by the given amount
|
182
|
-
# @see #shift
|
183
|
-
# @see #shift_to!
|
184
|
-
def shift! time_delta
|
185
|
-
map!{|time,events| [time+time_delta, events] }
|
186
|
-
end
|
187
|
-
|
188
|
-
# shifts the times so that the start of the timeline is at the given time
|
189
|
-
# @see #shift_to!
|
190
|
-
# @see #shift
|
191
|
-
def shift_to absolute_time
|
192
|
-
start = times.first
|
193
|
-
if start
|
194
|
-
shift absolute_time - start
|
195
|
-
else
|
196
|
-
clone
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
# shifts the times in place so that the start of the timeline is at the given time
|
201
|
-
# @see #shift_to
|
202
|
-
# @see #shift!
|
203
|
-
def shift_to! absolute_time
|
204
|
-
start = times.first
|
205
|
-
if start
|
206
|
-
shift! absolute_time - start
|
207
|
-
end
|
208
|
-
self
|
209
|
-
end
|
210
|
-
|
211
|
-
def to_s
|
212
|
-
times = self.times
|
213
|
-
last = times.last
|
214
|
-
width = sprintf("%d",last).length + 3 # nicely align the '=>' against the longest number
|
215
|
-
times.map{|t| sprintf("%#{width}.2f",t)+" => #{@timeline[t].join ', '}" }.join "\n"
|
216
|
-
end
|
217
|
-
|
218
|
-
def inspect
|
219
|
-
@timeline.inspect
|
220
|
-
end
|
221
|
-
|
222
|
-
def self.quantize_time time, interval
|
223
|
-
upper = interval * (time.to_f/interval).ceil
|
224
|
-
lower = upper - interval
|
225
|
-
(time - lower) < (upper - time) ? lower : upper
|
226
|
-
end
|
227
|
-
|
228
|
-
end
|
229
|
-
|
230
|
-
end
|