Class: Wavify::Sequencer::Track

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

Overview

Immutable sequencer track definition consumed by Engine.

Constant Summary collapse

CHORD_INTERVALS =

Chord suffix to semitone interval mapping.

{
  "" => [0, 4, 7],
  "M" => [0, 4, 7],
  "MAJ" => [0, 4, 7],
  "MAJ7" => [0, 4, 7, 11],
  "7" => [0, 4, 7, 10],
  "M7" => [0, 4, 7, 11],
  "MIN" => [0, 3, 7],
  "MIN7" => [0, 3, 7, 10],
  "MIN9" => [0, 3, 7, 10, 14],
  "MINOR" => [0, 3, 7],
  "M7B5" => [0, 3, 6, 10],
  "M7-5" => [0, 3, 6, 10],
  "DIM" => [0, 3, 6],
  "DIM7" => [0, 3, 6, 9],
  "AUG" => [0, 4, 8],
  "SUS2" => [0, 2, 7],
  "SUS4" => [0, 5, 7],
  "MAJ9" => [0, 4, 7, 11, 14]
}.merge(
  "m" => [0, 3, 7],
  "m7" => [0, 3, 7, 10],
  "m9" => [0, 3, 7, 10, 14],
  "maj7" => [0, 4, 7, 11],
  "maj9" => [0, 4, 7, 11, 14],
  "sus2" => [0, 2, 7],
  "sus4" => [0, 5, 7],
  "dim" => [0, 3, 6],
  "dim7" => [0, 3, 6, 9],
  "aug" => [0, 4, 8]
).freeze
SCALE_INTERVALS =
{
  major: [0, 2, 4, 5, 7, 9, 11],
  minor: [0, 2, 3, 5, 7, 8, 10],
  chromatic: (0..11).to_a,
  pentatonic_major: [0, 2, 4, 7, 9],
  pentatonic_minor: [0, 3, 5, 7, 10]
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, **options) ⇒ Track

Returns a new instance of Track.

Parameters:

  • name (String, Symbol)
  • options (Object)


52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/wavify/sequencer/track.rb', line 52

def initialize(name, **options)
  @name = validate_name!(name)
  pattern_resolution = options.fetch(:pattern_resolution, 16)
  note_resolution = options.fetch(:note_resolution, 8)
  default_octave = options.fetch(:default_octave, 4)

  @pattern_resolution = validate_resolution!(pattern_resolution, :pattern_resolution)
  @note_resolution = validate_resolution!(note_resolution, :note_resolution)
  @default_octave = validate_default_octave!(default_octave)
  @waveform = options.fetch(:waveform, :sine).to_sym
  @synth_options = validate_synth_options!(options.fetch(:synth_options, {}))
  @gain_db = validate_numeric!(options.fetch(:gain_db, 0.0), :gain_db).to_f
  @pan_position = validate_pan!(options.fetch(:pan_position, 0.0))
  @envelope = validate_envelope!(options[:envelope])
  @effects = validate_effects!(options.fetch(:effects, []))
  @key_root = normalize_key_root(options[:key])
  @scale = normalize_scale(options[:scale])
  @chord_voicing = normalize_chord_voicing(options[:chord_voicing])

  @pattern = coerce_pattern(options[:pattern])
  @note_sequence = coerce_note_sequence(options[:note_sequence])
  @chord_progression = coerce_chord_progression(options[:chord_progression])
  freeze
end

Instance Attribute Details

#chord_progressionArray[untyped]? (readonly)

Returns the value of attribute chord_progression.

Returns:

  • (Array[untyped], nil)


48
49
50
# File 'lib/wavify/sequencer/track.rb', line 48

def chord_progression
  @chord_progression
end

#chord_voicingSymbol (readonly)

Returns the value of attribute chord_voicing.

Returns:

  • (Symbol)


48
49
50
# File 'lib/wavify/sequencer/track.rb', line 48

def chord_voicing
  @chord_voicing
end

#default_octaveInteger (readonly)

Returns the value of attribute default_octave.

Returns:

  • (Integer)


48
49
50
# File 'lib/wavify/sequencer/track.rb', line 48

def default_octave
  @default_octave
end

#effectsArray[untyped] (readonly)

Returns the value of attribute effects.

Returns:

  • (Array[untyped])


48
49
50
# File 'lib/wavify/sequencer/track.rb', line 48

def effects
  @effects
end

#envelopeDSP::Envelope? (readonly)

Returns the value of attribute envelope.

Returns:



