Class: Musa::Chords::Chord

Inherits:
Object show all
Defined in:
lib/musa-dsl/music/chords.rb

Overview

Instantiated chord with specific root and scale context.

Chord represents an actual chord instance with a root note, scale context, and chord definition. It provides access to chord tones, voicing modifications, and navigation between related chords.

Creation

Chords are typically created from scale notes rather than directly:

c_major = Scales.default_system.default_tuning.major[60]
chord = c_major.tonic.chord              # C major triad
chord = c_major.tonic.chord :seventh     # C major seventh
chord = c_major.dominant.chord :ninth    # G ninth chord

c_major is the scale every example below is written against.

Accessing Chord Tones

Chord tones are accessed by their position name (root, third, fifth, etc.):

chord.root      # Returns NoteInScale for root
chord.third     # Returns NoteInScale for third
chord.fifth     # Returns NoteInScale for fifth
chord.seventh   # Returns NoteInScale for seventh (if exists)

When notes are duplicated, use all: true to get all instances:

chord.root(all: true)  # Returns array of all root notes

Features and Navigation

Chords have features (quality, size) and can navigate to related chords:

chord.features          # => { quality: :major, size: :triad }
chord.quality           # => :major (dynamic method)
chord.size              # => :triad (dynamic method)

chord.with_quality(:minor)     # Change to minor
chord.with_size(:seventh)      # Add seventh
chord.featuring(size: :ninth)  # Change multiple features

Voicing Modifications

Move - Relocate specific chord tones to different octaves:

chord.with_move(root: -1, seventh: 1)
# Root down one octave, seventh up one octave
chord.move  # => { root: -1, seventh: 1 } (current settings)

Duplicate - Add copies of chord tones in other octaves:

chord.with_duplicate(root: -2, third: [-1, 1])
# Add root 2 octaves down, third 1 octave down and 1 up
chord.duplicate  # => { root: -2, third: [-1, 1] } (current settings)

Octave - Transpose entire chord:

chord.octave(-1)  # Move entire chord down one octave

Pitch Extraction

chord.pitches                    # All pitches sorted by pitch
chord.pitches(:root, :third)     # Only specified chord tones
chord.notes                      # Sorted ChordGradeNote structs

Scale Context

Chords maintain their scale context. When navigating to chords with non-diatonic notes (e.g., major to minor), the scale may become nil:

major_chord = c_major.tonic.chord
major_chord.scale  # => C major scale

minor_chord = major_chord.with_quality(:minor)
minor_chord.scale  # => nil (Eb not in C major)

Examples:

Basic triad creation

scale = Scales.default_system.default_tuning.major[60]
chord = scale.tonic.chord
chord.root.pitch   # => 60 (C)
chord.third.pitch  # => 64 (E)
chord.fifth.pitch  # => 67 (G)

Seventh chord

chord = scale.tonic.chord :seventh
chord.seventh.pitch  # => 71 (B)

Voicing with move and duplicate

scale = Scales.default_system.default_tuning.major[60]
chord = scale.dominant.chord(:seventh)
  .with_move(root: -1, third: -1)
  .with_duplicate(fifth: [0, 1])

Feature navigation

scale = Scales.default_system.default_tuning.major[60]
maj_triad = scale.tonic.chord
min_triad = maj_triad.with_quality(:minor)
maj_seventh = maj_triad.with_size(:seventh)

See Also:

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#chord_definitionChordDefinition (readonly)

Chord definition template.

Returns:



347
348
349
# File 'lib/musa-dsl/music/chords.rb', line 347

def chord_definition
  @chord_definition
end

#duplicateHash{Symbol => Integer, Array<Integer>} (readonly)

Octave duplications applied to positions.

Returns:



355
356
357
# File 'lib/musa-dsl/music/chords.rb', line 355

def duplicate
  @duplicate
end

#moveHash{Symbol => Integer} (readonly)

Octave moves applied to positions.

Returns:

  • (Hash{Symbol => Integer})


351
352
353
# File 'lib/musa-dsl/music/chords.rb', line 351

def move
  @move
end

#scaleScale? (readonly)

Scale context (nil if chord contains non-diatonic notes).

Returns:

  • (Scale, nil)


343
344
345
# File 'lib/musa-dsl/music/chords.rb', line 343

def scale
  @scale
end

Class Method Details

.with_root(root_note_or_pitch_or_symbol, scale: nil, allow_chromatic: false, name: nil, move: nil, duplicate: nil, **features) ⇒ Chord

Creates a chord with specified root.

Factory method for creating chords by specifying the root note and either a chord definition name or features. The root can be a NoteInScale, pitch number, or scale degree symbol.

