xi-lang 0.1.2 → 0.1.3

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: c7ee847da08f4dd79a890c7586a6c410d49994b5
4
- data.tar.gz: 098aa22714b668efef3e238a8bb7abac2be2476a
3
+ metadata.gz: 65bceb35fd0dbad39331e9c89bea0a83254fe075
4
+ data.tar.gz: a20c3dd41afe001b698aab57d8cd3577e66d6dcb
5
5
  SHA512:
6
- metadata.gz: 72f6b2715e0753723b848a9dc1b3a7754d7417bac71ac96cfa9869dc5c0f8ce0d497b7d76897e41af6c5f903a7166eeff90f8993adbf2e4fd29c52cb15437800
7
- data.tar.gz: fc8186a9b0685c9ee1045424c3cafb1ecd171c4de2b3e6fb156570ad4c40f1cce144c6ab1fe1ae1fbf8cff4213e929ce8252d392b44d8d9ee23034eae3df6293
6
+ metadata.gz: e92c7bf49fa2bba86bf0eb153d7c0d1fd363a21060d52faf3a472655ffce291c8e57563edc303ae1ef65c591dc6b6db746d2f64592e469e927ba56a35ce6cd31
7
+ data.tar.gz: 3ab57ef7a85d4cac9a9e260aba0b030972b80105e320036feefc3ef3a418b22aec361180685cdda28ef92ab72ee39639ca7786595c030000b96f76d4b04c5be5
data/lib/xi/clock.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require 'thread'
2
- require 'logger'
3
2
  require 'set'
4
3
 
5
4
  Thread.abort_on_exception = true
@@ -7,7 +6,7 @@ Thread.abort_on_exception = true
7
6
  module Xi
8
7
  class Clock
9
8
  DEFAULT_CPS = 1.0
10
- INTERVAL_SEC = 20 / 1000.0
9
+ INTERVAL_SEC = 10 / 1000.0
11
10
 
12
11
  def initialize(cps: DEFAULT_CPS)
13
12
  @mutex = Mutex.new
@@ -83,11 +82,7 @@ module Xi
83
82
  return unless playing?
84
83
  @streams.each { |s| s.notify(cycles) }
85
84
  rescue => err
86
- logger.error(err)
87
- end
88
-
89
- def logger
90
- @logger ||= Logger.new(STDOUT)
85
+ error(err)
91
86
  end
92
87
  end
93
88
  end
@@ -0,0 +1,5 @@
1
+ require "xi/logger"
2
+
3
+ class Object
4
+ include Xi::Logger
5
+ end
data/lib/xi/core_ext.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'xi/core_ext/enumerable'
2
2
  require 'xi/core_ext/fixnum'
3
3
  require 'xi/core_ext/numeric'
4
+ require 'xi/core_ext/object'
4
5
  require 'xi/core_ext/simple'
data/lib/xi/logger.rb ADDED
@@ -0,0 +1,25 @@
1
+ require 'tmpdir'
2
+ require 'logger'
3
+
4
+ module Xi::Logger
5
+ LOG_FILE = File.join(Dir.tmpdir, 'xi.log')
6
+
7
+ def logger
8
+ @@logger ||= begin
9
+ logger = ::Logger.new(LOG_FILE)
10
+ logger.formatter = proc do |severity, datetime, progname, msg|
11
+ "[#{datetime.strftime("%F %T %L")}] #{msg}\n"
12
+ end
13
+ logger
14
+ end
15
+ end
16
+
17
+ def debug(*args)
18
+ logger.debug(args.map(&:to_s).join(' '.freeze))
19
+ end
20
+
21
+ def error(error)
22
+ logger.error("#{error}:\n#{error.backtrace.join("\n".freeze)}")
23
+ ErrorLog.instance << error.to_s
24
+ end
25
+ end
@@ -0,0 +1,47 @@
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
@@ -210,18 +210,33 @@ module Xi
210
210
  end
211
211
  end
212
212
 
213
- # Traverses the pattern in order and then in reverse order
213
+ # Traverses the pattern in order and then in reverse order, skipping
214
+ # first and last values if +skip_extremes+ is true.
214
215
  #
215
216
  # @example
216
- # peek (0..3).p.bounce #=> [0, 1, 2, 3, 3, 2, 1, 0]
217
+ # peek (0..3).p.bounce #=> [0, 1, 2, 3, 2, 1]
218
+ # peek 10.p.bounce #=> [10]
217
219
  #
