Class: Wavify::Sequencer::Track
- Inherits:
-
Object
- Object
- Wavify::Sequencer::Track
- 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
-
#chord_progression ⇒ Array[untyped]?
readonly
Returns the value of attribute chord_progression.
-
#chord_voicing ⇒ Symbol
readonly
Returns the value of attribute chord_voicing.
-
#default_octave ⇒ Integer
readonly
Returns the value of attribute default_octave.
-
#effects ⇒ Array[untyped]
readonly
Returns the value of attribute effects.
-
#envelope ⇒ DSP::Envelope?
readonly
Returns the value of attribute envelope.
-
#gain_db ⇒ Float
readonly
Returns the value of attribute gain_db.
-
#key_root ⇒ Symbol?
readonly
Returns the value of attribute key_root.
-
#name ⇒ Symbol
readonly
Returns the value of attribute name.
-
#note_resolution ⇒ Integer
readonly
Returns the value of attribute note_resolution.
-
#note_sequence ⇒ NoteSequence?
readonly
Returns the value of attribute note_sequence.
-
#pan_position ⇒ Float
readonly
Returns the value of attribute pan_position.
-
#pattern ⇒ Pattern?
readonly
Returns the value of attribute pattern.
-
#pattern_resolution ⇒ Integer
readonly
Returns the value of attribute pattern_resolution.
-
#scale ⇒ Symbol?
readonly
Returns the value of attribute scale.
-
#synth_options ⇒ Hash[Symbol, untyped]
readonly
Returns the value of attribute synth_options.
-
#waveform ⇒ Symbol
readonly
Returns the value of attribute waveform.
Class Method Summary collapse
- .apply_chord_inversion(midi_notes, bass_name, root_midi, default_octave) ⇒ Array[Integer]
- .apply_chord_voicing(midi_notes, voicing) ⇒ Array[Integer]
- .parse_chord_token(token, default_octave:) ⇒ Object
- .parse_chords(chords, default_octave: 4) ⇒ Array[untyped]
- .quantize_midi_note(midi_note, key:, scale:) ⇒ Integer
Instance Method Summary collapse
- #chords? ⇒ Boolean
-
#copy(**overrides) ⇒ Track
Copy constructor used by immutable builder helpers.
- #effects? ⇒ Boolean
- #event_sources? ⇒ Boolean
-
#initialize(name, **options) ⇒ Track
constructor
A new instance of Track.
- #notes? ⇒ Boolean
-
#parse_chords(chords) ⇒ Array<Hash>
Parses chord notation using this track's default octave.
- #pattern? ⇒ Boolean
- #quantize_midi_note(midi_note) ⇒ Integer
-
#with_chord_voicing(voicing) ⇒ Track
Returns a copy with chord voicing settings.
-
#with_chords(chords, default_octave: @default_octave) ⇒ Track
Returns a copy with a new chord progression.
-
#with_effects(effects) ⇒ Track
Returns a copy with effect processors.
-
#with_envelope(envelope) ⇒ Track
Returns a copy with an envelope object.
-
#with_gain(db) ⇒ Track
Returns a copy with updated gain in dB.
-
#with_key(key, scale: @scale) ⇒ Track
Returns a copy with scale quantization settings.
-
#with_notes(notes, default_octave: @default_octave) ⇒ Track
Returns a copy with a new note sequence.
-
#with_pan(position) ⇒ Track
Returns a copy with updated pan position.
-
#with_pattern(pattern) ⇒ Track
Returns a copy with a new pattern.
-
#with_synth(waveform) ⇒ Track
Returns a copy with a different oscillator waveform.
Constructor Details
#initialize(name, **options) ⇒ Track
Returns a new instance of Track.
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, **) @name = validate_name!(name) pattern_resolution = .fetch(:pattern_resolution, 16) note_resolution = .fetch(:note_resolution, 8) default_octave = .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 = .fetch(:waveform, :sine).to_sym @synth_options = (.fetch(:synth_options, {})) @gain_db = validate_numeric!(.fetch(:gain_db, 0.0), :gain_db).to_f @pan_position = validate_pan!(.fetch(:pan_position, 0.0)) @envelope = validate_envelope!([:envelope]) @effects = validate_effects!(.fetch(:effects, [])) @key_root = normalize_key_root([:key]) @scale = normalize_scale([:scale]) @chord_voicing = normalize_chord_voicing([:chord_voicing]) @pattern = coerce_pattern([:pattern]) @note_sequence = coerce_note_sequence([:note_sequence]) @chord_progression = coerce_chord_progression([:chord_progression]) freeze end |
Instance Attribute Details
#chord_progression ⇒ Array[untyped]? (readonly)
Returns the value of attribute chord_progression.
48 49 50 |
# File 'lib/wavify/sequencer/track.rb', line 48 def chord_progression @chord_progression end |
#chord_voicing ⇒ Symbol (readonly)
Returns the value of attribute chord_voicing.
48 49 50 |
# File 'lib/wavify/sequencer/track.rb', line 48 def chord_voicing @chord_voicing end |
#default_octave ⇒ Integer (readonly)
Returns the value of attribute default_octave.
48 49 50 |
# File 'lib/wavify/sequencer/track.rb', line 48 def default_octave @default_octave end |
#effects ⇒ Array[untyped] (readonly)
Returns the value of attribute effects.
48 49 50 |
# File 'lib/wavify/sequencer/track.rb', line 48 def effects @effects end |
#envelope ⇒ DSP::Envelope? (readonly)
Returns the value of attribute envelope.
48 49 50 |
# File 'lib/wavify/sequencer/track.rb', line 48 def envelope @envelope end |
#gain_db ⇒ Float (readonly)
Returns the value of attribute gain_db.
48 49 50 |
# File 'lib/wavify/sequencer/track.rb', line 48 def gain_db @gain_db end |
#key_root ⇒ Symbol? (readonly)
Returns the value of attribute key_root.
48 49 50 |
# File 'lib/wavify/sequencer/track.rb', line 48 def key_root @key_root end |
#name ⇒ Symbol (readonly)
Returns the value of attribute name.
48 49 50 |
# File 'lib/wavify/sequencer/track.rb', line 48 def name @name end |
#note_resolution ⇒ Integer (readonly)
Returns the value of attribute note_resolution.
48 49 50 |
# File 'lib/wavify/sequencer/track.rb', line 48 def note_resolution @note_resolution end |
#note_sequence ⇒ NoteSequence? (readonly)
Returns the value of attribute note_sequence.
48 49 50 |
# File 'lib/wavify/sequencer/track.rb', line 48 def note_sequence @note_sequence end |
#pan_position ⇒ Float (readonly)
Returns the value of attribute pan_position.
48 49 50 |
# File 'lib/wavify/sequencer/track.rb', line 48 def pan_position @pan_position end |
#pattern ⇒ Pattern? (readonly)
Returns the value of attribute pattern.
48 49 50 |
# File 'lib/wavify/sequencer/track.rb', line 48 def pattern @pattern end |
#pattern_resolution ⇒ Integer (readonly)
Returns the value of attribute pattern_resolution.
48 49 50 |
# File 'lib/wavify/sequencer/track.rb', line 48 def pattern_resolution @pattern_resolution end |
#scale ⇒ Symbol? (readonly)
Returns the value of attribute scale.
48 49 50 |
# File 'lib/wavify/sequencer/track.rb', line 48 def scale @scale end |
#synth_options ⇒ Hash[Symbol, untyped] (readonly)
Returns the value of attribute synth_options.
48 49 50 |
# File 'lib/wavify/sequencer/track.rb', line 48 def @synth_options end |
#waveform ⇒ Symbol (readonly)
Returns the value of attribute waveform.
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]
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]
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
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]
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
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
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.
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
176 177 178 |
# File 'lib/wavify/sequencer/track.rb', line 176 def effects? !@effects.empty? end |
#event_sources? ⇒ Boolean
160 161 162 |
# File 'lib/wavify/sequencer/track.rb', line 160 def event_sources? pattern? || notes? || chords? end |
#notes? ⇒ 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.
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
164 165 166 |
# File 'lib/wavify/sequencer/track.rb', line 164 def pattern? !@pattern.nil? end |
#quantize_midi_note(midi_note) ⇒ 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
124 125 126 |
# File 'lib/wavify/sequencer/track.rb', line 124 def with_synth(waveform) copy(waveform: waveform) end |