Examples:

With note from scale

Chord.with_root(scale.tonic, name: :maj7)

With MIDI pitch and scale

Chord.with_root(60, scale: c_major, name: :min)

With scale degree

Chord.with_root(:dominant, scale: c_major, quality: :dominant, size: :seventh)

With features instead of name

Chord.with_root(60, scale: c_major, quality: :major, size: :triad)

Without a scale, on a bare MIDI pitch

# The fallback is a major scale rooted on the pitch itself, so the
# chord can be re-rooted anywhere without a scale in hand.
Chord.with_root(60, quality: :major, size: :triad).pitches  # => [60, 64, 67]
Chord.with_root(63, quality: :major, size: :triad).pitches  # => [63, 67, 70]

A quality that leaves the fallback scale

# Searching by features is constrained by the scale, and a minor third
# is not in the major scale the pitch just rooted. Either let it be
# chromatic, or name the definition, which skips the search.
Chord.with_root(60, quality: :minor, size: :triad, allow_chromatic: true).pitches
# => [60, 63, 67]
Chord.with_root(60, name: :min).pitches  # => [60, 63, 67]
Chord.with_root(60, name: :dim).pitches  # => [60, 63, 66]

With voicing parameters

Chord.with_root(60, scale: c_major, name: :maj7,
                move: {root: -1}, duplicate: {fifth: 1})

Parameters:

  • root_note_or_pitch_or_symbol (NoteInScale, Integer, Symbol)

    chord root

    • NoteInScale: use note directly
    • Integer (MIDI pitch): find note in scale, or create C major if no scale
    • Symbol (scale degree): requires scale parameter (e.g., :tonic, :dominant)
  • scale (Scale, nil) (defaults to: nil)

    scale context for finding notes

  • allow_chromatic (Boolean) (defaults to: false)

    allow non-diatonic notes

  • name (Symbol, nil) (defaults to: nil)

    chord definition name (:maj, :min7, etc.)

  • move (Hash{Symbol => Integer}, nil) (defaults to: nil)

    initial octave moves (e.g., {root: -1})

  • duplicate (Hash{Symbol => Integer, Array<Integer>}, nil) (defaults to: nil)

    initial duplications

  • features (Hash)

    chord features if not using name (quality:, size:, etc.)

Returns:

  • (Chord)

    new chord instance



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/musa-dsl/music/chords.rb', line 161

def self.with_root(root_note_or_pitch_or_symbol, scale: nil, allow_chromatic: false, name: nil, move: nil, duplicate: nil, **features)
  root =
    case root_note_or_pitch_or_symbol
    when Scales::NoteInScale
      root_note_or_pitch_or_symbol
    when Numeric
      if scale
        scale.note_of_pitch(root_note_or_pitch_or_symbol, allow_chromatic: allow_chromatic)
      else
        # A major scale rooted on the pitch itself, which is the documented
        # fallback. The scale kind comes first and the pitch roots it --
        # `tuning.major[pitch]`, not `tuning[pitch].major`, which asks the
        # tuning for a scale kind called 60.
        #
        # allow_chromatic has nothing to decide here: the pitch is this
        # scale's own tonic, so it is always in it.
        scale = Musa::Scales::Scales.default_system.default_tuning.major[root_note_or_pitch_or_symbol]
        scale.note_of_pitch(root_note_or_pitch_or_symbol)
      end
    when Symbol
      raise ArgumentError, "Missing scale parameter to calculate root note for #{root_note_or_pitch_or_symbol}" unless scale

      scale[root_note_or_pitch_or_symbol]
    else
      raise ArgumentError, "Unexpected #{root_note_or_pitch_or_symbol}"
    end

  scale ||= root.scale

  if name
    raise ArgumentError, "Received name parameter with value #{name}: features parameter is not allowed" if features.any?

    chord_definition = ChordDefinition[name]

  elsif features.any?
    chord_definition = Helper.find_definition_by_features(root.pitch, features, scale, allow_chromatic: allow_chromatic)

  else
    raise ArgumentError, "Don't know how to find a chord definition without name or features parameters"
  end

  unless chord_definition
    raise ArgumentError,
          "Unable to find chord definition for root #{root}" \
          "#{" with name #{name}" if name}" \
          "#{" with features #{features}" if features.any?}"
  end

  source_notes_map = Helper.compute_source_notes_map(root, chord_definition, scale)

  Chord.new(root, scale, chord_definition, move, duplicate, source_notes_map)
end

Instance Method Details

#==(other) ⇒ Boolean

Checks chord equality.

Chords are equal if they have the same notes and chord definition.

