Class: HeadMusic::Analysis::Dyad

Inherits:
Object
  • Object
show all
Defined in:
lib/head_music/analysis/dyad.rb

Overview

A Dyad is a two-pitch combination that can imply various chords. It analyzes the harmonic implications of two pitches sounding together.

Constant Summary collapse

TRICHORD_INTERVALS =
[
  %w[M3 P5],   # major triad
  %w[m3 P5],   # minor triad
  %w[m3 d5],   # diminished triad
  %w[M3 A5],   # augmented triad
  %w[P4 P5],   # sus4 (not a triad)
  %w[M2 P5]    # sus2 (not a triad)
].freeze
SEVENTH_CHORD_INTERVALS =
[
  %w[M3 P5 M7],   # major seventh
  %w[M3 P5 m7],   # dominant seventh (major-minor)
  %w[m3 P5 m7],   # minor seventh
  %w[m3 P5 M7],   # minor-major seventh
  %w[m3 d5 m7],   # half-diminished seventh
  %w[m3 d5 d7],   # diminished seventh
  %w[M2 M3 P5 m7], # dominant ninth
  %w[m2 M3 P5 m7], # dominant minor ninth
  %w[M2 m3 P5 m7], # minor ninth
  %w[M2 M3 P5 M7]  # major ninth
].freeze
ALTERATION_SIGNS =
{-2 => "bb", -1 => "b", 0 => "", 1 => "#", 2 => "##"}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(pitch1, pitch2, key: nil) ⇒ Dyad

Returns a new instance of Dyad.



9
10
11
12
13
14
15
# File 'lib/head_music/analysis/dyad.rb', line 9

def initialize(pitch1, pitch2, key: nil)
  @pitch1, @pitch2 = [
    HeadMusic::Rudiment::Pitch.get(pitch1),
    HeadMusic::Rudiment::Pitch.get(pitch2)
  ].sort
  @key = key ? HeadMusic::Rudiment::Key.get(key) : nil
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_name, *args, &block) ⇒ Object



61
62
63
# File 'lib/head_music/analysis/dyad.rb', line 61

def method_missing(method_name, *args, &block)
  respond_to_missing?(method_name) ? interval.send(method_name, *args, &block) : super
end

Instance Attribute Details

#keyObject (readonly)

Returns the value of attribute key.



7
8
9
# File 'lib/head_music/analysis/dyad.rb', line 7

def key
  @key
end

#pitch1Object (readonly)

Returns the value of attribute pitch1.



7
8
9
# File 'lib/head_music/analysis/dyad.rb', line 7

def pitch1
  @pitch1
end

#pitch2Object (readonly)

Returns the value of attribute pitch2.



7
8
9
# File 'lib/head_music/analysis/dyad.rb', line 7

def pitch2
  @pitch2
end

Instance Method Details

#diatonic_spellingsObject (private)



137
138
139
# File 'lib/head_music/analysis/dyad.rb', line 137

def diatonic_spellings
  @diatonic_spellings ||= key.scale.spellings
end

#enharmonic_equivalents_for(pitch) ⇒ Object (private)



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/head_music/analysis/dyad.rb', line 162

def enharmonic_equivalents_for(pitch)
  target_pitch_class = pitch.pitch_class
  equivalents = [pitch]

  HeadMusic::Rudiment::LetterName.all.each do |letter_name|
    ALTERATION_SIGNS.each_value do |sign|
      spelling = HeadMusic::Rudiment::Spelling.get("#{letter_name}#{sign}")
      next unless spelling && spelling.pitch_class == target_pitch_class
      next if equivalents.any? { |equiv| equiv.spelling == spelling }

      equivalents << HeadMusic::Rudiment::Pitch.fetch_or_create(spelling, pitch.register)
    end
  end

  equivalents
end

#enharmonic_respellingsObject



53
54
55
# File 'lib/head_music/analysis/dyad.rb', line 53

def enharmonic_respellings
  @enharmonic_respellings ||= generate_enharmonic_respellings
end

#filter_by_key(pitch_collections) ⇒ Object (private)



121
122
123
124
125
126
127
# File 'lib/head_music/analysis/dyad.rb', line 121

def filter_by_key(pitch_collections)
  return pitch_collections unless key

  pitch_collections.select do |pitch_collection|
    pitch_collection.pitches.all? { |pitch| diatonic_spellings.include?(pitch.spelling) }
  end
end

#generate_enharmonic_respellingsObject (private)



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/head_music/analysis/dyad.rb', line 141

