cyclotone 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +121 -0
- data/Rakefile +12 -0
- data/cyclotone.gemspec +29 -0
- data/exe/cyclotone +12 -0
- data/lib/cyclotone/backends/midi_backend.rb +114 -0
- data/lib/cyclotone/backends/midi_file_backend.rb +178 -0
- data/lib/cyclotone/backends/midi_message_support.rb +80 -0
- data/lib/cyclotone/backends/osc_backend.rb +117 -0
- data/lib/cyclotone/controls.rb +142 -0
- data/lib/cyclotone/dsl.rb +141 -0
- data/lib/cyclotone/errors.rb +27 -0
- data/lib/cyclotone/euclidean.rb +65 -0
- data/lib/cyclotone/event.rb +65 -0
- data/lib/cyclotone/harmony.rb +159 -0
- data/lib/cyclotone/mini_notation/ast.rb +199 -0
- data/lib/cyclotone/mini_notation/compiler.rb +115 -0
- data/lib/cyclotone/mini_notation/parser.rb +350 -0
- data/lib/cyclotone/oscillators.rb +131 -0
- data/lib/cyclotone/pattern.rb +361 -0
- data/lib/cyclotone/scheduler.rb +168 -0
- data/lib/cyclotone/state.rb +49 -0
- data/lib/cyclotone/stream.rb +185 -0
- data/lib/cyclotone/support/deterministic.rb +42 -0
- data/lib/cyclotone/time_span.rb +99 -0
- data/lib/cyclotone/transforms/accumulation.rb +45 -0
- data/lib/cyclotone/transforms/alteration.rb +173 -0
- data/lib/cyclotone/transforms/concatenation.rb +15 -0
- data/lib/cyclotone/transforms/condition.rb +63 -0
- data/lib/cyclotone/transforms/sample.rb +82 -0
- data/lib/cyclotone/transforms/time.rb +93 -0
- data/lib/cyclotone/transition.rb +204 -0
- data/lib/cyclotone/version.rb +5 -0
- data/lib/cyclotone.rb +32 -0
- metadata +79 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cyclotone
|
|
4
|
+
module Oscillators
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def sine
|
|
8
|
+
Pattern.continuous { |time| (Math.sin(phase(time)) + 1.0) / 2.0 }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def cosine
|
|
12
|
+
Pattern.continuous { |time| (Math.cos(phase(time)) + 1.0) / 2.0 }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def tri
|
|
16
|
+
Pattern.continuous do |time|
|
|
17
|
+
position = cycle_position(time)
|
|
18
|
+
position < 0.5 ? position * 2.0 : (1.0 - position) * 2.0
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def saw
|
|
23
|
+
Pattern.continuous { |time| cycle_position(time) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def isaw
|
|
27
|
+
Pattern.continuous { |time| 1.0 - cycle_position(time) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def square
|
|
31
|
+
Pattern.continuous { |time| cycle_position(time) < 0.5 ? 0.0 : 1.0 }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def rand
|
|
35
|
+
Pattern.continuous do |time|
|
|
36
|
+
cycle = time.floor
|
|
37
|
+
step = ((cycle_position(time) * 128).floor).to_i
|
|
38
|
+
Support::Deterministic.float(:rand, cycle, step)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def irand(maximum)
|
|
43
|
+
rand.fmap { |value| (value * maximum.to_i).floor }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def perlin
|
|
47
|
+
Pattern.continuous do |time|
|
|
48
|
+
left = time.floor
|
|
49
|
+
right = left + 1
|
|
50
|
+
amount = cycle_position(time)
|
|
51
|
+
smooth = amount * amount * (3.0 - (2.0 * amount))
|
|
52
|
+
left_value = Support::Deterministic.float(:perlin, left)
|
|
53
|
+
right_value = Support::Deterministic.float(:perlin, right)
|
|
54
|
+
|
|
55
|
+
left_value + ((right_value - left_value) * smooth)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def range(low, high, pattern)
|
|
60
|
+
Pattern.ensure_pattern(pattern).fmap do |value|
|
|
61
|
+
low.to_f + ((high.to_f - low.to_f) * value.to_f)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def smooth(pattern)
|
|
66
|
+
source = Pattern.ensure_pattern(pattern)
|
|
67
|
+
return source if source.continuous?
|
|
68
|
+
|
|
69
|
+
Pattern.continuous do |time|
|
|
70
|
+
interpolate(source, Pattern.to_rational(time))
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def cycle_position(time)
|
|
75
|
+
time.to_f - time.floor
|
|
76
|
+
end
|
|
77
|
+
private_class_method :cycle_position
|
|
78
|
+
|
|
79
|
+
def phase(time)
|
|
80
|
+
cycle_position(time) * Math::PI * 2.0
|
|
81
|
+
end
|
|
82
|
+
private_class_method :phase
|
|
83
|
+
|
|
84
|
+
def interpolate(source, time)
|
|
85
|
+
anchors = anchors_for(source, time)
|
|
86
|
+
return source.query_point(time) if anchors.empty?
|
|
87
|
+
|
|
88
|
+
left = anchors.reverse.find { |anchor| anchor[:time] <= time } || anchors.first
|
|
89
|
+
right = anchors.find { |anchor| anchor[:time] >= time } || anchors.last
|
|
90
|
+
return left[:value] if left[:time] == right[:time]
|
|
91
|
+
|
|
92
|
+
amount = (time - left[:time]).to_f / (right[:time] - left[:time]).to_f
|
|
93
|
+
interpolate_value(left[:value], right[:value], amount)
|
|
94
|
+
end
|
|
95
|
+
private_class_method :interpolate
|
|
96
|
+
|
|
97
|
+
def anchors_for(source, time)
|
|
98
|
+
window = TimeSpan.new(time.floor - 1, time.floor + 2)
|
|
99
|
+
|
|
100
|
+
source.query_span(window).each_with_object([]) do |event, anchors|
|
|
101
|
+
next if event.part.duration.zero?
|
|
102
|
+
|
|
103
|
+
anchors << { time: event.part.midpoint, value: event.value }
|
|
104
|
+
end.sort_by { |anchor| anchor[:time] }
|
|
105
|
+
end
|
|
106
|
+
private_class_method :anchors_for
|
|
107
|
+
|
|
108
|
+
def interpolate_value(left, right, amount)
|
|
109
|
+
if left.is_a?(Numeric) && right.is_a?(Numeric)
|
|
110
|
+
left.to_f + ((right.to_f - left.to_f) * amount)
|
|
111
|
+
elsif left.is_a?(Hash) && right.is_a?(Hash)
|
|
112
|
+
interpolate_hash(left, right, amount)
|
|
113
|
+
else
|
|
114
|
+
amount >= 0.5 ? right : left
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
private_class_method :interpolate_value
|
|
118
|
+
|
|
119
|
+
def interpolate_hash(left, right, amount)
|
|
120
|
+
(left.keys | right.keys).each_with_object({}) do |key, result|
|
|
121
|
+
result[key] =
|
|
122
|
+
if left.key?(key) && right.key?(key)
|
|
123
|
+
interpolate_value(left[key], right[key], amount)
|
|
124
|
+
else
|
|
125
|
+
amount >= 0.5 ? right.fetch(key, left[key]) : left.fetch(key, right[key])
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
private_class_method :interpolate_hash
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cyclotone
|
|
4
|
+
class Pattern
|
|
5
|
+
include Transforms::Time
|
|
6
|
+
include Transforms::Concatenation
|
|
7
|
+
include Transforms::Accumulation
|
|
8
|
+
include Transforms::Alteration
|
|
9
|
+
include Transforms::Condition
|
|
10
|
+
include Transforms::Sample
|
|
11
|
+
|
|
12
|
+
SAMPLE_EPSILON = Rational(1, 1024)
|
|
13
|
+
|
|
14
|
+
attr_reader :query
|
|
15
|
+
|
|
16
|
+
def initialize(continuous: false, &query_func)
|
|
17
|
+
raise ArgumentError, "Pattern requires a query block" unless query_func
|
|
18
|
+
|
|
19
|
+
@continuous = continuous
|
|
20
|
+
@query = query_func
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def continuous?
|
|
25
|
+
@continuous
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def query_span(span)
|
|
29
|
+
cycle_spans = span.cycle_spans
|
|
30
|
+
cycle_spans = [span] if cycle_spans.empty? && continuous?
|
|
31
|
+
|
|
32
|
+
cycle_spans.flat_map { |cycle_span| query.call(cycle_span) }.then do |events|
|
|
33
|
+
self.class.sort_events(events)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def query_cycle(cycle_number)
|
|
38
|
+
query_span(TimeSpan.new(cycle_number, Rational(cycle_number) + 1))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def query_event_at(time)
|
|
42
|
+
query_span(TimeSpan.new(time, time + SAMPLE_EPSILON)).find { |event| event.covers_time?(time) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def query_point(time)
|
|
46
|
+
query_event_at(time)&.value
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def fmap(&transform)
|
|
50
|
+
raise ArgumentError, "fmap requires a block" unless transform
|
|
51
|
+
|
|
52
|
+
Pattern.new(continuous: continuous?) do |span|
|
|
53
|
+
query_span(span).map { |event| event.with_value(transform.call(event.value)) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def map_events(&transform)
|
|
58
|
+
Pattern.new(continuous: continuous?) do |span|
|
|
59
|
+
query_span(span).map { |event| transform.call(event) }.compact
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def flat_map_events(&transform)
|
|
64
|
+
Pattern.new(continuous: continuous?) do |span|
|
|
65
|
+
query_span(span).flat_map { |event| Array(transform.call(event)) }.compact
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def select_events(&predicate)
|
|
70
|
+
Pattern.new(continuous: continuous?) do |span|
|
|
71
|
+
query_span(span).select { |event| predicate.call(event) }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def combine_left(other, &operation)
|
|
76
|
+
other_pattern = self.class.ensure_pattern(other)
|
|
77
|
+
|
|
78
|
+
Pattern.new(continuous: continuous?) do |span|
|
|
79
|
+
query_span(span).map do |event|
|
|
80
|
+
time = event.onset || event.part.start
|
|
81
|
+
other_value = other_pattern.query_point(time)
|
|
82
|
+
event.with_value(operation.call(event.value, other_value))
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def combine_right(other, &operation)
|
|
88
|
+
self.class.ensure_pattern(other).combine_left(self) do |right_value, left_value|
|
|
89
|
+
operation.call(left_value, right_value)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def combine_both(other, &operation)
|
|
94
|
+
other_pattern = self.class.ensure_pattern(other)
|
|
95
|
+
|
|
96
|
+
Pattern.new do |span|
|
|
97
|
+
left_events = query_span(span)
|
|
98
|
+
right_events = other_pattern.query_span(span)
|
|
99
|
+
|
|
100
|
+
left_events.flat_map do |left_event|
|
|
101
|
+
right_events.filter_map do |right_event|
|
|
102
|
+
overlap = left_event.active_span.intersection(right_event.active_span)
|
|
103
|
+
next unless overlap
|
|
104
|
+
|
|
105
|
+
part = overlap.intersection(span)
|
|
106
|
+
next unless part
|
|
107
|
+
|
|
108
|
+
whole = left_event.whole && right_event.whole ? left_event.whole.intersection(right_event.whole) : nil
|
|
109
|
+
|
|
110
|
+
Event.new(
|
|
111
|
+
whole: whole,
|
|
112
|
+
part: part,
|
|
113
|
+
value: operation.call(left_event.value, right_event.value)
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def add(other, structure: :both)
|
|
121
|
+
apply_binary_operation(other, structure: structure) { |left, right| combine_scalar(left, right, :+) }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def sub(other, structure: :both)
|
|
125
|
+
apply_binary_operation(other, structure: structure) { |left, right| combine_scalar(left, right, :-) }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def mul(other, structure: :both)
|
|
129
|
+
apply_binary_operation(other, structure: structure) { |left, right| combine_scalar(left, right, :*) }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def div(other, structure: :both)
|
|
133
|
+
apply_binary_operation(other, structure: structure) { |left, right| combine_scalar(left, right, :/) }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def mod(other, structure: :both)
|
|
137
|
+
apply_binary_operation(other, structure: structure) { |left, right| combine_scalar(left, right, :%) }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def merge(other)
|
|
141
|
+
combine_left(other) do |left, right|
|
|
142
|
+
if left.is_a?(Hash) && right.is_a?(Hash)
|
|
143
|
+
left.merge(right)
|
|
144
|
+
elsif right.nil?
|
|
145
|
+
left
|
|
146
|
+
else
|
|
147
|
+
right
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def +(other)
|
|
153
|
+
add(other)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def -(other)
|
|
157
|
+
sub(other)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def *(other)
|
|
161
|
+
mul(other)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def /(other)
|
|
165
|
+
div(other)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def %(other)
|
|
169
|
+
mod(other)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
class << self
|
|
173
|
+
def pure(value)
|
|
174
|
+
Pattern.new do |span|
|
|
175
|
+
cycle_start = Rational(span.cycle_number)
|
|
176
|
+
whole = TimeSpan.new(cycle_start, cycle_start + 1)
|
|
177
|
+
[Event.new(whole: whole, part: span, value: value)]
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
alias atom pure
|
|
182
|
+
|
|
183
|
+
def silence
|
|
184
|
+
Pattern.new { |_span| [] }
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def continuous(&sampler)
|
|
188
|
+
Pattern.new(continuous: true) do |span|
|
|
189
|
+
[Event.new(whole: nil, part: span, value: sampler.call(span.midpoint))]
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def ensure_pattern(value)
|
|
194
|
+
value.is_a?(Pattern) ? value : pure(value)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def to_rational(value)
|
|
198
|
+
return value if value.is_a?(Rational)
|
|
199
|
+
return Rational(value, 1) if value.is_a?(Integer)
|
|
200
|
+
|
|
201
|
+
Rational(value.to_s)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def timecat(weighted_patterns)
|
|
205
|
+
normalized = Array(weighted_patterns)
|
|
206
|
+
raise ArgumentError, "timecat requires patterns" if normalized.empty?
|
|
207
|
+
|
|
208
|
+
total_weight = normalized.sum { |weight, _pattern| to_rational(weight) }
|
|
209
|
+
|
|
210
|
+
Pattern.new do |span|
|
|
211
|
+
cycle_start = Rational(span.cycle_number)
|
|
212
|
+
cursor = cycle_start
|
|
213
|
+
|
|
214
|
+
normalized.flat_map do |weight, pattern|
|
|
215
|
+
normalized_weight = to_rational(weight)
|
|
216
|
+
segment_length = normalized_weight / total_weight
|
|
217
|
+
segment_span = TimeSpan.new(cursor, cursor + segment_length)
|
|
218
|
+
overlap = span.intersection(segment_span)
|
|
219
|
+
cursor += segment_length
|
|
220
|
+
|
|
221
|
+
next [] unless overlap
|
|
222
|
+
|
|
223
|
+
local_span = TimeSpan.new(
|
|
224
|
+
(overlap.start - segment_span.start) / segment_length,
|
|
225
|
+
(overlap.stop - segment_span.start) / segment_length
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
ensure_pattern(pattern).query_span(local_span).map do |event|
|
|
229
|
+
map_event(event) do |time|
|
|
230
|
+
segment_span.start + (time * segment_length)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def fastcat(patterns)
|
|
238
|
+
timecat(Array(patterns).map { |pattern| [1, pattern] })
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def cat(patterns)
|
|
242
|
+
normalized = Array(patterns)
|
|
243
|
+
raise ArgumentError, "cat requires patterns" if normalized.empty?
|
|
244
|
+
|
|
245
|
+
Pattern.new do |span|
|
|
246
|
+
cycle = span.cycle_number
|
|
247
|
+
index = cycle % normalized.length
|
|
248
|
+
local_cycle = cycle / normalized.length
|
|
249
|
+
cycle_offset = cycle - local_cycle
|
|
250
|
+
local_span = span.shift(-cycle_offset)
|
|
251
|
+
|
|
252
|
+
ensure_pattern(normalized[index]).query_span(local_span).map do |event|
|
|
253
|
+
shift_event(event, cycle_offset)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def randcat(patterns)
|
|
259
|
+
normalized = Array(patterns)
|
|
260
|
+
|
|
261
|
+
Pattern.new do |span|
|
|
262
|
+
index = Support::Deterministic.int(normalized.length, :randcat, span.cycle_number)
|
|
263
|
+
ensure_pattern(normalized[index]).query_span(span)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def append(first, second)
|
|
268
|
+
cat([first, second])
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def fast_append(first, second)
|
|
272
|
+
fastcat([first, second])
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def stack(patterns)
|
|
276
|
+
normalized_patterns = Array(patterns).map { |pattern| ensure_pattern(pattern) }
|
|
277
|
+
raise ArgumentError, "stack requires at least one pattern" if normalized_patterns.empty?
|
|
278
|
+
|
|
279
|
+
Pattern.new do |span|
|
|
280
|
+
normalized_patterns.flat_map { |pattern| pattern.query_span(span) }.then do |events|
|
|
281
|
+
sort_events(events)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def overlay(first, second)
|
|
287
|
+
stack([first, second])
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def mn(string)
|
|
291
|
+
compiler.compile(parser.parse(string))
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def parser
|
|
295
|
+
@parser ||= MiniNotation::Parser.new
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def compiler
|
|
299
|
+
@compiler ||= MiniNotation::Compiler.new
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def sort_events(events)
|
|
303
|
+
events.sort_by do |event|
|
|
304
|
+
[event.onset || event.part.start, event.offset || event.part.stop, event.value.to_s]
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def map_span(span, &block)
|
|
309
|
+
return nil unless span
|
|
310
|
+
|
|
311
|
+
TimeSpan.new(block.call(span.start), block.call(span.stop))
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def map_event(event, &block)
|
|
315
|
+
Event.new(
|
|
316
|
+
whole: map_span(event.whole, &block),
|
|
317
|
+
part: map_span(event.part, &block),
|
|
318
|
+
value: event.value
|
|
319
|
+
)
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def shift_event(event, amount)
|
|
323
|
+
offset = to_rational(amount)
|
|
324
|
+
map_event(event) { |time| time + offset }
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
private
|
|
329
|
+
|
|
330
|
+
def apply_binary_operation(other, structure:, &operation)
|
|
331
|
+
case structure
|
|
332
|
+
when :left
|
|
333
|
+
combine_left(other, &operation)
|
|
334
|
+
when :right
|
|
335
|
+
combine_right(other, &operation)
|
|
336
|
+
else
|
|
337
|
+
combine_both(other, &operation)
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def combine_scalar(left, right, operator)
|
|
342
|
+
if left.is_a?(Hash) && right.is_a?(Hash)
|
|
343
|
+
keys = left.keys | right.keys
|
|
344
|
+
keys.each_with_object({}) do |key, result|
|
|
345
|
+
result[key] =
|
|
346
|
+
if left.key?(key) && right.key?(key) && left[key].respond_to?(operator)
|
|
347
|
+
left[key].public_send(operator, right[key])
|
|
348
|
+
else
|
|
349
|
+
right.fetch(key, left[key])
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
elsif right.nil?
|
|
353
|
+
left
|
|
354
|
+
elsif left.respond_to?(operator)
|
|
355
|
+
left.public_send(operator, right)
|
|
356
|
+
else
|
|
357
|
+
left
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thread"
|
|
4
|
+
|
|
5
|
+
module Cyclotone
|
|
6
|
+
class Scheduler
|
|
7
|
+
LOOKAHEAD = 0.3
|
|
8
|
+
INTERVAL = 0.05
|
|
9
|
+
DEFAULT_CPS = Rational(9, 16)
|
|
10
|
+
|
|
11
|
+
attr_reader :backend, :cps, :lookahead, :interval
|
|
12
|
+
|
|
13
|
+
def initialize(cps: DEFAULT_CPS, backend:, lookahead: LOOKAHEAD, interval: INTERVAL, logger: nil)
|
|
14
|
+
@backend = backend
|
|
15
|
+
@cps = cps.to_f
|
|
16
|
+
@lookahead = lookahead
|
|
17
|
+
@interval = interval
|
|
18
|
+
@logger = logger
|
|
19
|
+
@mutex = Mutex.new
|
|
20
|
+
@patterns = {}
|
|
21
|
+
@sent = {}
|
|
22
|
+
@running = false
|
|
23
|
+
@start_monotonic = monotonic_time
|
|
24
|
+
@start_wall_time = Time.now.to_f
|
|
25
|
+
@start_cycle = 0.0
|
|
26
|
+
@last_cycle = 0.0
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def start
|
|
30
|
+
return if @running
|
|
31
|
+
|
|
32
|
+
@running = true
|
|
33
|
+
@thread = Thread.new do
|
|
34
|
+
while @running
|
|
35
|
+
begin
|
|
36
|
+
tick
|
|
37
|
+
rescue StandardError => error
|
|
38
|
+
log_runtime_error(error)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
sleep(@interval)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def stop
|
|
47
|
+
@running = false
|
|
48
|
+
@thread&.join
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def tick(now = monotonic_time)
|
|
52
|
+
state = snapshot_state
|
|
53
|
+
logical_end = time_to_cycle(now + lookahead, state[:cps], state[:start_cycle], state[:start_monotonic])
|
|
54
|
+
dispatch_until(logical_end, state)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def update_pattern(slot_id, pattern)
|
|
58
|
+
@mutex.synchronize { @patterns[slot_id] = Pattern.ensure_pattern(pattern) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def remove_pattern(slot_id)
|
|
62
|
+
@mutex.synchronize { @patterns.delete(slot_id) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def setcps(value)
|
|
66
|
+
now = monotonic_time
|
|
67
|
+
wall_now = Time.now.to_f
|
|
68
|
+
|
|
69
|
+
@mutex.synchronize do
|
|
70
|
+
current_cycle = time_to_cycle(now, @cps, @start_cycle, @start_monotonic)
|
|
71
|
+
|
|
72
|
+
@start_cycle = current_cycle
|
|
73
|
+
@start_monotonic = now
|
|
74
|
+
@start_wall_time = wall_now
|
|
75
|
+
@cps = value.to_f
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def reset_cycles
|
|
80
|
+
set_cycle(0)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def set_cycle(value)
|
|
84
|
+
@mutex.synchronize do
|
|
85
|
+
@start_cycle = value.to_f
|
|
86
|
+
@start_monotonic = monotonic_time
|
|
87
|
+
@start_wall_time = Time.now.to_f
|
|
88
|
+
@last_cycle = value.to_f
|
|
89
|
+
@sent.clear
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def backend=(backend)
|
|
94
|
+
@mutex.synchronize { @backend = backend }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def current_cycle(now = monotonic_time)
|
|
98
|
+
@mutex.synchronize { time_to_cycle(now, @cps, @start_cycle, @start_monotonic) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def running?
|
|
102
|
+
@running
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def render(duration:)
|
|
106
|
+
duration_value = duration.to_f
|
|
107
|
+
raise ArgumentError, "render duration must be non-negative" if duration_value.negative?
|
|
108
|
+
|
|
109
|
+
state = snapshot_state
|
|
110
|
+
state[:backend].begin_capture(at: state[:start_wall_time]) if state[:backend].respond_to?(:begin_capture)
|
|
111
|
+
|
|
112
|
+
logical_end = state[:start_cycle] + (duration_value * state[:cps])
|
|
113
|
+
dispatch_until(logical_end, state)
|
|
114
|
+
self
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def monotonic_time
|
|
120
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def snapshot_state
|
|
124
|
+
@mutex.synchronize do
|
|
125
|
+
{
|
|
126
|
+
patterns: @patterns.dup,
|
|
127
|
+
cps: @cps,
|
|
128
|
+
last_cycle: @last_cycle,
|
|
129
|
+
start_cycle: @start_cycle,
|
|
130
|
+
start_monotonic: @start_monotonic,
|
|
131
|
+
start_wall_time: @start_wall_time,
|
|
132
|
+
backend: @backend
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def dispatch_until(logical_end, state)
|
|
138
|
+
return if logical_end <= state[:last_cycle]
|
|
139
|
+
|
|
140
|
+
query_span = TimeSpan.new(Rational(state[:last_cycle].to_r), Rational(logical_end.to_r))
|
|
141
|
+
|
|
142
|
+
state[:patterns].each do |slot_id, pattern|
|
|
143
|
+
pattern.query_span(query_span).each do |event|
|
|
144
|
+
next unless event.onset
|
|
145
|
+
|
|
146
|
+
key = [slot_id, event.onset, event.value]
|
|
147
|
+
next if @sent[key]
|
|
148
|
+
|
|
149
|
+
absolute_time = state[:start_wall_time] + ((event.onset.to_f - state[:start_cycle]) / state[:cps])
|
|
150
|
+
state[:backend].send_event(event, at: absolute_time, cps: state[:cps])
|
|
151
|
+
@sent[key] = true
|
|
152
|
+
rescue StandardError => error
|
|
153
|
+
log_runtime_error(error)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
@mutex.synchronize { @last_cycle = logical_end }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def time_to_cycle(time, cps_value, start_cycle, start_monotonic)
|
|
161
|
+
start_cycle + ((time - start_monotonic) * cps_value)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def log_runtime_error(error)
|
|
165
|
+
@logger&.call("[Cyclotone::Scheduler] #{error.class}: #{error.message}")
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "singleton"
|
|
4
|
+
require "thread"
|
|
5
|
+
|
|
6
|
+
module Cyclotone
|
|
7
|
+
class State
|
|
8
|
+
include Singleton
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@values = {}
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def set_f(key, value)
|
|
16
|
+
write(key, value.to_f)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def set_i(key, value)
|
|
20
|
+
write(key, value.to_i)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def set_s(key, value)
|
|
24
|
+
write(key, value.to_s)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def get_f(key)
|
|
28
|
+
read(key)&.to_f
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def get_i(key)
|
|
32
|
+
read(key)&.to_i
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def get_s(key)
|
|
36
|
+
read(key)&.to_s
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def write(key, value)
|
|
42
|
+
@mutex.synchronize { @values[key.to_sym] = value }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def read(key)
|
|
46
|
+
@mutex.synchronize { @values[key.to_sym] }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|