Class: Wavify::Sequencer::Engine

Inherits:
Object
  • Object
show all
Defined in:
lib/wavify/sequencer/engine.rb,
sig/sequencer.rbs

Overview

Schedules sequencer tracks and renders them into audio.

Constant Summary collapse

DEFAULT_BEATS_PER_BAR =

Default beats-per-bar value used when omitted.

4
MAX_BARS =

Maximum bars accepted by a single render request.

100_000
MAX_ARRANGEMENT_SECTIONS =

Maximum structural sections accepted before repeat expansion.

10_000
MAX_EXPANDED_SECTIONS =

Maximum sections produced by lazy arrangement repeat expansion.

100_000
MAX_SECTION_REPEAT =

Maximum repeat count on one arrangement section.

10_000
MAX_EVENTS =

Maximum scheduled timeline events in one build.

1_000_000
MASTER_CEILING_DB =

Final sequencer ceiling leaves conversion margin below digital full scale.

-0.1
# The master limiter uses its full lookahead window to ramp gain before peaks.
MASTER_LOOKAHEAD_SECONDS =

The master limiter uses its full lookahead window to ramp gain before peaks.

0.005

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(tempo:, format: Wavify::Core::Format::CD_QUALITY, beats_per_bar: DEFAULT_BEATS_PER_BAR, swing: 0.5) ⇒ Engine

Returns a new instance of Engine.

Parameters:

  • tempo: (Numeric)
  • format: (Core::Format) (defaults to: Wavify::Core::Format::CD_QUALITY)
  • beats_per_bar: (Integer) (defaults to: DEFAULT_BEATS_PER_BAR)
  • swing: (Numeric) (defaults to: 0.5)


26
27
28
29
30
31
32
# File 'lib/wavify/sequencer/engine.rb', line 26

def initialize(tempo:, format: Wavify::Core::Format::CD_QUALITY, beats_per_bar: DEFAULT_BEATS_PER_BAR, swing: 0.5)
  @tempo = validate_tempo!(tempo)
  @format = validate_format!(format)
  @beats_per_bar = validate_beats_per_bar!(beats_per_bar)
  @swing = validate_swing!(swing)
  @section_engine_cache = {}
end

Instance Attribute Details

#beats_per_barInteger (readonly)

Returns the value of attribute beats_per_bar.

Returns:

  • (Integer)


24
25
26
# File 'lib/wavify/sequencer/engine.rb', line 24

def beats_per_bar
  @beats_per_bar
end

#formatCore::Format (readonly)

Returns the value of attribute format.

Returns:



24
25
26
# File 'lib/wavify/sequencer/engine.rb', line 24

def format
  @format
end

#swingFloat (readonly)

Returns the value of attribute swing.

Returns:

  • (Float)


24
25
26
# File 'lib/wavify/sequencer/engine.rb', line 24

def swing
  @swing
end

#tempoFloat (readonly)

Returns the value of attribute tempo.

Returns:

  • (Float)


24
25
26
# File 'lib/wavify/sequencer/engine.rb', line 24

def tempo
  @tempo
end

Instance Method Details

#bar_duration_secondsFloat

Returns duration of one bar in seconds.

Returns:

  • (Float)

    duration of one bar in seconds



40
41
42
# File 'lib/wavify/sequencer/engine.rb', line 40

def bar_duration_seconds
  bar_duration_time.to_f
end

#build_timeline(tracks:, arrangement: nil, default_bars: 1) ⇒ Array<Hash>

Builds a combined event timeline for tracks and optional arrangement.

Parameters:

  • tracks (Array<Wavify::Sequencer::Track>)
  • arrangement (Array<Hash>, nil) (defaults to: nil)
  • default_bars (Integer) (defaults to: 1)
  • tracks: (Array[Track])
  • arrangement: (Array[Hash[Symbol, untyped]], nil) (defaults to: nil)
  • default_bars: (Integer) (defaults to: 1)

Returns:

  • (Array<Hash>)


99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/wavify/sequencer/engine.rb', line 99

def build_timeline(tracks:, arrangement: nil, default_bars: 1)
  track_map = normalize_tracks(tracks)

  if arrangement
    build_arranged_timeline(track_map, arrangement)
  else
    unless default_bars.is_a?(Integer) && default_bars.between?(1, MAX_BARS)
      raise SequencerError, "default_bars must be an Integer in 1..#{MAX_BARS}"
    end

    events = track_map.values.flat_map do |track|
      timeline_for_track(track, bars: default_bars)
    end
    events.sort_by { |event| [event[:start_time], event[:track].to_s] }
  end
end

#expand_pattern_step(step, start_time:, duration:) ⇒ Array[Hash[Symbol, untyped]]

Parameters:

Returns:

  • (Array[Hash[Symbol, untyped]])


