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
-
#beam_break_before ⇒ Object
Authored beam grouping relative to the previous placement, set after construction (the Bar-style side-metadata pattern).
-
#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.
20 21 22 |
# File 'lib/head_music/content/placement.rb', line 20 def initialize(voice, position, rhythmic_value, sound_or_sounds = nil) ensure_attributes(voice, position, rhythmic_value, sound_or_sounds) end |
Instance Attribute Details
#beam_break_before ⇒ Object
Authored beam grouping relative to the previous placement, set after construction (the Bar-style side-metadata pattern). Tri-state: nil = use the meter-derived default, true = force a beam break before this placement, false = force a beam join to the previous placement. Consumed by the MusicXML writer, which prefers it over the default grouping.
15 16 17 |
# File 'lib/head_music/content/placement.rb', line 15 def beam_break_before @beam_break_before end |
#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
82 83 84 |
# File 'lib/head_music/content/placement.rb', line 82 def <=>(other) position <=> other.position end |
#chord? ⇒ Boolean
57 58 59 |
# File 'lib/head_music/content/placement.rb', line 57 def chord? pitches.length > 1 end |
#during?(other_placement) ⇒ Boolean
86 87 88 |
# File 'lib/head_music/content/placement.rb', line 86 def during?(other_placement) starts_during?(other_placement) || ends_during?(other_placement) || wraps?(other_placement) end |
#ends_during?(other_placement) ⇒ Boolean (private)
120 121 122 |
# File 'lib/head_music/content/placement.rb', line 120 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)
128 129 130 131 132 133 |
# File 'lib/head_music/content/placement.rb', line 128 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)
175 176 177 178 179 180 181 |
# File 'lib/head_music/content/placement.rb', line 175 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)
135 136 137 138 139 140 |
# File 'lib/head_music/content/placement.rb', line 135 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.
68 69 70 71 72 73 74 75 76 |
# File 'lib/head_music/content/placement.rb', line 68 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
78 79 80 |
# File 'lib/head_music/content/placement.rb', line 78 def next_position @next_position ||= position + rhythmic_value end |
#note? ⇒ Boolean
45 46 47 |
# File 'lib/head_music/content/placement.rb', line 45 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).
33 34 35 |
# File 'lib/head_music/content/placement.rb', line 33 def pitch pitches.max end |
#pitched? ⇒ Boolean
61 62 63 |
# File 'lib/head_music/content/placement.rb', line 61 def pitched? sounds.any?(&:pitched?) end |
#pitched_note? ⇒ Boolean
49 50 51 |
# File 'lib/head_music/content/placement.rb', line 49 def pitched_note? note? && pitched? end |
#pitches ⇒ Object
24 25 26 |
# File 'lib/head_music/content/placement.rb', line 24 def pitches sounds.select(&:pitched?) end |
#resolve_sound(value) ⇒ Object (private)
142 143 144 145 146 147 148 149 150 |
# File 'lib/head_music/content/placement.rb', line 142 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
37 38 39 |
# File 'lib/head_music/content/placement.rb', line 37 def rest? sounds.empty? end |
#sound_datum(sound) ⇒ Object (private)
112 113 114 |
# File 'lib/head_music/content/placement.rb', line 112 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.
108 109 110 |
# File 'lib/head_music/content/placement.rb', line 108 def sound_label(sound) sound.pitched? ? sound.to_s : "[#{sound}]" end |
#sounded? ⇒ Boolean
41 42 43 |
# File 'lib/head_music/content/placement.rb', line 41 def sounded? sounds.any? end |
#starts_during?(other_placement) ⇒ Boolean (private)
116 117 118 |
# File 'lib/head_music/content/placement.rb', line 116 def starts_during?(other_placement) position >= other_placement.position && position < other_placement.next_position end |
#to_h ⇒ Object
94 95 96 97 98 99 100 101 102 |
# File 'lib/head_music/content/placement.rb', line 94 def to_h hash = { "position" => position.to_s, "rhythmic_value" => rhythmic_value.to_s, "sounds" => sounds.map { |sound| sound_datum(sound) } } hash["beam_break_before"] = beam_break_before unless beam_break_before.nil? hash end |
#to_s ⇒ Object
90 91 92 |
# File 'lib/head_music/content/placement.rb', line 90 def to_s "#{rhythmic_value} #{sounds.any? ? sounds.map { |sound| sound_label(sound) }.join(" ") : "rest"} at #{position}" end |
#unknown_sound_message(value) ⇒ Object (private)
166 167 168 169 170 171 172 173 |
# File 'lib/head_music/content/placement.rb', line 166 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
53 54 55 |
# File 'lib/head_music/content/placement.rb', line 53 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.
156 157 158 159 160 161 162 163 164 |
# File 'lib/head_music/content/placement.rb', line 156 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)
124 125 126 |
# File 'lib/head_music/content/placement.rb', line 124 def wraps?(other_placement) position <= other_placement.position && next_position >= other_placement.next_position end |