xi-lang 0.1.0 → 0.1.2

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: b3feae76d4f61becebc296554b3628ee870aa429
4
- data.tar.gz: cc3ad564f9d0b4559f136dfb9f1ce93c26c01efb
3
+ metadata.gz: c7ee847da08f4dd79a890c7586a6c410d49994b5
4
+ data.tar.gz: 098aa22714b668efef3e238a8bb7abac2be2476a
5
5
  SHA512:
6
- metadata.gz: a7a6f4fea97a1600b38eaf844c847b8a34efc7fa6d21f33244aabd9a4dc6603528ba92651440db11510b40196dd835f04695c5ea83f22e30dfaeb3fd708702c9
7
- data.tar.gz: 209c7db3b0e7cb2cd3c512756817ce7b0692bf4662658c436b9f6513651345d211dfa177732892a375d45e98ccbe6ad640ef437faf93d56c6a2cd692f96c0142
6
+ metadata.gz: 72f6b2715e0753723b848a9dc1b3a7754d7417bac71ac96cfa9869dc5c0f8ce0d497b7d76897e41af6c5f903a7166eeff90f8993adbf2e4fd29c52cb15437800
7
+ data.tar.gz: fc8186a9b0685c9ee1045424c3cafb1ecd171c4de2b3e6fb156570ad4c40f1cce144c6ab1fe1ae1fbf8cff4213e929ce8252d392b44d8d9ee23034eae3df6293
data/README.md CHANGED
@@ -9,18 +9,58 @@ Xi is only a patterns library, but can talk to different backends:
9
9
  - [SuperCollider](https://github.com/supercollider/supercollider)
10
10
  - MIDI devices
11
11
 
12
+ *NOTE*: Be advised that this project is in very early alpha stages. There are a
13
+ multiple known bugs, missing features, documentation and tests.
14
+
15
+ ## Example
16
+
17
+ ```ruby
18
+ k = MIDI::Stream.new
19
+
20
+ k.set degree: [0, 3, 5, 7],
21
+ octave: [1, 2],
22
+ scale: [Scale.egyptian],
23
+ voice: (0..6).p.scale(0, 6, 0, 127),
24
+ vel: [30, 25, 20].p + 20,
25
+ cutoff: P.sin1(32, 2) * 128,
26
+ delay_feedback: 1/2 * 128,
27
+ delay_time: [0, 0x7f].p(1/16)
28
+ ```
29
+
12
30
  ## Installation
13
31
 
32
+ ### Quickstart
33
+
14
34
  You will need Ruby 2.1+ installed on your system. Check by running `ruby
15
- -v`. You will also need Bundler. Install with `gem install bundler`.
35
+ -v`. To install Xi you must install the core libraries and REPL, and then one
36
+ or more backends.
37
+
38
+ If you want to use Xi with SuperCollider:
39
+
40
+ $ gem install xi-lang xi-supercollider
41
+
42
+ Or with MIDI:
43
+
44
+ $ gem install xi-lang xi-midi
45
+
46
+ Then run Xi REPL with:
47
+
48
+ $ xi
49
+
50
+ There is a configuration file that is written automatically for you when run
51
+ for the first time at `~/.config/xi/init.rb`. You can add require lines and
52
+ define all the function helpers you want.
53
+
54
+ ### Repository
16
55
 
17
- Becase Xi is still in **alpha** stage, you will have to checkout this
18
- repository using Git:
56
+ Becase Xi is still in **alpha** stage, you might want to clone the repository
57
+ using Git instead:
19
58
 
20
- $ git clone https://github.com/munshkr/xi
59
+ $ git clone https://github.com/xi-livecode/xi
21
60
 
22
61
  After that, change into the new directory and install gem dependencies with
23
- Bundler:
62
+ Bundler. If you don't have Bundler installed, run `gem install bundler` first.
63
+ Then:
24
64
 
25
65
  $ cd xi
26
66
  $ bundle install
@@ -41,9 +81,9 @@ git commits and tags, and push the `.gem` file to
41
81
  ## Contributing
42
82
 
43
83
  Bug reports and pull requests are welcome on GitHub at
44
- https://github.com/munshkr/xi. This project is intended to be a safe, welcoming
45
- space for collaboration, and contributors are expected to adhere to the
46
- [Contributor Covenant](http://contributor-covenant.org) code of conduct.
84
+ https://github.com/xi-livecode/xi. This project is intended to be a safe,
85
+ welcoming space for collaboration, and contributors are expected to adhere to
86
+ the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
47
87
 
48
88
  ## License
49
89
 
data/lib/xi/clock.rb CHANGED
@@ -7,7 +7,7 @@ Thread.abort_on_exception = true
7
7
  module Xi
8
8
  class Clock
9
9
  DEFAULT_CPS = 1.0
10
- INTERVAL_SEC = 10 / 1000.0
10
+ INTERVAL_SEC = 20 / 1000.0
11
11
 
12
12
  def initialize(cps: DEFAULT_CPS)
13
13
  @mutex = Mutex.new
@@ -41,6 +41,10 @@ module Xi
41
41
  !playing?
42
42
  end
43
43
 
44
+ def now
45
+ Time.now.to_f * cps
46
+ end
47
+
44
48
  def play
45
49
  @mutex.synchronize { @playing = true }
46
50
  self
@@ -53,6 +57,14 @@ module Xi
53
57
  end
54
58
  alias_method :pause, :play
55
59
 
60
+ def seconds_per_cycle
61
+ @mutex.synchronize { 1.0 / @cps }
62
+ end
63
+
64
+ def at(cycle_pos)
65
+ Time.at(cycle_pos * seconds_per_cycle)
66
+ end
67
+
56
68
  def inspect
57
69
  "#<#{self.class.name}:#{"0x%014x" % object_id} cps=#{cps.inspect} #{playing? ? :playing : :stopped}>"
58
70
  end
@@ -12,9 +12,12 @@ module Xi
12
12
  # @return [Pattern]
13
13
  #
14
14
  def -@
15
- Pattern.new(self) do |y|
16
- each { |v| y << (v.respond_to?(:-@) ? -v : v) }
17
- end
15
+ Pattern.new(self) { |y|
16
+ each_event { |e|
17
+ v = e.value
18
+ y << E[(v.respond_to?(:-@) ? -v : v), e.start, e.duration]
19
+ }
20
+ }
18
21
  end
19
22
 
20
23
  # Concatenate +object+ pattern or perform a scalar sum with +object+
@@ -36,14 +39,17 @@ module Xi
36
39
  #
37
40
  def +(object)
38
41
  if object.is_a?(Pattern)
39
- Pattern.new(self, size: size + object.size) do |y|
42
+ Pattern.new(self, size: size + object.size) { |y|
40
43
  each { |v| y << v }
41
44
  object.each { |v| y << v }
42
- end
45
+ }
43
46
  else
44
- Pattern.new(self) do |y|
45
- each { |v| y << (v.respond_to?(:+) ? v + object : v) }
46
- end
47
+ Pattern.new(self) { |y|
48
+ each_event { |e|
49
+ v = e.value
50
+ y << E[(v.respond_to?(:+) ? v + object : v), e.start, e.duration]
51
+ }
52
+ }
47
53
  end
48
54
  end
49
55
 
@@ -60,9 +66,12 @@ module Xi
60
66
  # @return [Pattern]
61
67
  #
62
68
  def -(numeric)
63
- Pattern.new(self) do |y|
64
- each { |v| y << (v.respond_to?(:-) ? v - numeric : v) }
65
- end
69
+ Pattern.new(self) { |y|
70
+ each_event { |e|
71
+ v = e.value
72
+ y << E[(v.respond_to?(:-) ? v - numeric : v), e.start, e.duration]
73
+ }
74
+ }
66
75
  end
67
76
 
68
77
  # Performs a scalar multiplication with +numeric+
@@ -78,9 +87,12 @@ module Xi
78
87
  # @return [Pattern]
79
88
  #
80
89
  def *(numeric)
81
- Pattern.new(self) do |y|
82
- each { |v| y << (v.respond_to?(:*) ? v * numeric : v) }
83
- end
90
+ Pattern.new(self) { |y|
91
+ each_event { |e|
92
+ v = e.value
93
+ y << E[(v.respond_to?(:*) ? v * numeric : v), e.start, e.duration]
94
+ }
95
+ }
84
96
  end
85
97
 
86
98
  # Performs a scalar division by +numeric+
@@ -96,9 +108,12 @@ module Xi
96
108
  # @return [Pattern]
97
109
  #
98
110
  def /(numeric)
99
- Pattern.new(self) do |y|
100
- each { |v| y << (v.respond_to?(:/) ? v / numeric : v) }
101
- end
111
+ Pattern.new(self) { |y|
112
+ each_event { |e|
113
+ v = e.value
114
+ y << E[(v.respond_to?(:/) ? v / numeric : v), e.start, e.duration]
115
+ }
116
+ }
102
117
  end
103
118
 
104
119
  # Performs a scalar modulo against +numeric+
@@ -114,9 +129,12 @@ module Xi
114
129
  # @return [Pattern]
115
130
  #
116
131
  def %(numeric)
117
- Pattern.new(self) do |y|
118
- each { |v| y << (v.respond_to?(:%) ? v % numeric : v) }
119
- end
132
+ Pattern.new(self) { |y|
133
+ each_event { |e|
134
+ v = e.value
135
+ y << E[(v.respond_to?(:%) ? v % numeric : v), e.start, e.duration]
136
+ }
137
+ }
120
138
  end
121
139
 
122
140
  # Raises each value to the power of +numeric+, which may be negative or
@@ -132,9 +150,12 @@ module Xi
132
150
  # @return [Pattern]
133
151
  #
134
152
  def **(numeric)
135
- Pattern.new(self) do |y|
136
- each { |v| y << (v.respond_to?(:**) ? v ** numeric : v) }
137
- end
153
+ Pattern.new(self) { |y|
154
+ each_event { |e|
155
+ v = e.value
156
+ y << E[(v.respond_to?(:**) ? v ** numeric : v), e.start, e.duration]
157
+ }
158
+ }
138
159
  end
139
160
  alias_method :^, :**
140
161
 
@@ -218,9 +239,12 @@ module Xi
218
239
  # @return [Pattern]
219
240
  #
220
241
  def normalize(min, max)
221
- Pattern.new(self) do |y|
222
- each { |v| y << (v.respond_to?(:-) ? (v - min) / (max - min) : v) }
223
- end
242
+ Pattern.new(self) { |y|
243
+ each_event { |e|
244
+ v = e.value
245
+ y << E[(v.respond_to?(:-) ? (v - min) / (max - min) : v), e.start, e.duration]
246
+ }
247
+ }
224
248
  end
225
249
 
226
250
  # Scales a pattern of normalized values (0..1) to a custom range
@@ -240,9 +264,12 @@ module Xi
240
264
  # @return [Pattern]
241
265
  #
242
266
  def denormalize(min, max)
243
- Pattern.new(self) do |y|
244
- each { |v| y << (v.respond_to?(:*) ? (max - min) * v + min : v) }
245
- end
267
+ Pattern.new(self) { |y|
268
+ each_event { |e|
269
+ v = e.value
270
+ y << E[(v.respond_to?(:*) ? (max - min) * v + min : v), e.start, e.duration]
271
+ }
272
+ }
246
273
  end
247
274
 
248
275
  # Scale from one range of values to another range of values
@@ -262,16 +289,16 @@ module Xi
262
289
 
263
290
  # TODO Document
264
291
  def decelerate(num)
265
- Pattern.new(self) do |y|
292
+ Pattern.new(self) { |y|
266
293
  each_event { |e| y << E[e.value, e.start * num, e.duration * num] }
267
- end
294
+ }
268
295
  end
269
296
 
270
297
  # TODO Document
271
298
  def accelerate(num)
272
- Pattern.new(self) do |y|
299
+ Pattern.new(self) { |y|
273
300
  each_event { |e| y << E[e.value, e.start / num, e.duration / num] }
274
- end
301
+ }
275
302
  end
276
303
 
277
304
  # Based on +probability+, it yields original value or nil
data/lib/xi/pattern.rb CHANGED
@@ -16,24 +16,26 @@ module Xi
16
16
 
17
17
  def_delegators :@source, :size
18
18
 
19
- def initialize(enum=nil, size: nil, **metadata)
20
- size ||= enum.size if enum.respond_to?(:size)
19
+ def initialize(source=nil, size: nil, **metadata)
20
+ if source.nil? && !block_given?
21
+ fail ArgumentError, 'must provide source or block'
22
+ end
23
+
24
+ size ||= source.size if source.respond_to?(:size)
21
25
 
22
26
  @source = if block_given?
23
27
  Enumerator.new(size) { |y| yield y }
24
- elsif enum
25
- enum
26
28
  else
27
- fail ArgumentError, 'must provide source or block'
29
+ source
28
30
  end
29
31
 
30
32
  @is_infinite = @source.size.nil? || @source.size == Float::INFINITY
31
33
 
32
34
  @event_duration = metadata.delete(:dur) || metadata.delete(:event_duration)
33
- @event_duration ||= enum.event_duration if enum.respond_to?(:event_duration)
35
+ @event_duration ||= source.event_duration if source.respond_to?(:event_duration)
34
36
  @event_duration ||= 1
35
37
 
36
- @metadata = enum.respond_to?(:metadata) ? enum.metadata : {}
38
+ @metadata = source.respond_to?(:metadata) ? source.metadata : {}
37
39
  @metadata.merge!(metadata)
38
40
 
39
41
  if @is_infinite
@@ -65,7 +67,7 @@ module Xi
65
67
 
66
68
  def p(dur=nil, **metadata)
67
69
  Pattern.new(@source, dur: dur || @event_duration,
68
- **@metadata.merge(metadata))
70
+ size: size, **@metadata.merge(metadata))
69
71
  end
70
72
 
71
73
  def each_event
@@ -114,19 +116,19 @@ module Xi
114
116
 
115
117
  def map_events
116
118
  return enum_for(__method__) unless block_given?
117
- Pattern.new(dur: dur, **metadata) { |y| each_event { |e| y << yield(e) } }
119
+ Pattern.new(self) { |y| each_event { |e| y << yield(e) } }
118
120
  end
119
121
  alias_method :collect_events, :map_events
120
122
 
121
123
  def select_events
122
124
  return enum_for(__method__) unless block_given?
123
- Pattern.new { |y| each_event { |e| y << e if yield(e) } }
125
+ Pattern.new(self) { |y| each_event { |e| y << e if yield(e) } }
124
126
  end
125
127
  alias_method :find_all_events, :select_events
126
128
 
127
129
  def reject_events
128
130
  return enum_for(__method__) unless block_given?
129
- Pattern.new { |y| each_event { |e| y << e unless yield(e) } }
131
+ Pattern.new(self) { |y| each_event { |e| y << e unless yield(e) } }
130
132
  end
131
133
 
132
134
  def to_events
data/lib/xi/stream.rb CHANGED
@@ -2,16 +2,16 @@ require 'set'
2
2
 
3
3
  module Xi
4
4
  class Stream
5
- WINDOW_SEC = 0.05
6
-
7
5
  attr_reader :clock, :source, :source_patterns, :state, :event_duration, :gate
8
6
 
9
7
  def initialize(clock)
10
8
  @mutex = Mutex.new
11
9
  @playing = false
10
+ @last_sound_object_id = 0
12
11
  @state = {}
13
- @new_sound_object_id = 0
14
12
  @changed_params = [].to_set
13
+ @playing_sound_objects = {}
14
+ @prev_end = {}
15
15
 
16
16
  self.clock = clock
17
17
  end
@@ -76,7 +76,8 @@ module Xi
76
76
  alias_method :pause, :play
77
77
 
78
78
  def inspect
79
- "#<#{self.class.name}:#{"0x%014x" % object_id} clock=#{@clock.inspect} #{playing? ? :playing : :stopped}>"
79
+ "#<#{self.class.name}:#{"0x%014x" % object_id} " \
80
+ "clock=#{@clock.inspect} #{playing? ? :playing : :stopped}>"
80
81
  rescue => err
81
82
  logger.error(err)
82
83
  end
@@ -106,15 +107,19 @@ module Xi
106
107
  def forward_enums(now)
107
108
  @enums.each do |p, (enum, total_dur)|
108
109
  cur_pos = now % total_dur
109
- next_ev = enum.peek
110
+ start_pos = now - cur_pos
111
+
112
+ loop do
113
+ next_ev = enum.peek
114
+ distance = (cur_pos - next_ev.start) % total_dur
110
115
 
111
- while distance = (cur_pos - next_ev.start) % total_dur do
116
+ @prev_end[p] = start_pos + next_ev.end
112
117
  enum.next
113
118
 
114
119
  break if distance <= next_ev.duration
115
- next_ev = enum.peek
116
120
  end
117
121
  end
122
+
118
123
  @must_forward = false
119
124
  end
120
125
 
@@ -124,48 +129,62 @@ module Xi
124
129
 
125
130
  @enums.each do |p, (enum, total_dur)|
126
131
  cur_pos = now % total_dur
127
- next_ev = enum.peek
132
+ start_pos = now - cur_pos
128
133
 
129
134
  # Check if there are any currently playing sound objects that
130
135
  # must be gated off
131
- @playing_sound_objects.dup.each do |end_pos, so_ids|
132
- if (cur_pos - end_pos) % total_dur <= WINDOW_SEC
133
- gate_off = so_ids
136
+ @playing_sound_objects.dup.each do |end_pos, h|
137
+ if now >= h[:at] - latency_sec
138
+ gate_off << h
134
139
  @playing_sound_objects.delete(end_pos)
135
140
  end
136
141
  end
137
142
 
143
+ next_ev = enum.peek
144
+
138
145
  # Do we need to play next event now? If not, skip this parameter
139
- if (cur_pos - next_ev.start) % total_dur <= WINDOW_SEC
146
+ if (@prev_end[p].nil? || now >= @prev_end[p]) && cur_pos >= next_ev.start - latency_sec
147
+ #logger.info "cur_pos=#{cur_pos} >= next_ev.start=#{next_ev.start}"
140
148
  # Update state based on pattern value
149
+ # TODO: Pass as parameter exact time (start_ts + next_ev.start)
141
150
  update_state(p, next_ev.value)
142
151
 
143
152
  # If this parameter is a gate, mark it as gate on as
144
153
  # a new sound object
145
154
  if p == @gate
146
- new_so_ids = Array(next_ev.value).size.times.map do
147
- so_id = @new_sound_object_id
148
- @new_sound_object_id += 1
149
- so_id
150
- end
151
- gate_on = new_so_ids
152
- @playing_sound_objects[next_ev.end] = new_so_ids
155
+ new_so_ids = Array(next_ev.value)
156
+ .size.times.map { new_sound_object_id }
157
+
158
+ gate_on << {
159
+ so_ids: new_so_ids,
160
+ at: start_pos + next_ev.start
161
+ }
162
+
163
+ @playing_sound_objects[rand(100000)] = {
164
+ so_ids: new_so_ids,
165
+ duration: total_dur,
166
+ at: start_pos + next_ev.end,
167
+ }
153
168
  end
154
169
 
155
170
  # Because we already processed event, advance enumerator
156
- enum.next
171
+ next_ev = enum.next
172
+ @prev_end[p] = start_pos + next_ev.end
157
173
  end
158
174
  end
159
175
 
160
176
  [gate_on, gate_off]
161
177
  end
162
178
 
179
+ def new_sound_object_id
180
+ @last_sound_object_id += 1
181
+ end
182
+
163
183
  def update_internal_structures
164
- @playing_sound_objects ||= {}
165
184
  @must_forward = true
166
185
  @enums = @source.map { |k, v|
167
186
  pat = v.p(@event_duration)
168
- [k, [infinite_enum(pat), pat.total_duration]]
187
+ [k, [enum_for(:loop_events, pat), pat.total_duration]]
169
188
  }.to_h
170
189
  end
171
190
 
@@ -178,21 +197,28 @@ module Xi
178
197
  end
179
198
 
180
199
  def do_state_change
181
- logger.info "State change: #{@state.select { |k, v| @changed_params.include?(k) }.to_h}"
200
+ logger.info "State change: #{@state
201
+ .select { |k, v| @changed_params.include?(k) }.to_h}"
182
202
  end
183
203
 
184
204
  def update_state(p, v)
185
- logger.debug "Update state of :#{p}: #{v}"
186
- @changed_params << p if v != @state[p]
187
- @state[p] = v
205
+ if v != @state[p]
206
+ logger.debug "Update state of :#{p}: #{v}"
207
+ @changed_params << p
208
+ @state[p] = v
209
+ end
188
210
  end
189
211
 
190
212
  def state_changed?
191
213
  !@changed_params.empty?
192
214
  end
193
215
 
194
- def infinite_enum(p)
195
- Enumerator.new { |y| loop { p.each_event { |e| y << e } } }
216
+ def loop_events(pattern)
217
+ loop { pattern.each_event { |e| yield e } }
218
+ end
219
+
220
+ def latency_sec
221
+ 0.05
196
222
  end
197
223
 
198
224
  def logger
data/lib/xi/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Xi
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.2"
3
3
  end
data/xi.gemspec CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.summary = %q{Musical pattern language for livecoding}
13
13
  spec.description = %q{A musical pattern language inspired in Tidal and SuperCollider
14
14
  for building higher-level musical constructs easily.}
15
- spec.homepage = "https://github.com/munshkr/xi"
15
+ spec.homepage = "https://github.com/xi-livecode/xi"
16
16
  spec.license = "MIT"
17
17
 
18
18
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
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.0
4
+ version: 0.1.2
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-13 00:00:00.000000000 Z
11
+ date: 2017-02-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -143,7 +143,7 @@ files:
143
143
  - lib/xi/stream.rb
144
144
  - lib/xi/version.rb
145
145
  - xi.gemspec
146
- homepage: https://github.com/munshkr/xi
146
+ homepage: https://github.com/xi-livecode/xi
147
147
  licenses:
148
148
  - MIT
149
149
  metadata: {}