xi-lang 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,302 @@
1
+ module Xi
2
+ class Pattern
3
+ module Transforms
4
+ # Negates every number in the pattern
5
+ #
6
+ # Non-numeric values are ignored.
7
+ #
8
+ # @example
9
+ # peek -[10, 20, 30].p #=> [-10, -20, -30]
10
+ # peek -[1, -2, 3].p #=> [-1, 2, -3]
11
+ #
12
+ # @return [Pattern]
13
+ #
14
+ def -@
15
+ Pattern.new(self) do |y|
16
+ each { |v| y << (v.respond_to?(:-@) ? -v : v) }
17
+ end
18
+ end
19
+
20
+ # Concatenate +object+ pattern or perform a scalar sum with +object+
21
+ #
22
+ # If +object+ is a Pattern, concatenate the two patterns.
23
+ # Else, for each value from pattern, sum with +object+.
24
+ # Values that do not respond to #+ are ignored.
25
+ #
26
+ # @example Concatenation of patterns
27
+ # peek [1, 2, 3].p + [4, 5, 6].p #=> [1, 2, 3, 4, 5, 6]
28
+ #
29
+ # @example Scalar sum
30
+ # peek [1, 2, 3].p + 60 #=> [61, 62, 63]
31
+ # peek [0.25, 0.5].p + 0.125 #=> [0.375, 0.625]
32
+ # peek [0, :foo, 2].p + 1 #=> [1, :foo, 3]
33
+ #
34
+ # @param object [Pattern, Numeric] pattern or numeric
35
+ # @return [Pattern]
36
+ #
37
+ def +(object)
38
+ if object.is_a?(Pattern)
39
+ Pattern.new(self, size: size + object.size) do |y|
40
+ each { |v| y << v }
41
+ object.each { |v| y << v }
42
+ end
43
+ else
44
+ Pattern.new(self) do |y|
45
+ each { |v| y << (v.respond_to?(:+) ? v + object : v) }
46
+ end
47
+ end
48
+ end
49
+
50
+ # Performs a scalar substraction with +numeric+
51
+ #
52
+ # For each value from pattern, substract with +numeric+.
53
+ # Values that do not respond to #- are ignored.
54
+ #
55
+ # @example
56
+ # peek [1, 2, 3].p - 10 #=> [-9, -8, -7]
57
+ # peek [1, :foo, 3].p - 10 #=> [-9, :foo, -7]
58
+ #
59
+ # @param numeric [Numeric]
60
+ # @return [Pattern]
61
+ #
62
+ def -(numeric)
63
+ Pattern.new(self) do |y|
64
+ each { |v| y << (v.respond_to?(:-) ? v - numeric : v) }
65
+ end
66
+ end
67
+
68
+ # Performs a scalar multiplication with +numeric+
69
+ #
70
+ # For each value from pattern, multiplicate with +numeric+.
71
+ # Values that do not respond to #* are ignored.
72
+ #
73
+ # @example
74
+ # peek [1, 2, 4].p * 2 #=> [2, 4, 8]
75
+ # peek [1, :foo].p * 2 #=> [2, :foo]
76
+ #
77
+ # @param numeric [Numeric]
78
+ # @return [Pattern]
79
+ #
80
+ def *(numeric)
81
+ Pattern.new(self) do |y|
82
+ each { |v| y << (v.respond_to?(:*) ? v * numeric : v) }
83
+ end
84
+ end
85
+
86
+ # Performs a scalar division by +numeric+
87
+ #
88
+ # For each value from pattern, divide by +numeric+.
89
+ # Values that do not respond to #/ are ignored.
90
+ #
91
+ # @example
92
+ # peek [1, 2, 4].p / 2 #=> [(1/2), (1/1), (2/1)]
93
+ # peek [0.5, :foo].p / 2 #=> [0.25, :foo]
94
+ #
95
+ # @param numeric [Numeric]
96
+ # @return [Pattern]
97
+ #
98
+ def /(numeric)
99
+ Pattern.new(self) do |y|
100
+ each { |v| y << (v.respond_to?(:/) ? v / numeric : v) }
101
+ end
102
+ end
103
+
104
+ # Performs a scalar modulo against +numeric+
105
+ #
106
+ # For each value from pattern, return modulo of value divided by +numeric+.
107
+ # Values from pattern that do not respond to #% are ignored.
108
+ #
109
+ # @example
110
+ # peek (1..5).p % 2 #=> [1, 0, 1, 0, 1]
111
+ # peek [0, 1, 2, :bar, 4, 5, 6].p % 3 #=> [0, 1, 2, :bar, l, 2, 0]
112
+ #
113
+ # @param numeric [Numeric]
114
+ # @return [Pattern]
115
+ #
116
+ def %(numeric)
117
+ Pattern.new(self) do |y|
118
+ each { |v| y << (v.respond_to?(:%) ? v % numeric : v) }
119
+ end
120
+ end
121
+
122
+ # Raises each value to the power of +numeric+, which may be negative or
123
+ # fractional.
124
+ #
125
+ # Values from pattern that do not respond to #** are ignored.
126
+ #
127
+ # @example
128
+ # peek (0..5).p ** 2 #=> [0, 1, 4, 9, 16, 25]
129
+ # peek [1, 2, 3].p ** -2 #=> [1, (1/4), (1/9)]
130
+ #
131
+ # @param numeric [Numeric]
132
+ # @return [Pattern]
133
+ #
134
+ def **(numeric)
135
+ Pattern.new(self) do |y|
136
+ each { |v| y << (v.respond_to?(:**) ? v ** numeric : v) }
137
+ end
138
+ end
139
+ alias_method :^, :**
140
+
141
+ # Cycles pattern +repeats+ number of times, shifted by +offset+
142
+ #
143
+ # @example
144
+ # peek [1, 2, 3].p.seq #=> [1, 2, 3]
145
+ # peek [1, 2, 3].p.seq(2) #=> [1, 2, 3, 1, 2, 3]
146
+ # peek [1, 2, 3].p.seq(1, 1) #=> [2, 3, 1]
147
+ # peek [1, 2, 3].p.seq(2, 2) #=> [3, 2, 1, 3, 2, 1]
148
+ # peek [1, 2].p.seq(inf, 1) #=> [2, 1, 2, 1, 2, 1, 2, 1, 2, 1]
149
+ #
150
+ # @param repeats [Fixnum, Symbol] number or inf (defaut: 1)
151
+ # @param offset [Fixnum] (default: 0)
152
+ # @return [Pattern]
153
+ #
154
+ def seq(repeats=1, offset=0)
155
+ unless (repeats.is_a?(Fixnum) && repeats >= 0) || repeats == inf
156
+ fail ArgumentError, "repeats must be a non-negative Fixnum or inf"
157
+ end
158
+ unless offset.is_a?(Fixnum) && offset >= 0
159
+ fail ArgumentError, "offset must be a non-negative Fixnum"
160
+ end
161
+
162
+ Pattern.new(self, size: size * repeats) do |y|
163
+ rep = repeats
164
+
165
+ loop do
166
+ if rep != inf
167
+ rep -= 1
168
+ break if rep < 0
169
+ end
170
+
171
+ c = offset
172
+ offset_items = []
173
+
174
+ is_empty = true
175
+ each do |v|
176
+ is_empty = false
177
+ if c > 0
178
+ offset_items << v
179
+ c -= 1
180
+ else
181
+ y << v
182
+ end
183
+ end
184
+
185
+ offset_items.each { |v| y << v }
186
+
187
+ break if is_empty
188
+ end
189
+ end
190
+ end
191
+
192
+ # Traverses the pattern in order and then in reverse order
193
+ #
194
+ # @example
195
+ # peek (0..3).p.bounce #=> [0, 1, 2, 3, 3, 2, 1, 0]
196
+ #
197
+ # @return [Pattern]
198
+ #
199
+ def bounce
200
+ Pattern.new(self, size: size * 2 - 1) do |y|
201
+ each.with_index { |v, i| y << v if i > 0 }
202
+ reverse_each.with_index { |v, i| y << v if i > 0 }
203
+ end
204
+ end
205
+
206
+ # Normalizes a pattern of values that range from +min+ to +max+ to 0..1
207
+ #
208
+ # Values from pattern that do not respond to #- are ignored.
209
+ #
210
+ # @example
211
+ # peek (1..5).p.normalize(0, 100)
212
+ # #=> [(1/100), (1/50), (3/100), (1/25), (1/20)]
213
+ # peek [0, 0x40, 0x80, 0xc0].p.normalize(0, 0x100)
214
+ # #=> [(0/1), (1/4), (1/2), (3/4)]
215
+ #
216
+ # @param min [Numeric]
217
+ # @param max [Numeric]
218
+ # @return [Pattern]
219
+ #
220
+ def normalize(min, max)
221
+ Pattern.new(self) do |y|
222
+ each { |v| y << (v.respond_to?(:-) ? (v - min) / (max - min) : v) }
223
+ end
224
+ end
225
+
226
+ # Scales a pattern of normalized values (0..1) to a custom range
227
+ # +min+..+max+
228
+ #
229
+ # This is inverse of {#normalize}
230
+ # Values from pattern that do not respond to #* are ignored.
231
+ #
232
+ # @example
233
+ # peek [0.01, 0.02, 0.03, 0.04, 0.05].p.denormalize(0, 100)
234
+ # #=> [1.0, 2.0, 3.0, 4.0, 5.0]
235
+ # peek [0, 0.25, 0.50, 0.75].p.denormalize(0, 0x100)
236
+ # #=> [0, 64.0, 128.0, 192.0]
237
+ #
238
+ # @param min [Numeric]
239
+ # @param max [Numeric]
240
+ # @return [Pattern]
241
+ #
242
+ def denormalize(min, max)
243
+ Pattern.new(self) do |y|
244
+ each { |v| y << (v.respond_to?(:*) ? (max - min) * v + min : v) }
245
+ end
246
+ end
247
+
248
+ # Scale from one range of values to another range of values
249
+ #
250
+ # @example
251
+ # peek [0,2,4,1,3,6].p.scale(0, 6, 0, 0x7f)
252
+ #
253
+ # @param min_from [Numeric]
254
+ # @param max_from [Numeric]
255
+ # @param min_to [Numeric]
256
+ # @param max_to [Numeric]
257
+ # @return [Pattern]
258
+ #
259
+ def scale(min_from, max_from, min_to, max_to)
260
+ normalize(min_from, max_from).denormalize(min_to, max_to)
261
+ end
262
+
263
+ # TODO Document
264
+ def decelerate(num)
265
+ Pattern.new(self) do |y|
266
+ each_event { |e| y << E[e.value, e.start * num, e.duration * num] }
267
+ end
268
+ end
269
+
270
+ # TODO Document
271
+ def accelerate(num)
272
+ Pattern.new(self) do |y|
273
+ each_event { |e| y << E[e.value, e.start / num, e.duration / num] }
274
+ end
275
+ end
276
+
277
+ # Based on +probability+, it yields original value or nil
278
+ # TODO Document
279
+ #
280
+ def sometimes(probability=0.5)
281
+ prob_pat = probability.p
282
+ Pattern.new(self, size: size * prob_pat.size) do |y|
283
+ prob_pat.each do |prob|
284
+ each { |v| y << (rand < prob ? v : nil) }
285
+ end
286
+ end
287
+ end
288
+
289
+ # Repeats each value +times+
290
+ # TODO Document
291
+ #
292
+ def repeat_each(times)
293
+ times_pat = times.p
294
+ Pattern.new(self, size: size * times_pat.size) do |y|
295
+ times_pat.each do |t|
296
+ each { |v| t.times { y << v } }
297
+ end
298
+ end
299
+ end
300
+ end
301
+ end
302
+ end
data/lib/xi/pattern.rb ADDED
@@ -0,0 +1,150 @@
1
+ require 'forwardable'
2
+ require 'xi/event'
3
+ require 'xi/pattern/transforms'
4
+ require 'xi/pattern/generators'
5
+
6
+ module Xi
7
+ class Pattern
8
+ include Enumerable
9
+ include Transforms
10
+ include Generators
11
+ extend Forwardable
12
+
13
+ attr_reader :source, :event_duration, :metadata, :total_duration
14
+
15
+ alias_method :dur, :event_duration
16
+
17
+ def_delegators :@source, :size
18
+
19
+ def initialize(enum=nil, size: nil, **metadata)
20
+ size ||= enum.size if enum.respond_to?(:size)
21
+
22
+ @source = if block_given?
23
+ Enumerator.new(size) { |y| yield y }
24
+ elsif enum
25
+ enum
26
+ else
27
+ fail ArgumentError, 'must provide source or block'
28
+ end
29
+
30
+ @is_infinite = @source.size.nil? || @source.size == Float::INFINITY
31
+
32
+ @event_duration = metadata.delete(:dur) || metadata.delete(:event_duration)
33
+ @event_duration ||= enum.event_duration if enum.respond_to?(:event_duration)
34
+ @event_duration ||= 1
35
+
36
+ @metadata = enum.respond_to?(:metadata) ? enum.metadata : {}
37
+ @metadata.merge!(metadata)
38
+
39
+ if @is_infinite
40
+ @total_duration = @event_duration
41
+ else
42
+ last_ev = each_event.take(@source.size).last
43
+ @total_duration = last_ev ? last_ev.start + last_ev.duration : 0
44
+ end
45
+ end
46
+
47
+ def self.[](*args, **metadata)
48
+ new(args, **metadata)
49
+ end
50
+
51
+ def infinite?
52
+ @is_infinite
53
+ end
54
+
55
+ def finite?
56
+ !infinite?
57
+ end
58
+
59
+ def ==(o)
60
+ self.class == o.class &&
61
+ source == o.source &&
62
+ event_duration == o.event_duration &&
63
+ metadata == o.metadata
64
+ end
65
+
66
+ def p(dur=nil, **metadata)
67
+ Pattern.new(@source, dur: dur || @event_duration,
68
+ **@metadata.merge(metadata))
69
+ end
70
+
71
+ def each_event
72
+ return enum_for(__method__) unless block_given?
73
+
74
+ dur = @event_duration
75
+ pos = 0
76
+
77
+ @source.each do |value|
78
+ if value.is_a?(Pattern)
79
+ value.each do |v|
80
+ yield Event.new(v, pos, dur)
81
+ pos += dur
82
+ end
83
+ elsif value.is_a?(Event)
84
+ yield value
85
+ pos += value.duration
86
+ else
87
+ yield Event.new(value, pos, dur)
88
+ pos += dur
89
+ end
90
+ end
91
+ end
92
+
93
+ def each
94
+ return enum_for(__method__) unless block_given?
95
+ each_event { |e| yield e.value }
96
+ end
97
+
98
+ def inspect
99
+ ss = if @source.respond_to?(:join)
100
+ @source.map(&:inspect).join(', ')
101
+ elsif @source.is_a?(Enumerator)
102
+ "?enum"
103
+ else
104
+ @source.inspect
105
+ end
106
+
107
+ ms = @metadata.reject { |_, v| v.nil? }
108
+ ms.merge!(dur: dur) if dur != 1
109
+ ms = ms.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')
110
+
111
+ "P[#{ss}#{", #{ms}" unless ms.empty?}]"
112
+ end
113
+ alias_method :to_s, :inspect
114
+
115
+ def map_events
116
+ return enum_for(__method__) unless block_given?
117
+ Pattern.new(dur: dur, **metadata) { |y| each_event { |e| y << yield(e) } }
118
+ end
119
+ alias_method :collect_events, :map_events
120
+
121
+ def select_events
122
+ return enum_for(__method__) unless block_given?
123
+ Pattern.new { |y| each_event { |e| y << e if yield(e) } }
124
+ end
125
+ alias_method :find_all_events, :select_events
126
+
127
+ def reject_events
128
+ return enum_for(__method__) unless block_given?
129
+ Pattern.new { |y| each_event { |e| y << e unless yield(e) } }
130
+ end
131
+
132
+ def to_events
133
+ each_event.to_a
134
+ end
135
+
136
+ def peek(limit=10)
137
+ values = take(limit + 1)
138
+ puts "There are more than #{limit} values..." if values.size > limit
139
+ values.take(limit)
140
+ end
141
+
142
+ def peek_events(limit=10)
143
+ events = each_event.take(limit + 1)
144
+ puts "There are more than #{limit} events..." if events.size > limit
145
+ events.take(limit)
146
+ end
147
+ end
148
+ end
149
+
150
+ P = Xi::Pattern
data/lib/xi/repl.rb ADDED
@@ -0,0 +1,66 @@
1
+ require "pry"
2
+ require 'io/console'
3
+ require "xi/error_log"
4
+
5
+ module Xi
6
+ module REPL
7
+ extend self
8
+
9
+ CONFIG_PATH = File.expand_path("~/.config/xi")
10
+ HISTORY_FILE = "history"
11
+ INIT_SCRIPT_FILE = "init.rb"
12
+
13
+ DEFAULT_INIT_SCRIPT =
14
+ "# Here you can customize or define functions that will be available in\n" \
15
+ "# Xi, e.g. new streams or a custom clock."
16
+
17
+ def start
18
+ configure
19
+ load_init_script
20
+
21
+ Pry.start
22
+ end
23
+
24
+ def configure
25
+ prepare_config_dir
26
+
27
+ if ENV["INSIDE_EMACS"]
28
+ Pry.config.correct_indent = false
29
+ Pry.config.pager = false
30
+ Pry.config.prompt = [ proc { "" }, proc { "" }]
31
+ else
32
+ Pry.config.prompt = [ proc { "xi> " }, proc { "..> " }]
33
+ end
34
+
35
+ Pry.config.history.file = history_path
36
+
37
+ Pry.hooks.add_hook(:after_eval, "check_for_errors") do |result, pry|
38
+ more_errors = ErrorLog.instance.more_errors?
39
+ ErrorLog.instance.each do |msg|
40
+ puts "(╯°□°)╯︵ ɹoɹɹǝ #{msg}"
41
+ end
42
+ puts "(⌣_⌣”) There were more errors..." if more_errors
43
+ end
44
+ end
45
+
46
+ def load_init_script
47
+ require(init_script_path)
48
+ end
49
+
50
+ def prepare_config_dir
51
+ FileUtils.mkdir_p(CONFIG_PATH)
52
+
53
+ unless File.exists?(init_script_path)
54
+ File.write(init_script_path, DEFAULT_INIT_SCRIPT)
55
+ end
56
+ end
57
+
58
+ def history_path
59
+ File.join(CONFIG_PATH, HISTORY_FILE)
60
+ end
61
+
62
+ def init_script_path
63
+ File.join(CONFIG_PATH, INIT_SCRIPT_FILE)
64
+ end
65
+ end
66
+ end
data/lib/xi/stream.rb ADDED
@@ -0,0 +1,203 @@
1
+ require 'set'
2
+
3
+ module Xi
4
+ class Stream
5
+ WINDOW_SEC = 0.05
6
+
7
+ attr_reader :clock, :source, :source_patterns, :state, :event_duration, :gate
8
+
9
+ def initialize(clock)
10
+ @mutex = Mutex.new
11
+ @playing = false
12
+ @state = {}
13
+ @new_sound_object_id = 0
14
+ @changed_params = [].to_set
15
+
16
+ self.clock = clock
17
+ end
18
+
19
+ def set(event_duration: nil, gate: nil, **source)
20
+ @mutex.synchronize do
21
+ @source = source
22
+ @gate = gate if gate
23
+ @event_duration = event_duration if event_duration
24
+ update_internal_structures
25
+ end
26
+ play
27
+ self
28
+ end
29
+ alias_method :<<, :set
30
+
31
+ def event_duration=(new_value)
32
+ @mutex.synchronize do
33
+ @event_duration = new_value
34
+ update_internal_structures
35
+ end
36
+ end
37
+
38
+ def gate=(new_value)
39
+ @mutex.synchronize do
40
+ @gate = new_value
41
+ update_internal_structures
42
+ end
43
+ end
44
+
45
+ def clock=(new_clock)
46
+ @clock.unsubscribe(self) if @clock
47
+ new_clock.subscribe(self) if playing?
48
+ @clock = new_clock
49
+ end
50
+
51
+ def playing?
52
+ @mutex.synchronize { @playing }
53
+ end
54
+
55
+ def stopped?
56
+ !playing?
57
+ end
58
+
59
+ def play
60
+ @mutex.synchronize do
61
+ @playing = true
62
+ @clock.subscribe(self)
63
+ end
64
+ self
65
+ end
66
+ alias_method :start, :play
67
+
68
+ def stop
69
+ @mutex.synchronize do
70
+ @playing = false
71
+ @state.clear
72
+ @clock.unsubscribe(self)
73
+ end
74
+ self
75
+ end
76
+ alias_method :pause, :play
77
+
78
+ def inspect
79
+ "#<#{self.class.name}:#{"0x%014x" % object_id} clock=#{@clock.inspect} #{playing? ? :playing : :stopped}>"
80
+ rescue => err
81
+ logger.error(err)
82
+ end
83
+
84
+ def notify(now)
85
+ return unless playing? && @source
86
+
87
+ @mutex.synchronize do
88
+ @changed_params.clear
89
+
90
+ forward_enums(now) if @must_forward
91
+
92
+ gate_on, gate_off = play_enums(now)
93
+
94
+ do_gate_off_change(gate_off) unless gate_off.empty?
95
+ do_state_change if state_changed?
96
+ do_gate_on_change(gate_on) unless gate_on.empty?
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def changed_state
103
+ @state.select { |k, _| @changed_params.include?(k) }
104
+ end
105
+
106
+ def forward_enums(now)
107
+ @enums.each do |p, (enum, total_dur)|
108
+ cur_pos = now % total_dur
109
+ next_ev = enum.peek
110
+
111
+ while distance = (cur_pos - next_ev.start) % total_dur do
112
+ enum.next
113
+
114
+ break if distance <= next_ev.duration
115
+ next_ev = enum.peek
116
+ end
117
+ end
118
+ @must_forward = false
119
+ end
120
+
121
+ def play_enums(now)
122
+ gate_off = []
123
+ gate_on = []
124
+
125
+ @enums.each do |p, (enum, total_dur)|
126
+ cur_pos = now % total_dur
127
+ next_ev = enum.peek
128
+
129
+ # Check if there are any currently playing sound objects that
130
+ # must be gated off
131
+ @playing_sound_objects.dup.each do |end_pos, so_ids|
132
+ if (cur_pos - end_pos) % total_dur <= WINDOW_SEC
133
+ gate_off = so_ids
134
+ @playing_sound_objects.delete(end_pos)
135
+ end
136
+ end
137
+
138
+ # Do we need to play next event now? If not, skip this parameter
139
+ if (cur_pos - next_ev.start) % total_dur <= WINDOW_SEC
140
+ # Update state based on pattern value
141
+ update_state(p, next_ev.value)
142
+
143
+ # If this parameter is a gate, mark it as gate on as
144
+ # a new sound object
145
+ if p == @gate
146
+ new_so_ids = Array(next_ev.value).size.times.map do
147
+ so_id = @new_sound_object_id
148
+ @new_sound_object_id += 1
149
+ so_id
150
+ end
151
+ gate_on = new_so_ids
152
+ @playing_sound_objects[next_ev.end] = new_so_ids
153
+ end
154
+
155
+ # Because we already processed event, advance enumerator
156
+ enum.next
157
+ end
158
+ end
159
+
160
+ [gate_on, gate_off]
161
+ end
162
+
163
+ def update_internal_structures
164
+ @playing_sound_objects ||= {}
165
+ @must_forward = true
166
+ @enums = @source.map { |k, v|
167
+ pat = v.p(@event_duration)
168
+ [k, [infinite_enum(pat), pat.total_duration]]
169
+ }.to_h
170
+ end
171
+
172
+ def do_gate_on_change(ss)
173
+ logger.info "Gate on change: #{ss}"
174
+ end
175
+
176
+ def do_gate_off_change(ss)
177
+ logger.info "Gate off change: #{ss}"
178
+ end
179
+
180
+ def do_state_change
181
+ logger.info "State change: #{@state.select { |k, v| @changed_params.include?(k) }.to_h}"
182
+ end
183
+
184
+ def update_state(p, v)
185
+ logger.debug "Update state of :#{p}: #{v}"
186
+ @changed_params << p if v != @state[p]
187
+ @state[p] = v
188
+ end
189
+
190
+ def state_changed?
191
+ !@changed_params.empty?
192
+ end
193
+
194
+ def infinite_enum(p)
195
+ Enumerator.new { |y| loop { p.each_event { |e| y << e } } }
196
+ end
197
+
198
+ def logger
199
+ # FIXME this should be configurable
200
+ @logger ||= Logger.new("/tmp/xi.log")
201
+ end
202
+ end
203
+ end