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.



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

#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



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

def <=>(other)
  position <=> other.position
end

#chord?Boolean

Returns:

  • (Boolean)


50
51
52
# File 'lib/head_music/content/placement.rb', line 50

def chord?
  pitches.length > 1
end

#during?(other_placement) ⇒ Boolean

Returns:

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

Returns:

  • (Boolean)


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_positionObject



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

def next_position
  @next_position ||= position + rhythmic_value
end

#note?Boolean

Returns:

  • (Boolean)


38
39
40
# File 'lib/head_music/content/placement.rb', line 38

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).



26
27
28
# File 'lib/head_music/content/placement.rb', line 26

def pitch
  pitches.max
end

#pitched?Boolean

Returns:

  • (Boolean)


54
55
56
# File 'lib/head_music/content/placement.rb', line 54

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

#pitched_note?Boolean

Returns:

  • (Boolean)


42
43
44
# File 'lib/head_music/content/placement.rb', line 42

def pitched_note?
  note? && pitched?
end

#pitchesObject



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, unknown_sound_message(value))
end

#rest?Boolean

Returns:

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

Returns:

  • (Boolean)


34
35
36
# File 'lib/head_music/content/placement.rb', line 34

def sounded?
  sounds.any?
end

#starts_during?(other_placement) ⇒ Boolean (private)

Returns:

  • (Boolean)


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_hObject



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_sObject



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

Returns:

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

Returns:

  • (Boolean)


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