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/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
|