220
+ # @example with skip_extremes=false
221
+ # peek (0..3).p.bounce(false) #=> [0, 1, 2, 3, 3, 2, 1, 0]
222
+ #
223
+ # @param skip_extremes [Boolean] Skip first and last values
224
+ # to avoid repeated values (default: true)
218
225
  # @return [Pattern]
219
226
  #
220
- def bounce
221
- Pattern.new(self, size: size * 2 - 1) do |y|
222
- each.with_index { |v, i| y << v if i > 0 }
223
- reverse_each.with_index { |v, i| y << v if i > 0 }
224
- end
227
+ def bounce(skip_extremes=true)
228
+ return self if size == 0 || size == 1
229
+
230
+ Pattern.new(self, size: size * 2 - 1) { |y|
231
+ last_id = 0
232
+ each.with_index { |v, i|
233
+ y << v
234
+ last_id = i
235
+ }
236
+ reverse_each.with_index { |v, i|
237
+ y << v unless skip_extremes && (i == 0 || i == last_id)
238
+ }
239
+ }
225
240
  end
226
241
 
227
242
  # Normalizes a pattern of values that range from +min+ to +max+ to 0..1
@@ -306,7 +321,12 @@ module Xi
306
321
  #
307
322
  def sometimes(probability=0.5)
308
323
  prob_pat = probability.p
309
- Pattern.new(self, size: size * prob_pat.size) do |y|
324
+
325
+ if times_pat.infinite?
326
+ fail ArgumentError, 'times must be a finite pattern'
327
+ end
328
+
329
+ Pattern.new(self, size: size * prob_pat.reduce(:+)) do |y|
310
330
  prob_pat.each do |prob|
311
331
  each { |v| y << (rand < prob ? v : nil) }
312
332
  end
@@ -318,7 +338,12 @@ module Xi
318
338
  #
319
339
  def repeat_each(times)
320
340
  times_pat = times.p
321
- Pattern.new(self, size: size * times_pat.size) do |y|
341
+
342
+ if times_pat.infinite?
343
+ fail ArgumentError, 'times must be a finite pattern'
344
+ end
345
+
346
+ Pattern.new(self, size: size * times_pat.reduce(:+)) do |y|
322
347
  times_pat.each do |t|
323
348
  each { |v| t.times { y << v } }
324
349
  end
data/lib/xi/repl.rb CHANGED
@@ -37,12 +37,16 @@ module Xi
37
37
  Pry.hooks.add_hook(:after_eval, "check_for_errors") do |result, pry|
38
38
  more_errors = ErrorLog.instance.more_errors?
39
39
  ErrorLog.instance.each do |msg|
40
- puts "(╯°□°)╯︵ ɹoɹɹǝ #{msg}"
40
+ puts red("Error: #{msg}")
41
41
  end
42
- puts "(⌣_⌣”) There were more errors..." if more_errors
42
+ puts red("There were more errors...") if more_errors
43
43
  end
44
44
  end
45
45
 
46
+ def red(string)
47
+ "\e[31m\e[1m#{string}\e[22m\e[0m"
48
+ end
49
+
46
50
  def load_init_script
47
51
  require(init_script_path)
48
52
  end
