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