Parameters:

  • other (Chord)

    chord to compare

Returns:

  • (Boolean)

    true if chords are equal



575
576
577
578
579
# File 'lib/musa-dsl/music/chords.rb', line 575

def ==(other)
  self.class == other.class &&
    @sorted_notes == other.notes &&
    @chord_definition == other.chord_definition
end

#as_chord_in_scale(scale) ⇒ Chord?

Creates an equivalent chord with the given scale as its context.

Returns a new Chord object representing the same chord but with the specified scale as its harmonic context. Returns nil if the chord is not contained in the scale.

Examples:

c_major = Scales.et12[440.0].major[60]
g7 = c_major.dominant.chord :seventh

g_mixolydian = Scales.et12[440.0].mixolydian[67]
g7_in_mixolydian = g7.as_chord_in_scale(g_mixolydian)
g7_in_mixolydian.scale  # => G Mixolydian scale

Parameters:

Returns:

  • (Chord, nil)

    new chord with target scale, or nil if not contained

See Also:



554
555
556
557
558
559
560
561
562
563
564
565
566
567
# File 'lib/musa-dsl/music/chords.rb', line 554

def as_chord_in_scale(scale)
  return nil unless scale.contains_chord?(self)

  root_note = scale.note_of_pitch(@root.pitch, allow_chromatic: false)
  return nil unless root_note

  Chord.with_root(
    root_note,
    scale: scale,
    name: @chord_definition.name,
    move: @move.empty? ? nil : @move,
    duplicate: @duplicate.empty? ? nil : @duplicate
  )
end

#featuresHash{Symbol => Symbol}

Returns chord features.

Examples:

c_major.tonic.chord.features  # => { quality: :major, size: :triad }

Returns:

  • (Hash{Symbol => Symbol})

    features hash (quality:, size:, etc.)



394
395
396
# File 'lib/musa-dsl/music/chords.rb', line 394

def features
  @chord_definition.features
end

#featuring(*values, allow_chromatic: false, **hash) ⇒ Chord

Creates new chord with modified features.

Returns a new chord with the same root but different features. Features can be specified as values (converted to feature hash) or as keyword arguments.

Examples:

Change size

c_triad = c_major.tonic.chord
c_triad.featuring(size: :seventh).features  # => { quality: :major, size: :seventh }

Change quality

# A C minor triad needs an Eb, which C major does not have, so the
# change has to be allowed to leave the scale.
c_triad.featuring(quality: :minor, allow_chromatic: true).features
# => { quality: :minor, size: :triad }

Change multiple features

c_major.dominant.chord.featuring(quality: :dominant, size: :ninth).features
# => { quality: :dominant, size: :ninth }

Parameters:

  • values (Array<Symbol>)

    feature values to change

  • allow_chromatic (Boolean) (defaults to: false)

    allow non-diatonic result

  • hash (Hash)

    feature key-value pairs to change

Returns:

  • (Chord)

    new chord with modified features

Raises:

  • (ArgumentError)

    if no matching chord definition found



423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
# File 'lib/musa-dsl/music/chords.rb', line 423

def featuring(*values, allow_chromatic: false, **hash)
  # create a new list of features based on current features but
  # replacing the values for the new ones and adding the new features
  #
  features = @chord_definition.features.dup
  ChordDefinition.features_from(values, hash).each { |k, v| features[k] = v }

  chord_definition = Helper.find_definition_by_features(@root.pitch, features, @scale, allow_chromatic: allow_chromatic)

  raise ArgumentError, "Unable to find a chord definition for #{features}" unless chord_definition

  source_notes_map = Helper.compute_source_notes_map(@root, chord_definition, @scale)

  Chord.new(@root,
            (@scale if chord_definition.in_scale?(@scale, chord_root_pitch: @root.pitch)),
            chord_definition,
            @move, @duplicate,
            source_notes_map)
end

#inspectString Also known as: to_s

Returns string representation.

Returns:



584
585
586
# File 'lib/musa-dsl/music/chords.rb', line 584

def inspect
  "<Chord #{@name} root #{@root} notes #{@sorted_notes.collect { |_| "#{_.grade}=#{_.note.grade}|#{_.note.pitch} "} }>"
end

#notesArray<ChordGradeNote>

Returns chord notes sorted by pitch.

Examples:

chord.notes.each do |chord_grade_note|
  puts "#{chord_grade_note.grade}: #{chord_grade_note.note.pitch}"
end

Returns:

  • (Array<ChordGradeNote>)

    sorted array of grade-note pairs



365
366
367
# File 'lib/musa-dsl/music/chords.rb', line 365

def notes
  @sorted_notes
end