data/lib/xi/scale.rb ADDED
@@ -0,0 +1,97 @@
1
+ class Xi::Scale
2
+ DEGREES = {
3
+ # TWELVE TONES PER OCTAVE
4
+ # 5 note scales
5
+ minorPentatonic: [0,3,5,7,10],
6
+ majorPentatonic: [0,2,4,7,9],
7
+ # another mode of major pentatonic
8
+ ritusen: [0,2,5,7,9],
9
+ # another mode of major pentatonic
10
+ egyptian: [0,2,5,7,10],
11
+
12
+ kumoi: [0,2,3,7,9],
13
+ hirajoshi: [0,2,3,7,8],
14
+
15
+ iwato: [0,1,5,6,10], # mode of hirajoshi
16
+ chinese: [0,4,6,7,11], # mode of hirajoshi
17
+ indian: [0,4,5,7,10],
18
+ pelog: [0,1,3,7,8],
19
+
20
+ prometheus: [0,2,4,6,11],
21
+ scriabin: [0,1,4,7,9],
22
+
23
+ # han chinese pentatonic scales
24
+ gong: [0,2,4,7,9],
25
+ shang: [0,2,5,7,10],
26
+ jiao: [0,3,5,8,10],
27
+ zhi: [0,2,5,7,9],
28
+ yu: [0,3,5,7,10],
29
+
30
+ # 6 note scales
31
+ whole: [0, 2, 4, 6, 8, 10],
32
+ augmented: [0,3,4,7,8,11],
33
+ augmented2: [0,1,4,5,8,9],
34
+
35
+ # hexatonic modes with no tritone
36
+ hexMajor7: [0,2,4,7,9,11],
37
+ hexDorian: [0,2,3,5,7,10],
38
+ hexPhrygian: [0,1,3,5,8,10],
39
+ hexSus: [0,2,5,7,9,10],
40
+ hexMajor6: [0,2,4,5,7,9],
41
+ hexAeolian: [0,3,5,7,8,10],
42
+
43
+ # 7 note scales
44
+ major: [0,2,4,5,7,9,11],
45
+ ionian: [0,2,4,5,7,9,11],
46
+ dorian: [0,2,3,5,7,9,10],
47
+ phrygian: [0,1,3,5,7,8,10],
48
+ lydian: [0,2,4,6,7,9,11],
49
+ mixolydian: [0,2,4,5,7,9,10],
50
+ aeolian: [0,2,3,5,7,8,10],
51
+ minor: [0,2,3,5,7,8,10],
52
+ locrian: [0,1,3,5,6,8,10],
53
+
54
+ harmonicMinor: [0,2,3,5,7,8,11],
55
+ harmonicMajor: [0,2,4,5,7,8,11],
56
+
57
+ melodicMinor: [0,2,3,5,7,9,11],
58
+ melodicMinorDesc: [0,2,3,5,7,8,10],
59
+ melodicMajor: [0,2,4,5,7,8,10],
60
+
61
+ bartok: [0,2,4,5,7,8,10],
62
+ hindu: [0,2,4,5,7,8,10],
63
+
64
+ # raga modes
65
+ todi: [0,1,3,6,7,8,11],
66
+ purvi: [0,1,4,6,7,8,11],
67
+ marva: [0,1,4,6,7,9,11],
68
+ bhairav: [0,1,4,5,7,8,11],
69
+ ahirbhairav: [0,1,4,5,7,9,10],
70
+
71
+ superLocrian: [0,1,3,4,6,8,10],
72
+ romanianMinor: [0,2,3,6,7,9,10],
73
+ hungarianMinor: [0,2,3,6,7,8,11],
74
+ neapolitanMinor: [0,1,3,5,7,8,11],
75
+ enigmatic: [0,1,4,6,8,10,11],
76
+ spanish: [0,1,4,5,7,8,10],
77
+
78
+ # modes of whole tones with added note ->
79
+ leadingWhole: [0,2,4,6,8,10,11],
80
+ lydianMinor: [0,2,4,6,7,8,10],
81
+ neapolitanMajor: [0,1,3,5,7,9,11],
82
+ locrianMajor: [0,2,4,5,6,8,10],
83
+
84
+ # 8 note scales
85
+ diminished: [0,1,3,4,6,7,9,10],
86
+ diminished2: [0,2,3,5,6,8,9,11],
87
+
88
+ # 12 note scales
89
+ chromatic: (0..11).to_a,
90
+ }
91
+
92
+ class << self
93
+ DEGREES.each do |name, list|
94
+ define_method(name) { list }
95
+ end
96
+ end
97
+ end
data/lib/xi/stream.rb CHANGED
@@ -1,10 +1,16 @@
1
1
  require 'set'
2
+ require 'xi/music_parameters'
2
3
 
3
4
  module Xi
4
5
  class Stream
5
- attr_reader :clock, :source, :source_patterns, :state, :event_duration, :gate
6
+ prepend MusicParameters
7
+
8
+ attr_reader :clock, :opts, :source, :state, :event_duration, :gate
9
+
10
+ def initialize(name, clock, **opts)
11
+ @name = name.to_sym
12
+ @opts = opts
6
13
 
7
- def initialize(clock)
8
14
  @mutex = Mutex.new
9
15
  @playing = false
10
16
  @last_sound_object_id = 0
@@ -18,6 +24,7 @@ module Xi
18
24
 
19
25
  def set(event_duration: nil, gate: nil, **source)
20
26
  @mutex.synchronize do
27
+ source[:s] ||= @name
21
28
  @source = source
