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 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