xi-lang 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +70 -0
- data/Rakefile +10 -0
- data/bin/xi +8 -0
- data/lib/xi/clock.rb +81 -0
- data/lib/xi/core_ext/enumerable.rb +25 -0
- data/lib/xi/core_ext/fixnum.rb +11 -0
- data/lib/xi/core_ext/numeric.rb +34 -0
- data/lib/xi/core_ext/simple.rb +15 -0
- data/lib/xi/core_ext.rb +4 -0
- data/lib/xi/error_log.rb +45 -0
- data/lib/xi/event.rb +41 -0
- data/lib/xi/pattern/generators.rb +141 -0
- data/lib/xi/pattern/transforms.rb +302 -0
- data/lib/xi/pattern.rb +150 -0
- data/lib/xi/repl.rb +66 -0
- data/lib/xi/stream.rb +203 -0
- data/lib/xi/version.rb +3 -0
- data/lib/xi.rb +26 -0
- data/xi.gemspec +33 -0
- metadata +170 -0
@@ -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
|