22
29
  @gate = gate if gate
23
30
  @event_duration = event_duration if event_duration
@@ -26,7 +33,7 @@ module Xi
26
33
  play
27
34
  self
28
35
  end
29
- alias_method :<<, :set
36
+ alias_method :call, :set
30
37
 
31
38
  def event_duration=(new_value)
32
39
  @mutex.synchronize do
@@ -76,10 +83,11 @@ module Xi
76
83
  alias_method :pause, :play
77
84
 
78
85
  def inspect
79
- "#<#{self.class.name}:#{"0x%014x" % object_id} " \
80
- "clock=#{@clock.inspect} #{playing? ? :playing : :stopped}>"
86
+ "#<#{self.class.name} :#{@name} " \
87
+ "#{playing? ? :playing : :stopped} at #{@clock.cps}cps" \
88
+ "#{" #{@opts}" if @opts.any?}>"
81
89
  rescue => err
82
- logger.error(err)
90
+ error(err)
83
91
  end
84
92
 
85
93
  def notify(now)
@@ -89,9 +97,10 @@ module Xi
89
97
  @changed_params.clear
90
98
 
91
99
  forward_enums(now) if @must_forward
100
+ gate_off = gate_off_old_sound_objects(now)
101
+ gate_on = play_enums(now)
92
102
 
93
- gate_on, gate_off = play_enums(now)
94
-
103
+ # Call hooks
95
104
  do_gate_off_change(gate_off) unless gate_off.empty?
96
105
  do_state_change if state_changed?
97
106
  do_gate_on_change(gate_on) unless gate_on.empty?
@@ -106,6 +115,8 @@ module Xi
106
115
 
107
116
  def forward_enums(now)
108
117
  @enums.each do |p, (enum, total_dur)|
118
+ next if total_dur == 0
119
+
109
120
  cur_pos = now % total_dur
110
121
  start_pos = now - cur_pos
111
122
 
@@ -123,34 +134,40 @@ module Xi
123
134
  @must_forward = false
124
135
  end
125
136
 
126
- def play_enums(now)
137
+ def gate_off_old_sound_objects(now)
127
138
  gate_off = []
139
+
140
+ # Check if there are any currently playing sound objects that
141
+ # must be gated off
142
+ @playing_sound_objects.dup.each do |end_pos, h|
143
+ if now >= h[:at] - latency_sec
144
+ gate_off << h
145
+ @playing_sound_objects.delete(end_pos)
146
+ end
147
+ end
148
+
149
+ gate_off
150
+ end
151
+
152
+ def play_enums(now)
128
153
  gate_on = []
129
154
 
130
155
  @enums.each do |p, (enum, total_dur)|
156
+ next if total_dur == 0
157
+
131
158
  cur_pos = now % total_dur
132
159
  start_pos = now - cur_pos
133
160
 
134
- # Check if there are any currently playing sound objects that
135
- # must be gated off
136
- @playing_sound_objects.dup.each do |end_pos, h|
137
- if now >= h[:at] - latency_sec
138
- gate_off << h
139
- @playing_sound_objects.delete(end_pos)
140
- end
141
- end
142
-
143
161
  next_ev = enum.peek
144
162
 
145
163
  # Do we need to play next event now? If not, skip this parameter
146
164
  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}"
148
165
  # Update state based on pattern value
149
166
  # TODO: Pass as parameter exact time (start_ts + next_ev.start)
150
167
  update_state(p, next_ev.value)
168
+ transform_state
151
169
 
152
- # If this parameter is a gate, mark it as gate on as
153
- # a new sound object
170
+ # If a gate parameter changed, create a new sound object
154
171
  if p == @gate
155
172
  new_so_ids = Array(next_ev.value)
156
173
  .size.times.map { new_sound_object_id }
@@ -173,7 +190,11 @@ module Xi
173
190
  end
174
191
  end
175
192
 
176
- [gate_on, gate_off]
193
+ gate_on
194
+ end
195
+
196
+ # @override
197
+ def transform_state
177
198
  end
178
199
 
179
200
  def new_sound_object_id
@@ -189,21 +210,21 @@ module Xi
189
210
  end
190
211
 
191
212
  def do_gate_on_change(ss)
192
- logger.info "Gate on change: #{ss}"
213
+ debug "Gate on change: #{ss}"
193
214
  end
194
215
 
195
216
  def do_gate_off_change(ss)
