xi-lang 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 65bceb35fd0dbad39331e9c89bea0a83254fe075
4
- data.tar.gz: a20c3dd41afe001b698aab57d8cd3577e66d6dcb
3
+ metadata.gz: 38043c0ad025b398a49d63e1c93681992c0b184a
4
+ data.tar.gz: fbc0e32315abf8272f388bbe59917236633a0d09
5
5
  SHA512:
6
- metadata.gz: e92c7bf49fa2bba86bf0eb153d7c0d1fd363a21060d52faf3a472655ffce291c8e57563edc303ae1ef65c591dc6b6db746d2f64592e469e927ba56a35ce6cd31
7
- data.tar.gz: 3ab57ef7a85d4cac9a9e260aba0b030972b80105e320036feefc3ef3a418b22aec361180685cdda28ef92ab72ee39639ca7786595c030000b96f76d4b04c5be5
6
+ metadata.gz: 10f089d5ecb56a89e70091b2845654fdbdf7beb9c7b3fc23366038ae078769065166b3b89fd08de19fca0ec99612bce60853f16554aacb2d269036e895df4a29
7
+ data.tar.gz: e4fa8eef3158cbf1d14d8f9b8d4241dfe83de117a1ef6730c947992baf555a9c110f425e9d7828eaafdf9201cf71f52d9c2cd2737100e85c198397ce22048a68
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Xi
1
+ # Xi [![Build Status](https://travis-ci.org/xi-livecode/xi.svg?branch=master)](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} cps=#{cps.inspect} #{playing? ? :playing : :stopped}>"
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
- @streams.each { |s| s.notify(cycles) }
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
@@ -3,3 +3,4 @@ require 'xi/core_ext/fixnum'
3
3
  require 'xi/core_ext/numeric'
4
4
  require 'xi/core_ext/object'
5
5
  require 'xi/core_ext/simple'
6
+ require 'xi/core_ext/string'
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].p.rand #=> [2]
58
- # peek [1, 2, 3, 4].p.rand(6) #=> [1, 3, 2, 2, 4, 3]
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].p.xrand #=> [4]
75
- # peek [1, 2, 3].p.xrand(8) #=> [1, 3, 2, 3, 1, 2, 3, 2]
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].p.xrand #=> [4]
96
- # peek [1, 2, 3].p.xrand(8) #=> [1, 3, 2, 3, 1, 2, 3, 2]
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
- # TODO Document
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
- # TODO Document
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
- # TODO Document
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
- # TODO Document
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
- # TODO Document
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 times_pat.infinite?
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
- # TODO Document
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
- dur = @event_duration
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
- start_pos = now - cur_pos
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] = start_pos + next_ev.end
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
- start_pos = now - cur_pos
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]) && cur_pos >= next_ev.start - latency_sec
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: start_pos + next_ev.start
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: start_pos + next_ev.end,
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] = start_pos + next_ev.end
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(p, v)
226
- if v != @state[p]
227
- debug "Update state of :#{p}: #{v}"
228
- @changed_params << p
229
- @state[p] = v
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
@@ -1,3 +1,3 @@
1
1
  module Xi
2
- VERSION = "0.1.3"
2
+ VERSION = "0.1.4"
3
3
  end
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 peek(pattern, *args)
23
- pattern.peek(*args)
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 peek_events(pattern, *args)
27
- pattern.peek_events(*args)
35
+ def start_all
36
+ @streams.each { |_, ss| ss.each { |_, s| s.start } }
28
37
  end
29
38
 
30
- def clock
31
- @default_clock ||= Clock.new
39
+ def peek(pattern, *args)
40
+ pattern.peek(*args)
32
41
  end
33
42
 
34
- def stop_all
35
- @streams.each do |backend, ss|
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
- s = @streams[backend][method] ||= begin
58
+ stream = @streams[backend][method] ||= begin
55
59
  require "xi/#{backend}"
56
- cls = Class.const_get("#{backend.to_s.capitalize}::Stream")
57
- cls.new(method, self.clock, **opts)
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
- b = Pry.binding_for(self)
61
- b.local_variable_set(method, s)
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
- s
68
+ stream
64
69
  end
65
70
  end
66
71
  end
data/xi.gemspec CHANGED
@@ -30,4 +30,5 @@ Gem::Specification.new do |spec|
30
30
  spec.add_development_dependency "guard-minitest"
31
31
 
32
32
  spec.add_dependency 'pry'
33
+ spec.add_dependency 'websocket'
33
34
  end
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.3
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-17 00:00:00.000000000 Z
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
@@ -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