class Xi::Stream

Constants

DEFAULT_PARAMS

Attributes

clock[R]
delta[R]
gate[R]
opts[R]
source[R]
state[R]

Public Class Methods

new(name, clock, **opts) click to toggle source
# File lib/xi/stream.rb, line 16
def initialize(name, clock, **opts)
  Array(opts.delete(:include)).each { |m| include_mixin(m) }

  @name = name.to_sym
  @opts = opts

  @mutex = Mutex.new
  @playing = false
  @last_sound_object_id = 0
  @state = {}
  @changed_params = [].to_set
  @playing_sound_objects = {}
  @prev_ts = {}
  @prev_delta = {}

  self.clock = clock
end

Public Instance Methods

call(delta: nil, gate: nil, **source)
Alias for: set
clock=(new_clock) click to toggle source
# File lib/xi/stream.rb, line 62
def clock=(new_clock)
  @clock.unsubscribe(self) if @clock
  new_clock.subscribe(self) if playing?
  @clock = new_clock
end
delta=(new_value) click to toggle source
# File lib/xi/stream.rb, line 48
def delta=(new_value)
  @mutex.synchronize do
    @delta = new_value
    update_internal_structures
  end
end
gate=(new_value) click to toggle source
# File lib/xi/stream.rb, line 55
def gate=(new_value)
  @mutex.synchronize do
    @gate = new_value
    update_internal_structures
  end
end
inspect() click to toggle source
# File lib/xi/stream.rb, line 97
def inspect
  "#<#{self.class.name} :#{@name} " \
    "#{playing? ? :playing : :stopped} at #{@clock.cps}cps" \
    "#{" #{@opts}" if @opts.any?}>"
rescue => err
  error(err)
end
notify(now, cps) click to toggle source
# File lib/xi/stream.rb, line 105
def notify(now, cps)
  return unless playing? && @source

  @mutex.synchronize do
    @changed_params.clear

    update_all_state if @reset

    gate_off = gate_off_old_sound_objects(now)
    gate_on = play_enums(now, cps)

    # Call hooks
    do_gate_off_change(gate_off) unless gate_off.empty?
    do_state_change if state_changed?
    do_gate_on_change(gate_on) unless gate_on.empty?
  end
end
pause()
Alias for: play
play() click to toggle source
# File lib/xi/stream.rb, line 76
def play
  @mutex.synchronize do
    @playing = true
    @clock.subscribe(self)
  end
  self
end
Also aliased as: start, pause
playing?() click to toggle source
# File lib/xi/stream.rb, line 68
def playing?
  @mutex.synchronize { @playing }
end
set(delta: nil, gate: nil, **source) click to toggle source
# File lib/xi/stream.rb, line 34
def set(delta: nil, gate: nil, **source)
  @mutex.synchronize do
    remove_parameters_from_prev_source(source)
    @source = source
    @gate = gate || parameter_with_smallest_delta(source)
    @delta = delta if delta
    @reset = true unless @playing
    update_internal_structures
  end
  play
  self
end
Also aliased as: call
start()
Alias for: play
stop() click to toggle source
# File lib/xi/stream.rb, line 85
def stop
  @mutex.synchronize do
    @playing = false
    @state.clear
    @prev_ts.clear
    @prev_delta.clear
    @clock.unsubscribe(self)
  end
  self
end
stopped?() click to toggle source
# File lib/xi/stream.rb, line 72
def stopped?
  !playing?
end

Private Instance Methods

changed_param?(*params) click to toggle source
# File lib/xi/stream.rb, line 237
def changed_param?(*params)
  @changed_params.any? { |p| params.include?(p) }
end
changed_state() click to toggle source
# File lib/xi/stream.rb, line 136
def changed_state
  @state.select { |k, _| @changed_params.include?(k) }
end
do_gate_off_change(ss) click to toggle source
# File lib/xi/stream.rb, line 254
def do_gate_off_change(ss)
  debug "Gate off change: #{ss}"
end
do_gate_on_change(ss) click to toggle source
# File lib/xi/stream.rb, line 250
def do_gate_on_change(ss)
  debug "Gate on change: #{ss}"
end
do_state_change() click to toggle source
# File lib/xi/stream.rb, line 258
def do_state_change
  debug "State change: #{@state
    .select { |k, v| @changed_params.include?(k) }.to_h}"
end
gate_off_old_sound_objects(now) click to toggle source
# File lib/xi/stream.rb, line 140
def gate_off_old_sound_objects(now)
  gate_off = []

  # Check if there are any currently playing sound objects that
  # must be gated off
  @playing_sound_objects.dup.each do |start_pos, h|
    if now + @clock.init_ts >= h[:at] - latency_sec
      gate_off << h
      @playing_sound_objects.delete(start_pos)
    end
  end

  gate_off
