xi-lang 0.1.2 → 0.1.3

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