196
- logger.info "Gate off change: #{ss}"
217
+ debug "Gate off change: #{ss}"
197
218
  end
198
219
 
199
220
  def do_state_change
200
- logger.info "State change: #{@state
221
+ debug "State change: #{@state
201
222
  .select { |k, v| @changed_params.include?(k) }.to_h}"
202
223
  end
203
224
 
204
225
  def update_state(p, v)
205
226
  if v != @state[p]
206
- logger.debug "Update state of :#{p}: #{v}"
227
+ debug "Update state of :#{p}: #{v}"
207
228
  @changed_params << p
208
229
  @state[p] = v
209
230
  end
@@ -220,10 +241,5 @@ module Xi
220
241
  def latency_sec
221
242
  0.05
222
243
  end
223
-
224
- def logger
225
- # FIXME this should be configurable
226
- @logger ||= Logger.new("/tmp/xi.log")
227
- end
228
244
  end
229
245
  end
data/lib/xi/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Xi
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.3"
3
3
  end
data/lib/xi.rb CHANGED
@@ -9,18 +9,68 @@ def inf
9
9
  Float::INFINITY
10
10
  end
11
11
 
12
- module Xi::Init
13
- def default_clock
14
- @default_clock ||= Clock.new
12
+ module Xi
13
+ def self.default_backend
14
+ @default_backend
15
15
  end
16
16
 
17
- def peek(pattern, *args)
18
- pattern.peek(*args)
17
+ def self.default_backend=(new_name)
18
+ @default_backend = new_name && new_name.to_sym
19
19
  end
20
20
 
21
- def peek_events(pattern, *args)
22
- pattern.peek_events(*args)
21
+ module Init
22
+ def peek(pattern, *args)
23
+ pattern.peek(*args)
24
+ end
25
+
26
+ def peek_events(pattern, *args)
27
+ pattern.peek_events(*args)
28
+ end
29
+
30
+ def clock
31
+ @default_clock ||= Clock.new
32
+ end
33
+
34
+ def stop_all
35
+ @streams.each do |backend, ss|
36
+ ss.each do |name, stream|
37
+ stream.stop
38
+ end
39
+ end
40
+ end
41
+ alias_method :hush, :stop_all
42
+
43
+ def method_missing(method, backend=nil, **opts)
44
+ backend ||= Xi.default_backend
45
+ super if backend.nil?
46
+
47
+ if !backend.is_a?(String) && !backend.is_a?(Symbol)
48
+ fail ArgumentError, "invalid backend '#{backend}'"
49
+ end
50
+
51
+ @streams ||= {}
52
+ @streams[backend] ||= {}
53
+
54
+ s = @streams[backend][method] ||= begin
55
+ require "xi/#{backend}"
56
+ cls = Class.const_get("#{backend.to_s.capitalize}::Stream")
57
+ cls.new(method, self.clock, **opts)
58
+ end
59
+
60
+ b = Pry.binding_for(self)
61
+ b.local_variable_set(method, s)
62
+
63
+ s
64
+ end
23
65
  end
24
66
  end
25
67
 
26
- self.extend Xi::Init
68
+ singleton_class.include Xi::Init
69
+
70
+ # Try to load Supercollider backend and set it as default if installed
71
+ begin
72
+ require "xi/supercollider"
73
+ Xi.default_backend = :supercollider
74
+ rescue LoadError
75
+ Xi.default_backend = nil
76
+ 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.2
4
+ version: 0.1.3
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-16 00:00:00.000000000 Z
11
+ date: 2017-02-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -133,13 +133,17 @@ files:
133
133
  - lib/xi/core_ext/enumerable.rb
134
134
  - lib/xi/core_ext/fixnum.rb
135
135
  - lib/xi/core_ext/numeric.rb
136
+ - lib/xi/core_ext/object.rb
136
137
  - lib/xi/core_ext/simple.rb
137
138
  - lib/xi/error_log.rb
138
139
  - lib/xi/event.rb
140
+ - lib/xi/logger.rb
141
+ - lib/xi/music_parameters.rb
139
142
  - lib/xi/pattern.rb
140
143
  - lib/xi/pattern/generators.rb
141
144
  - lib/xi/pattern/transforms.rb
142
145
  - lib/xi/repl.rb
146
+ - lib/xi/scale.rb
143
147
  - lib/xi/stream.rb
144
148
  - lib/xi/version.rb
145
149
  - xi.gemspec