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
-
#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
-
#rest? ⇒ Boolean
-
#sing(text, verse: 1, hyphen_after: false) ⇒ Object
Assigns the syllable for a verse (default verse 1).
-
#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
-
#sung? ⇒ Boolean
-
#syllable(verse = 1) ⇒ Object
-
#syllables ⇒ Object
Authored sung text: at most one Syllable per verse, keyed by verse number.
-
#to_h ⇒ Object
-
#to_s ⇒ Object
-
#unpitched_note? ⇒ Boolean
-
#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
110 111 112 |
# File 'lib/head_music/content/placement.rb', line 110 def <=>(other) position <=> other.position end |
#chord? ⇒ Boolean
83 84 85 |
# File 'lib/head_music/content/placement.rb', line 83 def chord? pitches.length > 1 end |
#during?(other_placement) ⇒ Boolean
114 115 116 |
# File 'lib/head_music/content/placement.rb', line 114 def during?(other_placement) starts_during?(other_placement) || ends_during?(other_placement) || wraps?(other_placement) end |
#ends_during?(other_placement) ⇒ Boolean (private)
149 150 151 |
# File 'lib/head_music/content/placement.rb', line 149 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)
157 158 159 160 161 162 |
# File 'lib/head_music/content/placement.rb', line 157 def ensure_attributes(voice, position, rhythmic_value, sound_or_sounds) @voice = voice ensure_position(position) @rhythmic_value = HeadMusic::Rudiment::RhythmicValue.get(rhythmic_value) @sounds = HeadMusic::Content::SoundResolver.resolve(sound_or_sounds) end |
#ensure_position(position) ⇒ Object (private)
164 165 166 167 168 169 170 |
# File 'lib/head_music/content/placement.rb', line 164 def ensure_position(position) @position = if position.is_a?(HeadMusic::Content::Position) position else HeadMusic::Content::Position.new(composition, position) end 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. Syllables are left untouched: a chord sings one syllable per verse, and the receiver (the placement already at this position) keeps its own.
96 97 98 99 100 101 102 103 104 |
# File 'lib/head_music/content/placement.rb', line 96 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
106 107 108 |
# File 'lib/head_music/content/placement.rb', line 106 def next_position @next_position ||= position + rhythmic_value end |
#note? ⇒ Boolean
71 72 73 |
# File 'lib/head_music/content/placement.rb', line 71 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).
59 60 61 |
# File 'lib/head_music/content/placement.rb', line 59 def pitch pitches.max end |
#pitched? ⇒ Boolean
87 88 89 |
# File 'lib/head_music/content/placement.rb', line 87 def pitched? sounds.any?(&:pitched?) end |
#pitched_note? ⇒ Boolean
75 76 77 |
# File 'lib/head_music/content/placement.rb', line 75 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 |
#rest? ⇒ Boolean
63 64 65 |
# File 'lib/head_music/content/placement.rb', line 63 def rest? sounds.empty? end |
#sing(text, verse: 1, hyphen_after: false) ⇒ Object
Assigns the syllable for a verse (default verse 1). Returns self so calls chain across verses. Keys by the Syllable’s coerced verse (not the raw argument) so syllable(2) finds what sing(verse: “2”) stored and mixed-type keys never make syllables.keys.sort raise.
40 41 42 43 44 |
# File 'lib/head_music/content/placement.rb', line 40 def sing(text, verse: 1, hyphen_after: false) syllable = HeadMusic::Content::Syllable.new(text, verse: verse, hyphen_after: hyphen_after) syllables[syllable.verse] = syllable self end |
#sound_datum(sound) ⇒ Object (private)
141 142 143 |
# File 'lib/head_music/content/placement.rb', line 141 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.
137 138 139 |
# File 'lib/head_music/content/placement.rb', line 137 def sound_label(sound) sound.pitched? ? sound.to_s : "[#{sound}]" end |
#sounded? ⇒ Boolean
67 68 69 |
# File 'lib/head_music/content/placement.rb', line 67 def sounded? sounds.any? end |
#starts_during?(other_placement) ⇒ Boolean (private)
145 146 147 |
# File 'lib/head_music/content/placement.rb', line 145 def starts_during?(other_placement) position >= other_placement.position && position < other_placement.next_position end |
#sung? ⇒ Boolean
50 51 52 |
# File 'lib/head_music/content/placement.rb', line 50 def sung? syllables.any? end |
#syllable(verse = 1) ⇒ Object
46 47 48 |
# File 'lib/head_music/content/placement.rb', line 46 def syllable(verse = 1) syllables[verse] end |
#syllables ⇒ Object
Authored sung text: at most one Syllable per verse, keyed by verse number.
Empty for un-texted placements and rests. Set after construction, like
beam_break_before. The MusicXML writer derives
32 33 34 |
# File 'lib/head_music/content/placement.rb', line 32 def syllables @syllables ||= {} end |
#to_h ⇒ Object
122 123 124 125 126 127 128 129 130 131 |
# File 'lib/head_music/content/placement.rb', line 122 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["syllables"] = syllables.keys.sort.map { |verse| syllables[verse].to_h } unless syllables.empty? hash end |
#to_s ⇒ Object
118 119 120 |
# File 'lib/head_music/content/placement.rb', line 118 def to_s "#{rhythmic_value} #{sounds.any? ? sounds.map { |sound| sound_label(sound) }.join(" ") : "rest"} at #{position}" end |
#unpitched_note? ⇒ Boolean
79 80 81 |
# File 'lib/head_music/content/placement.rb', line 79 def unpitched_note? note? && !pitched? end |
#wraps?(other_placement) ⇒ Boolean (private)
153 154 155 |
# File 'lib/head_music/content/placement.rb', line 153 def wraps?(other_placement) position <= other_placement.position && next_position >= other_placement.next_position end |