Class: HeadMusic::Style::Guidelines::Contoured
- Inherits:
-
Annotation
- Object
- Annotation
- HeadMusic::Style::Guidelines::Contoured
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
Class Method Details
.default_weight ⇒ Object
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
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.
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
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
|
#contour ⇒ Object
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
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
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.
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
|
#matches_contour? ⇒ Boolean
51
52
53
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 51
def matches_contour?
send("#{contour}?")
end
|
#message ⇒ Object
32
33
34
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 32
def message
"Write a melody with the #{contour} contour."
end
|
#pitch_numbers ⇒ Object
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
|
#trend_directions ⇒ Object
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 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
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
74
75
76
|
# File 'lib/head_music/style/guidelines/contoured.rb', line 74
def wave?
trend_directions.length >= 3
end
|