48
49
50
# File 'lib/wavify/sequencer/track.rb', line 48

def envelope
  @envelope
end

#gain_dbFloat (readonly)

Returns the value of attribute gain_db.

Returns:

  • (Float)


48
49
50
# File 'lib/wavify/sequencer/track.rb', line 48

def gain_db
  @gain_db
end

#key_rootSymbol? (readonly)

Returns the value of attribute key_root.

Returns:

  • (Symbol, nil)


48
49
50
# File 'lib/wavify/sequencer/track.rb', line 48

def key_root
  @key_root
end

#nameSymbol (readonly)

Returns the value of attribute name.

Returns:

  • (Symbol)


48
49
50
# File 'lib/wavify/sequencer/track.rb', line 48

def name
  @name
end

#note_resolutionInteger (readonly)

Returns the value of attribute note_resolution.

Returns:

  • (Integer)


48
49
50
# File 'lib/wavify/sequencer/track.rb', line 48

def note_resolution
  @note_resolution
end

#note_sequenceNoteSequence? (readonly)

Returns the value of attribute note_sequence.

Returns:



48
49
50
# File 'lib/wavify/sequencer/track.rb', line 48

def note_sequence
  @note_sequence
end

#pan_positionFloat (readonly)

Returns the value of attribute pan_position.

Returns:

  • (Float)


48
49
50
# File 'lib/wavify/sequencer/track.rb', line 48

def pan_position
  @pan_position
end

#patternPattern? (readonly)

Returns the value of attribute pattern.

Returns:



48
49
50
# File 'lib/wavify/sequencer/track.rb', line 48

def pattern
  @pattern
end

#pattern_resolutionInteger (readonly)

Returns the value of attribute pattern_resolution.

Returns:

  • (Integer)


48
49
50
# File 'lib/wavify/sequencer/track.rb', line 48

def pattern_resolution
  @pattern_resolution
end

#scaleSymbol? (readonly)

Returns the value of attribute scale.

Returns:

  • (Symbol, nil)


48
49
50
# File 'lib/wavify/sequencer/track.rb', line 48

def scale
  @scale
end

#synth_optionsHash[Symbol, untyped] (readonly)

Returns the value of attribute synth_options.

Returns:

  • (Hash[Symbol, untyped])


48
49
50
# File 'lib/wavify/sequencer/track.rb', line 48

def synth_options
  @synth_options
end

#waveformSymbol (readonly)

Returns the value of attribute waveform.

Returns:

  • (Symbol)


48
49
50
# File 'lib/wavify/sequencer/track.rb', line 48

def waveform
  @waveform
end

Class Method Details

.apply_chord_inversion(midi_notes, bass_name, root_midi, default_octave) ⇒ Array[Integer]

Parameters:

  • midi_notes (Array[Integer])
  • bass_name (String)
  • root_midi (Integer)
  • default_octave (Integer)

Returns:

  • (Array[Integer])


249
250
251
252
253
254
# File 'lib/wavify/sequencer/track.rb', line 249

def self.apply_chord_inversion(midi_notes, bass_name, root_midi, default_octave)
  bass = NoteSequence.new("#{bass_name}#{default_octave}", default_octave: default_octave).midi_notes.first
  bass -= 12 while bass > root_midi
  remaining = midi_notes.reject { |midi| midi % 12 == bass % 12 }
  [bass, *remaining].sort
end

.apply_chord_voicing(midi_notes, voicing) ⇒ Array[Integer]

Parameters:

  • midi_notes (Array[Integer])
  • voicing (Symbol)

Returns:

  • (Array[Integer])


256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/wavify/sequencer/track.rb', line 256

def self.apply_chord_voicing(midi_notes, voicing)
  case normalize_chord_voicing(voicing)
  when nil, :root
    midi_notes
  when :open
    midi_notes.each_with_index.map { |midi, index| index.odd? ? midi + 12 : midi }.sort
  when :drop2
    return midi_notes if midi_notes.length < 3

    ordered = midi_notes.sort
    dropped = ordered.delete_at(-2) - 12
    [dropped, *ordered].sort
  end
end

.parse_chord_token(token, default_octave:) ⇒ Object

Parameters:

  • token (String)
  • default_octave: (Integer)

Returns:

  • (Object)

Raises:



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/wavify/sequencer/track.rb', line 227

