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,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyclotone
4
+ module Transforms
5
+ module Time
6
+ def fast(amount)
7
+ factor = Pattern.to_rational(amount)
8
+
9
+ Pattern.new do |span|
10
+ source_span = TimeSpan.new(span.start * factor, span.stop * factor)
11
+
12
+ query_span(source_span).map do |event|
13
+ Pattern.map_event(event) { |time| time / factor }
14
+ end
15
+ end
16
+ end
17
+
18
+ def slow(amount)
19
+ fast(Rational(1, 1) / Pattern.to_rational(amount))
20
+ end
21
+
22
+ def early(amount)
23
+ offset = Pattern.to_rational(amount)
24
+
25
+ Pattern.new do |span|
26
+ query_span(span.shift(offset)).map do |event|
27
+ Pattern.shift_event(event, -offset)
28
+ end
29
+ end
30
+ end
31
+
32
+ def late(amount)
33
+ early(-Pattern.to_rational(amount))
34
+ end
35
+
36
+ def rev
37
+ Pattern.new do |span|
38
+ cycle_start = Rational(span.cycle_number)
39
+ reversed_span = span.reverse_within(cycle_start)
40
+
41
+ query_span(reversed_span).map do |event|
42
+ event.with_span(
43
+ new_whole: event.whole&.reverse_within(cycle_start),
44
+ new_part: event.part.reverse_within(cycle_start)
45
+ )
46
+ end
47
+ end
48
+ end
49
+
50
+ def palindrome
51
+ Pattern.new do |span|
52
+ span.cycle_number.even? ? rev.query_span(span) : query_span(span)
53
+ end
54
+ end
55
+
56
+ def hurry(amount)
57
+ fast(amount).fmap do |value|
58
+ next value unless value.is_a?(Hash)
59
+
60
+ current_speed = value[:speed] || 1.0
61
+ value.merge(speed: current_speed * amount.to_f)
62
+ end
63
+ end
64
+
65
+ def off(amount, &block)
66
+ transformed = block.call(self).early(amount)
67
+ Pattern.stack([self, transformed])
68
+ end
69
+
70
+ def swing(amount, div = 4)
71
+ step_shift = Pattern.to_rational(amount) / Pattern.to_rational(div)
72
+
73
+ map_events do |event|
74
+ next event unless event.onset
75
+
76
+ cycle_start = Rational(event.onset.floor)
77
+ step_index = (((event.onset - cycle_start) * div).floor).to_i
78
+ next event if step_index.even?
79
+
80
+ Pattern.shift_event(event, step_shift)
81
+ end
82
+ end
83
+
84
+ def inside(amount, &block)
85
+ slow(amount).then(&block).fast(amount)
86
+ end
87
+
88
+ def outside(amount, &block)
89
+ fast(amount).then(&block).slow(amount)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyclotone
4
+ module Transition
5
+ DEFAULT_DURATION = 2
6
+
7
+ def xfade(id, pattern)
8
+ xfade_in(id, DEFAULT_DURATION, pattern)
9
+ end
10
+
11
+ def xfade_in(id, cycles, pattern)
12
+ slot_id = normalize_slot_reference(id)
13
+ current = @slots[slot_id] || Pattern.silence
14
+ replacement = Pattern.ensure_pattern(pattern)
15
+ duration = Pattern.to_rational(cycles)
16
+ return assign(slot_id, replacement) if duration <= 0
17
+
18
+ start_cycle = transition_start_cycle
19
+
20
+ mixed = Pattern.stack([
21
+ apply_gain_envelope(current, start_cycle: start_cycle, duration: duration, direction: :out),
22
+ apply_gain_envelope(replacement, start_cycle: start_cycle, duration: duration, direction: :in)
23
+ ])
24
+
25
+ assign(slot_id, mixed)
26
+ end
27
+
28
+ def clutch(id, pattern)
29
+ clutch_in(id, DEFAULT_DURATION, pattern)
30
+ end
31
+
32
+ def clutch_in(id, cycles, pattern)
33
+ slot_id = normalize_slot_reference(id)
34
+ current = @slots[slot_id] || Pattern.silence
35
+ replacement = Pattern.ensure_pattern(pattern)
36
+ duration = Pattern.to_rational(cycles)
37
+ return assign(slot_id, replacement) if duration <= 0
38
+
39
+ start_cycle = transition_start_cycle
40
+ swapped = Pattern.new do |span|
41
+ source_events = current.query_span(span).map { |event| [event, :current] }
42
+ target_events = replacement.query_span(span).map { |event| [event, :replacement] }
43
+
44
+ (source_events + target_events).filter_map do |event, source|
45
+ time = event.onset || event.part.start
46
+ desired_source = clutch_source(slot_id, time, event.value, start_cycle, duration)
47
+ event if source == desired_source
48
+ end.then { |events| Pattern.sort_events(events) }
49
+ end
50
+
51
+ assign(slot_id, swapped)
52
+ end
53
+
54
+ def interpolate(id, pattern)
55
+ interpolate_in(id, 4, pattern)
56
+ end
57
+
58
+ def interpolate_in(id, cycles, pattern)
59
+ slot_id = normalize_slot_reference(id)
60
+ current = @slots[slot_id] || Pattern.silence
61
+ replacement = Pattern.ensure_pattern(pattern)
62
+ duration = Pattern.to_rational(cycles)
63
+ return assign(slot_id, replacement) if duration <= 0
64
+
65
+ start_cycle = transition_start_cycle
66
+ morphed = Pattern.new do |span|
67
+ anchor_times = Pattern.stack([current, replacement]).query_span(span).map do |event|
68
+ event.onset || event.part.start
69
+ end.uniq.sort
70
+
71
+ anchor_times.filter_map do |time|
72
+ source_event = current.query_event_at(time)
73
+ target_event = replacement.query_event_at(time)
74
+ base_event = target_event || source_event
75
+ next unless base_event
76
+
77
+ progress = transition_progress(time, start_cycle, duration)
78
+ base_event.with_value(interpolate_value(source_event&.value, target_event&.value, progress))
79
+ end.then { |events| Pattern.sort_events(events) }
80
+ end
81
+
82
+ assign(slot_id, morphed)
83
+ end
84
+
85
+ def jump(id, pattern)
86
+ assign(normalize_slot_reference(id), pattern)
87
+ end
88
+
89
+ def jump_in(id, cycles, pattern)
90
+ slot_id = normalize_slot_reference(id)
91
+ current = @slots[slot_id] || Pattern.silence
92
+ replacement = Pattern.ensure_pattern(pattern)
93
+ switch_cycle = transition_start_cycle + Pattern.to_rational(cycles)
94
+ return assign(slot_id, replacement) if switch_cycle <= transition_start_cycle
95
+
96
+ delayed = Pattern.new { |span| split_query(span, switch_cycle, current, replacement) }
97
+
98
+ assign(slot_id, delayed)
99
+ end
100
+
101
+ def anticipate(id, pattern)
102
+ jump_in(id, 8, pattern)
103
+ end
104
+
105
+ def fade_in(cycles)
106
+ start_cycle = transition_start_cycle
107
+ duration = Pattern.to_rational(cycles)
108
+ return self if duration <= 0
109
+
110
+ @slots.keys.each do |slot_id|
111
+ assign(slot_id, apply_gain_envelope(@slots.fetch(slot_id), start_cycle: start_cycle, duration: duration, direction: :in))
112
+ end
113
+
114
+ self
115
+ end
116
+
117
+ def fade_out(cycles)
118
+ start_cycle = transition_start_cycle
119
+ duration = Pattern.to_rational(cycles)
120
+ return self if duration <= 0
121
+
122
+ @slots.keys.each do |slot_id|
123
+ assign(slot_id, apply_gain_envelope(@slots.fetch(slot_id), start_cycle: start_cycle, duration: duration, direction: :out))
124
+ end
125
+
126
+ self
127
+ end
128
+
129
+ private
130
+
131
+ def apply_gain_envelope(pattern, start_cycle:, duration:, direction:)
132
+ Pattern.ensure_pattern(pattern).fmap do |value|
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
+ time = event.onset || event.part.start
145
+ value = event.value.dup
146
+ envelope = value.delete(:gain_envelope)
147
+ current_gain = value[:gain] || 1.0
148
+ value[:gain] = current_gain * envelope.call(time)
149
+ event.with_value(value)
150
+ end
151
+ end
152
+
153
+ def split_query(span, switch_cycle, current, replacement)
154
+ if span.stop <= switch_cycle
155
+ current.query_span(span)
156
+ elsif span.start >= switch_cycle
157
+ replacement.query_span(span)
158
+ else
159
+ before = current.query_span(TimeSpan.new(span.start, switch_cycle))
160
+ after = replacement.query_span(TimeSpan.new(switch_cycle, span.stop))
161
+ Pattern.sort_events(before + after)
162
+ end
163
+ end
164
+
165
+ def transition_start_cycle
166
+ Pattern.to_rational(@scheduler.current_cycle.floor)
167
+ end
168
+
169
+ def transition_progress(time, start_cycle, duration)
170
+ ((time.to_f - start_cycle.to_f) / duration.to_f).clamp(0.0, 1.0)
171
+ end
172
+
173
+ def clutch_source(slot_id, time, value, start_cycle, duration)
174
+ progress = transition_progress(time, start_cycle, duration)
175
+ chosen = Support::Deterministic.float(:clutch, slot_id, time, value) < progress
176
+
177
+ chosen ? :replacement : :current
178
+ end
179
+
180
+ def interpolate_value(source, target, progress)
181
+ return source if target.nil?
182
+ return target if source.nil?
183
+
184
+ if source.is_a?(Numeric) && target.is_a?(Numeric)
185
+ source.to_f + ((target.to_f - source.to_f) * progress)
186
+ elsif source.is_a?(Hash) && target.is_a?(Hash)
187
+ interpolate_hash(source, target, progress)
188
+ else
189
+ progress < 0.5 ? source : target
190
+ end
191
+ end
192
+
193
+ def interpolate_hash(source, target, progress)
194
+ (source.keys | target.keys).each_with_object({}) do |key, result|
195
+ result[key] =
196
+ if source.key?(key) && target.key?(key)
197
+ interpolate_value(source[key], target[key], progress)
198
+ else
199
+ progress < 0.5 ? source.fetch(key, target[key]) : target.fetch(key, source[key])
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cyclotone
4
+ VERSION = "0.1.0"
5
+ end
data/lib/cyclotone.rb ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "cyclotone/errors"
4
+ require_relative "cyclotone/support/deterministic"
5
+ require_relative "cyclotone/version"
6
+ require_relative "cyclotone/euclidean"
7
+ require_relative "cyclotone/mini_notation/ast"
8
+ require_relative "cyclotone/mini_notation/parser"
9
+ require_relative "cyclotone/time_span"
10
+ require_relative "cyclotone/event"
11
+ require_relative "cyclotone/transforms/time"
12
+ require_relative "cyclotone/transforms/concatenation"
13
+ require_relative "cyclotone/transforms/accumulation"
14
+ require_relative "cyclotone/transforms/alteration"
15
+ require_relative "cyclotone/transforms/condition"
16
+ require_relative "cyclotone/transforms/sample"
17
+ require_relative "cyclotone/pattern"
18
+ require_relative "cyclotone/mini_notation/compiler"
19
+ require_relative "cyclotone/controls"
20
+ require_relative "cyclotone/oscillators"
21
+ require_relative "cyclotone/harmony"
22
+ require_relative "cyclotone/state"
23
+ require_relative "cyclotone/backends/osc_backend"
24
+ require_relative "cyclotone/backends/midi_backend"
25
+ require_relative "cyclotone/backends/midi_file_backend"
26
+ require_relative "cyclotone/scheduler"
27
+ require_relative "cyclotone/transition"
28
+ require_relative "cyclotone/stream"
29
+ require_relative "cyclotone/dsl"
30
+
31
+ module Cyclotone
32
+ end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cyclotone
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yudai Takada
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Cyclotone provides rational-time spans, immutable pattern events, and
13
+ composable pattern primitives for building live coding music tools in Ruby.
14
+ email:
15
+ - t.yudai92@gmail.com
16
+ executables:
17
+ - cyclotone
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - LICENSE.txt
22
+ - README.md
23
+ - Rakefile
24
+ - cyclotone.gemspec
25
+ - exe/cyclotone
26
+ - lib/cyclotone.rb
27
+ - lib/cyclotone/backends/midi_backend.rb
28
+ - lib/cyclotone/backends/midi_file_backend.rb
29
+ - lib/cyclotone/backends/midi_message_support.rb
30
+ - lib/cyclotone/backends/osc_backend.rb
31
+ - lib/cyclotone/controls.rb
32
+ - lib/cyclotone/dsl.rb
33
+ - lib/cyclotone/errors.rb
34
+ - lib/cyclotone/euclidean.rb
35
+ - lib/cyclotone/event.rb
36
+ - lib/cyclotone/harmony.rb
37
+ - lib/cyclotone/mini_notation/ast.rb
38
+ - lib/cyclotone/mini_notation/compiler.rb
39
+ - lib/cyclotone/mini_notation/parser.rb
40
+ - lib/cyclotone/oscillators.rb
41
+ - lib/cyclotone/pattern.rb
42
+ - lib/cyclotone/scheduler.rb
43
+ - lib/cyclotone/state.rb
44
+ - lib/cyclotone/stream.rb
45
+ - lib/cyclotone/support/deterministic.rb
46
+ - lib/cyclotone/time_span.rb
47
+ - lib/cyclotone/transforms/accumulation.rb
48
+ - lib/cyclotone/transforms/alteration.rb
49
+ - lib/cyclotone/transforms/concatenation.rb
50
+ - lib/cyclotone/transforms/condition.rb
51
+ - lib/cyclotone/transforms/sample.rb
52
+ - lib/cyclotone/transforms/time.rb
53
+ - lib/cyclotone/transition.rb
54
+ - lib/cyclotone/version.rb
55
+ homepage: https://github.com/ydah/cyclotone
56
+ licenses:
57
+ - MIT
58
+ metadata:
59
+ source_code_uri: https://github.com/ydah/cyclotone
60
+ changelog_uri: https://github.com/ydah/cyclotone/releases
61
+ rubygems_mfa_required: 'true'
62
+ rdoc_options: []
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '3.1'
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ requirements: []
76
+ rubygems_version: 4.0.6
77
+ specification_version: 4
78
+ summary: Pattern-based live coding primitives for Ruby.
79
+ test_files: []