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
|
@@ -8,11 +8,16 @@ module Cyclotone
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def superimpose(&block)
|
|
11
|
+
raise ArgumentError, "superimpose requires a block" unless block
|
|
12
|
+
|
|
11
13
|
overlay(block.call(self))
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def layer(functions)
|
|
15
|
-
|
|
17
|
+
normalized_functions = Array(functions)
|
|
18
|
+
raise ArgumentError, "layer requires functions" if normalized_functions.empty?
|
|
19
|
+
|
|
20
|
+
Pattern.stack(normalized_functions.map { |function| function.call(self) })
|
|
16
21
|
end
|
|
17
22
|
|
|
18
23
|
def jux(&block)
|
|
@@ -20,8 +25,11 @@ module Cyclotone
|
|
|
20
25
|
end
|
|
21
26
|
|
|
22
27
|
def jux_by(amount, &block)
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
raise ArgumentError, "jux_by requires a block" unless block
|
|
29
|
+
|
|
30
|
+
offset = amount.to_f / 2.0
|
|
31
|
+
left = merge(Controls.pan((0.5 - offset).clamp(0.0, 1.0)))
|
|
32
|
+
right = block.call(self).merge(Controls.pan((0.5 + offset).clamp(0.0, 1.0)))
|
|
25
33
|
Pattern.stack([left, right])
|
|
26
34
|
end
|
|
27
35
|
|
|
@@ -34,12 +42,27 @@ module Cyclotone
|
|
|
34
42
|
end
|
|
35
43
|
|
|
36
44
|
def weave_with(count, pattern, functions)
|
|
45
|
+
normalized_count = normalize_weave_count(count)
|
|
46
|
+
normalized_functions = Array(functions)
|
|
47
|
+
raise ArgumentError, "weave functions must not be empty" if normalized_functions.empty?
|
|
48
|
+
|
|
37
49
|
Pattern.fastcat(
|
|
38
|
-
Array.new(
|
|
39
|
-
|
|
50
|
+
Array.new(normalized_count) do |index|
|
|
51
|
+
normalized_functions[index % normalized_functions.length].call(pattern)
|
|
40
52
|
end
|
|
41
53
|
)
|
|
42
54
|
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def normalize_weave_count(count)
|
|
59
|
+
normalized_count = Integer(count)
|
|
60
|
+
raise ArgumentError, "weave count must be positive" unless normalized_count.positive?
|
|
61
|
+
|
|
62
|
+
normalized_count
|
|
63
|
+
rescue ArgumentError, TypeError => error
|
|
64
|
+
raise ArgumentError, "invalid weave count: #{error.message}"
|
|
65
|
+
end
|
|
43
66
|
end
|
|
44
67
|
end
|
|
45
68
|
end
|
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
module Cyclotone
|
|
4
4
|
module Transforms
|
|
5
5
|
module Alteration
|
|
6
|
+
MAX_SEGMENTS = 4096
|
|
7
|
+
|
|
6
8
|
def every(period, &block)
|
|
7
9
|
every_with_offset(period, 0, &block)
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
def every_with_offset(period, offset, &block)
|
|
13
|
+
normalized_period = validate_positive_rational(period, "every period")
|
|
14
|
+
normalized_offset = Pattern.to_rational(offset)
|
|
15
|
+
raise ArgumentError, "every requires a block" unless block
|
|
16
|
+
|
|
11
17
|
Pattern.new do |span|
|
|
12
|
-
if ((span.cycle_number -
|
|
18
|
+
if ((span.cycle_number - normalized_offset) % normalized_period).zero?
|
|
13
19
|
block.call(self).query_span(span)
|
|
14
20
|
else
|
|
15
21
|
query_span(span)
|
|
@@ -27,9 +33,12 @@ module Cyclotone
|
|
|
27
33
|
sometimes_by(0.5, &block)
|
|
28
34
|
end
|
|
29
35
|
|
|
30
|
-
def sometimes_by(probability, &block)
|
|
36
|
+
def sometimes_by(probability, namespace: :sometimes, &block)
|
|
37
|
+
normalized_probability = validate_probability(probability, "sometimes probability")
|
|
38
|
+
raise ArgumentError, "sometimes_by requires a block" unless block
|
|
39
|
+
|
|
31
40
|
Pattern.new do |span|
|
|
32
|
-
if Support::Deterministic.float(
|
|
41
|
+
if Support::Deterministic.float(namespace, normalized_probability, span.cycle_number) < normalized_probability
|
|
33
42
|
block.call(self).query_span(span)
|
|
34
43
|
else
|
|
35
44
|
query_span(span)
|
|
@@ -50,10 +59,13 @@ module Cyclotone
|
|
|
50
59
|
end
|
|
51
60
|
|
|
52
61
|
def chunk(count, &block)
|
|
62
|
+
normalized_count = validate_segment_count(count, "chunk count")
|
|
63
|
+
raise ArgumentError, "chunk requires a block" unless block
|
|
64
|
+
|
|
53
65
|
Pattern.new do |span|
|
|
54
|
-
selected = span.cycle_number %
|
|
55
|
-
pieces = Array.new(
|
|
56
|
-
segment = zoom(Rational(index,
|
|
66
|
+
selected = span.cycle_number % normalized_count
|
|
67
|
+
pieces = Array.new(normalized_count) do |index|
|
|
68
|
+
segment = zoom(Rational(index, normalized_count), Rational(index + 1, normalized_count))
|
|
57
69
|
index == selected ? block.call(segment) : segment
|
|
58
70
|
end
|
|
59
71
|
|
|
@@ -62,8 +74,10 @@ module Cyclotone
|
|
|
62
74
|
end
|
|
63
75
|
|
|
64
76
|
def scramble(count)
|
|
65
|
-
|
|
66
|
-
|
|
77
|
+
normalized_count = validate_segment_count(count, "scramble count")
|
|
78
|
+
|
|
79
|
+
reorder_segments(normalized_count) do |cycle|
|
|
80
|
+
Array.new(normalized_count) { |index| index }.shuffle(random: Support::Deterministic.random(:scramble, cycle))
|
|
67
81
|
end
|
|
68
82
|
end
|
|
69
83
|
|
|
@@ -72,22 +86,28 @@ module Cyclotone
|
|
|
72
86
|
end
|
|
73
87
|
|
|
74
88
|
def iter(count)
|
|
75
|
-
|
|
76
|
-
|
|
89
|
+
normalized_count = validate_segment_count(count, "iter count")
|
|
90
|
+
|
|
91
|
+
reorder_segments(normalized_count) do |cycle|
|
|
92
|
+
Array.new(normalized_count) { |index| (index + cycle) % normalized_count }
|
|
77
93
|
end
|
|
78
94
|
end
|
|
79
95
|
|
|
80
96
|
def iter_back(count)
|
|
81
|
-
|
|
82
|
-
|
|
97
|
+
normalized_count = validate_segment_count(count, "iter_back count")
|
|
98
|
+
|
|
99
|
+
reorder_segments(normalized_count) do |cycle|
|
|
100
|
+
Array.new(normalized_count) { |index| (index - cycle) % normalized_count }
|
|
83
101
|
end
|
|
84
102
|
end
|
|
85
103
|
|
|
86
104
|
def degrade_by(probability)
|
|
105
|
+
normalized_probability = validate_probability(probability, "degrade probability")
|
|
106
|
+
|
|
87
107
|
select_events do |event|
|
|
88
108
|
cycle = (event.onset || event.part.start).floor
|
|
89
|
-
seed = [:degrade,
|
|
90
|
-
Support::Deterministic.float(seed) >=
|
|
109
|
+
seed = [:degrade, normalized_probability, cycle, event.value, event.part.start]
|
|
110
|
+
Support::Deterministic.float(seed) >= normalized_probability
|
|
91
111
|
end
|
|
92
112
|
end
|
|
93
113
|
|
|
@@ -97,6 +117,7 @@ module Cyclotone
|
|
|
97
117
|
|
|
98
118
|
def trunc(amount)
|
|
99
119
|
limit = Pattern.to_rational(amount)
|
|
120
|
+
raise ArgumentError, "trunc amount must be between 0 and 1" if limit.negative? || limit > 1
|
|
100
121
|
|
|
101
122
|
Pattern.new do |span|
|
|
102
123
|
cycle_start = Rational(span.cycle_number)
|
|
@@ -123,6 +144,7 @@ module Cyclotone
|
|
|
123
144
|
window_start = Pattern.to_rational(start_point)
|
|
124
145
|
window_end = Pattern.to_rational(end_point)
|
|
125
146
|
window_length = window_end - window_start
|
|
147
|
+
raise ArgumentError, "zoom end must be greater than start" unless window_length.positive?
|
|
126
148
|
|
|
127
149
|
Pattern.new do |span|
|
|
128
150
|
cycle_start = Rational(span.cycle_number)
|
|
@@ -148,26 +170,68 @@ module Cyclotone
|
|
|
148
170
|
end
|
|
149
171
|
|
|
150
172
|
def spread(function, values)
|
|
151
|
-
|
|
173
|
+
normalized_values = validate_values(values, "spread values")
|
|
174
|
+
raise ArgumentError, "spread requires a callable function" unless function.respond_to?(:call)
|
|
175
|
+
|
|
176
|
+
sequence = Pattern.cat(normalized_values.map { |value| function.call(self, value) })
|
|
152
177
|
Pattern.new { |span| sequence.query_span(span) }
|
|
153
178
|
end
|
|
154
179
|
|
|
155
180
|
def fastspread(function, values)
|
|
156
|
-
|
|
181
|
+
normalized_values = validate_values(values, "fastspread values")
|
|
182
|
+
raise ArgumentError, "fastspread requires a callable function" unless function.respond_to?(:call)
|
|
183
|
+
|
|
184
|
+
Pattern.fastcat(normalized_values.map { |value| function.call(self, value) })
|
|
157
185
|
end
|
|
158
186
|
|
|
159
187
|
private
|
|
160
188
|
|
|
161
189
|
def reorder_segments(count)
|
|
190
|
+
normalized_count = validate_segment_count(count, "segment count")
|
|
191
|
+
|
|
162
192
|
Pattern.new do |span|
|
|
163
193
|
order = yield(span.cycle_number)
|
|
164
|
-
segment_patterns = Array.new(
|
|
165
|
-
zoom(Rational(order[index],
|
|
194
|
+
segment_patterns = Array.new(normalized_count) do |index|
|
|
195
|
+
zoom(Rational(order[index], normalized_count), Rational(order[index] + 1, normalized_count))
|
|
166
196
|
end
|
|
167
197
|
|
|
168
198
|
Pattern.fastcat(segment_patterns).query_span(span)
|
|
169
199
|
end
|
|
170
200
|
end
|
|
201
|
+
|
|
202
|
+
def validate_positive_rational(value, label)
|
|
203
|
+
normalized = Pattern.to_rational(value)
|
|
204
|
+
raise ArgumentError, "#{label} must be positive" unless normalized.positive?
|
|
205
|
+
|
|
206
|
+
normalized
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def validate_probability(value, label)
|
|
210
|
+
normalized = Float(value)
|
|
211
|
+
raise ArgumentError, "#{label} must be finite" unless normalized.finite?
|
|
212
|
+
raise ArgumentError, "#{label} must be between 0 and 1" unless normalized.between?(0.0, 1.0)
|
|
213
|
+
|
|
214
|
+
normalized
|
|
215
|
+
rescue ArgumentError, TypeError => error
|
|
216
|
+
raise ArgumentError, "invalid #{label}: #{error.message}"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def validate_segment_count(value, label)
|
|
220
|
+
normalized = Integer(value)
|
|
221
|
+
raise ArgumentError, "#{label} must be positive" unless normalized.positive?
|
|
222
|
+
raise ArgumentError, "#{label} must be <= #{MAX_SEGMENTS}" if normalized > MAX_SEGMENTS
|
|
223
|
+
|
|
224
|
+
normalized
|
|
225
|
+
rescue ArgumentError, TypeError => error
|
|
226
|
+
raise ArgumentError, "invalid #{label}: #{error.message}"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def validate_values(values, label)
|
|
230
|
+
normalized = Array(values)
|
|
231
|
+
raise ArgumentError, "#{label} must not be empty" if normalized.empty?
|
|
232
|
+
|
|
233
|
+
normalized
|
|
234
|
+
end
|
|
171
235
|
end
|
|
172
236
|
end
|
|
173
237
|
end
|
|
@@ -4,12 +4,18 @@ module Cyclotone
|
|
|
4
4
|
module Transforms
|
|
5
5
|
module Condition
|
|
6
6
|
def when_mod(period, minimum, &block)
|
|
7
|
+
normalized_period = Pattern.to_rational(period)
|
|
8
|
+
raise ArgumentError, "when_mod period must be positive" unless normalized_period.positive?
|
|
9
|
+
raise ArgumentError, "when_mod requires a block" unless block
|
|
10
|
+
|
|
7
11
|
Pattern.new do |span|
|
|
8
|
-
(span.cycle_number %
|
|
12
|
+
(span.cycle_number % normalized_period) >= Pattern.to_rational(minimum) ? block.call(self).query_span(span) : query_span(span)
|
|
9
13
|
end
|
|
10
14
|
end
|
|
11
15
|
|
|
12
16
|
def fix(control_pattern, &block)
|
|
17
|
+
raise ArgumentError, "fix requires a block" unless block
|
|
18
|
+
|
|
13
19
|
transformed = block.call(self)
|
|
14
20
|
|
|
15
21
|
Pattern.new do |span|
|
|
@@ -25,10 +31,15 @@ module Cyclotone
|
|
|
25
31
|
end
|
|
26
32
|
|
|
27
33
|
def unfix(control_pattern, &block)
|
|
34
|
+
raise ArgumentError, "unfix requires a block" unless block
|
|
35
|
+
|
|
28
36
|
contrast(block, proc { |pattern| pattern }, control_pattern)
|
|
29
37
|
end
|
|
30
38
|
|
|
31
39
|
def contrast(true_function, false_function, control_pattern)
|
|
40
|
+
raise ArgumentError, "contrast requires a true function" unless true_function.respond_to?(:call)
|
|
41
|
+
raise ArgumentError, "contrast requires a false function" unless false_function.respond_to?(:call)
|
|
42
|
+
|
|
32
43
|
true_pattern = true_function.call(self)
|
|
33
44
|
false_pattern = false_function.call(self)
|
|
34
45
|
|
|
@@ -49,14 +60,15 @@ module Cyclotone
|
|
|
49
60
|
|
|
50
61
|
def struct(bool_pattern)
|
|
51
62
|
Pattern.ensure_pattern(bool_pattern).combine_left(self) do |gate, value|
|
|
52
|
-
gate ? value : nil
|
|
63
|
+
truthy?(gate) ? value : nil
|
|
53
64
|
end.select_events { |event| !event.value.nil? }
|
|
54
65
|
end
|
|
55
66
|
|
|
56
67
|
private
|
|
57
68
|
|
|
58
69
|
def truthy?(value)
|
|
59
|
-
|
|
70
|
+
falsey_number = value.is_a?(Numeric) && value.zero?
|
|
71
|
+
!(value.nil? || value == false || falsey_number || value == {})
|
|
60
72
|
end
|
|
61
73
|
end
|
|
62
74
|
end
|
|
@@ -4,9 +4,13 @@ module Cyclotone
|
|
|
4
4
|
module Transforms
|
|
5
5
|
module Sample
|
|
6
6
|
def chop(count)
|
|
7
|
+
normalized_count = validate_positive_integer(count, "chop count")
|
|
8
|
+
|
|
7
9
|
Pattern.fastcat(
|
|
8
|
-
Array.new(
|
|
9
|
-
merge(Controls.begin(index
|
|
10
|
+
Array.new(normalized_count) do |index|
|
|
11
|
+
merge(Controls.begin(Rational(index, normalized_count))).merge(
|
|
12
|
+
Controls.end(Rational(index + 1, normalized_count))
|
|
13
|
+
)
|
|
10
14
|
end
|
|
11
15
|
)
|
|
12
16
|
end
|
|
@@ -16,11 +20,15 @@ module Cyclotone
|
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
def slice(count, pattern)
|
|
23
|
+
normalized_count = validate_positive_integer(count, "slice count")
|
|
19
24
|
selection = Pattern.ensure_pattern(pattern)
|
|
20
25
|
|
|
21
26
|
map_events do |event|
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
selected_value = selection.query_point(event.onset || event.part.start)
|
|
28
|
+
next nil if selected_value.nil?
|
|
29
|
+
|
|
30
|
+
index = selected_value.to_i % normalized_count
|
|
31
|
+
merge_controls(event, begin: Rational(index, normalized_count), end: Rational(index + 1, normalized_count))
|
|
24
32
|
end
|
|
25
33
|
end
|
|
26
34
|
|
|
@@ -37,22 +45,29 @@ module Cyclotone
|
|
|
37
45
|
end
|
|
38
46
|
|
|
39
47
|
def randslice(count)
|
|
48
|
+
normalized_count = validate_positive_integer(count, "randslice count")
|
|
49
|
+
|
|
40
50
|
map_events do |event|
|
|
41
|
-
index = Support::Deterministic.int(
|
|
42
|
-
merge_controls(event, begin: index
|
|
51
|
+
index = Support::Deterministic.int(normalized_count, :randslice, event.value, event.part.start)
|
|
52
|
+
merge_controls(event, begin: Rational(index, normalized_count), end: Rational(index + 1, normalized_count))
|
|
43
53
|
end
|
|
44
54
|
end
|
|
45
55
|
|
|
46
56
|
def loop_at(cycles)
|
|
47
|
-
|
|
57
|
+
normalized_cycles = Pattern.to_rational(cycles)
|
|
58
|
+
raise ArgumentError, "loop_at cycles must be positive" unless normalized_cycles.positive?
|
|
59
|
+
|
|
60
|
+
merge(Controls.speed(1.0 / normalized_cycles.to_f))
|
|
48
61
|
end
|
|
49
62
|
|
|
50
63
|
def segment(count)
|
|
64
|
+
normalized_count = validate_positive_integer(count, "segment count")
|
|
65
|
+
|
|
51
66
|
Pattern.new do |span|
|
|
52
67
|
cycle_start = Rational(span.cycle_number)
|
|
53
|
-
segment_length = Rational(1,
|
|
68
|
+
segment_length = Rational(1, normalized_count)
|
|
54
69
|
|
|
55
|
-
Array.new(
|
|
70
|
+
Array.new(normalized_count) { |index| index }.filter_map do |index|
|
|
56
71
|
segment_span = TimeSpan.new(
|
|
57
72
|
cycle_start + (segment_length * index),
|
|
58
73
|
cycle_start + (segment_length * (index + 1))
|
|
@@ -77,6 +92,15 @@ module Cyclotone
|
|
|
77
92
|
|
|
78
93
|
event.with_value(merged_value)
|
|
79
94
|
end
|
|
95
|
+
|
|
96
|
+
def validate_positive_integer(value, label)
|
|
97
|
+
normalized = Integer(value)
|
|
98
|
+
raise ArgumentError, "#{label} must be positive" unless normalized.positive?
|
|
99
|
+
|
|
100
|
+
normalized
|
|
101
|
+
rescue ArgumentError, TypeError => error
|
|
102
|
+
raise ArgumentError, "invalid #{label}: #{error.message}"
|
|
103
|
+
end
|
|
80
104
|
end
|
|
81
105
|
end
|
|
82
106
|
end
|
|
@@ -5,6 +5,7 @@ module Cyclotone
|
|
|
5
5
|
module Time
|
|
6
6
|
def fast(amount)
|
|
7
7
|
factor = Pattern.to_rational(amount)
|
|
8
|
+
raise ArgumentError, "fast amount must be positive" unless factor.positive?
|
|
8
9
|
|
|
9
10
|
Pattern.new do |span|
|
|
10
11
|
source_span = TimeSpan.new(span.start * factor, span.stop * factor)
|
|
@@ -16,7 +17,10 @@ module Cyclotone
|
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def slow(amount)
|
|
19
|
-
|
|
20
|
+
factor = Pattern.to_rational(amount)
|
|
21
|
+
raise ArgumentError, "slow amount must be positive" unless factor.positive?
|
|
22
|
+
|
|
23
|
+
fast(Rational(1, 1) / factor)
|
|
20
24
|
end
|
|
21
25
|
|
|
22
26
|
def early(amount)
|
|
@@ -57,35 +61,50 @@ module Cyclotone
|
|
|
57
61
|
fast(amount).fmap do |value|
|
|
58
62
|
next value unless value.is_a?(Hash)
|
|
59
63
|
|
|
60
|
-
current_speed = value
|
|
64
|
+
current_speed = value.fetch(:speed, 1.0) || 1.0
|
|
61
65
|
value.merge(speed: current_speed * amount.to_f)
|
|
62
66
|
end
|
|
63
67
|
end
|
|
64
68
|
|
|
65
69
|
def off(amount, &block)
|
|
70
|
+
raise ArgumentError, "off requires a block" unless block
|
|
71
|
+
|
|
66
72
|
transformed = block.call(self).early(amount)
|
|
67
73
|
Pattern.stack([self, transformed])
|
|
68
74
|
end
|
|
69
75
|
|
|
70
76
|
def swing(amount, div = 4)
|
|
71
|
-
|
|
77
|
+
normalized_div = Pattern.to_rational(div)
|
|
78
|
+
raise ArgumentError, "swing division must be positive" unless normalized_div.positive?
|
|
79
|
+
|
|
80
|
+
step_shift = Pattern.to_rational(amount) / normalized_div
|
|
72
81
|
|
|
73
82
|
map_events do |event|
|
|
74
83
|
next event unless event.onset
|
|
75
84
|
|
|
76
85
|
cycle_start = Rational(event.onset.floor)
|
|
77
|
-
step_index = ((
|
|
86
|
+
step_index = ((event.onset - cycle_start) * div).floor.to_i
|
|
78
87
|
next event if step_index.even?
|
|
79
88
|
|
|
80
|
-
Pattern.shift_event(event, step_shift)
|
|
89
|
+
shifted = Pattern.shift_event(event, step_shift)
|
|
90
|
+
cycle_span = TimeSpan.new(cycle_start, cycle_start + 1)
|
|
91
|
+
clipped_part = shifted.part.intersection(cycle_span)
|
|
92
|
+
next nil unless clipped_part
|
|
93
|
+
|
|
94
|
+
clipped_whole = shifted.whole&.intersection(cycle_span) || shifted.whole
|
|
95
|
+
shifted.with_span(part: clipped_part, whole: clipped_whole)
|
|
81
96
|
end
|
|
82
97
|
end
|
|
83
98
|
|
|
84
99
|
def inside(amount, &block)
|
|
100
|
+
raise ArgumentError, "inside requires a block" unless block
|
|
101
|
+
|
|
85
102
|
slow(amount).then(&block).fast(amount)
|
|
86
103
|
end
|
|
87
104
|
|
|
88
105
|
def outside(amount, &block)
|
|
106
|
+
raise ArgumentError, "outside requires a block" unless block
|
|
107
|
+
|
|
89
108
|
fast(amount).then(&block).slow(amount)
|
|
90
109
|
end
|
|
91
110
|
end
|
data/lib/cyclotone/transition.rb
CHANGED
|
@@ -10,19 +10,21 @@ module Cyclotone
|
|
|
10
10
|
|
|
11
11
|
def xfade_in(id, cycles, pattern)
|
|
12
12
|
slot_id = normalize_slot_reference(id)
|
|
13
|
-
current =
|
|
13
|
+
current = pattern_for_transition(slot_id)
|
|
14
14
|
replacement = Pattern.ensure_pattern(pattern)
|
|
15
15
|
duration = Pattern.to_rational(cycles)
|
|
16
16
|
return assign(slot_id, replacement) if duration <= 0
|
|
17
17
|
|
|
18
18
|
start_cycle = transition_start_cycle
|
|
19
19
|
|
|
20
|
-
mixed = Pattern.stack(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
mixed = Pattern.stack(
|
|
21
|
+
[
|
|
22
|
+
apply_gain_envelope(current, start_cycle: start_cycle, duration: duration, direction: :out),
|
|
23
|
+
apply_gain_envelope(replacement, start_cycle: start_cycle, duration: duration, direction: :in)
|
|
24
|
+
]
|
|
25
|
+
)
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
assign_transition(slot_id, mixed, replacement: replacement, finish_cycle: start_cycle + duration)
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
def clutch(id, pattern)
|
|
@@ -31,7 +33,7 @@ module Cyclotone
|
|
|
31
33
|
|
|
32
34
|
def clutch_in(id, cycles, pattern)
|
|
33
35
|
slot_id = normalize_slot_reference(id)
|
|
34
|
-
current =
|
|
36
|
+
current = pattern_for_transition(slot_id)
|
|
35
37
|
replacement = Pattern.ensure_pattern(pattern)
|
|
36
38
|
duration = Pattern.to_rational(cycles)
|
|
37
39
|
return assign(slot_id, replacement) if duration <= 0
|
|
@@ -48,7 +50,7 @@ module Cyclotone
|
|
|
48
50
|
end.then { |events| Pattern.sort_events(events) }
|
|
49
51
|
end
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
assign_transition(slot_id, swapped, replacement: replacement, finish_cycle: start_cycle + duration)
|
|
52
54
|
end
|
|
53
55
|
|
|
54
56
|
def interpolate(id, pattern)
|
|
@@ -57,20 +59,19 @@ module Cyclotone
|
|
|
57
59
|
|
|
58
60
|
def interpolate_in(id, cycles, pattern)
|
|
59
61
|
slot_id = normalize_slot_reference(id)
|
|
60
|
-
current =
|
|
62
|
+
current = pattern_for_transition(slot_id)
|
|
61
63
|
replacement = Pattern.ensure_pattern(pattern)
|
|
62
64
|
duration = Pattern.to_rational(cycles)
|
|
63
65
|
return assign(slot_id, replacement) if duration <= 0
|
|
64
66
|
|
|
65
67
|
start_cycle = transition_start_cycle
|
|
66
68
|
morphed = Pattern.new do |span|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
end.uniq.sort
|
|
69
|
+
source_events = current.query_span(span)
|
|
70
|
+
target_events = replacement.query_span(span)
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
source_event =
|
|
73
|
-
target_event =
|
|
72
|
+
transition_anchor_times(source_events, target_events).filter_map do |time|
|
|
73
|
+
source_event = event_at_time(source_events, time)
|
|
74
|
+
target_event = event_at_time(target_events, time)
|
|
74
75
|
base_event = target_event || source_event
|
|
75
76
|
next unless base_event
|
|
76
77
|
|
|
@@ -79,7 +80,7 @@ module Cyclotone
|
|
|
79
80
|
end.then { |events| Pattern.sort_events(events) }
|
|
80
81
|
end
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
assign_transition(slot_id, morphed, replacement: replacement, finish_cycle: start_cycle + duration)
|
|
83
84
|
end
|
|
84
85
|
|
|
85
86
|
def jump(id, pattern)
|
|
@@ -88,14 +89,14 @@ module Cyclotone
|
|
|
88
89
|
|
|
89
90
|
def jump_in(id, cycles, pattern)
|
|
90
91
|
slot_id = normalize_slot_reference(id)
|
|
91
|
-
current =
|
|
92
|
+
current = pattern_for_transition(slot_id)
|
|
92
93
|
replacement = Pattern.ensure_pattern(pattern)
|
|
93
94
|
switch_cycle = transition_start_cycle + Pattern.to_rational(cycles)
|
|
94
95
|
return assign(slot_id, replacement) if switch_cycle <= transition_start_cycle
|
|
95
96
|
|
|
96
97
|
delayed = Pattern.new { |span| split_query(span, switch_cycle, current, replacement) }
|
|
97
98
|
|
|
98
|
-
|
|
99
|
+
assign_transition(slot_id, delayed, replacement: replacement, finish_cycle: switch_cycle)
|
|
99
100
|
end
|
|
100
101
|
|
|
101
102
|
def anticipate(id, pattern)
|
|
@@ -107,8 +108,14 @@ module Cyclotone
|
|
|
107
108
|
duration = Pattern.to_rational(cycles)
|
|
108
109
|
return self if duration <= 0
|
|
109
110
|
|
|
110
|
-
@slots.
|
|
111
|
-
|
|
111
|
+
@slots.each_key do |slot_id|
|
|
112
|
+
replacement = @slots.fetch(slot_id)
|
|
113
|
+
assign_transition(
|
|
114
|
+
slot_id,
|
|
115
|
+
apply_gain_envelope(replacement, start_cycle: start_cycle, duration: duration, direction: :in),
|
|
116
|
+
replacement: replacement,
|
|
117
|
+
finish_cycle: start_cycle + duration
|
|
118
|
+
)
|
|
112
119
|
end
|
|
113
120
|
|
|
114
121
|
self
|
|
@@ -119,8 +126,13 @@ module Cyclotone
|
|
|
119
126
|
duration = Pattern.to_rational(cycles)
|
|
120
127
|
return self if duration <= 0
|
|
121
128
|
|
|
122
|
-
@slots.
|
|
123
|
-
|
|
129
|
+
@slots.each_key do |slot_id|
|
|
130
|
+
assign_transition(
|
|
131
|
+
slot_id,
|
|
132
|
+
apply_gain_envelope(@slots.fetch(slot_id), start_cycle: start_cycle, duration: duration, direction: :out),
|
|
133
|
+
replacement: Pattern.silence,
|
|
134
|
+
finish_cycle: start_cycle + duration
|
|
135
|
+
)
|
|
124
136
|
end
|
|
125
137
|
|
|
126
138
|
self
|
|
@@ -129,23 +141,13 @@ module Cyclotone
|
|
|
129
141
|
private
|
|
130
142
|
|
|
131
143
|
def apply_gain_envelope(pattern, start_cycle:, duration:, direction:)
|
|
132
|
-
Pattern.ensure_pattern(pattern).
|
|
133
|
-
next value unless value.is_a?(Hash)
|
|
134
|
-
|
|
135
|
-
factor = lambda do |time|
|
|
136
|
-
progress = ((time - start_cycle.to_f) / duration.to_f).clamp(0.0, 1.0)
|
|
137
|
-
direction == :in ? progress : 1.0 - progress
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
value.merge(gain_envelope: factor)
|
|
141
|
-
end.map_events do |event|
|
|
142
|
-
next event unless event.value.is_a?(Hash)
|
|
143
|
-
|
|
144
|
+
Pattern.ensure_pattern(pattern).map_events do |event|
|
|
144
145
|
time = event.onset || event.part.start
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
value
|
|
146
|
+
progress = transition_progress(time, start_cycle, duration)
|
|
147
|
+
factor = direction == :in ? progress : 1.0 - progress
|
|
148
|
+
value = event.value.is_a?(Hash) ? event.value.dup : { value: event.value }
|
|
149
|
+
current_gain = value.fetch(:gain, 1.0) || 1.0
|
|
150
|
+
value[:gain] = current_gain * factor
|
|
149
151
|
event.with_value(value)
|
|
150
152
|
end
|
|
151
153
|
end
|
|
@@ -170,9 +172,17 @@ module Cyclotone
|
|
|
170
172
|
((time.to_f - start_cycle.to_f) / duration.to_f).clamp(0.0, 1.0)
|
|
171
173
|
end
|
|
172
174
|
|
|
173
|
-
def
|
|
175
|
+
def transition_anchor_times(source_events, target_events)
|
|
176
|
+
(source_events + target_events).map { |event| event.onset || event.part.start }.uniq.sort
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def event_at_time(events, time)
|
|
180
|
+
events.find { |event| event.covers_time?(time) }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def clutch_source(slot_id, time, _value, start_cycle, duration)
|
|
174
184
|
progress = transition_progress(time, start_cycle, duration)
|
|
175
|
-
chosen = Support::Deterministic.float(:clutch, slot_id, time
|
|
185
|
+
chosen = Support::Deterministic.float(:clutch, slot_id, time) < progress
|
|
176
186
|
|
|
177
187
|
chosen ? :replacement : :current
|
|
178
188
|
end
|
|
@@ -191,13 +201,15 @@ module Cyclotone
|
|
|
191
201
|
end
|
|
192
202
|
|
|
193
203
|
def interpolate_hash(source, target, progress)
|
|
194
|
-
(source.keys | target.keys).
|
|
195
|
-
|
|
204
|
+
(source.keys | target.keys).to_h do |key|
|
|
205
|
+
value =
|
|
196
206
|
if source.key?(key) && target.key?(key)
|
|
197
207
|
interpolate_value(source[key], target[key], progress)
|
|
198
208
|
else
|
|
199
209
|
progress < 0.5 ? source.fetch(key, target[key]) : target.fetch(key, source[key])
|
|
200
210
|
end
|
|
211
|
+
|
|
212
|
+
[key, value]
|
|
201
213
|
end
|
|
202
214
|
end
|
|
203
215
|
end
|
data/lib/cyclotone/version.rb
CHANGED
data/lib/cyclotone.rb
CHANGED
|
@@ -20,6 +20,7 @@ require_relative "cyclotone/controls"
|
|
|
20
20
|
require_relative "cyclotone/oscillators"
|
|
21
21
|
require_relative "cyclotone/harmony"
|
|
22
22
|
require_relative "cyclotone/state"
|
|
23
|
+
require_relative "cyclotone/backends/null_backend"
|
|
23
24
|
require_relative "cyclotone/backends/osc_backend"
|
|
24
25
|
require_relative "cyclotone/backends/midi_backend"
|
|
25
26
|
require_relative "cyclotone/backends/midi_file_backend"
|