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,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,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
|