Class: HeadMusic::Content::Placement

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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_beforeObject

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

#positionObject (readonly)

Returns the value of attribute position.



8
9
10
# File 'lib/head_music/content/placement.rb', line 8

def position
  @position
end

#rhythmic_valueObject (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

#soundsObject (readonly)

Returns the value of attribute sounds.



8
9
10
# File 'lib/head_music/content/placement.rb', line 8

def sounds
  @sounds
end

#voiceObject (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

Returns:

  • (Boolean)


83
84
85
# File 'lib/head_music/content/placement.rb', line 83

def chord?
  pitches.length > 1
end

#during?(other_placement) ⇒ Boolean

Returns:

  • (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)

Returns:

  • (Boolean)


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_positionObject



106
107
108
# File 'lib/head_music/content/placement.rb', line 106

def next_position
  @next_position ||= position + rhythmic_value
end

#note?Boolean

Returns:

  • (Boolean)


71
72
73
# File 'lib/head_music/content/placement.rb', line 71

def note?
  sounds.length == 1
end

#pitchObject

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

Returns:

  • (Boolean)


87
88
89
# File 'lib/head_music/content/placement.rb', line 87

def pitched?
  sounds.any?(&:pitched?)
end

#pitched_note?Boolean

Returns:

  • (Boolean)


75
76
77
# File 'lib/head_music/content/placement.rb', line 75

def pitched_note?
  note? && pitched?
end

#pitchesObject



24
25
26
# File 'lib/head_music/content/placement.rb', line 24

def pitches
  sounds.select(&:pitched?)
end

#rest?Boolean

Returns:

  • (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

Returns:

  • (Boolean)


67
68
69
# File 'lib/head_music/content/placement.rb', line 67

def sounded?
  sounds.any?
end

#starts_during?(other_placement) ⇒ Boolean (private)

Returns:

  • (Boolean)


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

Returns:

  • (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

#syllablesObject

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 from these plus neighboring placements; melisma is the absence of a syllable here.



32
33
34
# File 'lib/head_music/content/placement.rb', line 32

def syllables
  @syllables ||= {}
end

#to_hObject



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_sObject



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

Returns:

  • (Boolean)


79
80
81
# File 'lib/head_music/content/placement.rb', line 79

def unpitched_note?
  note? && !pitched?
end

#wraps?(other_placement) ⇒ Boolean (private)

Returns:

  • (Boolean)


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