mtk 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +3 -2
- data/DEVELOPMENT_NOTES.md +114 -0
- data/INTRO.md +64 -8
- data/LICENSE.txt +1 -1
- data/README.md +31 -102
- data/Rakefile +56 -18
- data/bin/mtk +215 -0
- data/examples/crescendo.rb +5 -5
- data/examples/drum_pattern1.rb +23 -0
- data/examples/dynamic_pattern.rb +8 -11
- data/examples/gets_and_play.rb +26 -0
- data/examples/notation.rb +22 -0
- data/examples/play_midi.rb +8 -10
- data/examples/random_tone_row.rb +2 -2
- data/examples/syntax_to_midi.rb +28 -0
- data/examples/test_output.rb +8 -0
- data/examples/tone_row_melody.rb +6 -6
- data/lib/mtk.rb +52 -40
- data/lib/mtk/chord.rb +55 -0
- data/lib/mtk/constants/durations.rb +57 -0
- data/lib/mtk/constants/intensities.rb +61 -0
- data/lib/mtk/constants/intervals.rb +73 -0
- data/lib/mtk/constants/pitch_classes.rb +29 -0
- data/lib/mtk/constants/pitches.rb +52 -0
- data/lib/mtk/duration.rb +211 -0
- data/lib/mtk/events/event.rb +119 -0
- data/lib/mtk/events/note.rb +112 -0
- data/lib/mtk/events/parameter.rb +54 -0
- data/lib/mtk/helpers/collection.rb +164 -0
- data/lib/mtk/helpers/convert.rb +36 -0
- data/lib/mtk/helpers/lilypond.rb +162 -0
- data/lib/mtk/helpers/output_selector.rb +67 -0
- data/lib/mtk/helpers/pitch_collection.rb +23 -0
- data/lib/mtk/helpers/pseudo_constants.rb +26 -0
- data/lib/mtk/intensity.rb +156 -0
- data/lib/mtk/interval.rb +155 -0
- data/lib/mtk/lang/mtk_grammar.citrus +190 -13
- data/lib/mtk/lang/parser.rb +29 -0
- data/lib/mtk/melody.rb +94 -0
- data/lib/mtk/midi/dls_synth_device.rb +144 -0
- data/lib/mtk/midi/dls_synth_output.rb +62 -0
- data/lib/mtk/midi/file.rb +67 -32
- data/lib/mtk/midi/input.rb +97 -0
- data/lib/mtk/midi/jsound_input.rb +36 -17
- data/lib/mtk/midi/jsound_output.rb +48 -46
- data/lib/mtk/midi/output.rb +195 -0
- data/lib/mtk/midi/unimidi_input.rb +117 -0
- data/lib/mtk/midi/unimidi_output.rb +121 -0
- data/lib/mtk/{_numeric_extensions.rb → numeric_extensions.rb} +12 -0
- data/lib/mtk/patterns/chain.rb +49 -0
- data/lib/mtk/{pattern → patterns}/choice.rb +14 -8
- data/lib/mtk/patterns/cycle.rb +18 -0
- data/lib/mtk/patterns/for_each.rb +71 -0
- data/lib/mtk/patterns/function.rb +39 -0
- data/lib/mtk/{pattern → patterns}/lines.rb +11 -17
- data/lib/mtk/{pattern → patterns}/palindrome.rb +11 -8
- data/lib/mtk/patterns/pattern.rb +171 -0
- data/lib/mtk/patterns/sequence.rb +20 -0
- data/lib/mtk/pitch.rb +7 -6
- data/lib/mtk/pitch_class.rb +124 -46
- data/lib/mtk/pitch_class_set.rb +58 -35
- data/lib/mtk/sequencers/event_builder.rb +131 -0
- data/lib/mtk/sequencers/legato_sequencer.rb +24 -0
- data/lib/mtk/sequencers/rhythmic_sequencer.rb +28 -0
- data/lib/mtk/{sequencer/abstract_sequencer.rb → sequencers/sequencer.rb} +37 -11
- data/lib/mtk/{sequencer → sequencers}/step_sequencer.rb +4 -4
- data/lib/mtk/timeline.rb +39 -22
- data/lib/mtk/variable.rb +32 -0
- data/spec/mtk/chord_spec.rb +83 -0
- data/spec/mtk/{_constants → constants}/durations_spec.rb +12 -41
- data/spec/mtk/{_constants → constants}/intensities_spec.rb +13 -37
- data/spec/mtk/{_constants → constants}/intervals_spec.rb +14 -32
- data/spec/mtk/{_constants → constants}/pitch_classes_spec.rb +8 -4
- data/spec/mtk/{_constants → constants}/pitches_spec.rb +5 -1
- data/spec/mtk/duration_spec.rb +372 -0
- data/spec/mtk/events/event_spec.rb +234 -0
- data/spec/mtk/events/note_spec.rb +174 -0
- data/spec/mtk/events/parameter_spec.rb +220 -0
- data/spec/mtk/{helper → helpers}/collection_spec.rb +86 -3
- data/spec/mtk/{helper → helpers}/pseudo_constants_spec.rb +2 -2
- data/spec/mtk/intensity_spec.rb +289 -0
- data/spec/mtk/interval_spec.rb +265 -0
- data/spec/mtk/lang/parser_spec.rb +597 -0
- data/spec/mtk/melody_spec.rb +223 -0
- data/spec/mtk/midi/file_spec.rb +16 -16
- data/spec/mtk/midi/jsound_input_spec.rb +11 -0
- data/spec/mtk/midi/jsound_output_spec.rb +11 -0
- data/spec/mtk/midi/output_spec.rb +102 -0
- data/spec/mtk/midi/unimidi_input_spec.rb +11 -0
- data/spec/mtk/midi/unimidi_output_spec.rb +11 -0
- data/spec/mtk/{_numeric_extensions_spec.rb → numeric_extensions_spec.rb} +1 -0
- data/spec/mtk/patterns/chain_spec.rb +110 -0
- data/spec/mtk/{pattern → patterns}/choice_spec.rb +20 -30
- data/spec/mtk/{pattern → patterns}/cycle_spec.rb +25 -35
- data/spec/mtk/patterns/for_each_spec.rb +136 -0
- data/spec/mtk/{pattern → patterns}/function_spec.rb +17 -30
- data/spec/mtk/{pattern → patterns}/lines_spec.rb +11 -27
- data/spec/mtk/{pattern → patterns}/palindrome_spec.rb +13 -29
- data/spec/mtk/patterns/pattern_spec.rb +132 -0
- data/spec/mtk/patterns/sequence_spec.rb +203 -0
- data/spec/mtk/pitch_class_set_spec.rb +23 -21
- data/spec/mtk/pitch_class_spec.rb +151 -39
- data/spec/mtk/pitch_spec.rb +22 -1
- data/spec/mtk/sequencers/event_builder_spec.rb +245 -0
- data/spec/mtk/sequencers/legato_sequencer_spec.rb +45 -0
- data/spec/mtk/sequencers/rhythmic_sequencer_spec.rb +84 -0
- data/spec/mtk/sequencers/sequencer_spec.rb +215 -0
- data/spec/mtk/{sequencer → sequencers}/step_sequencer_spec.rb +35 -13
- data/spec/mtk/timeline_spec.rb +109 -16
- data/spec/mtk/variable_spec.rb +52 -0
- data/spec/spec_coverage.rb +2 -0
- data/spec/spec_helper.rb +3 -0
- metadata +188 -91
- data/lib/mtk/_constants/durations.rb +0 -80
- data/lib/mtk/_constants/intensities.rb +0 -81
- data/lib/mtk/_constants/intervals.rb +0 -85
- data/lib/mtk/_constants/pitch_classes.rb +0 -35
- data/lib/mtk/_constants/pitches.rb +0 -49
- data/lib/mtk/event.rb +0 -70
- data/lib/mtk/helper/collection.rb +0 -114
- data/lib/mtk/helper/event_builder.rb +0 -85
- data/lib/mtk/helper/pseudo_constants.rb +0 -26
- data/lib/mtk/lang/grammar.rb +0 -17
- data/lib/mtk/note.rb +0 -63
- data/lib/mtk/pattern/abstract_pattern.rb +0 -132
- data/lib/mtk/pattern/cycle.rb +0 -51
- data/lib/mtk/pattern/enumerator.rb +0 -26
- data/lib/mtk/pattern/function.rb +0 -46
- data/lib/mtk/pattern/sequence.rb +0 -30
- data/lib/mtk/pitch_set.rb +0 -84
- data/lib/mtk/sequencer/rhythmic_sequencer.rb +0 -29
- data/lib/mtk/transform/invertible.rb +0 -15
- data/lib/mtk/transform/mappable.rb +0 -18
- data/lib/mtk/transform/set_theory_operations.rb +0 -34
- data/lib/mtk/transform/transposable.rb +0 -14
- data/spec/mtk/event_spec.rb +0 -139
- data/spec/mtk/helper/event_builder_spec.rb +0 -92
- data/spec/mtk/lang/grammar_spec.rb +0 -100
- data/spec/mtk/note_spec.rb +0 -115
- data/spec/mtk/pattern/abstract_pattern_spec.rb +0 -45
- data/spec/mtk/pattern/note_cycle_spec.rb.bak +0 -116
- data/spec/mtk/pattern/pitch_cycle_spec.rb.bak +0 -47
- data/spec/mtk/pattern/pitch_sequence_spec.rb.bak +0 -37
- data/spec/mtk/pattern/sequence_spec.rb +0 -151
- data/spec/mtk/pitch_set_spec.rb +0 -198
- data/spec/mtk/sequencer/abstract_sequencer_spec.rb +0 -159
- data/spec/mtk/sequencer/rhythmic_sequencer_spec.rb +0 -49
data/lib/mtk/pitch_class_set.rb
CHANGED
@@ -1,44 +1,53 @@
|
|
1
1
|
module MTK
|
2
2
|
|
3
|
-
# An ordered
|
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
|
4
9
|
#
|
5
10
|
class PitchClassSet
|
6
|
-
|
7
|
-
include Helper::Collection
|
8
|
-
include Transform::Mappable
|
9
|
-
include Transform::Transposable
|
10
|
-
include Transform::Invertible
|
11
|
-
include Transform::SetTheoryOperations
|
11
|
+
include Helpers::PitchCollection
|
12
12
|
|
13
13
|
attr_reader :pitch_classes
|
14
14
|
|
15
15
|
def self.random_row
|
16
|
-
new(PitchClasses::PITCH_CLASSES.shuffle)
|
16
|
+
new(Constants::PitchClasses::PITCH_CLASSES.shuffle)
|
17
17
|
end
|
18
18
|
|
19
19
|
def self.all
|
20
|
-
@all ||= new(PitchClasses::PITCH_CLASSES)
|
20
|
+
@all ||= new(Constants::PitchClasses::PITCH_CLASSES)
|
21
21
|
end
|
22
22
|
|
23
|
+
# @param pitch_classes [#to_a] the collection of pitch classes
|
24
|
+
#
|
25
|
+
# @see MTK#PitchClassSet
|
26
|
+
#
|
23
27
|
def initialize(pitch_classes)
|
24
|
-
@pitch_classes = pitch_classes.to_a.
|
28
|
+
@pitch_classes = pitch_classes.to_a.clone.freeze
|
25
29
|
end
|
26
30
|
|
31
|
+
# @see Helper::Collection
|
27
32
|
def elements
|
28
33
|
@pitch_classes
|
29
34
|
end
|
30
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
|
+
|
31
40
|
def self.from_a enumerable
|
32
41
|
new enumerable
|
33
42
|
end
|
34
43
|
|
35
44
|
def normal_order
|
36
|
-
ordering = Array.new(@pitch_classes.sort)
|
45
|
+
ordering = Array.new(@pitch_classes.uniq.sort)
|
37
46
|
min_span, start_index_for_normal_order = nil, nil
|
38
47
|
|
39
48
|
# check every rotation for the minimal span:
|
40
49
|
size.times do |index|
|
41
|
-
span = self.class.
|
50
|
+
span = self.class.span_between ordering.first, ordering.last
|
42
51
|
|
43
52
|
if min_span.nil? or span < min_span
|
44
53
|
# best so far
|
@@ -79,6 +88,31 @@ module MTK
|
|
79
88
|
norder.map{|pitch_class| (pitch_class.to_i - first_pc_val) % 12 }
|
80
89
|
end
|
81
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
|
+
|
82
116
|
# @param other [#pitch_classes, #to_a, Array]
|
83
117
|
def == other
|
84
118
|
if other.respond_to? :pitch_classes
|
@@ -90,19 +124,15 @@ module MTK
|
|
90
124
|
end
|
91
125
|
end
|
92
126
|
|
93
|
-
# Compare for equality, ignoring order
|
94
|
-
# @param other [#pitch_classes,
|
127
|
+
# Compare for equality, ignoring order and duplicates
|
128
|
+
# @param other [#pitch_classes, Array, #to_a]
|
95
129
|
def =~ other
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
elsif other.respond_to? :sort
|
103
|
-
@pitch_classes.sort == other.sort
|
104
|
-
else
|
105
|
-
@pitch_classes.sort == 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
|
106
136
|
end
|
107
137
|
end
|
108
138
|
|
@@ -114,24 +144,17 @@ module MTK
|
|
114
144
|
@pitch_classes.inspect
|
115
145
|
end
|
116
146
|
|
117
|
-
def self.span_for(pitch_classes)
|
118
|
-
span_between pitch_classes.first, pitch_classes.last
|
119
|
-
end
|
120
|
-
|
121
147
|
def self.span_between(pc1, pc2)
|
122
148
|
(pc2.to_i - pc1.to_i) % 12
|
123
149
|
end
|
124
150
|
|
125
151
|
end
|
126
152
|
|
127
|
-
|
153
|
+
|
154
|
+
# Construct a {PitchClassSet}
|
155
|
+
# @see PitchClassSet#initialize
|
128
156
|
def PitchClassSet(*anything)
|
129
|
-
|
130
|
-
case anything
|
131
|
-
when Array then PitchClassSet.new(anything.map{|elem| PitchClass(elem) })
|
132
|
-
when PitchClassSet then anything
|
133
|
-
else PitchClassSet.new([PitchClass(anything)])
|
134
|
-
end
|
157
|
+
PitchClassSet.new Helpers::Convert.to_pitch_classes(*anything)
|
135
158
|
end
|
136
159
|
module_function :PitchClassSet
|
137
160
|
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module MTK
|
2
|
+
module Sequencers
|
3
|
+
|
4
|
+
# A special pattern that takes a list of event properties and/or patterns and emits lists of {Events::Event}s
|
5
|
+
class EventBuilder
|
6
|
+
|
7
|
+
DEFAULT_PITCH = ::MTK::Constants::Pitches::C4
|
8
|
+
DEFAULT_DURATION = ::MTK::Constants::Durations::q
|
9
|
+
DEFAULT_INTENSITY = ::MTK::Constants::Intensities::o
|
10
|
+
|
11
|
+
def initialize(patterns, options={})
|
12
|
+
@patterns = patterns
|
13
|
+
@options = options
|
14
|
+
@default_pitch = if options.has_key? :default_pitch then MTK::Pitch( options[:default_pitch]) else DEFAULT_PITCH end
|
15
|
+
@default_duration = if options.has_key? :default_duration then MTK::Duration( options[:default_duration]) else DEFAULT_DURATION end
|
16
|
+
@default_intensity = if options.has_key? :default_intensity then MTK::Intensity(options[:default_intensity]) else DEFAULT_INTENSITY end
|
17
|
+
@max_interval = options.fetch(:max_interval, 127)
|
18
|
+
rewind
|
19
|
+
end
|
20
|
+
|
21
|
+
# Build a list of events from the next element in each {Patterns::Pattern}
|
22
|
+
# @return [Array] an array of events
|
23
|
+
def next
|
24
|
+
pitches = []
|
25
|
+
intensities = []
|
26
|
+
duration = nil
|
27
|
+
|
28
|
+
@patterns.each do |pattern|
|
29
|
+
pattern_value = pattern.next
|
30
|
+
|
31
|
+
elements = pattern_value.is_a?(Enumerable) ? pattern_value : [pattern_value]
|
32
|
+
elements.each do |element|
|
33
|
+
return nil if element.nil? or element == :skip
|
34
|
+
|
35
|
+
case element
|
36
|
+
when ::MTK::Pitch then pitches << element
|
37
|
+
when ::MTK::PitchClass then pitches += pitches_for_pitch_classes([element], @previous_pitch)
|
38
|
+
when ::MTK::PitchClassSet then pitches += pitches_for_pitch_classes(element, @previous_pitch)
|
39
|
+
when ::MTK::Helpers::PitchCollection then pitches += element.pitches # this must be after the PitchClassSet case, because that is also a PitchCollection
|
40
|
+
|
41
|
+
when ::MTK::Duration
|
42
|
+
duration ||= 0
|
43
|
+
duration += element
|
44
|
+
|
45
|
+
when ::MTK::Intensity
|
46
|
+
intensities << element
|
47
|
+
|
48
|
+
when ::MTK::Interval
|
49
|
+
if @previous_pitches
|
50
|
+
pitches += @previous_pitches.map{|pitch| pitch + element }
|
51
|
+
else
|
52
|
+
pitches << (@previous_pitch + element)
|
53
|
+
end
|
54
|
+
|
55
|
+
# TODO? String/Symbols for special behaviors like :skip, or :break (something like StopIteration for the current Pattern?)
|
56
|
+
|
57
|
+
else STDERR.puts "#{self.class}#next: Unexpected type '#{element.class}'"
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
pitches << @previous_pitch if pitches.empty?
|
64
|
+
duration ||= @previous_duration
|
65
|
+
|
66
|
+
if intensities.empty?
|
67
|
+
intensity = @previous_intensity
|
68
|
+
else
|
69
|
+
intensity = MTK::Intensity[intensities.map{|i| i.to_f }.inject(:+)/intensities.length] # average the intensities
|
70
|
+
end
|
71
|
+
|
72
|
+
# Not using this yet, maybe later...
|
73
|
+
# return nil if duration==:skip or intensities.include? :skip or pitches.include? :skip
|
74
|
+
|
75
|
+
constrain_pitch(pitches)
|
76
|
+
|
77
|
+
@previous_pitch = pitches.last # Consider doing something different, maybe averaging?
|
78
|
+
@previous_pitches = pitches.length > 1 ? pitches : nil
|
79
|
+
@previous_intensity = intensity
|
80
|
+
@previous_duration = duration
|
81
|
+
|
82
|
+
pitches.map{|pitch| MTK::Events::Note.new(pitch,duration,intensity) }
|
83
|
+
end
|
84
|
+
|
85
|
+
# Reset the EventBuilder to its initial state
|
86
|
+
def rewind
|
87
|
+
@previous_pitch = @default_pitch
|
88
|
+
@previous_pitches = [@default_pitch]
|
89
|
+
@previous_intensity = @default_intensity
|
90
|
+
@previous_duration = @default_duration
|
91
|
+
@max_pitch = nil
|
92
|
+
@min_pitch = nil
|
93
|
+
@patterns.each{|pattern| pattern.rewind if pattern.is_a? MTK::Patterns::Pattern }
|
94
|
+
end
|
95
|
+
|
96
|
+
########################
|
97
|
+
private
|
98
|
+
|
99
|
+
def pitches_for_pitch_classes(pitch_classes, previous_pitch)
|
100
|
+
pitch_classes.map{|pitch_class| previous_pitch.nearest(pitch_class) }
|
101
|
+
end
|
102
|
+
|
103
|
+
def constrain_pitch(pitches)
|
104
|
+
if @max_pitch.nil? or @min_pitch.nil?
|
105
|
+
first_pitch = pitches.first
|
106
|
+
|
107
|
+
@max_pitch = first_pitch + @max_interval
|
108
|
+
@max_pitch = 127 if @max_pitch > 127
|
109
|
+
|
110
|
+
@min_pitch = first_pitch - @max_interval
|
111
|
+
@min_pitch = 0 if @min_pitch < 0
|
112
|
+
|
113
|
+
@small_max_span = (@max_pitch - @min_pitch < 12)
|
114
|
+
end
|
115
|
+
|
116
|
+
pitches.map! do |pitch|
|
117
|
+
if @small_max_span
|
118
|
+
pitch = @max_pitch if pitch > @max_pitch
|
119
|
+
pitch = @min_pitch if pitch < @max_pitch
|
120
|
+
else
|
121
|
+
pitch -= 12 while pitch > @max_pitch
|
122
|
+
pitch += 12 while pitch < @min_pitch
|
123
|
+
end
|
124
|
+
pitch
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module MTK
|
2
|
+
module Sequencers
|
3
|
+
|
4
|
+
# A Sequencer which uses the longest duration of the events at each step to determine
|
5
|
+
# the delta times between entries in the {Timeline}.
|
6
|
+
class LegatoSequencer < Sequencer
|
7
|
+
|
8
|
+
# (see Sequencer#next)
|
9
|
+
def next
|
10
|
+
@previous_events = super
|
11
|
+
end
|
12
|
+
|
13
|
+
########################
|
14
|
+
protected
|
15
|
+
|
16
|
+
# (see Sequencer#advance)
|
17
|
+
def advance
|
18
|
+
@time += @previous_events.map{|event| event.length }.max
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module MTK
|
2
|
+
module Sequencers
|
3
|
+
|
4
|
+
# A Sequencer which uses a :rhythm type {Patterns::Pattern} to determine the delta times between entries in the {Timeline}.
|
5
|
+
class RhythmicSequencer < Sequencer
|
6
|
+
|
7
|
+
def initialize(patterns, options={})
|
8
|
+
super
|
9
|
+
@rhythm = options[:rhythm] or raise ArgumentError.new(":rhythm option is required")
|
10
|
+
end
|
11
|
+
|
12
|
+
def rewind
|
13
|
+
super
|
14
|
+
@rhythm.rewind if @rhythm
|
15
|
+
end
|
16
|
+
|
17
|
+
########################
|
18
|
+
protected
|
19
|
+
|
20
|
+
# (see Sequencer#advance)
|
21
|
+
def advance
|
22
|
+
@time += @rhythm.next.length
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -1,11 +1,11 @@
|
|
1
1
|
module MTK
|
2
|
-
module
|
2
|
+
module Sequencers
|
3
3
|
|
4
|
-
# A Sequencer produces {Timeline}s from a collection of {Pattern}s.
|
4
|
+
# A Sequencer produces {Timeline}s from a collection of {Patterns::Pattern}s.
|
5
5
|
#
|
6
6
|
# @abstract Subclass and override {#advance} to implement a Sequencer.
|
7
7
|
#
|
8
|
-
class
|
8
|
+
class Sequencer
|
9
9
|
|
10
10
|
# The maximum number of [time,event_list] entries that will be generated for the {Timeline}.
|
11
11
|
# nil means no maximum (be careful of infinite loops!)
|
@@ -15,7 +15,7 @@ module MTK
|
|
15
15
|
# nil means no maximum (be careful of infinite loops!)
|
16
16
|
attr_accessor :max_time
|
17
17
|
|
18
|
-
# Used by {#to_timeline} to builds event lists from the results of
|
18
|
+
# Used by {#to_timeline} to builds event lists from the results of {Patterns::Pattern#next} for the {Patterns::Pattern}s in this Sequencer.
|
19
19
|
attr_reader :event_builder
|
20
20
|
|
21
21
|
# The current time offset for the sequencer. Used for the {Timeline} times.
|
@@ -24,24 +24,37 @@ module MTK
|
|
24
24
|
# The current sequencer step index (the number of times-1 that {#next} has been called), or -1 if the sequencer has not yet started.
|
25
25
|
attr_reader :step
|
26
26
|
|
27
|
+
attr_reader :patterns
|
28
|
+
|
29
|
+
# @param patterns [Array] the list of patterns to be sequenced into a {Timeline}
|
30
|
+
# @param options [Hash] the options to create a message with.
|
31
|
+
# @option options [String] :max_steps set {#max_steps}
|
32
|
+
# @option options [String] :max_time set {#max_time}
|
33
|
+
# @option options [Proc] :filter a Proc that will replace the events generated by {#next} with the results of the Proc[events]
|
34
|
+
# @option options [Class] :event_builder replace the {Sequencers::EventBuilder} with a custom Event pattern
|
27
35
|
def initialize(patterns, options={})
|
28
36
|
@patterns = patterns
|
29
37
|
@max_steps = options[:max_steps]
|
30
38
|
@max_time = options[:max_time]
|
39
|
+
@filter = options[:filter]
|
31
40
|
|
32
|
-
event_builder_class = options.fetch :event_builder,
|
41
|
+
event_builder_class = options.fetch :event_builder, ::MTK::Sequencers::EventBuilder
|
33
42
|
@event_builder = event_builder_class.new(patterns, options)
|
43
|
+
|
34
44
|
rewind
|
35
45
|
end
|
36
46
|
|
37
47
|
|
38
|
-
# Produce a {Timeline} from the {Pattern}s in this Sequencer.
|
48
|
+
# Produce a {Timeline} from the {Patterns::Pattern}s in this Sequencer.
|
39
49
|
def to_timeline
|
40
50
|
rewind
|
41
51
|
timeline = Timeline.new
|
42
52
|
loop do
|
43
53
|
events = self.next
|
44
|
-
|
54
|
+
if events
|
55
|
+
events = events.reject{|e| e.rest? }
|
56
|
+
timeline[@time] = events unless events.empty?
|
57
|
+
end
|
45
58
|
end
|
46
59
|
timeline
|
47
60
|
end
|
@@ -52,12 +65,14 @@ module MTK
|
|
52
65
|
# so you can ignore this method unless you want to hack on sequencers at a lower level.
|
53
66
|
def next
|
54
67
|
if @step >= 0
|
55
|
-
advance
|
68
|
+
advance
|
56
69
|
raise StopIteration if @max_time and @time > @max_time
|
57
70
|
end
|
58
71
|
@step += 1
|
59
72
|
raise StopIteration if @max_steps and @step >= @max_steps
|
60
|
-
@event_builder.next
|
73
|
+
events = @event_builder.next
|
74
|
+
events = @filter[events] if @filter
|
75
|
+
events
|
61
76
|
end
|
62
77
|
|
63
78
|
|
@@ -67,7 +82,7 @@ module MTK
|
|
67
82
|
def rewind
|
68
83
|
@time = 0
|
69
84
|
@step = -1
|
70
|
-
event_builder.rewind
|
85
|
+
@event_builder.rewind
|
71
86
|
end
|
72
87
|
|
73
88
|
|
@@ -75,10 +90,21 @@ module MTK
|
|
75
90
|
protected
|
76
91
|
|
77
92
|
# Advance @time to the next time for the {Timeline} being produced by {#to_timeline}
|
78
|
-
def advance
|
93
|
+
def advance
|
79
94
|
@time += 1 # default behavior simply advances one beat at a time
|
80
95
|
end
|
81
96
|
|
97
|
+
def self.inherited(subclass)
|
98
|
+
# Define a convenience method like MTK::Patterns.Sequence()
|
99
|
+
# that can handle varargs or a single array argument, plus any Hash options
|
100
|
+
classname = subclass.name.sub /.*::/, '' # Strip off module prefixes
|
101
|
+
MTK::Sequencers.define_singleton_method classname do |*args|
|
102
|
+
options = (args[-1].is_a? Hash) ? args.pop : {}
|
103
|
+
args = args[0] if args.length == 1 and args[0].is_a? Array
|
104
|
+
subclass.new(args,options)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
82
108
|
end
|
83
109
|
|
84
110
|
end
|