xi-lang 0.1.3 → 0.1.4
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 +4 -4
- data/README.md +1 -1
- data/lib/xi/clock.rb +14 -12
- data/lib/xi/core_ext/string.rb +50 -0
- data/lib/xi/core_ext.rb +1 -0
- data/lib/xi/event.rb +41 -0
- data/lib/xi/pattern/generators.rb +57 -9
- data/lib/xi/pattern/transforms.rb +105 -5
- data/lib/xi/pattern.rb +26 -4
- data/lib/xi/stream.rb +76 -25
- data/lib/xi/tidal_clock.rb +118 -0
- data/lib/xi/version.rb +1 -1
- data/lib/xi.rb +24 -19
- data/xi.gemspec +1 -0
- metadata +18 -3
- data/lib/xi/music_parameters.rb +0 -47
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 38043c0ad025b398a49d63e1c93681992c0b184a
|
4
|
+
data.tar.gz: fbc0e32315abf8272f388bbe59917236633a0d09
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 10f089d5ecb56a89e70091b2845654fdbdf7beb9c7b3fc23366038ae078769065166b3b89fd08de19fca0ec99612bce60853f16554aacb2d269036e895df4a29
|
7
|
+
data.tar.gz: e4fa8eef3158cbf1d14d8f9b8d4241dfe83de117a1ef6730c947992baf555a9c110f425e9d7828eaafdf9201cf71f52d9c2cd2737100e85c198397ce22048a68
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Xi
|
1
|
+
# Xi [](https://travis-ci.org/xi-livecode/xi)
|
2
2
|
|
3
3
|
Xi (pronounced /ˈzaɪ/) is a musical pattern language inspired in Tidal and
|
4
4
|
SuperCollider for building higher-level musical constructs easily. It is
|
data/lib/xi/clock.rb
CHANGED
@@ -8,11 +8,15 @@ module Xi
|
|
8
8
|
DEFAULT_CPS = 1.0
|
9
9
|
INTERVAL_SEC = 10 / 1000.0
|
10
10
|
|
11
|
+
attr_reader :init_ts, :latency
|
12
|
+
|
11
13
|
def initialize(cps: DEFAULT_CPS)
|
12
14
|
@mutex = Mutex.new
|
13
15
|
@cps = cps
|
14
16
|
@playing = true
|
15
17
|
@streams = [].to_set
|
18
|
+
@init_ts = Time.now.to_f
|
19
|
+
@latency = 0.0
|
16
20
|
@play_thread = Thread.new { thread_routine }
|
17
21
|
end
|
18
22
|
|
@@ -29,7 +33,11 @@ module Xi
|
|
29
33
|
end
|
30
34
|
|
31
35
|
def cps=(new_cps)
|
32
|
-
@mutex.synchronize { @cps = new_cps }
|
36
|
+
@mutex.synchronize { @cps = new_cps.to_f }
|
37
|
+
end
|
38
|
+
|
39
|
+
def latency=(new_latency)
|
40
|
+
@latency = new_latency.to_f
|
33
41
|
end
|
34
42
|
|
35
43
|
def playing?
|
@@ -40,10 +48,6 @@ module Xi
|
|
40
48
|
!playing?
|
41
49
|
end
|
42
50
|
|
43
|
-
def now
|
44
|
-
Time.now.to_f * cps
|
45
|
-
end
|
46
|
-
|
47
51
|
def play
|
48
52
|
@mutex.synchronize { @playing = true }
|
49
53
|
self
|
@@ -60,12 +64,9 @@ module Xi
|
|
60
64
|
@mutex.synchronize { 1.0 / @cps }
|
61
65
|
end
|
62
66
|
|
63
|
-
def at(cycle_pos)
|
64
|
-
Time.at(cycle_pos * seconds_per_cycle)
|
65
|
-
end
|
66
|
-
|
67
67
|
def inspect
|
68
|
-
"#<#{self.class.name}:#{"0x%014x" % object_id}
|
68
|
+
"#<#{self.class.name}:#{"0x%014x" % object_id} " \
|
69
|
+
"cps=#{cps.inspect} #{playing? ? :playing : :stopped}>"
|
69
70
|
end
|
70
71
|
|
71
72
|
private
|
@@ -78,9 +79,10 @@ module Xi
|
|
78
79
|
end
|
79
80
|
|
80
81
|
def do_tick
|
81
|
-
cycles = Time.now.to_f * cps
|
82
82
|
return unless playing?
|
83
|
-
|
83
|
+
now = Time.now.to_f - @init_ts + @latency
|
84
|
+
cps = self.cps
|
85
|
+
@streams.each { |s| s.notify(now, cps) }
|
84
86
|
rescue => err
|
85
87
|
error(err)
|
86
88
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# String Inflectors, taken from ActiveSupport 5.0 source code
|
2
|
+
module Xi::Inflectors
|
3
|
+
# Converts strings to UpperCamelCase.
|
4
|
+
# If the +uppercase_first_letter+ parameter is set to false, then produces
|
5
|
+
# lowerCamelCase.
|
6
|
+
#
|
7
|
+
# Also converts '/' to '::' which is useful for converting
|
8
|
+
# paths to namespaces.
|
9
|
+
#
|
10
|
+
# camelize('active_model') # => "ActiveModel"
|
11
|
+
# camelize('active_model', false) # => "activeModel"
|
12
|
+
# camelize('active_model/errors') # => "ActiveModel::Errors"
|
13
|
+
# camelize('active_model/errors', false) # => "activeModel::Errors"
|
14
|
+
#
|
15
|
+
# As a rule of thumb you can think of +camelize+ as the inverse of
|
16
|
+
# #underscore, though there are cases where that does not hold:
|
17
|
+
#
|
18
|
+
# camelize(underscore('SSLError')) # => "SslError"
|
19
|
+
def camelize
|
20
|
+
string = self.sub(/^[a-z\d]*/) { |match| match.capitalize }
|
21
|
+
string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" }
|
22
|
+
string.gsub!('/'.freeze, '::'.freeze)
|
23
|
+
string
|
24
|
+
end
|
25
|
+
|
26
|
+
# Makes an underscored, lowercase form from the expression in the string.
|
27
|
+
#
|
28
|
+
# Changes '::' to '/' to convert namespaces to paths.
|
29
|
+
#
|
30
|
+
# underscore('ActiveModel') # => "active_model"
|
31
|
+
# underscore('ActiveModel::Errors') # => "active_model/errors"
|
32
|
+
#
|
33
|
+
# As a rule of thumb you can think of +underscore+ as the inverse of
|
34
|
+
# #camelize, though there are cases where that does not hold:
|
35
|
+
#
|
36
|
+
# camelize(underscore('SSLError')) # => "SslError"
|
37
|
+
def underscore
|
38
|
+
return self unless self =~ /[A-Z-]|::/
|
39
|
+
word = self.to_s.gsub('::'.freeze, '/'.freeze)
|
40
|
+
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2'.freeze)
|
41
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2'.freeze)
|
42
|
+
word.tr!("-".freeze, "_".freeze)
|
43
|
+
word.downcase!
|
44
|
+
word
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class String
|
49
|
+
include Xi::Inflectors
|
50
|
+
end
|
data/lib/xi/core_ext.rb
CHANGED
data/lib/xi/event.rb
CHANGED
@@ -1,13 +1,44 @@
|
|
1
1
|
module Xi
|
2
|
+
# An Event is an object that represents a scalar +value+ of some type, and
|
3
|
+
# has a +start+ position or onset, and +duration+ in time.
|
4
|
+
#
|
5
|
+
# Both +start+ and +duration+ are in terms of cycles.
|
6
|
+
#
|
7
|
+
# Usually you don't create events, they are created by a Pattern when
|
8
|
+
# assigned to a Stream, or by some transformation methods on Pattern, so you
|
9
|
+
# don't need to worry about them. Most of the time, you will manually build
|
10
|
+
# Patterns from values and let the Pattern handle when values are applied in
|
11
|
+
# time, based on its default event duration, for example.
|
12
|
+
#
|
13
|
+
# You can instantiate an Event using {.[]}, like this
|
14
|
+
#
|
15
|
+
# Event.new(42, 0, 2) #=> E[42,0,2]
|
16
|
+
# Event[:a, 1, 1/2] #=> E[:a,1,1/2]
|
17
|
+
#
|
18
|
+
# E is an alias of Event, so you can build them using E instead. Note that
|
19
|
+
# the string representation of the object can be used to build the same event
|
20
|
+
# again (almost the same ignoring whitespace between constructor arguments).
|
21
|
+
#
|
22
|
+
# E[:a, 1, 1/4] #=> E[:a,1,1/4]
|
23
|
+
#
|
2
24
|
class Event
|
3
25
|
attr_reader :value, :start, :duration
|
4
26
|
|
27
|
+
# Creates a new Event with +value+, with both +start+ position and
|
28
|
+
# +duration+ in cycles
|
29
|
+
#
|
30
|
+
# @param value [Object]
|
31
|
+
# @param start [Numeric] default: 0
|
32
|
+
# @param duration [Numeric] default: 1
|
33
|
+
# @return [Event]
|
34
|
+
#
|
5
35
|
def initialize(value, start=0, duration=1)
|
6
36
|
@value = value
|
7
37
|
@start = start
|
8
38
|
@duration = duration
|
9
39
|
end
|
10
40
|
|
41
|
+
# @see #initialize
|
11
42
|
def self.[](*args)
|
12
43
|
new(*args)
|
13
44
|
end
|
@@ -19,10 +50,20 @@ module Xi
|
|
19
50
|
duration == o.duration
|
20
51
|
end
|
21
52
|
|
53
|
+
# Return the end position in cycles
|
54
|
+
#
|
55
|
+
# @return [Numeric]
|
56
|
+
#
|
22
57
|
def end
|
23
58
|
@start + @duration
|
24
59
|
end
|
25
60
|
|
61
|
+
# Creates a Pattern that only yields this event
|
62
|
+
#
|
63
|
+
# @param dur [Numeric, #each] event duration
|
64
|
+
# @param metadata [Hash]
|
65
|
+
# @return [Pattern]
|
66
|
+
#
|
26
67
|
def p(dur=nil, **metadata)
|
27
68
|
[self].p(dur, metadata)
|
28
69
|
end
|
@@ -51,12 +51,17 @@ module Xi
|
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
|
-
# Choose items from the list randomly
|
54
|
+
# Choose items from the +list+ randomly, +repeats+ number of times
|
55
|
+
#
|
56
|
+
# +list+ can be a *finite* enumerable or Pattern.
|
57
|
+
#
|
58
|
+
# @see Pattern::Transforms#rand
|
55
59
|
#
|
56
60
|
# @example
|
57
|
-
# peek [1, 2, 3]
|
58
|
-
# peek [1, 2, 3, 4]
|
61
|
+
# peek P.rand([1, 2, 3]) #=> [2]
|
62
|
+
# peek P.rand([1, 2, 3, 4], 6) #=> [1, 3, 2, 2, 4, 3]
|
59
63
|
#
|
64
|
+
# @param list [#each] list of values
|
60
65
|
# @param repeats [Fixnum, Symbol] number or inf (default: 1)
|
61
66
|
# @return [Pattern]
|
62
67
|
#
|
@@ -70,10 +75,15 @@ module Xi
|
|
70
75
|
# Choose randomly, but only allow repeating the same item after yielding
|
71
76
|
# all items from the list.
|
72
77
|
#
|
78
|
+
# +list+ can be a *finite* enumerable or Pattern.
|
79
|
+
#
|
80
|
+
# @see Pattern::Transforms#xrand
|
81
|
+
#
|
73
82
|
# @example
|
74
|
-
# peek [1, 2, 3, 4, 5]
|
75
|
-
# peek [1, 2, 3]
|
83
|
+
# peek P.xrand([1, 2, 3, 4, 5]) #=> [4]
|
84
|
+
# peek P.xrand([1, 2, 3], 8) #=> [1, 3, 2, 3, 1, 2, 3, 2]
|
76
85
|
#
|
86
|
+
# @param list [#each] list of values
|
77
87
|
# @param repeats [Fixnum, Symbol] number or inf (default: 1)
|
78
88
|
# @return [Pattern]
|
79
89
|
#
|
@@ -91,10 +101,15 @@ module Xi
|
|
91
101
|
# Shuffle the list in random order, and use the same random order
|
92
102
|
# +repeats+ times
|
93
103
|
#
|
104
|
+
# +list+ can be a *finite* enumerable or Pattern.
|
105
|
+
#
|
106
|
+
# @see Pattern::Transforms#shuf
|
107
|
+
#
|
94
108
|
# @example
|
95
|
-
# peek [1, 2, 3, 4, 5]
|
96
|
-
# peek [1, 2, 3]
|
109
|
+
# peek P.shuf([1, 2, 3, 4, 5]) #=> [5, 3, 4, 1, 2]
|
110
|
+
# peek P.shuf([1, 2, 3], 3) #=> [2, 3, 1, 2, 3, 1, 2, 3, 1]
|
97
111
|
#
|
112
|
+
# @param list [#each] list of values
|
98
113
|
# @param repeats [Fixnum, Symbol] number or inf (default: 1)
|
99
114
|
# @return [Pattern]
|
100
115
|
#
|
@@ -107,7 +122,26 @@ module Xi
|
|
107
122
|
end
|
108
123
|
end
|
109
124
|
|
110
|
-
#
|
125
|
+
# Generates values from a sinewave discretized to +quant+ events
|
126
|
+
# for the duration of +dur+ cycles.
|
127
|
+
#
|
128
|
+
# Values range from -1 to 1
|
129
|
+
#
|
130
|
+
# @see #sin1 for the same function but constrained on 0 to 1 values
|
131
|
+
#
|
132
|
+
# @example
|
133
|
+
# P.sin(8).map { |i| i.round(2) }
|
134
|
+
# #=> [0.0, 0.71, 1.0, 0.71, 0.0, -0.71, -1.0, -0.71]
|
135
|
+
#
|
136
|
+
# @example +quant+ determines the size, +dur+ the total duration
|
137
|
+
# P.sin(8).size #=> 8
|
138
|
+
# P.sin(22).total_duration #=> (1/1)
|
139
|
+
# P.sin(19, 2).total_duration #=> (2/1)
|
140
|
+
#
|
141
|
+
# @param quant [Fixnum]
|
142
|
+
# @param dur [Fixnum] (default: 1)
|
143
|
+
# @return [Pattern]
|
144
|
+
#
|
111
145
|
def sin(quant, dur=1)
|
112
146
|
Pattern.new(size: quant, dur: dur / quant) do |y|
|
113
147
|
quant.times do |i|
|
@@ -116,7 +150,21 @@ module Xi
|
|
116
150
|
end
|
117
151
|
end
|
118
152
|
|
119
|
-
#
|
153
|
+
# Generates values from a sinewave discretized to +quant+ events
|
154
|
+
# for the duration of +dur+ cycles.
|
155
|
+
#
|
156
|
+
# Values range from 0 to 1
|
157
|
+
#
|
158
|
+
# @see #sin
|
159
|
+
#
|
160
|
+
# @example
|
161
|
+
# P.sin1(8).map { |i| i.round(2) }
|
162
|
+
# #=> [0.5, 0.85, 1.0, 0.85, 0.5, 0.15, 0.0, 0.15]
|
163
|
+
#
|
164
|
+
# @param quant [Fixnum]
|
165
|
+
# @param dur [Fixnum] (default: 1)
|
166
|
+
# @return [Pattern]
|
167
|
+
#
|
120
168
|
def sin1(quant, dur=1)
|
121
169
|
sin(quant, dur).scale(-1, 1, 0, 1)
|
122
170
|
end
|
@@ -291,6 +291,7 @@ module Xi
|
|
291
291
|
#
|
292
292
|
# @example
|
293
293
|
# peek [0,2,4,1,3,6].p.scale(0, 6, 0, 0x7f)
|
294
|
+
# #=> [(0/1), (127/3), (254/3), (127/6), (127/2), (127/1)]
|
294
295
|
#
|
295
296
|
# @param min_from [Numeric]
|
296
297
|
# @param max_from [Numeric]
|
@@ -302,14 +303,38 @@ module Xi
|
|
302
303
|
normalize(min_from, max_from).denormalize(min_to, max_to)
|
303
304
|
end
|
304
305
|
|
305
|
-
#
|
306
|
+
# Slows down a pattern by stretching start and duration of events
|
307
|
+
# +num+ times.
|
308
|
+
#
|
309
|
+
# It is the inverse operation of #accelerate
|
310
|
+
#
|
311
|
+
# @example
|
312
|
+
# peek_events %w(a b c d).p([1/4, 1/8, 1/6]).decelerate(2)
|
313
|
+
# #=> [E["a",0,1/2], E["b",1/2,1/4], E["c",3/4,1/3], E["d",13/12,1/2]]
|
314
|
+
#
|
315
|
+
# @param num [Numeric]
|
316
|
+
# @return [Pattern]
|
317
|
+
# @see #accelerate
|
318
|
+
#
|
306
319
|
def decelerate(num)
|
307
320
|
Pattern.new(self) { |y|
|
308
321
|
each_event { |e| y << E[e.value, e.start * num, e.duration * num] }
|
309
322
|
}
|
310
323
|
end
|
311
324
|
|
312
|
-
#
|
325
|
+
# Advance a pattern by shrinking start and duration of events
|
326
|
+
# +num+ times.
|
327
|
+
#
|
328
|
+
# It is the inverse operation of #decelerate
|
329
|
+
#
|
330
|
+
# @example
|
331
|
+
# peek_events %w(a b c d).p([1/2, 1/4]).accelerate(2)
|
332
|
+
# #=> [E["a",0,1/4], E["b",1/4,1/8], E["c",3/8,1/4], E["d",5/8,1/8]]
|
333
|
+
#
|
334
|
+
# @param num [Numeric]
|
335
|
+
# @return [Pattern]
|
336
|
+
# @see #decelerate
|
337
|
+
#
|
313
338
|
def accelerate(num)
|
314
339
|
Pattern.new(self) { |y|
|
315
340
|
each_event { |e| y << E[e.value, e.start / num, e.duration / num] }
|
@@ -317,12 +342,26 @@ module Xi
|
|
317
342
|
end
|
318
343
|
|
319
344
|
# Based on +probability+, it yields original value or nil
|
320
|
-
#
|
345
|
+
#
|
346
|
+
# +probability+ can also be an enumerable or a *finite* Pattern. In this
|
347
|
+
# case, for each value in +probability+ it will enumerate original
|
348
|
+
# pattern based on that probability value.
|
349
|
+
#
|
350
|
+
# @example
|
351
|
+
# peek (1..6).p.sometimes #=> [1, nil, 3, nil, 5, 6]
|
352
|
+
# peek (1..6).p.sometimes(1/4) #=> [nil, nil, nil, 4, nil, 6]
|
353
|
+
#
|
354
|
+
# @example
|
355
|
+
# peek (1..6).p.sometimes([0.5, 1]), 12
|
356
|
+
# #=> [1, 2, nil, nil, 5, 6, 1, 2, 3, 4, 5, 6]
|
357
|
+
#
|
358
|
+
# @param probability [Numeric, #each] (default=0.5)
|
359
|
+
# @return [Pattern]
|
321
360
|
#
|
322
361
|
def sometimes(probability=0.5)
|
323
362
|
prob_pat = probability.p
|
324
363
|
|
325
|
-
if
|
364
|
+
if prob_pat.infinite?
|
326
365
|
fail ArgumentError, 'times must be a finite pattern'
|
327
366
|
end
|
328
367
|
|
@@ -334,7 +373,21 @@ module Xi
|
|
334
373
|
end
|
335
374
|
|
336
375
|
# Repeats each value +times+
|
337
|
-
#
|
376
|
+
#
|
377
|
+
# +times+ can also be an enumerable or a *finite* Pattern. In this case,
|
378
|
+
# for each value in +times+, it will yield each value of original pattern
|
379
|
+
# repeated a number of times based on that +times+ value.
|
380
|
+
#
|
381
|
+
# @example
|
382
|
+
# peek [1, 2, 3].p.repeat_each(2) #=> [1, 1, 2, 2, 3, 3]
|
383
|
+
# peek [1, 2, 3].p.repeat_each(3) #=> [1, 1, 1, 2, 2, 2, 3, 3, 3]
|
384
|
+
#
|
385
|
+
# @example
|
386
|
+
# peek [1, 2, 3].p.repeat_each([3,2]), 15
|
387
|
+
# #=> [1, 1, 1, 2, 2, 2, 3, 3, 3, 1, 1, 2, 2, 3, 3]
|
388
|
+
#
|
389
|
+
# @param times [Numeric, #each]
|
390
|
+
# @return [Pattern]
|
338
391
|
#
|
339
392
|
def repeat_each(times)
|
340
393
|
times_pat = times.p
|
@@ -349,6 +402,53 @@ module Xi
|
|
349
402
|
end
|
350
403
|
end
|
351
404
|
end
|
405
|
+
|
406
|
+
# Choose items from the list randomly, +repeats+ number of times
|
407
|
+
#
|
408
|
+
# @see Pattern::Generators::ClassMethods#rand
|
409
|
+
#
|
410
|
+
# @example
|
411
|
+
# peek [1, 2, 3].p.rand #=> [2]
|
412
|
+
# peek [1, 2, 3, 4].p.rand(6) #=> [1, 3, 2, 2, 4, 3]
|
413
|
+
#
|
414
|
+
# @param repeats [Fixnum, Symbol] number or inf (default: 1)
|
415
|
+
# @return [Pattern]
|
416
|
+
#
|
417
|
+
def rand(repeats=1)
|
418
|
+
P.rand(self, repeats)
|
419
|
+
end
|
420
|
+
|
421
|
+
# Choose randomly, but only allow repeating the same item after yielding
|
422
|
+
# all items from the list.
|
423
|
+
#
|
424
|
+
# @see Pattern::Generators::ClassMethods#xrand
|
425
|
+
#
|
426
|
+
# @example
|
427
|
+
# peek [1, 2, 3, 4, 5].p.xrand #=> [4]
|
428
|
+
# peek [1, 2, 3].p.xrand(8) #=> [1, 3, 2, 3, 1, 2, 3, 2]
|
429
|
+
#
|
430
|
+
# @param repeats [Fixnum, Symbol] number or inf (default: 1)
|
431
|
+
# @return [Pattern]
|
432
|
+
#
|
433
|
+
def xrand(repeats=1)
|
434
|
+
P.xrand(self, repeats)
|
435
|
+
end
|
436
|
+
|
437
|
+
# Shuffle the list in random order, and use the same random order
|
438
|
+
# +repeats+ times
|
439
|
+
#
|
440
|
+
# @see Pattern::Generators::ClassMethods#shuf
|
441
|
+
#
|
442
|
+
# @example
|
443
|
+
# peek [1, 2, 3, 4, 5].p.xrand #=> [4]
|
444
|
+
# peek [1, 2, 3].p.xrand(8) #=> [1, 3, 2, 3, 1, 2, 3, 2]
|
445
|
+
#
|
446
|
+
# @param repeats [Fixnum, Symbol] number or inf (default: 1)
|
447
|
+
# @return [Pattern]
|
448
|
+
#
|
449
|
+
def shuf(repeats=1)
|
450
|
+
P.shuf(self, repeats)
|
451
|
+
end
|
352
452
|
end
|
353
453
|
end
|
354
454
|
end
|
data/lib/xi/pattern.rb
CHANGED
@@ -16,6 +16,13 @@ module Xi
|
|
16
16
|
|
17
17
|
def_delegators :@source, :size
|
18
18
|
|
19
|
+
# Creates a new Pattern given either a +source+ or a block
|
20
|
+
# that yields values
|
21
|
+
#
|
22
|
+
# @param source [#each]
|
23
|
+
# @param size [Fixnum] number of elements (default: nil)
|
24
|
+
# @param dur [Hash]
|
25
|
+
#
|
19
26
|
def initialize(source=nil, size: nil, **metadata)
|
20
27
|
if source.nil? && !block_given?
|
21
28
|
fail ArgumentError, 'must provide source or block'
|
@@ -29,8 +36,6 @@ module Xi
|
|
29
36
|
source
|
30
37
|
end
|
31
38
|
|
32
|
-
@is_infinite = @source.size.nil? || @source.size == Float::INFINITY
|
33
|
-
|
34
39
|
@event_duration = metadata.delete(:dur) || metadata.delete(:event_duration)
|
35
40
|
@event_duration ||= source.event_duration if source.respond_to?(:event_duration)
|
36
41
|
@event_duration ||= 1
|
@@ -38,8 +43,14 @@ module Xi
|
|
38
43
|
@metadata = source.respond_to?(:metadata) ? source.metadata : {}
|
39
44
|
@metadata.merge!(metadata)
|
40
45
|
|
46
|
+
@is_infinite = @source.size.nil? || @source.size == Float::INFINITY
|
47
|
+
|
41
48
|
if @is_infinite
|
42
|
-
@total_duration = @event_duration
|
49
|
+
@total_duration = if @event_duration.respond_to?(:each)
|
50
|
+
@event_duration.each.first
|
51
|
+
else
|
52
|
+
@event_duration
|
53
|
+
end
|
43
54
|
else
|
44
55
|
last_ev = each_event.take(@source.size).last
|
45
56
|
@total_duration = last_ev ? last_ev.start + last_ev.duration : 0
|
@@ -73,12 +84,13 @@ module Xi
|
|
73
84
|
def each_event
|
74
85
|
return enum_for(__method__) unless block_given?
|
75
86
|
|
76
|
-
|
87
|
+
dur_enum = each_event_duration
|
77
88
|
pos = 0
|
78
89
|
|
79
90
|
@source.each do |value|
|
80
91
|
if value.is_a?(Pattern)
|
81
92
|
value.each do |v|
|
93
|
+
dur = dur_enum.next
|
82
94
|
yield Event.new(v, pos, dur)
|
83
95
|
pos += dur
|
84
96
|
end
|
@@ -86,6 +98,7 @@ module Xi
|
|
86
98
|
yield value
|
87
99
|
pos += value.duration
|
88
100
|
else
|
101
|
+
dur = dur_enum.next
|
89
102
|
yield Event.new(value, pos, dur)
|
90
103
|
pos += dur
|
91
104
|
end
|
@@ -97,6 +110,15 @@ module Xi
|
|
97
110
|
each_event { |e| yield e.value }
|
98
111
|
end
|
99
112
|
|
113
|
+
def each_event_duration
|
114
|
+
return enum_for(__method__) unless block_given?
|
115
|
+
if @event_duration.respond_to?(:each)
|
116
|
+
loop { @event_duration.each { |v| yield v } }
|
117
|
+
else
|
118
|
+
loop { yield @event_duration }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
100
122
|
def inspect
|
101
123
|
ss = if @source.respond_to?(:join)
|
102
124
|
@source.map(&:inspect).join(', ')
|
data/lib/xi/stream.rb
CHANGED
@@ -1,13 +1,21 @@
|
|
1
|
+
require 'xi/scale'
|
1
2
|
require 'set'
|
2
|
-
require 'xi/music_parameters'
|
3
3
|
|
4
4
|
module Xi
|
5
5
|
class Stream
|
6
|
-
prepend MusicParameters
|
7
|
-
|
8
6
|
attr_reader :clock, :opts, :source, :state, :event_duration, :gate
|
9
7
|
|
8
|
+
DEFAULT_PARAMS = {
|
9
|
+
degree: 0,
|
10
|
+
octave: 5,
|
11
|
+
root: 0,
|
12
|
+
scale: Xi::Scale.major,
|
13
|
+
steps_per_octave: 12,
|
14
|
+
}
|
15
|
+
|
10
16
|
def initialize(name, clock, **opts)
|
17
|
+
Array(opts.delete(:include)).each { |m| include_mixin(m) }
|
18
|
+
|
11
19
|
@name = name.to_sym
|
12
20
|
@opts = opts
|
13
21
|
|
@@ -24,7 +32,6 @@ module Xi
|
|
24
32
|
|
25
33
|
def set(event_duration: nil, gate: nil, **source)
|
26
34
|
@mutex.synchronize do
|
27
|
-
source[:s] ||= @name
|
28
35
|
@source = source
|
29
36
|
@gate = gate if gate
|
30
37
|
@event_duration = event_duration if event_duration
|
@@ -90,15 +97,15 @@ module Xi
|
|
90
97
|
error(err)
|
91
98
|
end
|
92
99
|
|
93
|
-
def notify(now)
|
100
|
+
def notify(now, cps)
|
94
101
|
return unless playing? && @source
|
95
102
|
|
96
103
|
@mutex.synchronize do
|
97
104
|
@changed_params.clear
|
98
105
|
|
99
|
-
forward_enums(now) if @must_forward
|
106
|
+
forward_enums(now, cps) if @must_forward
|
100
107
|
gate_off = gate_off_old_sound_objects(now)
|
101
|
-
gate_on = play_enums(now)
|
108
|
+
gate_on = play_enums(now, cps)
|
102
109
|
|
103
110
|
# Call hooks
|
104
111
|
do_gate_off_change(gate_off) unless gate_off.empty?
|
@@ -109,22 +116,33 @@ module Xi
|
|
109
116
|
|
110
117
|
private
|
111
118
|
|
119
|
+
def include_mixin(module_or_name)
|
120
|
+
mod = if module_or_name.is_a?(Module)
|
121
|
+
module_or_name
|
122
|
+
else
|
123
|
+
name = module_or_name.to_s
|
124
|
+
require "#{self.class.name.underscore}/#{name}"
|
125
|
+
self.class.const_get(name.camelize)
|
126
|
+
end
|
127
|
+
singleton_class.send(:include, mod)
|
128
|
+
end
|
129
|
+
|
112
130
|
def changed_state
|
113
131
|
@state.select { |k, _| @changed_params.include?(k) }
|
114
132
|
end
|
115
133
|
|
116
|
-
def forward_enums(now)
|
134
|
+
def forward_enums(now, cps)
|
117
135
|
@enums.each do |p, (enum, total_dur)|
|
118
136
|
next if total_dur == 0
|
119
137
|
|
120
|
-
cur_pos = now % total_dur
|
121
|
-
|
138
|
+
cur_pos = (now * cps) % total_dur
|
139
|
+
start_ts = now - (cur_pos / cps)
|
122
140
|
|
123
141
|
loop do
|
124
142
|
next_ev = enum.peek
|
125
143
|
distance = (cur_pos - next_ev.start) % total_dur
|
126
144
|
|
127
|
-
@prev_end[p] =
|
145
|
+
@prev_end[p] = @clock.init_ts + start_ts + (next_ev.end / cps)
|
128
146
|
enum.next
|
129
147
|
|
130
148
|
break if distance <= next_ev.duration
|
@@ -140,7 +158,7 @@ module Xi
|
|
140
158
|
# Check if there are any currently playing sound objects that
|
141
159
|
# must be gated off
|
142
160
|
@playing_sound_objects.dup.each do |end_pos, h|
|
143
|
-
if now >= h[:at] - latency_sec
|
161
|
+
if now + @clock.init_ts >= h[:at] - latency_sec
|
144
162
|
gate_off << h
|
145
163
|
@playing_sound_objects.delete(end_pos)
|
146
164
|
end
|
@@ -149,19 +167,21 @@ module Xi
|
|
149
167
|
gate_off
|
150
168
|
end
|
151
169
|
|
152
|
-
def play_enums(now)
|
170
|
+
def play_enums(now, cps)
|
153
171
|
gate_on = []
|
154
172
|
|
155
173
|
@enums.each do |p, (enum, total_dur)|
|
156
174
|
next if total_dur == 0
|
157
175
|
|
158
|
-
cur_pos = now % total_dur
|
159
|
-
|
176
|
+
cur_pos = (now * cps) % total_dur
|
177
|
+
start_ts = now - (cur_pos / cps)
|
160
178
|
|
161
179
|
next_ev = enum.peek
|
162
180
|
|
163
181
|
# Do we need to play next event now? If not, skip this parameter
|
164
|
-
if (@prev_end[p].nil? || now >= @prev_end[p]) &&
|
182
|
+
if (@prev_end[p].nil? || now + @clock.init_ts >= @prev_end[p]) &&
|
183
|
+
cur_pos >= next_ev.start - latency_sec
|
184
|
+
|
165
185
|
# Update state based on pattern value
|
166
186
|
# TODO: Pass as parameter exact time (start_ts + next_ev.start)
|
167
187
|
update_state(p, next_ev.value)
|
@@ -174,27 +194,55 @@ module Xi
|
|
174
194
|
|
175
195
|
gate_on << {
|
176
196
|
so_ids: new_so_ids,
|
177
|
-
at:
|
197
|
+
at: @clock.init_ts + start_ts + (next_ev.start / cps),
|
178
198
|
}
|
179
199
|
|
180
200
|
@playing_sound_objects[rand(100000)] = {
|
181
201
|
so_ids: new_so_ids,
|
182
202
|
duration: total_dur,
|
183
|
-
at:
|
203
|
+
at: @clock.init_ts + start_ts + (next_ev.end / cps),
|
184
204
|
}
|
185
205
|
end
|
186
206
|
|
187
207
|
# Because we already processed event, advance enumerator
|
188
208
|
next_ev = enum.next
|
189
|
-
@prev_end[p] =
|
209
|
+
@prev_end[p] = @clock.init_ts + start_ts + (next_ev.end / cps)
|
190
210
|
end
|
191
211
|
end
|
192
212
|
|
193
213
|
gate_on
|
194
214
|
end
|
195
215
|
|
196
|
-
# @override
|
197
216
|
def transform_state
|
217
|
+
@state = DEFAULT_PARAMS.merge(@state)
|
218
|
+
|
219
|
+
@state[:s] ||= @name
|
220
|
+
|
221
|
+
if !changed_param?(:note) && changed_param?(:degree, :scale, :steps_per_octave)
|
222
|
+
@state[:note] = reduce_to_note
|
223
|
+
@changed_params << :note
|
224
|
+
end
|
225
|
+
|
226
|
+
if !changed_param?(:midinote) && changed_param?(:note)
|
227
|
+
@state[:midinote] = reduce_to_midinote
|
228
|
+
@changed_params << :midinote
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def reduce_to_midinote
|
233
|
+
Array(@state[:note]).compact.map { |n|
|
234
|
+
@state[:root].to_i + @state[:octave].to_i * @state[:steps_per_octave] + n
|
235
|
+
}
|
236
|
+
end
|
237
|
+
|
238
|
+
def reduce_to_note
|
239
|
+
Array(@state[:degree]).compact.map do |d|
|
240
|
+
d.degree_to_key(Array(@state[:scale]), @state[:steps_per_octave])
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def changed_param?(*params)
|
245
|
+
@changed_params.any? { |p| params.include?(p) }
|
198
246
|
end
|
199
247
|
|
200
248
|
def new_sound_object_id
|
@@ -222,11 +270,14 @@ module Xi
|
|
222
270
|
.select { |k, v| @changed_params.include?(k) }.to_h}"
|
223
271
|
end
|
224
272
|
|
225
|
-
def update_state(
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
273
|
+
def update_state(param, value)
|
274
|
+
kv = value.is_a?(Hash) ? value : {param => value}
|
275
|
+
kv.each do |k, v|
|
276
|
+
if v != @state[k]
|
277
|
+
debug "Update state of :#{k}: #{v}"
|
278
|
+
@changed_params << k
|
279
|
+
@state[k] = v
|
280
|
+
end
|
230
281
|
end
|
231
282
|
end
|
232
283
|
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require "websocket"
|
2
|
+
require "time"
|
3
|
+
|
4
|
+
module Xi
|
5
|
+
class TidalClock < Clock
|
6
|
+
SYNC_INTERVAL_SEC = 100 / 1000.0
|
7
|
+
|
8
|
+
attr_reader :server, :port, :attached
|
9
|
+
alias_method :attached?, :attached
|
10
|
+
|
11
|
+
def initialize(server: 'localhost', port: 9160, **opts)
|
12
|
+
@server = server
|
13
|
+
@port = port
|
14
|
+
@attached = true
|
15
|
+
|
16
|
+
super(opts)
|
17
|
+
|
18
|
+
@ws_thread = Thread.new { ws_thread_routine }
|
19
|
+
end
|
20
|
+
|
21
|
+
def cps=(new_cps)
|
22
|
+
fail NotImplementedError, 'cps is read-only'
|
23
|
+
end
|
24
|
+
|
25
|
+
def dettach
|
26
|
+
@attached = false
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
def attach
|
31
|
+
@attached = true
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def ws_thread_routine
|
38
|
+
loop do
|
39
|
+
do_ws_sync
|
40
|
+
sleep INTERVAL_SEC
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def do_ws_sync
|
45
|
+
return unless @attached
|
46
|
+
|
47
|
+
# Try to connect to websocket server
|
48
|
+
connect
|
49
|
+
return if @socket.nil? || @socket.closed?
|
50
|
+
|
51
|
+
# Offer a handshake
|
52
|
+
@handshake = WebSocket::Handshake::Client.new(url: "ws://#{@server}:#{@port}")
|
53
|
+
@socket.puts @handshake.to_s
|
54
|
+
|
55
|
+
# Read server response
|
56
|
+
while line = @socket.gets
|
57
|
+
@handshake << line
|
58
|
+
break if @handshake.finished?
|
59
|
+
end
|
60
|
+
|
61
|
+
unless @handshake.finished?
|
62
|
+
debug(__method__, "Handshake didn't finished. Disconnect")
|
63
|
+
@socket.close
|
64
|
+
return
|
65
|
+
end
|
66
|
+
|
67
|
+
unless @handshake.valid?
|
68
|
+
debug(__method__, "Handshake is not valid. Disconnect")
|
69
|
+
@socket.close
|
70
|
+
return
|
71
|
+
end
|
72
|
+
|
73
|
+
frame = WebSocket::Frame::Incoming::Server.new(version: @handshake.version)
|
74
|
+
|
75
|
+
# Read loop
|
76
|
+
loop do
|
77
|
+
data, _ = @socket.recvfrom(4096)
|
78
|
+
break if data.empty?
|
79
|
+
|
80
|
+
frame << data
|
81
|
+
while f = frame.next
|
82
|
+
if (f.type == :close)
|
83
|
+
debug(__method__, "Close frame received. Disconnect")
|
84
|
+
@socket.close
|
85
|
+
return
|
86
|
+
else
|
87
|
+
debug(__method__, "Frame: #{f}")
|
88
|
+
hash = parse_frame_body(f.to_s)
|
89
|
+
update_clock_from_server_data(hash)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
rescue => err
|
95
|
+
error(err)
|
96
|
+
end
|
97
|
+
|
98
|
+
def connect
|
99
|
+
@socket = TCPSocket.new(@server, @port)
|
100
|
+
rescue => err
|
101
|
+
error(err)
|
102
|
+
sleep 1
|
103
|
+
end
|
104
|
+
|
105
|
+
def parse_frame_body(body)
|
106
|
+
h = {}
|
107
|
+
ts, _, cps = body.split(',')
|
108
|
+
h[:ts] = Time.parse(ts)
|
109
|
+
h[:cps] = cps.to_f
|
110
|
+
h
|
111
|
+
end
|
112
|
+
|
113
|
+
def update_clock_from_server_data(h)
|
114
|
+
@init_ts = h[:ts].to_f
|
115
|
+
@cps = h[:cps]
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
data/lib/xi/version.rb
CHANGED
data/lib/xi.rb
CHANGED
@@ -18,27 +18,31 @@ module Xi
|
|
18
18
|
@default_backend = new_name && new_name.to_sym
|
19
19
|
end
|
20
20
|
|
21
|
+
def self.default_clock
|
22
|
+
@default_clock ||= Clock.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.default_clock=(new_clock)
|
26
|
+
@default_clock = new_clock
|
27
|
+
end
|
28
|
+
|
21
29
|
module Init
|
22
|
-
def
|
23
|
-
|
30
|
+
def stop_all
|
31
|
+
@streams.each { |_, ss| ss.each { |_, s| s.stop } }
|
24
32
|
end
|
33
|
+
alias_method :hush, :stop_all
|
25
34
|
|
26
|
-
def
|
27
|
-
|
35
|
+
def start_all
|
36
|
+
@streams.each { |_, ss| ss.each { |_, s| s.start } }
|
28
37
|
end
|
29
38
|
|
30
|
-
def
|
31
|
-
|
39
|
+
def peek(pattern, *args)
|
40
|
+
pattern.peek(*args)
|
32
41
|
end
|
33
42
|
|
34
|
-
def
|
35
|
-
|
36
|
-
ss.each do |name, stream|
|
37
|
-
stream.stop
|
38
|
-
end
|
39
|
-
end
|
43
|
+
def peek_events(pattern, *args)
|
44
|
+
pattern.peek_events(*args)
|
40
45
|
end
|
41
|
-
alias_method :hush, :stop_all
|
42
46
|
|
43
47
|
def method_missing(method, backend=nil, **opts)
|
44
48
|
backend ||= Xi.default_backend
|
@@ -51,16 +55,17 @@ module Xi
|
|
51
55
|
@streams ||= {}
|
52
56
|
@streams[backend] ||= {}
|
53
57
|
|
54
|
-
|
58
|
+
stream = @streams[backend][method] ||= begin
|
55
59
|
require "xi/#{backend}"
|
56
|
-
|
57
|
-
cls.
|
60
|
+
|
61
|
+
cls = Class.const_get("#{backend.to_s.camelize}::Stream")
|
62
|
+
cls.new(method, Xi.default_clock, **opts)
|
58
63
|
end
|
59
64
|
|
60
|
-
|
61
|
-
|
65
|
+
# Define (or overwrite) a local variable named +method+ with the stream
|
66
|
+
Pry.binding_for(self).local_variable_set(method, stream)
|
62
67
|
|
63
|
-
|
68
|
+
stream
|
64
69
|
end
|
65
70
|
end
|
66
71
|
end
|
data/xi.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: xi-lang
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Damián Silvani
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-02-
|
11
|
+
date: 2017-02-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -108,6 +108,20 @@ dependencies:
|
|
108
108
|
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: websocket
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
111
125
|
description: |-
|
112
126
|
A musical pattern language inspired in Tidal and SuperCollider
|
113
127
|
for building higher-level musical constructs easily.
|
@@ -135,16 +149,17 @@ files:
|
|
135
149
|
- lib/xi/core_ext/numeric.rb
|
136
150
|
- lib/xi/core_ext/object.rb
|
137
151
|
- lib/xi/core_ext/simple.rb
|
152
|
+
- lib/xi/core_ext/string.rb
|
138
153
|
- lib/xi/error_log.rb
|
139
154
|
- lib/xi/event.rb
|
140
155
|
- lib/xi/logger.rb
|
141
|
-
- lib/xi/music_parameters.rb
|
142
156
|
- lib/xi/pattern.rb
|
143
157
|
- lib/xi/pattern/generators.rb
|
144
158
|
- lib/xi/pattern/transforms.rb
|
145
159
|
- lib/xi/repl.rb
|
146
160
|
- lib/xi/scale.rb
|
147
161
|
- lib/xi/stream.rb
|
162
|
+
- lib/xi/tidal_clock.rb
|
148
163
|
- lib/xi/version.rb
|
149
164
|
- xi.gemspec
|
150
165
|
homepage: https://github.com/xi-livecode/xi
|
data/lib/xi/music_parameters.rb
DELETED
@@ -1,47 +0,0 @@
|
|
1
|
-
require 'xi/scale'
|
2
|
-
|
3
|
-
class Xi::Stream
|
4
|
-
module MusicParameters
|
5
|
-
DEFAULT = {
|
6
|
-
degree: 0,
|
7
|
-
octave: 5,
|
8
|
-
root: 0,
|
9
|
-
scale: Xi::Scale.major,
|
10
|
-
steps_per_octave: 12,
|
11
|
-
}
|
12
|
-
|
13
|
-
private
|
14
|
-
|
15
|
-
def transform_state
|
16
|
-
super
|
17
|
-
|
18
|
-
@state = DEFAULT.merge(@state)
|
19
|
-
|
20
|
-
if !changed_param?(:note) && changed_param?(:degree, :scale, :steps_per_octave)
|
21
|
-
@state[:note] = reduce_to_note
|
22
|
-
@changed_params << :note
|
23
|
-
end
|
24
|
-
|
25
|
-
if !changed_param?(:midinote) && changed_param?(:note)
|
26
|
-
@state[:midinote] = reduce_to_midinote
|
27
|
-
@changed_params << :midinote
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def reduce_to_midinote
|
32
|
-
Array(@state[:note]).compact.map { |n|
|
33
|
-
@state[:root].to_i + @state[:octave].to_i * @state[:steps_per_octave] + n
|
34
|
-
}
|
35
|
-
end
|
36
|
-
|
37
|
-
def reduce_to_note
|
38
|
-
Array(@state[:degree]).compact.map do |d|
|
39
|
-
d.degree_to_key(Array(@state[:scale]), @state[:steps_per_octave])
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
def changed_param?(*params)
|
44
|
-
@changed_params.any? { |p| params.include?(p) }
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|