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 +4 -4
- data/README.md +48 -8
- data/lib/xi/clock.rb +13 -1
- data/lib/xi/pattern/transforms.rb +60 -33
- data/lib/xi/pattern.rb +13 -11
- data/lib/xi/stream.rb +54 -28
- data/lib/xi/version.rb +1 -1
- data/xi.gemspec +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c7ee847da08f4dd79a890c7586a6c410d49994b5
|
4
|
+
data.tar.gz: 098aa22714b668efef3e238a8bb7abac2be2476a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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`.
|
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
|
18
|
-
|
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/
|
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/
|
45
|
-
space for collaboration, and contributors are expected to adhere to
|
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
|
+
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)
|
16
|
-
|
17
|
-
|
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)
|
42
|
+
Pattern.new(self, size: size + object.size) { |y|
|
40
43
|
each { |v| y << v }
|
41
44
|
object.each { |v| y << v }
|
42
|
-
|
45
|
+
}
|
43
46
|
else
|
44
|
-
Pattern.new(self)
|
45
|
-
|
46
|
-
|
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)
|
64
|
-
|
65
|
-
|
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)
|
82
|
-
|
83
|
-
|
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)
|
100
|
-
|
101
|
-
|
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)
|
118
|
-
|
119
|
-
|
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)
|
136
|
-
|
137
|
-
|
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)
|
222
|
-
|
223
|
-
|
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)
|
244
|
-
|
245
|
-
|
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)
|
292
|
+
Pattern.new(self) { |y|
|
266
293
|
each_event { |e| y << E[e.value, e.start * num, e.duration * num] }
|
267
|
-
|
294
|
+
}
|
268
295
|
end
|
269
296
|
|
270
297
|
# TODO Document
|
271
298
|
def accelerate(num)
|
272
|
-
Pattern.new(self)
|
299
|
+
Pattern.new(self) { |y|
|
273
300
|
each_event { |e| y << E[e.value, e.start / num, e.duration / num] }
|
274
|
-
|
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(
|
20
|
-
|
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
|
-
|
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 ||=
|
35
|
+
@event_duration ||= source.event_duration if source.respond_to?(:event_duration)
|
34
36
|
@event_duration ||= 1
|
35
37
|
|
36
|
-
@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(
|
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}
|
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
|
-
|
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
|
-
|
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
|
-
|
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,
|
132
|
-
if
|
133
|
-
gate_off
|
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
|
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)
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
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, [
|
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
|
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
|
-
|
186
|
-
|
187
|
-
|
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
|
195
|
-
|
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
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/
|
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.
|
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-
|
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/
|
146
|
+
homepage: https://github.com/xi-livecode/xi
|
147
147
|
licenses:
|
148
148
|
- MIT
|
149
149
|
metadata: {}
|