end
include_mixin(module_or_name) click to toggle source
# File lib/xi/stream.rb, line 125
def include_mixin(module_or_name)
  mod = if module_or_name.is_a?(Module)
    module_or_name
  else
    name = module_or_name.to_s
    require "#{self.class.name.underscore}/#{name}"
    self.class.const_get(name.camelize)
  end
  singleton_class.send(:include, mod)
end
latency_sec() click to toggle source
# File lib/xi/stream.rb, line 298
def latency_sec
  0.05
end
new_sound_object_id() click to toggle source
# File lib/xi/stream.rb, line 241
def new_sound_object_id
  @last_sound_object_id += 1
end
parameter_with_smallest_delta(source) click to toggle source
# File lib/xi/stream.rb, line 287
def parameter_with_smallest_delta(source)
  source.min_by { |param, enum|
    delta = enum.p.delta
    delta.is_a?(Array) ? delta.min : delta
  }.first
end
play_enums(now, cps) click to toggle source
# File lib/xi/stream.rb, line 155
def play_enums(now, cps)
  gate_on = []

  @enums.each do |p, enum|
    next unless enum.next?

    n_value, n_start, n_dur = enum.peek

    @prev_ts[p]    ||= n_start / cps
    @prev_delta[p] ||= n_dur

    next_start = @prev_ts[p] + (@prev_delta[p] / cps)

    # Do we need to play next event? If not, skip this parameter value
    if now >= next_start - latency_sec
      # If it is too late to play this event, skip it
      if now < next_start
        starts_at = @clock.init_ts + next_start

        # Update state based on pattern value
        # TODO: Pass as parameter exact time: starts_at
        update_state(p, n_value)
        transform_state

        # If a gate parameter changed, create a new sound object
        if p == @gate
          # If these sounds objects are new,
          # consider them as new "gate on" events.
          unless @playing_sound_objects.key?(n_start)
            new_so_ids = Array(n_value)
              .size.times.map { new_sound_object_id }

            gate_on << {so_ids: new_so_ids, at: starts_at}
            @playing_sound_objects[n_start] = {so_ids: new_so_ids}
          end

          # Set (or update) ends_at timestamp
          legato = @state[:legato] || 1
          ends_at = @clock.init_ts + next_start + ((n_dur * legato) / cps)
          @playing_sound_objects[n_start][:at] = ends_at
        end
      end

      @prev_ts[p]    = next_start
      @prev_delta[p] = n_dur

      # Because we already processed event, advance enumerator
      enum.next
    end
  end

  gate_on
end
reduce_to_midinote() click to toggle source
# File lib/xi/stream.rb, line 225
def reduce_to_midinote
  Array(@state[:note]).compact.map { |n|
    @state[:root].to_i + @state[:octave].to_i * @state[:steps_per_octave] + n
  }
end
reduce_to_note() click to toggle source
# File lib/xi/stream.rb, line 231
def reduce_to_note
  Array(@state[:degree]).compact.map do |d|
    d.degree_to_key(Array(@state[:scale]), @state[:steps_per_octave])
  end
end
remove_parameters_from_prev_source(new_source) click to toggle source
# File lib/xi/stream.rb, line 294
def remove_parameters_from_prev_source(new_source)
  (@source.keys - new_source.keys).each { |k| @state.delete(k) } unless @source.nil?
end
state_changed?() click to toggle source
# File lib/xi/stream.rb, line 274
def state_changed?
  !@changed_params.empty?
end
transform_state() click to toggle source
# File lib/xi/stream.rb, line 209
def transform_state
  @state = DEFAULT_PARAMS.merge(@state)

  @state[:s] ||= @name

  if !changed_param?(:note) && changed_param?(:degree, :scale, :steps_per_octave)
    @state[:note] = reduce_to_note
    @changed_params << :note
  end

  if !changed_param?(:midinote) && changed_param?(:note)
    @state[:midinote] = reduce_to_midinote
    @changed_params << :midinote
  end
end
update_all_state() click to toggle source
# File lib/xi/stream.rb, line 278
def update_all_state
  @enums.each do |p, enum|
    n_value, _ = enum.peek
    update_state(p, n_value)
  end
  transform_state
  @reset = false
end
update_internal_structures() click to toggle source
# File lib/xi/stream.rb, line 245
def update_internal_structures
  cycle = @clock.current_cycle
  @enums = @source.map { |k, v| [k, v.p(@delta).each_event(cycle)] }.to_h
end
update_state(param, value) click to toggle source
# File lib/xi/stream.rb, line 263
def update_state(param, value)
  kv = value.is_a?(Hash) ? value : {param => value}
  kv.each do |k, v|
    if v != @state[k]
      debug "Update state of :#{k}: #{v}"
      @changed_params << k
      @state[k] = v
    end
  end
end