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