def generate_enharmonic_respellings
  respellings = []

  # Get enharmonic equivalents for each pitch
  pitch1_equivalents = enharmonic_equivalents_for(pitch1)
  pitch2_equivalents = enharmonic_equivalents_for(pitch2)

  # Generate all combinations
  pitch1_equivalents.each do |lower|
    pitch2_equivalents.each do |upper|
      next if lower.spelling == pitch1.spelling && upper.spelling == pitch2.spelling

      respellings << self.class.new(lower, upper, key: key)
    end
  end

  respellings
end

#generate_possible_chords(interval_sets) ⇒ Object (private)



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/head_music/analysis/dyad.rb', line 101

def generate_possible_chords(interval_sets)
  dyad_pitch_classes = [lower_pitch.pitch_class, upper_pitch.pitch_class]
  chords = []

  HeadMusic::Rudiment::Spelling::CHROMATIC_SPELLINGS.each do |root_spelling|
    root_pitch = HeadMusic::Rudiment::Pitch.get("#{root_spelling}4")

    interval_sets.each do |intervals|
      chord_pitches = [root_pitch] + intervals.map { |name| HeadMusic::Analysis::DiatonicInterval.get(name).above(root_pitch) }
      pitch_collection = HeadMusic::Analysis::PitchCollection.new(chord_pitches)

      if dyad_pitch_classes.all? { |pc| pitch_collection.pitch_classes.include?(pc) }
        chords << pitch_collection
      end
    end
  end

  chords.uniq { |chord| chord.pitch_classes.sort.map(&:to_i) }
end

#generate_possible_seventh_chordsObject (private)



97
98
99
# File 'lib/head_music/analysis/dyad.rb', line 97

def generate_possible_seventh_chords
  generate_possible_chords(SEVENTH_CHORD_INTERVALS)
end

#generate_possible_trichordsObject (private)



93
94
95
# File 'lib/head_music/analysis/dyad.rb', line 93

def generate_possible_trichords
  generate_possible_chords(TRICHORD_INTERVALS)
end

#intervalObject



17
18
19
# File 'lib/head_music/analysis/dyad.rb', line 17

def interval
  @interval ||= HeadMusic::Analysis::DiatonicInterval.new(lower_pitch, upper_pitch)
end

#lower_pitchObject



25
26
27
# File 'lib/head_music/analysis/dyad.rb', line 25

def lower_pitch
  @lower_pitch ||= [pitch1, pitch2].min
end

#pitchesObject



21
22
23
# File 'lib/head_music/analysis/dyad.rb', line 21

def pitches
  [pitch1, pitch2]
end

#possible_seventh_chordsObject



45
46
47
48
49
50
51
# File 'lib/head_music/analysis/dyad.rb', line 45

def possible_seventh_chords
  @possible_seventh_chords ||= begin
    seventh_chords = generate_possible_seventh_chords
    seventh_chords = filter_by_key(seventh_chords) if key
    sort_by_diatonic_agreement(seventh_chords)
  end
end

#possible_triadsObject



41
42
43
# File 'lib/head_music/analysis/dyad.rb', line 41

def possible_triads
  @possible_triads ||= possible_trichords.select(&:triad?)
end

#possible_trichordsObject



33
34
35
36
37
38
39
# File 'lib/head_music/analysis/dyad.rb', line 33

def possible_trichords
  @possible_trichords ||= begin
    trichords = generate_possible_trichords
    trichords = filter_by_key(trichords) if key
    sort_by_diatonic_agreement(trichords)
  end
end

#respond_to_missing?(method_name, *_args) ⇒ Boolean

Returns:

  • (Boolean)


65
66
67
# File 'lib/head_music/analysis/dyad.rb', line 65

def respond_to_missing?(method_name, *_args)
  interval.respond_to?(method_name)
end

#sort_by_diatonic_agreement(pitch_collections) ⇒ Object (private)



129
130
131
132
133
134
135
# File 'lib/head_music/analysis/dyad.rb', line 129

def sort_by_diatonic_agreement(pitch_collections)
  return pitch_collections unless key

  pitch_collections.sort_by do |pitch_collection|
    -pitch_collection.pitches.count { |pitch| diatonic_spellings.include?(pitch.spelling) }
  end
end

#to_sObject



57
58
59
# File 'lib/head_music/analysis/dyad.rb', line 57

def to_s
  "#{pitch1} - #{pitch2}"
end

#upper_pitchObject



29
30
31
# File 'lib/head_music/analysis/dyad.rb', line 29

def upper_pitch
  @upper_pitch ||= [pitch1, pitch2].max
end