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,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+ require "set"
5
+
6
+ module Cyclotone
7
+ class Stream
8
+ include Singleton
9
+ include Transition
10
+
11
+ attr_reader :scheduler
12
+
13
+ def initialize
14
+ @slots = {}
15
+ @muted = Set.new
16
+ @soloed = Set.new
17
+ @scheduler = Scheduler.new(backend: Backends::OSCBackend.new(socket: UDPSocket.new))
18
+ rescue StandardError
19
+ @scheduler = Scheduler.new(backend: NullBackend.new)
20
+ end
21
+
22
+ def d(slot_id, pattern)
23
+ assign(normalize_d_slot_id(slot_id), pattern)
24
+ end
25
+
26
+ def p(name, pattern)
27
+ assign(normalize_slot_reference(name), pattern)
28
+ end
29
+
30
+ def hush
31
+ @slots.keys.each { |slot_id| assign(slot_id, Pattern.silence) }
32
+ self
33
+ end
34
+
35
+ def solo(id)
36
+ @soloed << normalize_slot_reference(id)
37
+ sync_scheduler
38
+ end
39
+
40
+ def unsolo(id)
41
+ @soloed.delete(normalize_slot_reference(id))
42
+ sync_scheduler
43
+ end
44
+
45
+ def mute(id)
46
+ @muted << normalize_slot_reference(id)
47
+ sync_scheduler
48
+ end
49
+
50
+ def unmute(id)
51
+ @muted.delete(normalize_slot_reference(id))
52
+ sync_scheduler
53
+ end
54
+
55
+ def setcps(value)
56
+ @scheduler.setcps(value)
57
+ self
58
+ end
59
+
60
+ def reset_cycles
61
+ @scheduler.reset_cycles
62
+ self
63
+ end
64
+
65
+ def set_cycle(value)
66
+ @scheduler.set_cycle(value)
67
+ self
68
+ end
69
+
70
+ def use_backend(backend)
71
+ @scheduler.backend = backend
72
+ sync_scheduler
73
+ end
74
+
75
+ def start
76
+ @scheduler.start
77
+ self
78
+ end
79
+
80
+ def stop
81
+ @scheduler.stop
82
+ self
83
+ end
84
+
85
+ def running?
86
+ @scheduler.running?
87
+ end
88
+
89
+ def trigger
90
+ wait_until_cycle(@scheduler.current_cycle + (@scheduler.interval * @scheduler.cps))
91
+ end
92
+
93
+ def qtrigger
94
+ wait_until_cycle(@scheduler.current_cycle.ceil)
95
+ end
96
+
97
+ def mtrigger(period)
98
+ normalized_period = [period.to_i, 1].max
99
+ current_cycle = @scheduler.current_cycle
100
+ next_cycle = current_cycle.ceil
101
+ remainder = next_cycle % normalized_period
102
+ target_cycle = remainder.zero? ? next_cycle : next_cycle + (normalized_period - remainder)
103
+
104
+ wait_until_cycle(target_cycle)
105
+ end
106
+
107
+ def slot(slot_id)
108
+ @slots[normalize_slot_reference(slot_id)]
109
+ end
110
+
111
+ private
112
+
113
+ def assign(slot_id, pattern)
114
+ @slots[slot_id] = normalize_pattern(pattern)
115
+ sync_scheduler
116
+ @slots[slot_id]
117
+ end
118
+
119
+ def normalize_pattern(pattern)
120
+ return pattern if pattern.is_a?(Pattern)
121
+ return Pattern.mn(pattern) if pattern.is_a?(String)
122
+
123
+ Pattern.pure(pattern)
124
+ end
125
+
126
+ def normalize_d_slot_id(slot_id)
127
+ raw = slot_id.to_s
128
+ return raw.to_sym if raw.match?(/\Ad\d+\z/)
129
+ return :"d#{raw}" if raw.match?(/\A\d+\z/)
130
+
131
+ :"d#{raw}"
132
+ end
133
+
134
+ def normalize_slot_reference(slot_id)
135
+ raw = slot_id.to_s
136
+ return raw.to_sym if raw.match?(/\Ad\d+\z/)
137
+ return :"d#{raw}" if raw.match?(/\A\d+\z/)
138
+
139
+ raw.to_sym
140
+ end
141
+
142
+ def sync_scheduler
143
+ active_slots.each do |slot_id, pattern|
144
+ @scheduler.update_pattern(slot_id, pattern)
145
+ end
146
+
147
+ inactive_slots.each do |slot_id|
148
+ @scheduler.remove_pattern(slot_id)
149
+ end
150
+
151
+ self
152
+ end
153
+
154
+ def active_slots
155
+ if @soloed.empty?
156
+ @slots.reject { |slot_id, _| @muted.include?(slot_id) }
157
+ else
158
+ @slots.select { |slot_id, _| @soloed.include?(slot_id) && !@muted.include?(slot_id) }
159
+ end
160
+ end
161
+
162
+ def inactive_slots
163
+ @slots.keys - active_slots.keys
164
+ end
165
+
166
+ def wait_until_cycle(target_cycle)
167
+ cycles_remaining = target_cycle.to_f - @scheduler.current_cycle
168
+ seconds = cycles_remaining / @scheduler.cps
169
+ sleep(seconds) if seconds.positive?
170
+ self
171
+ end
172
+
173
+ class NullBackend
174
+ attr_reader :events
175
+
176
+ def initialize
177
+ @events = []
178
+ end
179
+
180
+ def send_event(event, at:, **options)
181
+ @events << { event: event, at: at, options: options }
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyclotone
4
+ module Support
5
+ module Deterministic
6
+ module_function
7
+
8
+ def seed_for(*parts)
9
+ parts.flatten.compact.reduce(17) do |memo, part|
10
+ ((memo * 31) ^ normalize(part).hash) & 0xFFFFFFFF
11
+ end
12
+ end
13
+
14
+ def random(*parts)
15
+ Random.new(seed_for(*parts))
16
+ end
17
+
18
+ def float(*parts)
19
+ random(*parts).rand
20
+ end
21
+
22
+ def int(max, *parts)
23
+ return 0 if max.to_i <= 0
24
+
25
+ random(*parts).rand(max.to_i)
26
+ end
27
+
28
+ private_class_method def self.normalize(value)
29
+ case value
30
+ when Rational
31
+ [value.numerator, value.denominator]
32
+ when Array
33
+ value.map { |entry| normalize(entry) }
34
+ when Hash
35
+ value.sort_by { |key, _| key.to_s }.map { |key, item| [key, normalize(item)] }
36
+ else
37
+ value
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyclotone
4
+ class TimeSpan
5
+ attr_reader :start, :stop
6
+
7
+ def initialize(start_time, stop_time)
8
+ @start = coerce_time(start_time)
9
+ @stop = coerce_time(stop_time)
10
+
11
+ raise ArgumentError, "stop time must be greater than or equal to start time" if @stop < @start
12
+
13
+ freeze
14
+ end
15
+
16
+ def duration
17
+ stop - start
18
+ end
19
+
20
+ def midpoint
21
+ (start + stop) / 2
22
+ end
23
+
24
+ def cycle_number
25
+ start.floor
26
+ end
27
+
28
+ def intersection(other)
29
+ intersection_start = [start, other.start].max
30
+ intersection_stop = [stop, other.stop].min
31
+
32
+ return nil if intersection_start >= intersection_stop
33
+
34
+ self.class.new(intersection_start, intersection_stop)
35
+ end
36
+
37
+ def includes?(time)
38
+ normalized_time = coerce_time(time)
39
+ start <= normalized_time && normalized_time < stop
40
+ end
41
+
42
+ def cycle_spans
43
+ return [] if duration.zero?
44
+
45
+ spans = []
46
+ current_start = start
47
+
48
+ while current_start < stop
49
+ cycle_boundary = [Rational(current_start.floor + 1), stop].min
50
+ spans << self.class.new(current_start, cycle_boundary)
51
+ current_start = cycle_boundary
52
+ end
53
+
54
+ spans
55
+ end
56
+
57
+ def shift(amount)
58
+ normalized_amount = coerce_time(amount)
59
+
60
+ self.class.new(start + normalized_amount, stop + normalized_amount)
61
+ end
62
+
63
+ def scale(factor)
64
+ normalized_factor = coerce_time(factor)
65
+
66
+ self.class.new(start * normalized_factor, stop * normalized_factor)
67
+ end
68
+
69
+ def reverse_within(cycle_start, cycle_length = 1)
70
+ normalized_start = coerce_time(cycle_start)
71
+ normalized_length = coerce_time(cycle_length)
72
+ mirror = (normalized_start * 2) + normalized_length
73
+
74
+ self.class.new(mirror - stop, mirror - start)
75
+ end
76
+
77
+ def ==(other)
78
+ other.is_a?(self.class) && start == other.start && stop == other.stop
79
+ end
80
+
81
+ alias eql? ==
82
+
83
+ def hash
84
+ [self.class, start, stop].hash
85
+ end
86
+
87
+ def to_s
88
+ "[#{start}, #{stop})"
89
+ end
90
+
91
+ private
92
+
93
+ def coerce_time(value)
94
+ return value if value.is_a?(Rational)
95
+
96
+ Rational(value)
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyclotone
4
+ module Transforms
5
+ module Accumulation
6
+ def overlay(other)
7
+ Pattern.stack([self, Pattern.ensure_pattern(other)])
8
+ end
9
+
10
+ def superimpose(&block)
11
+ overlay(block.call(self))
12
+ end
13
+
14
+ def layer(functions)
15
+ Pattern.stack(functions.map { |function| function.call(self) })
16
+ end
17
+
18
+ def jux(&block)
19
+ jux_by(1, &block)
20
+ end
21
+
22
+ def jux_by(amount, &block)
23
+ left = merge(Controls.pan(0.5 - (amount.to_f / 2.0)))
24
+ right = block.call(self).merge(Controls.pan(0.5 + (amount.to_f / 2.0)))
25
+ Pattern.stack([left, right])
26
+ end
27
+
28
+ def weave(count, pattern, controls)
29
+ functions = Array(controls).map do |control_pattern|
30
+ proc { |base| base.merge(control_pattern) }
31
+ end
32
+
33
+ weave_with(count, pattern, functions)
34
+ end
35
+
36
+ def weave_with(count, pattern, functions)
37
+ Pattern.fastcat(
38
+ Array.new(count) do |index|
39
+ functions[index % functions.length].call(pattern)
40
+ end
41
+ )
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyclotone
4
+ module Transforms
5
+ module Alteration
6
+ def every(period, &block)
7
+ every_with_offset(period, 0, &block)
8
+ end
9
+
10
+ def every_with_offset(period, offset, &block)
11
+ Pattern.new do |span|
12
+ if ((span.cycle_number - offset) % period).zero?
13
+ block.call(self).query_span(span)
14
+ else
15
+ query_span(span)
16
+ end
17
+ end
18
+ end
19
+
20
+ def fold_every(periods, &block)
21
+ Array(periods).reduce(self) do |pattern, period|
22
+ pattern.every(period, &block)
23
+ end
24
+ end
25
+
26
+ def sometimes(&block)
27
+ sometimes_by(0.5, &block)
28
+ end
29
+
30
+ def sometimes_by(probability, &block)
31
+ Pattern.new do |span|
32
+ if Support::Deterministic.float(:sometimes, probability, span.cycle_number) < probability.to_f
33
+ block.call(self).query_span(span)
34
+ else
35
+ query_span(span)
36
+ end
37
+ end
38
+ end
39
+
40
+ def rarely(&block)
41
+ sometimes_by(0.25, &block)
42
+ end
43
+
44
+ def almost_always(&block)
45
+ sometimes_by(0.9, &block)
46
+ end
47
+
48
+ def almost_never(&block)
49
+ sometimes_by(0.1, &block)
50
+ end
51
+
52
+ def chunk(count, &block)
53
+ Pattern.new do |span|
54
+ selected = span.cycle_number % count
55
+ pieces = Array.new(count) do |index|
56
+ segment = zoom(Rational(index, count), Rational(index + 1, count))
57
+ index == selected ? block.call(segment) : segment
58
+ end
59
+
60
+ Pattern.fastcat(pieces).query_span(span)
61
+ end
62
+ end
63
+
64
+ def scramble(count)
65
+ reorder_segments(count) do |cycle|
66
+ Array.new(count) { |index| index }.shuffle(random: Support::Deterministic.random(:scramble, cycle))
67
+ end
68
+ end
69
+
70
+ def shuffle(count)
71
+ scramble(count)
72
+ end
73
+
74
+ def iter(count)
75
+ reorder_segments(count) do |cycle|
76
+ Array.new(count) { |index| (index + cycle) % count }
77
+ end
78
+ end
79
+
80
+ def iter_back(count)
81
+ reorder_segments(count) do |cycle|
82
+ Array.new(count) { |index| (index - cycle) % count }
83
+ end
84
+ end
85
+
86
+ def degrade_by(probability)
87
+ select_events do |event|
88
+ cycle = (event.onset || event.part.start).floor
89
+ seed = [:degrade, probability, cycle, event.value, event.part.start]
90
+ Support::Deterministic.float(seed) >= probability.to_f
91
+ end
92
+ end
93
+
94
+ def degrade
95
+ degrade_by(0.5)
96
+ end
97
+
98
+ def trunc(amount)
99
+ limit = Pattern.to_rational(amount)
100
+
101
+ Pattern.new do |span|
102
+ cycle_start = Rational(span.cycle_number)
103
+ allowed = TimeSpan.new(cycle_start, cycle_start + limit)
104
+ overlap = span.intersection(allowed)
105
+ next [] unless overlap
106
+
107
+ query_span(overlap).map do |event|
108
+ clipped_part = event.part.intersection(allowed)
109
+ next unless clipped_part
110
+
111
+ clipped_whole = event.whole&.intersection(allowed) || event.whole
112
+
113
+ event.with_span(new_whole: clipped_whole, new_part: clipped_part)
114
+ end.compact
115
+ end
116
+ end
117
+
118
+ def linger(amount)
119
+ zoom(0, amount)
120
+ end
121
+
122
+ def zoom(start_point, end_point)
123
+ window_start = Pattern.to_rational(start_point)
124
+ window_end = Pattern.to_rational(end_point)
125
+ window_length = window_end - window_start
126
+
127
+ Pattern.new do |span|
128
+ cycle_start = Rational(span.cycle_number)
129
+ source_span = TimeSpan.new(
130
+ cycle_start + window_start + ((span.start - cycle_start) * window_length),
131
+ cycle_start + window_start + ((span.stop - cycle_start) * window_length)
132
+ )
133
+
134
+ query_span(source_span).map do |event|
135
+ Pattern.map_event(event) do |time|
136
+ cycle_start + ((time - cycle_start - window_start) / window_length)
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+ def stripe(count)
143
+ fast(count)
144
+ end
145
+
146
+ def slowstripe(count)
147
+ slow(count)
148
+ end
149
+
150
+ def spread(function, values)
151
+ sequence = Pattern.cat(Array(values).map { |value| function.call(self, value) })
152
+ Pattern.new { |span| sequence.query_span(span) }
153
+ end
154
+
155
+ def fastspread(function, values)
156
+ Pattern.fastcat(Array(values).map { |value| function.call(self, value) })
157
+ end
158
+
159
+ private
160
+
161
+ def reorder_segments(count)
162
+ Pattern.new do |span|
163
+ order = yield(span.cycle_number)
164
+ segment_patterns = Array.new(count) do |index|
165
+ zoom(Rational(order[index], count), Rational(order[index] + 1, count))
166
+ end
167
+
168
+ Pattern.fastcat(segment_patterns).query_span(span)
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyclotone
4
+ module Transforms
5
+ module Concatenation
6
+ def append(other)
7
+ Pattern.append(self, other)
8
+ end
9
+
10
+ def fast_append(other)
11
+ Pattern.fast_append(self, other)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyclotone
4
+ module Transforms
5
+ module Condition
6
+ def when_mod(period, minimum, &block)
7
+ Pattern.new do |span|
8
+ (span.cycle_number % period) >= minimum ? block.call(self).query_span(span) : query_span(span)
9
+ end
10
+ end
11
+
12
+ def fix(control_pattern, &block)
13
+ transformed = block.call(self)
14
+
15
+ Pattern.new do |span|
16
+ query_span(span).map do |event|
17
+ time = event.onset || event.part.start
18
+ control_value = Pattern.ensure_pattern(control_pattern).query_point(time)
19
+ next event unless truthy?(control_value)
20
+
21
+ transformed_event = transformed.query_event_at(time)
22
+ transformed_event ? transformed_event.with_span(new_whole: event.whole, new_part: event.part) : event
23
+ end
24
+ end
25
+ end
26
+
27
+ def unfix(control_pattern, &block)
28
+ contrast(block, proc { |pattern| pattern }, control_pattern)
29
+ end
30
+
31
+ def contrast(true_function, false_function, control_pattern)
32
+ true_pattern = true_function.call(self)
33
+ false_pattern = false_function.call(self)
34
+
35
+ Pattern.new do |span|
36
+ query_span(span).map do |event|
37
+ time = event.onset || event.part.start
38
+ target_pattern = truthy?(Pattern.ensure_pattern(control_pattern).query_point(time)) ? true_pattern : false_pattern
39
+ target_pattern.query_event_at(time)&.with_span(new_whole: event.whole, new_part: event.part) || event
40
+ end
41
+ end
42
+ end
43
+
44
+ def mask(bool_pattern)
45
+ select_events do |event|
46
+ truthy?(Pattern.ensure_pattern(bool_pattern).query_point(event.onset || event.part.start))
47
+ end
48
+ end
49
+
50
+ def struct(bool_pattern)
51
+ Pattern.ensure_pattern(bool_pattern).combine_left(self) do |gate, value|
52
+ gate ? value : nil
53
+ end.select_events { |event| !event.value.nil? }
54
+ end
55
+
56
+ private
57
+
58
+ def truthy?(value)
59
+ !(value.nil? || value == false || value == 0 || value == 0.0 || value == {})
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyclotone
4
+ module Transforms
5
+ module Sample
6
+ def chop(count)
7
+ Pattern.fastcat(
8
+ Array.new(count) do |index|
9
+ merge(Controls.begin(index.to_f / count)).merge(Controls.end((index + 1).to_f / count))
10
+ end
11
+ )
12
+ end
13
+
14
+ def striate(count)
15
+ chop(count)
16
+ end
17
+
18
+ def slice(count, pattern)
19
+ selection = Pattern.ensure_pattern(pattern)
20
+
21
+ map_events do |event|
22
+ index = selection.query_point(event.onset || event.part.start).to_i % count
23
+ merge_controls(event, begin: index.to_f / count, end: (index + 1).to_f / count)
24
+ end
25
+ end
26
+
27
+ def splice(count, pattern)
28
+ slice(count, pattern).merge(Controls.speed(count))
29
+ end
30
+
31
+ def bite(count, pattern)
32
+ slice(count, pattern).fast(count)
33
+ end
34
+
35
+ def chew(count, pattern)
36
+ bite(count, pattern)
37
+ end
38
+
39
+ def randslice(count)
40
+ map_events do |event|
41
+ index = Support::Deterministic.int(count, :randslice, event.value, event.part.start)
42
+ merge_controls(event, begin: index.to_f / count, end: (index + 1).to_f / count)
43
+ end
44
+ end
45
+
46
+ def loop_at(cycles)
47
+ merge(Controls.speed(1.0 / cycles.to_f))
48
+ end
49
+
50
+ def segment(count)
51
+ Pattern.new do |span|
52
+ cycle_start = Rational(span.cycle_number)
53
+ segment_length = Rational(1, count)
54
+
55
+ Array.new(count) { |index| index }.filter_map do |index|
56
+ segment_span = TimeSpan.new(
57
+ cycle_start + (segment_length * index),
58
+ cycle_start + (segment_length * (index + 1))
59
+ )
60
+ overlap = span.intersection(segment_span)
61
+ next unless overlap
62
+
63
+ value = query_point(segment_span.midpoint)
64
+ Event.new(whole: segment_span, part: overlap, value: value)
65
+ end
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def merge_controls(event, values)
72
+ merged_value = if event.value.is_a?(Hash)
73
+ event.value.merge(values)
74
+ else
75
+ values.merge(value: event.value)
76
+ end
77
+
78
+ event.with_value(merged_value)
79
+ end
80
+ end
81
+ end
82
+ end