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