Class: HeadMusic::Style::Guidelines::Contoured

Inherits:
Annotation
  • Object
show all
Defined in:
lib/head_music/style/guidelines/contoured.rb

Overview

Flags a melody without the configured contour Configure the contour with the factory, e.g. Contoured.with(:arch).

Constant Summary collapse

CONTOURS =
%i[ascending descending arch valley wave static].freeze
TREND_REVERSAL_SEMITONES =

a trend reversal must exceed a whole step

3
DEFAULT_WEIGHT =
HeadMusic::GOLDEN_RATIO_INVERSE

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

This class inherits a constructor from HeadMusic::Style::Annotation

Class Method Details

.default_weightObject



22
23
24
# File 'lib/head_music/style/guidelines/contoured.rb', line 22

def self.default_weight
  DEFAULT_WEIGHT
end

.with(contour_key, **options) ⇒ Object



13
14
15
16
17
18
19
20
# File 'lib/head_music/style/guidelines/contoured.rb', line 13

def self.with(contour_key, **options)
  contour = contour_key.to_s.downcase.to_sym
  unless CONTOURS.include?(contour)
    raise ArgumentError, "Contour must be one of: #{CONTOURS.join(", ")} (got #{contour_key.inspect})"
  end

  super(contour: contour, **options)
end

Instance Method Details

#arch?Boolean (private)

The climax is by definition the maximum, so “net rise before, net fall after” is equivalent to both endpoints sitting below the climax pitch. Climax uniqueness and consonance remain ConsonantClimax’s job.

Returns:

  • (Boolean)


66
67
68
# File 'lib/head_music/style/guidelines/contoured.rb', line 66

def arch?
  notes.length >= 3 && first_note.pitch < highest_pitch && last_note.pitch < highest_pitch
end

#ascending?Boolean (private)

Returns:

  • (Boolean)


55
56
57
# File 'lib/head_music/style/guidelines/contoured.rb', line 55

def ascending?
  first_note.pitch == lowest_pitch && last_note.pitch == highest_pitch
end

#contourObject (private)

Validated again here because Contoured.new(voice, contour: :bogus) bypasses .with.



39
40
41
42
43
44
45
46
47
48
49
# File 'lib/head_music/style/guidelines/contoured.rb', line 39

def contour
  @contour ||= begin
    key = options.fetch(:contour)
    contour = key.to_s.downcase.to_sym
    unless CONTOURS.include?(contour)
      raise ArgumentError, "Contour must be one of: #{CONTOURS.join(", ")} (got #{key.inspect})"
    end

    contour
  end
end

#descending?Boolean (private)

Returns:

  • (Boolean)


59
60
61
# File 'lib/head_music/style/guidelines/contoured.rb', line 59

def descending?
  first_note.pitch == highest_pitch && last_note.pitch == lowest_pitch
end

#directional_endpoints?Boolean (private)

The highest_pitch > lowest_pitch guard is load-bearing: without it, an all-same-pitch melody (first == lowest and last == highest simultaneously) would absurdly fail static.

Returns:

  • (Boolean)


85
86
87
88
89
# File 'lib/head_music/style/guidelines/contoured.rb', line 85

def directional_endpoints?
  highest_pitch > lowest_pitch &&
    ((first_note.pitch == lowest_pitch && last_note.pitch == highest_pitch) ||
      (first_note.pitch == highest_pitch && last_note.pitch == lowest_pitch))
end

#marksObject



26
27
28
29
30
# File 'lib/head_music/style/guidelines/contoured.rb', line 26

def marks
  return if notes.empty? || matches_contour?

  HeadMusic::Style::Mark.for_all(notes, fitness: HeadMusic::GOLDEN_RATIO_INVERSE**2)
end

#matches_contour?Boolean (private)

Returns:

  • (Boolean)


51
52
53
# File 'lib/head_music/style/guidelines/contoured.rb', line 51

def matches_contour?
  send("#{contour}?")
end

#messageObject



32
33
34
# File 'lib/head_music/style/guidelines/contoured.rb', line 32

def message
  "Write a melody with the #{contour} contour."
end

#pitch_numbersObject (private)



91
92
93
# File 'lib/head_music/style/guidelines/contoured.rb', line 91

def pitch_numbers
  @pitch_numbers ||= notes.map { |note| note.pitch.midi_note_number }
end

#static?Boolean (private)

Returns:

  • (Boolean)


78
79
80
# File 'lib/head_music/style/guidelines/contoured.rb', line 78

def static?
  range <= HeadMusic::Analysis::DiatonicInterval.get(:major_third) && !directional_endpoints?
end

#trend_directionsObject (private)

Zigzag walk: a trend reversal is confirmed only when the melody retraces at least TREND_REVERSAL_SEMITONES from the running extreme of the current trend, so stepwise neighbor-note undulation never registers as a trend change.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/head_music/style/guidelines/contoured.rb', line 98

def trend_directions
  @trend_directions ||= begin
    directions = []
    direction = nil
    high = low = pitch_numbers.first
    extreme = nil
    pitch_numbers.drop(1).each do |number|
      case direction
      when nil # no trend confirmed yet
        if number - low >= TREND_REVERSAL_SEMITONES
          direction = :ascending
          extreme = number
          directions << direction
        elsif high - number >= TREND_REVERSAL_SEMITONES
          direction = :descending
          extreme = number
          directions << direction
        else
          high = [high, number].max
          low = [low, number].min
        end
      when :ascending
        if number > extreme
          extreme = number
        elsif extreme - number >= TREND_REVERSAL_SEMITONES
          direction = :descending
          extreme = number
          directions << direction
        end
      when :descending
        if number < extreme
          extreme = number
        elsif number - extreme >= TREND_REVERSAL_SEMITONES
          direction = :ascending
          extreme = number
          directions << direction
        end
      end
    end
    directions
  end
end

#valley?Boolean (private)

Returns:

  • (Boolean)


70
71
72
# File 'lib/head_music/style/guidelines/contoured.rb', line 70

def valley?
  notes.length >= 3 && first_note.pitch > lowest_pitch && last_note.pitch > lowest_pitch
end

#wave?Boolean (private)

Returns:

  • (Boolean)


74
75
76
# File 'lib/head_music/style/guidelines/contoured.rb', line 74

def wave?
  trend_directions.length >= 3
end