#octave(octave) ⇒ Chord

Transposes entire chord to a different octave.

Moves all chord notes by the specified octave offset, preserving internal voicing structure (moves and duplications).

Examples:

Move chord down one octave

chord.octave(-1)

Move chord up two octaves

chord.octave(2)

Parameters:

  • octave (Integer)

    octave offset (positive = up, negative = down)

Returns:

  • (Chord)

    new chord in different octave



456
457
458
459
460
461
462
# File 'lib/musa-dsl/music/chords.rb', line 456

def octave(octave)
  source_notes_map = @source_notes_map.transform_values do |notes|
    notes.collect { |note| note.at_octave(octave) }.freeze
  end.freeze

  Chord.new(@root.at_octave(octave), @scale, chord_definition, @move, @duplicate, source_notes_map)
end

#pitches(*grades) ⇒ Array<Integer>

Returns MIDI pitches of chord notes.

Without arguments, returns all pitches sorted from low to high. With grade arguments, returns only pitches for those positions.

Examples:

All pitches

c_triad = c_major.tonic.chord
c_triad.pitches  # => [60, 64, 67]

Specific positions

c_triad.pitches(:root, :third)  # => [60, 64]

Parameters:

  • grades (Array<Symbol>)

    optional position names to filter

Returns:

  • (Array<Integer>)

    MIDI pitches sorted by pitch



383
384
385
386
# File 'lib/musa-dsl/music/chords.rb', line 383

def pitches(*grades)
  grades = @notes_map.keys if grades.empty?
  @sorted_notes.select { |_| grades.include?(_.grade) }.collect { |_| _.note.pitch }
end

#search_in_scales(roots: nil, **metadata) ⇒ Array<Chord>

Finds this chord in other scales.

Searches through scale kinds matching the given metadata criteria to find all scales that contain this chord. Returns new chord instances, each with its containing scale as context.

Examples:

Find G major triad in diatonic scales

g_triad = c_major.dominant.chord
g_triad.search_in_scales(family: :diatonic)

Find chord in scales with specific brightness

g7 = c_major.dominant.chord(:seventh)
g7.search_in_scales(brightness: -1..1)

Iterate over results

g7.search_in_scales(family: :greek_modes).each do |chord|
  scale = chord.scale
  degree = scale.degree_of_chord(chord)
  puts "#{scale.kind.class.id} on #{scale.root_pitch}: degree #{degree}"
end

Parameters:

  • roots (Range, Array, nil) (defaults to: nil)

    pitch offsets to search (default: full octave)

  • metadata (Hash)

    metadata filters for scale kinds (family:, brightness:, etc.)

Returns:

  • (Array<Chord>)

    this chord in different scale contexts

See Also:



530
531
532
533
# File 'lib/musa-dsl/music/chords.rb', line 530

def search_in_scales(roots: nil, **)
  tuning = @scale&.kind&.tuning || @root.scale.kind.tuning
  tuning.search_chord_in_scales(self, roots: roots, **)
end

#with_duplicate(**octaves) ⇒ Chord

Creates new chord with positions duplicated in other octaves.

Adds copies of specific chord positions in different octaves. Original positions remain at their current octave. Merges with existing duplications.

Examples:

Duplicate root two octaves down

chord.with_duplicate(root: -2)

Duplicate third in multiple octaves

chord.with_duplicate(third: [-1, 1])

Duplicate multiple positions

chord.with_duplicate(root: -1, fifth: 1)

Parameters:

  • octaves (Hash{Symbol => Integer, Array<Integer>})

    position to octave(s)

Returns:

  • (Chord)

    new chord with duplicated positions



499
500
501
# File 'lib/musa-dsl/music/chords.rb', line 499

def with_duplicate(**octaves)
  Chord.new(@root, @scale, @chord_definition, @move, @duplicate.merge(octaves), @source_notes_map)
end

#with_move(**octaves) ⇒ Chord

Creates new chord with positions moved to different octaves.

Relocates specific chord positions to different octaves while keeping other positions unchanged. Multiple positions can be moved at once. Merges with existing moves.

Examples:

Move root down, seventh up

chord.with_move(root: -1, seventh: 1)

Drop voicing (move third and seventh down)

chord.with_move(third: -1, seventh: -1)

Parameters:

  • octaves (Hash{Symbol => Integer})

    position to octave offset mapping

Returns:

  • (Chord)

    new chord with moved positions



478
479
480
# File 'lib/musa-dsl/music/chords.rb', line 478

def with_move(**octaves)
  Chord.new(@root, @scale, @chord_definition, @move.merge(octaves), @duplicate, @source_notes_map)
end