xi-lang 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/.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
|