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

Inherits:
HeadMusic::Style::Guideline 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).

Defined Under Namespace

Classes: TrendWalk

Constant Summary collapse

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

a trend reversal must exceed a whole step

3

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

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

Class Method Details

.normalized_contour(contour_key) ⇒ Object



18
19
20
21
22
23
24
25
# File 'lib/head_music/style/guidelines/contoured.rb', line 18

def self.normalized_contour(contour_key)
  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

  contour
end

.template_values(config) ⇒ Object



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

def self.template_values(config)
  {contour: HeadMusic::Style::Template.render("contours.#{normalized_contour(config.fetch(:contour))}")}
end

.with(contour_key, **options) ⇒ Object



14
15
16
# File 'lib/head_music/style/guidelines/contoured.rb', line 14

def self.with(contour_key, **options)
  super(contour: normalized_contour(contour_key), **options)
end

Instance Method Details

#advance_trend(walk, number) ⇒ Object (private)



101
102
103
104
105
106
107
# File 'lib/head_music/style/guidelines/contoured.rb', line 101

def advance_trend(walk, number)
  if walk.direction.nil?
    seek_trend(walk, number)
  else
    continue_trend(walk, number)
  end
end

#arch?Boolean (private)

The climax is the maximum by definition, so “rise then fall” reduces to both endpoints sitting below it. Uniqueness and consonance are ConsonantClimax’s.

Returns:

  • (Boolean)


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

def arch?
  endpoints_interior_to?(highest_pitch)
end

#ascending?Boolean (private)

Returns:

  • (Boolean)


48
49
50
# File 'lib/head_music/style/guidelines/contoured.rb', line 48

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

#continue_trend(walk, number) ⇒ Object (private)



120
121
122
123
124
125
126
127
128
# File 'lib/head_music/style/guidelines/contoured.rb', line 120

def continue_trend(walk, number)
  sign = (walk.direction == :ascending) ? 1 : -1
  delta = number - walk.extreme
  if sign * delta > 0
    walk.extreme = number
  elsif -sign * delta >= TREND_REVERSAL_SEMITONES
    start_trend(walk, opposite_direction(walk.direction), number)
  end
end

#contourObject (private)

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



40
41
42
# File 'lib/head_music/style/guidelines/contoured.rb', line 40

def contour
  @contour ||= self.class.normalized_contour(options.fetch(:contour))
end

#descending?Boolean (private)

Returns:

  • (Boolean)


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

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

#directional_endpoints?Boolean (private)

The range guard is load-bearing: an all-same-pitch melody is simultaneously ascending and descending, and would otherwise fail static.

Returns:

  • (Boolean)


82
83
84
# File 'lib/head_music/style/guidelines/contoured.rb', line 82

def directional_endpoints?
  highest_pitch > lowest_pitch && (ascending? || descending?)
end

#endpoints_interior_to?(extreme_pitch) ⇒ Boolean (private)

The pitch is a running extreme, so no endpoint can pass it and “interior” means neither endpoint touches it.

Returns:

  • (Boolean)


68
69
70
# File 'lib/head_music/style/guidelines/contoured.rb', line 68

def endpoints_interior_to?(extreme_pitch)
  notes.length >= 3 && first_note.pitch != extreme_pitch && last_note.pitch != extreme_pitch
end

#marksObject



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

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)


44
45
46
# File 'lib/head_music/style/guidelines/contoured.rb', line 44

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

#opposite_direction(direction) ⇒ Object (private)



136
137
138
# File 'lib/head_music/style/guidelines/contoured.rb', line 136

def opposite_direction(direction)
  (direction == :ascending) ? :descending : :ascending
end

#pitch_numbersObject (private)



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

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

#seek_trend(walk, number) ⇒ Object (private)



109
110
111
112
113
114
115
116
117
118
# File 'lib/head_music/style/guidelines/contoured.rb', line 109

def seek_trend(walk, number)
  if number - walk.low >= TREND_REVERSAL_SEMITONES
    start_trend(walk, :ascending, number)
  elsif walk.high - number >= TREND_REVERSAL_SEMITONES
    start_trend(walk, :descending, number)
  else
    walk.high = [walk.high, number].max
    walk.low = [walk.low, number].min
  end
end

#start_trend(walk, direction, number) ⇒ Object (private)



130
131
132
133
134
# File 'lib/head_music/style/guidelines/contoured.rb', line 130

def start_trend(walk, direction, number)
  walk.direction = direction
  walk.extreme = number
  walk.directions << direction
end

#static?Boolean (private)

Returns:

  • (Boolean)


76
77
78
# File 'lib/head_music/style/guidelines/contoured.rb', line 76

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

#trend_directionsObject (private)

A reversal counts only once the melody retraces TREND_REVERSAL_SEMITONES from the running extreme, so neighbor-note undulation is not a trend change.



92
93
94
95
96
97
98
99
# File 'lib/head_music/style/guidelines/contoured.rb', line 92

def trend_directions
  @trend_directions ||= begin
    first = pitch_numbers.first
    walk = TrendWalk.new([], nil, first, first, nil)
    pitch_numbers.drop(1).each { |number| advance_trend(walk, number) }
    walk.directions
  end
end

#valley?Boolean (private)

Returns:

  • (Boolean)


62
63
64
# File 'lib/head_music/style/guidelines/contoured.rb', line 62

def valley?
  endpoints_interior_to?(lowest_pitch)
end

#wave?Boolean (private)

Returns:

  • (Boolean)


72
73
74
# File 'lib/head_music/style/guidelines/contoured.rb', line 72

def wave?
  trend_directions.length >= 3
end