def self.parse_chord_token(token, default_octave:)
  chord_token, inline_voicing = token.to_s.split("@", 2)
  match = chord_token.match(/\A([A-Ga-g])([#b]?)(.*)\z/)
  raise InvalidNoteError, "invalid chord token: #{token.inspect}" unless match

  root_name = "#{match[1].upcase}#{match[2]}"
  suffix, bass_name = match[3].to_s.split("/", 2)
  suffix_key = normalize_chord_suffix(suffix)
  intervals = CHORD_INTERVALS[suffix_key] || CHORD_INTERVALS[suffix]
  raise InvalidNoteError, "unsupported chord quality: #{suffix.inspect}" unless intervals

  root_midi = NoteSequence.new("#{root_name}#{default_octave}", default_octave: default_octave).midi_notes.first
  midi_notes = intervals.map { |interval| root_midi + interval }
  midi_notes = apply_chord_inversion(midi_notes, bass_name, root_midi, default_octave) if bass_name
  midi_notes = apply_chord_voicing(midi_notes, inline_voicing) if inline_voicing
  {
    token: token,
    root_midi: root_midi,
    midi_notes: midi_notes
  }
end

.parse_chords(chords, default_octave: 4) ⇒ Array[untyped]

Parameters:

  • chords (String, Array[untyped])
  • default_octave: (Integer) (defaults to: 4)

Returns:

  • (Array[untyped])

Raises:



212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/wavify/sequencer/track.rb', line 212

def self.parse_chords(chords, default_octave: 4)
  tokens = case chords
           when String
             chords.split(/\s+/)
           when Array
             chords.map(&:to_s)
           else
             raise InvalidNoteError, "chords must be String or Array"
           end.reject(&:empty?)

  raise InvalidNoteError, "chords must not be empty" if tokens.empty?

  tokens.map { |token| parse_chord_token(token, default_octave: default_octave) }
end

.quantize_midi_note(midi_note, key:, scale:) ⇒ Integer

Parameters:

  • midi_note (Integer)
  • key: (Symbol)
  • scale: (Symbol)

Returns:

  • (Integer)


291
292
293
294
295
296
297
298
299
300
# File 'lib/wavify/sequencer/track.rb', line 291

def self.quantize_midi_note(midi_note, key:, scale:)
  key_pitch = NoteSequence::NOTE_OFFSETS.fetch(key.to_s.upcase)
  intervals = SCALE_INTERVALS.fetch(scale.to_sym)
  octave = midi_note / 12
  candidates = ((octave - 1)..(octave + 1)).flat_map do |candidate_octave|
    base = (candidate_octave * 12) + key_pitch
    intervals.map { |interval| base + interval }
  end.select { |candidate| candidate.between?(0, 127) }
  candidates.min_by { |candidate| [(candidate - midi_note).abs, candidate] } || midi_note
end

Instance Method Details

#chords?Boolean

Returns:

  • (Boolean)


172
173
174
# File 'lib/wavify/sequencer/track.rb', line 172

def chords?
  !@chord_progression.nil? && !@chord_progression.empty?
end

#copy(**overrides) ⇒ Track

Copy constructor used by immutable builder helpers.

Parameters:

  • overrides (Object)

Returns:



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/wavify/sequencer/track.rb', line 183

def copy(**overrides)
  self.class.new(
    overrides.fetch(:name, @name),
    pattern: overrides.fetch(:pattern, @pattern),
    note_sequence: overrides.fetch(:note_sequence, @note_sequence),
    chord_progression: overrides.fetch(:chord_progression, @chord_progression),
    waveform: overrides.fetch(:waveform, @waveform),
    synth_options: overrides.fetch(:synth_options, @synth_options),
    gain_db: overrides.fetch(:gain_db, @gain_db),
    pan_position: overrides.fetch(:pan_position, @pan_position),
    pattern_resolution: overrides.fetch(:pattern_resolution, @pattern_resolution),
    note_resolution: overrides.fetch(:note_resolution, @note_resolution),
    default_octave: overrides.fetch(:default_octave, @default_octave),
    envelope: overrides.fetch(:envelope, @envelope),
    effects: overrides.fetch(:effects, @effects),
    key: overrides.fetch(:key, @key_root),
    scale: overrides.fetch(:scale, @scale),
    chord_voicing: overrides.fetch(:chord_voicing, @chord_voicing)
  )
end

#effects?Boolean

Returns:

  • (Boolean)


176
177
178
# File 'lib/wavify/sequencer/track.rb', line 176

def effects?
  !@effects.empty?
end

#event_sources?Boolean

Returns:

  • (Boolean)


160
161
162
# File 'lib/wavify/sequencer/track.rb', line 160

def event_sources?
  pattern? || notes? || chords?
end

#notes?Boolean

Returns:

  • (Boolean)


168
169
170
# File 'lib/wavify/sequencer/track.rb', line 168

def notes?
  !@note_sequence.nil?
end

#parse_chords(chords) ⇒ Array<Hash>

Parses chord notation using this track's default octave.

Parameters:

  • chords (String, Array<String>)

Returns:

  • (Array<Hash>)


208
209
210
# File 'lib/wavify/sequencer/track.rb', line 208

def parse_chords(chords)
  self.class.parse_chords(chords, default_octave: @default_octave)
end

#pattern?Boolean

Returns:

  • (Boolean)


164
165
166
# File 'lib/wavify/sequencer/track.rb', line 164

def pattern?
  !@pattern.nil?
end

#quantize_midi_note(midi_note) ⇒ Integer

Parameters:

  • midi_note (Integer)

Returns:

  • (Integer)


285
286
287
288
289
# File 'lib/wavify/sequencer/track.rb', line 285

def quantize_midi_note(midi_note)
  return midi_note unless @key_root && @scale

  self.class.quantize_midi_note(midi_note, key: @key_root, scale: @scale)
end

#with_chord_voicing(voicing) ⇒ Track

Returns a copy with chord voicing settings.

Parameters:

  • voicing (Symbol, String, nil)

Returns:



116
117
118
# File 'lib/wavify/sequencer/track.rb', line 116

def with_chord_voicing(voicing)
  copy(chord_voicing: voicing)
end

#with_chords(chords, default_octave: @default_octave) ⇒ Track

Returns a copy with a new chord progression.

Parameters:

  • chords (String, Array<String>)
  • default_octave (Integer) (defaults to: @default_octave)
  • default_octave: (Integer) (defaults to: @default_octave)

Returns:



99
100
101
# File 'lib/wavify/sequencer/track.rb', line 99

def with_chords(chords, default_octave: @default_octave)
  copy(chord_progression: chords, default_octave: default_octave)
end

#with_effects(effects) ⇒ Track

Returns a copy with effect processors.

Parameters:

  • effects (Array<Object>)

Returns:



156
157
158
# File 'lib/wavify/sequencer/track.rb', line 156

def with_effects(effects)
  copy(effects: effects)
end

#with_envelope(envelope) ⇒ Track

Returns a copy with an envelope object.

Parameters:

Returns:



148
149
150
# File 'lib/wavify/sequencer/track.rb', line 148

def with_envelope(envelope)
  copy(envelope: envelope)
end

#with_gain(db) ⇒ Track

Returns a copy with updated gain in dB.

Parameters:

  • db (Numeric)

Returns:



132
133
134
# File 'lib/wavify/sequencer/track.rb', line 132

def with_gain(db)
  copy(gain_db: db)
end

#with_key(key, scale: @scale) ⇒ Track

Returns a copy with scale quantization settings.

Parameters:

  • key (Symbol, String, nil)
  • scale (Symbol, String, nil) (defaults to: @scale)
  • scale: (String, Symbol) (defaults to: @scale)

Returns:



108
109
110
# File 'lib/wavify/sequencer/track.rb', line 108

def with_key(key, scale: @scale)
  copy(key: key, scale: scale)
end

#with_notes(notes, default_octave: @default_octave) ⇒ Track

Returns a copy with a new note sequence.

Parameters:

  • notes (NoteSequence, String)
  • default_octave (Integer) (defaults to: @default_octave)
  • default_octave: (Integer) (defaults to: @default_octave)

Returns:



90
91
92
# File 'lib/wavify/sequencer/track.rb', line 90

def with_notes(notes, default_octave: @default_octave)
  copy(note_sequence: notes, default_octave: default_octave)
end

#with_pan(position) ⇒ Track

Returns a copy with updated pan position.

Parameters:

  • position (Numeric)

Returns:



140
141
142
# File 'lib/wavify/sequencer/track.rb', line 140

def with_pan(position)
  copy(pan_position: position)
end

#with_pattern(pattern) ⇒ Track

Returns a copy with a new pattern.

Parameters:

Returns:



81
82
83
# File 'lib/wavify/sequencer/track.rb', line 81

def with_pattern(pattern)
  copy(pattern: pattern)
end

#with_synth(waveform) ⇒ Track

Returns a copy with a different oscillator waveform.

Parameters:

  • waveform (Symbol, String)

Returns:



124
125
126
# File 'lib/wavify/sequencer/track.rb', line 124

def with_synth(waveform)
  copy(waveform: waveform)
end