cyclotone 0.1.0 → 1.0.0
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.
- checksums.yaml +4 -4
- data/README.md +8 -74
- data/Rakefile +37 -1
- data/cyclotone.gemspec +16 -3
- data/exe/cyclotone +12 -0
- data/lib/cyclotone/backends/midi_backend.rb +106 -21
- data/lib/cyclotone/backends/midi_file_backend.rb +182 -24
- data/lib/cyclotone/backends/midi_message_support.rb +111 -28
- data/lib/cyclotone/backends/null_backend.rb +33 -0
- data/lib/cyclotone/backends/osc_backend.rb +105 -17
- data/lib/cyclotone/controls.rb +64 -16
- data/lib/cyclotone/dsl.rb +5 -5
- data/lib/cyclotone/errors.rb +8 -3
- data/lib/cyclotone/event.rb +38 -3
- data/lib/cyclotone/harmony.rb +62 -8
- data/lib/cyclotone/mini_notation/ast.rb +85 -5
- data/lib/cyclotone/mini_notation/compiler.rb +18 -10
- data/lib/cyclotone/mini_notation/parser.rb +168 -34
- data/lib/cyclotone/oscillators.rb +130 -28
- data/lib/cyclotone/pattern.rb +211 -36
- data/lib/cyclotone/scheduler.rb +179 -40
- data/lib/cyclotone/state.rb +0 -1
- data/lib/cyclotone/stream.rb +91 -45
- data/lib/cyclotone/support/deterministic.rb +37 -1
- data/lib/cyclotone/time_span.rb +29 -7
- data/lib/cyclotone/transforms/accumulation.rb +28 -5
- data/lib/cyclotone/transforms/alteration.rb +82 -18
- data/lib/cyclotone/transforms/condition.rb +15 -3
- data/lib/cyclotone/transforms/sample.rb +33 -9
- data/lib/cyclotone/transforms/time.rb +24 -5
- data/lib/cyclotone/transition.rb +54 -42
- data/lib/cyclotone/version.rb +1 -1
- data/lib/cyclotone.rb +1 -0
- data/sig/cyclotone.rbs +99 -0
- metadata +4 -1
|
@@ -4,43 +4,51 @@ module Cyclotone
|
|
|
4
4
|
module Oscillators
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
|
-
def sine
|
|
8
|
-
|
|
7
|
+
def sine(freq: 1, phase: 0, bipolar: false)
|
|
8
|
+
oscillator(freq: freq, phase_offset: phase, bipolar: bipolar) do |time|
|
|
9
|
+
(Math.sin(phase(time)) + 1.0) / 2.0
|
|
10
|
+
end
|
|
9
11
|
end
|
|
10
12
|
|
|
11
|
-
def cosine
|
|
12
|
-
|
|
13
|
+
def cosine(freq: 1, phase: 0, bipolar: false)
|
|
14
|
+
oscillator(freq: freq, phase_offset: phase, bipolar: bipolar) do |time|
|
|
15
|
+
(Math.cos(phase(time)) + 1.0) / 2.0
|
|
16
|
+
end
|
|
13
17
|
end
|
|
14
18
|
|
|
15
|
-
def tri
|
|
16
|
-
|
|
19
|
+
def tri(freq: 1, phase: 0, bipolar: false)
|
|
20
|
+
oscillator(freq: freq, phase_offset: phase, bipolar: bipolar) do |time|
|
|
17
21
|
position = cycle_position(time)
|
|
18
22
|
position < 0.5 ? position * 2.0 : (1.0 - position) * 2.0
|
|
19
23
|
end
|
|
20
24
|
end
|
|
21
25
|
|
|
22
|
-
def saw
|
|
23
|
-
|
|
26
|
+
def saw(freq: 1, phase: 0, bipolar: false)
|
|
27
|
+
oscillator(freq: freq, phase_offset: phase, bipolar: bipolar) { |time| cycle_position(time) }
|
|
24
28
|
end
|
|
25
29
|
|
|
26
|
-
def isaw
|
|
27
|
-
|
|
30
|
+
def isaw(freq: 1, phase: 0, bipolar: false)
|
|
31
|
+
oscillator(freq: freq, phase_offset: phase, bipolar: bipolar) { |time| 1.0 - cycle_position(time) }
|
|
28
32
|
end
|
|
29
33
|
|
|
30
|
-
def square
|
|
31
|
-
|
|
34
|
+
def square(freq: 1, phase: 0, bipolar: false)
|
|
35
|
+
oscillator(freq: freq, phase_offset: phase, bipolar: bipolar) { |time| cycle_position(time) < 0.5 ? 0.0 : 1.0 }
|
|
32
36
|
end
|
|
33
37
|
|
|
34
|
-
def rand
|
|
38
|
+
def rand(steps: 128)
|
|
39
|
+
normalized_steps = positive_integer(steps, "rand steps")
|
|
40
|
+
|
|
35
41
|
Pattern.continuous do |time|
|
|
36
42
|
cycle = time.floor
|
|
37
|
-
step = (
|
|
43
|
+
step = (cycle_position(time) * normalized_steps).floor.to_i
|
|
38
44
|
Support::Deterministic.float(:rand, cycle, step)
|
|
39
45
|
end
|
|
40
46
|
end
|
|
41
47
|
|
|
42
48
|
def irand(maximum)
|
|
43
|
-
|
|
49
|
+
normalized_maximum = positive_integer(maximum, "irand maximum")
|
|
50
|
+
|
|
51
|
+
rand.fmap { |value| (value * normalized_maximum).floor }
|
|
44
52
|
end
|
|
45
53
|
|
|
46
54
|
def perlin
|
|
@@ -56,18 +64,64 @@ module Cyclotone
|
|
|
56
64
|
end
|
|
57
65
|
end
|
|
58
66
|
|
|
59
|
-
def range(low, high, pattern)
|
|
67
|
+
def range(low, high, pattern, mode: :raw)
|
|
60
68
|
Pattern.ensure_pattern(pattern).fmap do |value|
|
|
61
|
-
|
|
69
|
+
normalized_value = normalize_range_value(value.to_f, mode)
|
|
70
|
+
low.to_f + ((high.to_f - low.to_f) * normalized_value)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def bipolar(pattern)
|
|
75
|
+
Pattern.ensure_pattern(pattern).fmap { |value| (value.to_f * 2.0) - 1.0 }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def noise(steps: 128)
|
|
79
|
+
rand(steps: steps)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def sample_and_hold(pattern, steps: 8)
|
|
83
|
+
normalized_steps = positive_integer(steps, "sample_and_hold steps")
|
|
84
|
+
source = Pattern.ensure_pattern(pattern)
|
|
85
|
+
|
|
86
|
+
Pattern.continuous do |time|
|
|
87
|
+
sampled_time = Rational((time * normalized_steps).floor, normalized_steps)
|
|
88
|
+
source.query_point(sampled_time)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def brownian(step: 0.1)
|
|
93
|
+
normalized_step = step.to_f
|
|
94
|
+
raise ArgumentError, "brownian step must be positive" unless normalized_step.positive?
|
|
95
|
+
|
|
96
|
+
cache = { -1 => 0.5 }
|
|
97
|
+
highest_cycle = -1
|
|
98
|
+
|
|
99
|
+
Pattern.continuous do |time|
|
|
100
|
+
cycle = time.floor
|
|
101
|
+
|
|
102
|
+
while highest_cycle < cycle
|
|
103
|
+
next_cycle = highest_cycle + 1
|
|
104
|
+
cache[next_cycle] = brownian_step(cache.fetch(highest_cycle), next_cycle, normalized_step)
|
|
105
|
+
highest_cycle = next_cycle
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
cache.fetch(cycle, 0.5)
|
|
62
109
|
end
|
|
63
110
|
end
|
|
64
111
|
|
|
65
|
-
def smooth(pattern)
|
|
112
|
+
def smooth(pattern, interpolator: nil, &block)
|
|
66
113
|
source = Pattern.ensure_pattern(pattern)
|
|
67
114
|
return source if source.continuous?
|
|
68
115
|
|
|
116
|
+
interpolation = block || interpolator
|
|
117
|
+
cache = {}
|
|
69
118
|
Pattern.continuous do |time|
|
|
70
|
-
|
|
119
|
+
rational_time = Pattern.to_rational(time)
|
|
120
|
+
cache.fetch(rational_time) do
|
|
121
|
+
cache[rational_time] = interpolate(source, rational_time, interpolation)
|
|
122
|
+
cache.shift if cache.length > 256
|
|
123
|
+
cache[rational_time]
|
|
124
|
+
end
|
|
71
125
|
end
|
|
72
126
|
end
|
|
73
127
|
|
|
@@ -81,7 +135,51 @@ module Cyclotone
|
|
|
81
135
|
end
|
|
82
136
|
private_class_method :phase
|
|
83
137
|
|
|
84
|
-
def
|
|
138
|
+
def oscillator(freq:, phase_offset:, bipolar:)
|
|
139
|
+
normalized_freq = Pattern.to_rational(freq)
|
|
140
|
+
raise ArgumentError, "oscillator frequency must be positive" unless normalized_freq.positive?
|
|
141
|
+
|
|
142
|
+
offset = Pattern.to_rational(phase_offset)
|
|
143
|
+
Pattern.continuous do |time|
|
|
144
|
+
value = yield((time * normalized_freq) + offset)
|
|
145
|
+
bipolar ? (value * 2.0) - 1.0 : value
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
private_class_method :oscillator
|
|
149
|
+
|
|
150
|
+
def normalize_range_value(value, mode)
|
|
151
|
+
case mode.to_sym
|
|
152
|
+
when :raw
|
|
153
|
+
value
|
|
154
|
+
when :clamp
|
|
155
|
+
value.clamp(0.0, 1.0)
|
|
156
|
+
when :wrap
|
|
157
|
+
value % 1.0
|
|
158
|
+
when :fold
|
|
159
|
+
folded = value % 2.0
|
|
160
|
+
folded > 1.0 ? 2.0 - folded : folded
|
|
161
|
+
else
|
|
162
|
+
raise ArgumentError, "unknown range mode #{mode}"
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
private_class_method :normalize_range_value
|
|
166
|
+
|
|
167
|
+
def positive_integer(value, label)
|
|
168
|
+
normalized = Integer(value)
|
|
169
|
+
raise ArgumentError, "#{label} must be positive" unless normalized.positive?
|
|
170
|
+
|
|
171
|
+
normalized
|
|
172
|
+
rescue ArgumentError, TypeError => error
|
|
173
|
+
raise ArgumentError, "invalid #{label}: #{error.message}"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def brownian_step(value, cycle, step)
|
|
177
|
+
delta = (Support::Deterministic.float(:brownian, cycle) * 2.0) - 1.0
|
|
178
|
+
(value + (delta * step)).clamp(0.0, 1.0)
|
|
179
|
+
end
|
|
180
|
+
private_class_method :brownian_step
|
|
181
|
+
|
|
182
|
+
def interpolate(source, time, interpolation)
|
|
85
183
|
anchors = anchors_for(source, time)
|
|
86
184
|
return source.query_point(time) if anchors.empty?
|
|
87
185
|
|
|
@@ -89,8 +187,8 @@ module Cyclotone
|
|
|
89
187
|
right = anchors.find { |anchor| anchor[:time] >= time } || anchors.last
|
|
90
188
|
return left[:value] if left[:time] == right[:time]
|
|
91
189
|
|
|
92
|
-
amount = (time - left[:time]).to_f / (right[:time] - left[:time])
|
|
93
|
-
interpolate_value(left[:value], right[:value], amount)
|
|
190
|
+
amount = (time - left[:time]).to_f / (right[:time] - left[:time])
|
|
191
|
+
interpolate_value(left[:value], right[:value], amount, interpolation)
|
|
94
192
|
end
|
|
95
193
|
private_class_method :interpolate
|
|
96
194
|
|
|
@@ -105,25 +203,29 @@ module Cyclotone
|
|
|
105
203
|
end
|
|
106
204
|
private_class_method :anchors_for
|
|
107
205
|
|
|
108
|
-
def interpolate_value(left, right, amount)
|
|
206
|
+
def interpolate_value(left, right, amount, interpolation = nil)
|
|
109
207
|
if left.is_a?(Numeric) && right.is_a?(Numeric)
|
|
110
208
|
left.to_f + ((right.to_f - left.to_f) * amount)
|
|
111
209
|
elsif left.is_a?(Hash) && right.is_a?(Hash)
|
|
112
|
-
interpolate_hash(left, right, amount)
|
|
210
|
+
interpolate_hash(left, right, amount, interpolation)
|
|
211
|
+
elsif interpolation
|
|
212
|
+
interpolation.call(left, right, amount)
|
|
113
213
|
else
|
|
114
214
|
amount >= 0.5 ? right : left
|
|
115
215
|
end
|
|
116
216
|
end
|
|
117
217
|
private_class_method :interpolate_value
|
|
118
218
|
|
|
119
|
-
def interpolate_hash(left, right, amount)
|
|
120
|
-
(left.keys | right.keys).
|
|
121
|
-
|
|
219
|
+
def interpolate_hash(left, right, amount, interpolation)
|
|
220
|
+
(left.keys | right.keys).to_h do |key|
|
|
221
|
+
value =
|
|
122
222
|
if left.key?(key) && right.key?(key)
|
|
123
|
-
interpolate_value(left[key], right[key], amount)
|
|
223
|
+
interpolate_value(left[key], right[key], amount, interpolation)
|
|
124
224
|
else
|
|
125
225
|
amount >= 0.5 ? right.fetch(key, left[key]) : left.fetch(key, right[key])
|
|
126
226
|
end
|
|
227
|
+
|
|
228
|
+
[key, value]
|
|
127
229
|
end
|
|
128
230
|
end
|
|
129
231
|
private_class_method :interpolate_hash
|
data/lib/cyclotone/pattern.rb
CHANGED
|
@@ -10,6 +10,9 @@ module Cyclotone
|
|
|
10
10
|
include Transforms::Sample
|
|
11
11
|
|
|
12
12
|
SAMPLE_EPSILON = Rational(1, 1024)
|
|
13
|
+
CACHE_LIMIT = 128
|
|
14
|
+
CACHE_MUTEX = Mutex.new
|
|
15
|
+
COMPILER_MUTEX = Mutex.new
|
|
13
16
|
|
|
14
17
|
attr_reader :query
|
|
15
18
|
|
|
@@ -26,12 +29,17 @@ module Cyclotone
|
|
|
26
29
|
end
|
|
27
30
|
|
|
28
31
|
def query_span(span)
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
span = self.class.coerce_span(span)
|
|
33
|
+
emitted_cycle_span = false
|
|
34
|
+
events = []
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
span.each_cycle_span do |cycle_span|
|
|
37
|
+
emitted_cycle_span = true
|
|
38
|
+
events.concat(query.call(cycle_span))
|
|
34
39
|
end
|
|
40
|
+
|
|
41
|
+
events.concat(query.call(span)) if !emitted_cycle_span && continuous?
|
|
42
|
+
self.class.sort_events(events)
|
|
35
43
|
end
|
|
36
44
|
|
|
37
45
|
def query_cycle(cycle_number)
|
|
@@ -39,13 +47,23 @@ module Cyclotone
|
|
|
39
47
|
end
|
|
40
48
|
|
|
41
49
|
def query_event_at(time)
|
|
42
|
-
|
|
50
|
+
sample_time = self.class.to_rational(time)
|
|
51
|
+
query_span(TimeSpan.new(sample_time, sample_time + self.class.sample_epsilon)).find do |event|
|
|
52
|
+
event.covers_time?(sample_time)
|
|
53
|
+
end
|
|
43
54
|
end
|
|
44
55
|
|
|
45
56
|
def query_point(time)
|
|
46
57
|
query_event_at(time)&.value
|
|
47
58
|
end
|
|
48
59
|
|
|
60
|
+
def query_points(time)
|
|
61
|
+
sample_time = self.class.to_rational(time)
|
|
62
|
+
query_span(TimeSpan.new(sample_time, sample_time + self.class.sample_epsilon)).select do |event|
|
|
63
|
+
event.covers_time?(sample_time)
|
|
64
|
+
end.map(&:value)
|
|
65
|
+
end
|
|
66
|
+
|
|
49
67
|
def fmap(&transform)
|
|
50
68
|
raise ArgumentError, "fmap requires a block" unless transform
|
|
51
69
|
|
|
@@ -94,12 +112,24 @@ module Cyclotone
|
|
|
94
112
|
other_pattern = self.class.ensure_pattern(other)
|
|
95
113
|
|
|
96
114
|
Pattern.new do |span|
|
|
97
|
-
left_events = query_span(span)
|
|
115
|
+
left_events = query_span(span).sort_by { |event| event.active_span.start }
|
|
98
116
|
right_events = other_pattern.query_span(span)
|
|
117
|
+
right_events_by_start = right_events.sort_by { |event| event.active_span.start }
|
|
118
|
+
first_candidate = 0
|
|
99
119
|
|
|
100
120
|
left_events.flat_map do |left_event|
|
|
101
|
-
|
|
102
|
-
|
|
121
|
+
left_span = left_event.active_span
|
|
122
|
+
left_stop = left_span.stop
|
|
123
|
+
|
|
124
|
+
while first_candidate < right_events_by_start.length && right_events_by_start[first_candidate].active_span.stop <= left_span.start
|
|
125
|
+
first_candidate += 1
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
first_candidate.upto(right_events_by_start.length - 1).filter_map do |index|
|
|
129
|
+
right_event = right_events_by_start.fetch(index)
|
|
130
|
+
break [] if right_event.active_span.start >= left_stop
|
|
131
|
+
|
|
132
|
+
overlap = left_span.intersection(right_event.active_span)
|
|
103
133
|
next unless overlap
|
|
104
134
|
|
|
105
135
|
part = overlap.intersection(span)
|
|
@@ -138,14 +168,24 @@ module Cyclotone
|
|
|
138
168
|
end
|
|
139
169
|
|
|
140
170
|
def merge(other)
|
|
171
|
+
merge_right(other)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def merge_left(other)
|
|
141
175
|
combine_left(other) do |left, right|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
176
|
+
merge_values(right, left)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def merge_right(other)
|
|
181
|
+
combine_left(other) do |left, right|
|
|
182
|
+
merge_values(left, right)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def merge_deep(other)
|
|
187
|
+
combine_left(other) do |left, right|
|
|
188
|
+
deep_merge_values(left, right)
|
|
149
189
|
end
|
|
150
190
|
end
|
|
151
191
|
|
|
@@ -180,18 +220,39 @@ module Cyclotone
|
|
|
180
220
|
|
|
181
221
|
alias atom pure
|
|
182
222
|
|
|
223
|
+
def atom_at(value, at:, duration: 0)
|
|
224
|
+
onset_offset = to_rational(at)
|
|
225
|
+
event_duration = to_rational(duration)
|
|
226
|
+
raise ArgumentError, "atom duration must be non-negative" if event_duration.negative?
|
|
227
|
+
|
|
228
|
+
Pattern.new do |span|
|
|
229
|
+
cycle_start = Rational(span.cycle_number)
|
|
230
|
+
onset = cycle_start + onset_offset
|
|
231
|
+
whole = TimeSpan.new(onset, onset + event_duration)
|
|
232
|
+
trigger_span = TimeSpan.new(onset, onset + sample_epsilon)
|
|
233
|
+
part = span.intersection(trigger_span)
|
|
234
|
+
|
|
235
|
+
part ? [Event.new(whole: whole, part: part, value: value)] : []
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
183
239
|
def silence
|
|
184
240
|
Pattern.new { |_span| [] }
|
|
185
241
|
end
|
|
186
242
|
|
|
187
|
-
def continuous(&sampler)
|
|
243
|
+
def continuous(sample: :midpoint, &sampler)
|
|
244
|
+
raise ArgumentError, "continuous requires a sampler block" unless sampler
|
|
245
|
+
|
|
188
246
|
Pattern.new(continuous: true) do |span|
|
|
189
|
-
[Event.new(whole: nil, part: span, value: sampler.call(span
|
|
247
|
+
[Event.new(whole: nil, part: span, value: sampler.call(sample_time(span, sample)))]
|
|
190
248
|
end
|
|
191
249
|
end
|
|
192
250
|
|
|
193
|
-
def ensure_pattern(value)
|
|
194
|
-
value.is_a?(Pattern)
|
|
251
|
+
def ensure_pattern(value, strings: :literal)
|
|
252
|
+
return value if value.is_a?(Pattern)
|
|
253
|
+
return mn(value) if value.is_a?(String) && strings == :mini_notation
|
|
254
|
+
|
|
255
|
+
pure(value)
|
|
195
256
|
end
|
|
196
257
|
|
|
197
258
|
def to_rational(value)
|
|
@@ -199,21 +260,28 @@ module Cyclotone
|
|
|
199
260
|
return Rational(value, 1) if value.is_a?(Integer)
|
|
200
261
|
|
|
201
262
|
Rational(value.to_s)
|
|
263
|
+
rescue ArgumentError, TypeError => error
|
|
264
|
+
raise InvalidRationalError, "invalid rational value #{value.inspect}: #{error.message}"
|
|
202
265
|
end
|
|
203
266
|
|
|
204
267
|
def timecat(weighted_patterns)
|
|
205
|
-
normalized = Array(weighted_patterns)
|
|
268
|
+
normalized = Array(weighted_patterns).map do |weight, pattern|
|
|
269
|
+
normalized_weight = to_rational(weight)
|
|
270
|
+
raise ArgumentError, "timecat weights must be positive" unless normalized_weight.positive?
|
|
271
|
+
|
|
272
|
+
[normalized_weight, pattern]
|
|
273
|
+
end
|
|
206
274
|
raise ArgumentError, "timecat requires patterns" if normalized.empty?
|
|
207
275
|
|
|
208
|
-
total_weight = normalized.sum { |weight, _pattern|
|
|
276
|
+
total_weight = normalized.sum { |weight, _pattern| weight }
|
|
277
|
+
raise ArgumentError, "timecat total weight must be positive" unless total_weight.positive?
|
|
209
278
|
|
|
210
279
|
Pattern.new do |span|
|
|
211
280
|
cycle_start = Rational(span.cycle_number)
|
|
212
281
|
cursor = cycle_start
|
|
213
282
|
|
|
214
283
|
normalized.flat_map do |weight, pattern|
|
|
215
|
-
|
|
216
|
-
segment_length = normalized_weight / total_weight
|
|
284
|
+
segment_length = weight / total_weight
|
|
217
285
|
segment_span = TimeSpan.new(cursor, cursor + segment_length)
|
|
218
286
|
overlap = span.intersection(segment_span)
|
|
219
287
|
cursor += segment_length
|
|
@@ -255,11 +323,12 @@ module Cyclotone
|
|
|
255
323
|
end
|
|
256
324
|
end
|
|
257
325
|
|
|
258
|
-
def randcat(patterns)
|
|
326
|
+
def randcat(patterns, namespace: :randcat)
|
|
259
327
|
normalized = Array(patterns)
|
|
328
|
+
raise ArgumentError, "randcat requires patterns" if normalized.empty?
|
|
260
329
|
|
|
261
330
|
Pattern.new do |span|
|
|
262
|
-
index = Support::Deterministic.int(normalized.length,
|
|
331
|
+
index = Support::Deterministic.int(normalized.length, namespace, span.cycle_number)
|
|
263
332
|
ensure_pattern(normalized[index]).query_span(span)
|
|
264
333
|
end
|
|
265
334
|
end
|
|
@@ -272,9 +341,13 @@ module Cyclotone
|
|
|
272
341
|
fastcat([first, second])
|
|
273
342
|
end
|
|
274
343
|
|
|
275
|
-
def stack(patterns)
|
|
344
|
+
def stack(patterns, empty: :error)
|
|
276
345
|
normalized_patterns = Array(patterns).map { |pattern| ensure_pattern(pattern) }
|
|
277
|
-
|
|
346
|
+
if normalized_patterns.empty?
|
|
347
|
+
return silence if empty == :silence
|
|
348
|
+
|
|
349
|
+
raise ArgumentError, "stack requires at least one pattern"
|
|
350
|
+
end
|
|
278
351
|
|
|
279
352
|
Pattern.new do |span|
|
|
280
353
|
normalized_patterns.flat_map { |pattern| pattern.query_span(span) }.then do |events|
|
|
@@ -283,28 +356,69 @@ module Cyclotone
|
|
|
283
356
|
end
|
|
284
357
|
end
|
|
285
358
|
|
|
359
|
+
def stack_or_silence(patterns)
|
|
360
|
+
stack(patterns, empty: :silence)
|
|
361
|
+
end
|
|
362
|
+
|
|
286
363
|
def overlay(first, second)
|
|
287
364
|
stack([first, second])
|
|
288
365
|
end
|
|
289
366
|
|
|
290
367
|
def mn(string)
|
|
291
|
-
|
|
368
|
+
source = string.to_s
|
|
369
|
+
cached_pattern(source) { compiler.compile(parser.parse(source)) }
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def mn!(string)
|
|
373
|
+
mn(string)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def try_mn(string)
|
|
377
|
+
mn(string)
|
|
378
|
+
rescue ParseError, ArgumentError
|
|
379
|
+
nil
|
|
292
380
|
end
|
|
293
381
|
|
|
294
382
|
def parser
|
|
295
|
-
|
|
383
|
+
MiniNotation::Parser.new
|
|
296
384
|
end
|
|
297
385
|
|
|
298
386
|
def compiler
|
|
299
|
-
@compiler
|
|
387
|
+
return @compiler if @compiler
|
|
388
|
+
|
|
389
|
+
COMPILER_MUTEX.synchronize do
|
|
390
|
+
@compiler ||= MiniNotation::Compiler.new
|
|
391
|
+
end
|
|
300
392
|
end
|
|
301
393
|
|
|
302
394
|
def sort_events(events)
|
|
303
395
|
events.sort_by do |event|
|
|
304
|
-
[
|
|
396
|
+
[
|
|
397
|
+
event.onset || event.part.start,
|
|
398
|
+
event.offset || event.part.stop,
|
|
399
|
+
Support::Deterministic.canonical_key(event.value)
|
|
400
|
+
]
|
|
305
401
|
end
|
|
306
402
|
end
|
|
307
403
|
|
|
404
|
+
def coerce_span(value)
|
|
405
|
+
return value if value.is_a?(TimeSpan)
|
|
406
|
+
return TimeSpan.new(value.fetch(0), value.fetch(1)) if value.respond_to?(:fetch)
|
|
407
|
+
|
|
408
|
+
raise ArgumentError, "expected TimeSpan or [start, stop], got #{value.class}"
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def sample_epsilon
|
|
412
|
+
@sample_epsilon ||= SAMPLE_EPSILON
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def sample_epsilon=(value)
|
|
416
|
+
normalized = to_rational(value)
|
|
417
|
+
raise ArgumentError, "sample epsilon must be positive" unless normalized.positive?
|
|
418
|
+
|
|
419
|
+
@sample_epsilon = normalized
|
|
420
|
+
end
|
|
421
|
+
|
|
308
422
|
def map_span(span, &block)
|
|
309
423
|
return nil unless span
|
|
310
424
|
|
|
@@ -323,6 +437,38 @@ module Cyclotone
|
|
|
323
437
|
offset = to_rational(amount)
|
|
324
438
|
map_event(event) { |time| time + offset }
|
|
325
439
|
end
|
|
440
|
+
|
|
441
|
+
private
|
|
442
|
+
|
|
443
|
+
def sample_time(span, sample)
|
|
444
|
+
case sample
|
|
445
|
+
when :begin, :start
|
|
446
|
+
span.start
|
|
447
|
+
when :end, :stop
|
|
448
|
+
span.stop
|
|
449
|
+
when :midpoint, :center
|
|
450
|
+
span.midpoint
|
|
451
|
+
else
|
|
452
|
+
raise ArgumentError, "unknown continuous sample point #{sample.inspect}"
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def cached_pattern(source)
|
|
457
|
+
CACHE_MUTEX.synchronize do
|
|
458
|
+
@mn_cache ||= {}
|
|
459
|
+
@mn_cache_order ||= []
|
|
460
|
+
|
|
461
|
+
return @mn_cache[source] if @mn_cache.key?(source)
|
|
462
|
+
|
|
463
|
+
pattern = yield
|
|
464
|
+
@mn_cache[source] = pattern
|
|
465
|
+
@mn_cache_order << source
|
|
466
|
+
|
|
467
|
+
@mn_cache.delete(@mn_cache_order.shift) while @mn_cache_order.length > CACHE_LIMIT
|
|
468
|
+
|
|
469
|
+
pattern
|
|
470
|
+
end
|
|
471
|
+
end
|
|
326
472
|
end
|
|
327
473
|
|
|
328
474
|
private
|
|
@@ -339,23 +485,52 @@ module Cyclotone
|
|
|
339
485
|
end
|
|
340
486
|
|
|
341
487
|
def combine_scalar(left, right, operator)
|
|
488
|
+
return left if right.nil?
|
|
489
|
+
|
|
342
490
|
if left.is_a?(Hash) && right.is_a?(Hash)
|
|
343
491
|
keys = left.keys | right.keys
|
|
344
|
-
keys.
|
|
345
|
-
|
|
346
|
-
if left.key?(key) && right.key?(key) &&
|
|
492
|
+
keys.to_h do |key|
|
|
493
|
+
value =
|
|
494
|
+
if left.key?(key) && right.key?(key) && right[key].nil?
|
|
495
|
+
left[key]
|
|
496
|
+
elsif left.key?(key) && right.key?(key) && left[key].respond_to?(operator)
|
|
347
497
|
left[key].public_send(operator, right[key])
|
|
348
498
|
else
|
|
349
499
|
right.fetch(key, left[key])
|
|
350
500
|
end
|
|
501
|
+
|
|
502
|
+
[key, value]
|
|
351
503
|
end
|
|
352
|
-
elsif right.nil?
|
|
353
|
-
left
|
|
354
504
|
elsif left.respond_to?(operator)
|
|
355
505
|
left.public_send(operator, right)
|
|
356
506
|
else
|
|
357
507
|
left
|
|
358
508
|
end
|
|
359
509
|
end
|
|
510
|
+
|
|
511
|
+
def merge_values(left, right)
|
|
512
|
+
if left.is_a?(Hash) && right.is_a?(Hash)
|
|
513
|
+
left.merge(right.compact)
|
|
514
|
+
elsif right.nil?
|
|
515
|
+
left
|
|
516
|
+
else
|
|
517
|
+
right
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def deep_merge_values(left, right)
|
|
522
|
+
return left if right.nil?
|
|
523
|
+
return right unless left.is_a?(Hash) && right.is_a?(Hash)
|
|
524
|
+
|
|
525
|
+
right.each_with_object(left.dup) do |(key, value), merged|
|
|
526
|
+
next if value.nil?
|
|
527
|
+
|
|
528
|
+
merged[key] = if merged[key].is_a?(Hash) && value.is_a?(Hash)
|
|
529
|
+
deep_merge_values(merged[key], value)
|
|
530
|
+
else
|
|
531
|
+
value
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
end
|
|
360
535
|
end
|
|
361
536
|
end
|