58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/wavify/sequencer/engine.rb', line 58

def expand_pattern_step(step, start_time:, duration:)
  ratchet = step.ratchet || 1
  event_duration = duration / ratchet
  Array.new(ratchet) do |ratchet_index|
    {
      start_time: start_time + (event_duration * ratchet_index),
      duration: event_duration,
      velocity: step.velocity,
      probability: step.probability || 1.0,
      ratchet_index: ratchet_index,
      ratchet_count: ratchet
    }
  end
end

#render(tracks:, arrangement: nil, default_bars: 1) ⇒ Wavify::Audio

Renders tracks into a mixed audio object.

Parameters:

  • tracks (Array<Wavify::Sequencer::Track>)
  • arrangement (Array<Hash>, nil) (defaults to: nil)
  • default_bars (Integer) (defaults to: 1)
  • tracks: (Array[Track])
  • arrangement: (Array[Hash[Symbol, untyped]], nil) (defaults to: nil)
  • default_bars: (Integer) (defaults to: 1)

Returns:



122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/wavify/sequencer/engine.rb', line 122

def render(tracks:, arrangement: nil, default_bars: 1)
  track_map = normalize_tracks(tracks)
  sections = arrangement ? normalize_arrangement(arrangement, track_map) : nil

  rendered_audios = if sections
                      render_arranged_tracks(track_map, sections)
                    else
                      track_map.values.filter_map { |track| render_track_audio(track, bars: default_bars) }
                    end

  return Wavify::Audio.silence(0.0, format: @format) if rendered_audios.empty?

  master_audio(rendered_audios)
end

#seconds_per_beatFloat

Returns seconds per beat at the current tempo.

Returns:

  • (Float)

    seconds per beat at the current tempo



35
36
37
# File 'lib/wavify/sequencer/engine.rb', line 35

def seconds_per_beat
  seconds_per_beat_rational.to_f
end

#step_duration_at(index, resolution) ⇒ Float

Parameters:

  • index (Integer)
  • resolution (Integer)

Returns:

  • (Float)


54
55
56
# File 'lib/wavify/sequencer/engine.rb', line 54

def step_duration_at(index, resolution)
  step_duration_time_at(index, resolution).to_f
end

#step_duration_seconds(resolution) ⇒ Float

Parameters:

  • resolution (Integer)

Returns:

  • (Float)

Raises:



44
45
46
47
48
# File 'lib/wavify/sequencer/engine.rb', line 44

def step_duration_seconds(resolution)
  raise SequencerError, "resolution must be a positive Integer" unless resolution.is_a?(Integer) && resolution.positive?

  step_duration_time(resolution).to_f
end

#step_start_seconds(index, resolution) ⇒ Float

Parameters:

  • index (Integer)
  • resolution (Integer)

Returns:

  • (Float)


50
51
52
# File 'lib/wavify/sequencer/engine.rb', line 50

def step_start_seconds(index, resolution)
  step_start_time(index, resolution).to_f
end

#timeline_for_track(track, bars:, start_bar: 0, start_time: nil) ⇒ Array[Hash[Symbol, untyped]]

Parameters:

  • track (Track)
  • bars: (Integer)
  • start_bar: (Integer) (defaults to: 0)
  • start_time: (Numeric, nil) (defaults to: nil)

Returns:

  • (Array[Hash[Symbol, untyped]])

Raises:



73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/wavify/sequencer/engine.rb', line 73

def timeline_for_track(track, bars:, start_bar: 0, start_time: nil)
  raise SequencerError, "track must be a Sequencer::Track" unless track.is_a?(Track)
  unless bars.is_a?(Integer) && bars.between?(1, MAX_BARS)
    raise SequencerError, "bars must be an Integer in 1..#{MAX_BARS}"
  end
  raise SequencerError, "start_bar must be a non-negative Integer" unless start_bar.is_a?(Integer) && start_bar >= 0
  if start_time && !(start_time.is_a?(Numeric) && start_time.respond_to?(:finite?) && start_time.finite? && start_time >= 0)
    raise SequencerError, "start_time must be a non-negative finite Numeric"
  end

  events = []
  events.concat(schedule_pattern_events(track, bars: bars, start_bar: start_bar, start_time: start_time)) if track.pattern?
  events.concat(schedule_note_events(track, bars: bars, start_bar: start_bar, start_time: start_time)) if track.notes?
  events.concat(schedule_chord_events(track, bars: bars, start_bar: start_bar, start_time: start_time)) if track.chords?
  raise SequencerError, "timeline exceeds #{MAX_EVENTS} events" if events.length > MAX_EVENTS

  events.map { |event| decorate_event_timing(event) }
        .sort_by { |event| [event[:start_frame], event[:kind].to_s] }
end