xi-lang 0.1.0 → 0.1.2

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: 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: {}