Class: HeadMusic::Content::Placement
- Inherits:
-
Object
- Object
- HeadMusic::Content::Placement
- Includes:
- Comparable
- Defined in:
- lib/head_music/content/placement.rb
Overview
A placement is a note, chord, or rest at a position within a voice in a composition
Instance Attribute Summary collapse
-
#position ⇒ Object
readonly
Returns the value of attribute position.
-
#rhythmic_value ⇒ Object
readonly
Returns the value of attribute rhythmic_value.
-
#sounds ⇒ Object
readonly
Returns the value of attribute sounds.
-
#voice ⇒ Object
readonly
Returns the value of attribute voice.
Instance Method Summary collapse
-
#<=>(other) ⇒ Object
-
#chord? ⇒ Boolean
-
#during?(other_placement) ⇒ Boolean
-
#ends_during?(other_placement) ⇒ Boolean
private
-
#ensure_attributes(voice, position, rhythmic_value, sound_or_sounds) ⇒ Object
private
-
#ensure_position(position) ⇒ Object
private
-
#ensure_sounds(sound_or_sounds) ⇒ Object
private
-
#initialize(voice, position, rhythmic_value, sound_or_sounds = nil) ⇒ Placement
constructor
A new instance of Placement.
-
#merge(other) ⇒ Object
Voice#place merges a same-position placement into the existing one, so a position holds at most one placement.
-
#next_position ⇒ Object
-
#note? ⇒ Boolean
-
#pitch ⇒ Object
The top pitch of a chord (or the only pitch of a note), which melodic analysis treats as the melody note.
-
#pitched? ⇒ Boolean
-
#pitched_note? ⇒ Boolean
-
#pitches ⇒ Object
-
#resolve_sound(value) ⇒ Object
private
-
#rest? ⇒ Boolean
-
#sound_datum(sound) ⇒ Object
private
-
#sound_label(sound) ⇒ Object
private
Unpitched names may be multi-word, so they are bracketed to keep the space-delimited sound list unambiguous.
-
#sounded? ⇒ Boolean
-
#starts_during?(other_placement) ⇒ Boolean
private
-
#to_h ⇒ Object
-
#to_s ⇒ Object
-
#unknown_sound_message(value) ⇒ Object
private
-
#unpitched_note? ⇒ Boolean
-
#unpitched_sound(value) ⇒ Object
private
A bare name resolves to a percussive hit only on an unpitched instrument; naming a pitched instrument is ambiguous, so it raises instead.
-
#wraps?(other_placement) ⇒ Boolean
private
Constructor Details
#initialize(voice, position, rhythmic_value, sound_or_sounds = nil) ⇒ Placement
Returns a new instance of Placement.
13 14 15 |
# File 'lib/head_music/content/placement.rb', line 13 def initialize(voice, position, rhythmic_value, sound_or_sounds = nil) ensure_attributes(voice, position, rhythmic_value, sound_or_sounds) end |
Instance Attribute Details
#position ⇒ Object (readonly)
Returns the value of attribute position.
8 9 10 |
# File 'lib/head_music/content/placement.rb', line 8 def position @position end |
#rhythmic_value ⇒ Object (readonly)
Returns the value of attribute rhythmic_value.
8 9 10 |
# File 'lib/head_music/content/placement.rb', line 8 def rhythmic_value @rhythmic_value end |
#sounds ⇒ Object (readonly)
Returns the value of attribute sounds.
8 9 10 |
# File 'lib/head_music/content/placement.rb', line 8 def sounds @sounds end |
#voice ⇒ Object (readonly)
Returns the value of attribute voice.
8 9 10 |
# File 'lib/head_music/content/placement.rb', line 8 def voice @voice end |
Instance Method Details
#<=>(other) ⇒ Object
75 76 77 |
# File 'lib/head_music/content/placement.rb', line 75 def <=>(other) position <=> other.position end |
#chord? ⇒ Boolean
50 51 52 |
# File 'lib/head_music/content/placement.rb', line 50 def chord? pitches.length > 1 end |
#during?(other_placement) ⇒ Boolean
79 80 81 |
# File 'lib/head_music/content/placement.rb', line 79 def during?(other_placement) starts_during?(other_placement) || ends_during?(other_placement) || wraps?(other_placement) end |
#ends_during?(other_placement) ⇒ Boolean (private)
111 112 113 |
# File 'lib/head_music/content/placement.rb', line 111 def ends_during?(other_placement) next_position > other_placement.position && next_position <= other_placement.next_position end |
#ensure_attributes(voice, position, rhythmic_value, sound_or_sounds) ⇒ Object (private)
119 120 121 122 123 124 |
# File 'lib/head_music/content/placement.rb', line 119 def ensure_attributes(voice, position, rhythmic_value, sound_or_sounds) @voice = voice ensure_position(position) @rhythmic_value = HeadMusic::Rudiment::RhythmicValue.get(rhythmic_value) @sounds = ensure_sounds(sound_or_sounds) end |
#ensure_position(position) ⇒ Object (private)
166 167 168 169 170 171 172 |
# File 'lib/head_music/content/placement.rb', line 166 def ensure_position(position) @position = if position.is_a?(HeadMusic::Content::Position) position else HeadMusic::Content::Position.new(composition, position) end end |
#ensure_sounds(sound_or_sounds) ⇒ Object (private)
126 127 128 129 130 131 |
# File 'lib/head_music/content/placement.rb', line 126 def ensure_sounds(sound_or_sounds) return [].freeze if sound_or_sounds.nil? values = sound_or_sounds.is_a?(Array) ? sound_or_sounds : [sound_or_sounds] values.map { |value| resolve_sound(value) }.uniq.freeze end |
#merge(other) ⇒ Object
Voice#place merges a same-position placement into the existing one, so a position holds at most one placement. The sound union keeps the chord free of duplicates, making repeated placement of a sound idempotent.
61 62 63 64 65 66 67 68 69 |
# File 'lib/head_music/content/placement.rb', line 61 def merge(other) unless rhythmic_value == other.rhythmic_value raise ArgumentError, "cannot place a #{other.rhythmic_value} at #{position}: position occupied by a #{rhythmic_value}" end @sounds = (sounds + other.sounds).uniq.freeze self end |
#next_position ⇒ Object
71 72 73 |
# File 'lib/head_music/content/placement.rb', line 71 def next_position @next_position ||= position + rhythmic_value end |
#note? ⇒ Boolean
38 39 40 |
# File 'lib/head_music/content/placement.rb', line 38 def note? sounds.length == 1 end |
#pitch ⇒ Object
The top pitch of a chord (or the only pitch of a note), which melodic analysis treats as the melody note. Returns nil for rests and unpitched-only placements; pitched? is the guard. Enharmonic ties resolve to the first-listed pitch (MRI’s max keeps the earliest of equals; a spec pins the behavior).
26 27 28 |
# File 'lib/head_music/content/placement.rb', line 26 def pitch pitches.max end |
#pitched? ⇒ Boolean
54 55 56 |
# File 'lib/head_music/content/placement.rb', line 54 def pitched? sounds.any?(&:pitched?) end |
#pitched_note? ⇒ Boolean
42 43 44 |
# File 'lib/head_music/content/placement.rb', line 42 def pitched_note? note? && pitched? end |
#pitches ⇒ Object
17 18 19 |
# File 'lib/head_music/content/placement.rb', line 17 def pitches sounds.select(&:pitched?) end |
#resolve_sound(value) ⇒ Object (private)
133 134 135 136 137 138 139 140 141 |
# File 'lib/head_music/content/placement.rb', line 133 def resolve_sound(value) return value if value.is_a?(HeadMusic::Rudiment::UnpitchedSound) return HeadMusic::Rudiment::UnpitchedSound.get(value) if value.is_a?(HeadMusic::Instruments::Instrument) pitch = HeadMusic::Rudiment::Pitch.get(value) return pitch if pitch unpitched_sound(value) || raise(ArgumentError, (value)) end |
#rest? ⇒ Boolean
30 31 32 |
# File 'lib/head_music/content/placement.rb', line 30 def rest? sounds.empty? end |
#sound_datum(sound) ⇒ Object (private)
103 104 105 |
# File 'lib/head_music/content/placement.rb', line 103 def sound_datum(sound) sound.pitched? ? sound.to_s : {"unpitched" => sound.name_key&.to_s} end |
#sound_label(sound) ⇒ Object (private)
Unpitched names may be multi-word, so they are bracketed to keep the space-delimited sound list unambiguous.
99 100 101 |
# File 'lib/head_music/content/placement.rb', line 99 def sound_label(sound) sound.pitched? ? sound.to_s : "[#{sound}]" end |
#sounded? ⇒ Boolean
34 35 36 |
# File 'lib/head_music/content/placement.rb', line 34 def sounded? sounds.any? end |
#starts_during?(other_placement) ⇒ Boolean (private)
107 108 109 |
# File 'lib/head_music/content/placement.rb', line 107 def starts_during?(other_placement) position >= other_placement.position && position < other_placement.next_position end |
#to_h ⇒ Object
87 88 89 90 91 92 93 |
# File 'lib/head_music/content/placement.rb', line 87 def to_h { "position" => position.to_s, "rhythmic_value" => rhythmic_value.to_s, "sounds" => sounds.map { |sound| sound_datum(sound) } } end |
#to_s ⇒ Object
83 84 85 |
# File 'lib/head_music/content/placement.rb', line 83 def to_s "#{rhythmic_value} #{sounds.any? ? sounds.map { |sound| sound_label(sound) }.join(" ") : "rest"} at #{position}" end |
#unknown_sound_message(value) ⇒ Object (private)
157 158 159 160 161 162 163 164 |
# File 'lib/head_music/content/placement.rb', line 157 def (value) if HeadMusic::Instruments::Instrument.get(value)&.pitched? "#{value.inspect} is a pitched instrument; place a pitch such as \"D4\", " \ "or pass HeadMusic::Rudiment::UnpitchedSound.get(#{value.inspect}) for a percussive hit" else "unknown sound: #{value.inspect}" end end |
#unpitched_note? ⇒ Boolean
46 47 48 |
# File 'lib/head_music/content/placement.rb', line 46 def unpitched_note? note? && !pitched? end |
#unpitched_sound(value) ⇒ Object (private)
A bare name resolves to a percussive hit only on an unpitched instrument; naming a pitched instrument is ambiguous, so it raises instead. UnpitchedSound.get(nil) is the generic sound, so nil is excluded here to preserve nil-as-rest at the argument level and nil-raises inside arrays.
147 148 149 150 151 152 153 154 155 |
# File 'lib/head_music/content/placement.rb', line 147 def unpitched_sound(value) return nil if value.nil? sound = HeadMusic::Rudiment::UnpitchedSound.get(value) return nil unless sound&.instrument return nil if sound.instrument.pitched? sound end |
#wraps?(other_placement) ⇒ Boolean (private)
115 116 117 |
# File 'lib/head_music/content/placement.rb', line 115 def wraps?(other_placement) position <= other_placement.position && next_position >= other_placement.next_position end |