Class: HeadMusic::Content::Position

Inherits:
Object
  • Object
show all
Includes:
Comparable
Defined in:
lib/head_music/content/position.rb

Overview

A position is a moment in time within the rhythmic framework of a flow.

The coordinate itself is a HeadMusic::Time::MusicalPosition; this class is the binding to a flow that makes meter lookup possible. Time stays pure and flow-unaware, and there is one set of rollover rules rather than two.

A position is immutable: it is normalized once at construction and then frozen, along with the value inside it. Positions are sort keys -- Voice#place binary-searches over them -- and a mutable sort key is a wrong-note bug that raises nothing.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(flow, code_or_bar, count = nil, tick = nil, subtick = nil) ⇒ Position

Returns a new instance of Position.



22
23
24
25
26
27
28
# File 'lib/head_music/content/position.rb', line 22

def initialize(flow, code_or_bar, count = nil, tick = nil, subtick = nil)
  @flow = flow
  @value = self.class.value_for(code_or_bar, count, tick, subtick)
  normalize
  @value.freeze
  freeze
end

Instance Attribute Details

#flowObject (readonly)

Returns the value of attribute flow.



17
18
19
# File 'lib/head_music/content/position.rb', line 17

def flow
  @flow
end

#valueObject (readonly, private)

Returns the value of attribute value.



97
98
99
# File 'lib/head_music/content/position.rb', line 97

def value
  @value
end

Class Method Details

.value_for(code_or_bar, count, tick, subtick) ⇒ HeadMusic::Time::MusicalPosition

Returns the coordinate, unbound.

Returns:



31
32
33
34
35
36
37
# File 'lib/head_music/content/position.rb', line 31

def self.value_for(code_or_bar, count, tick, subtick)
  if code_or_bar.is_a?(String) && code_or_bar =~ /\D/
    HeadMusic::Time::MusicalPosition.parse(code_or_bar)
  else
    HeadMusic::Time::MusicalPosition.new(code_or_bar.to_i, (count || 1).to_i, (tick || 0).to_i, (subtick || 0).to_i)
  end
end

Instance Method Details

#+(other) ⇒ Object



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

def +(other)
  other = HeadMusic::Rudiment::RhythmicValue.new(other) if [HeadMusic::Rudiment::RhythmicUnit, Symbol, String].include?(other.class)
  self.class.new(flow, bar_number, count, tick + other.ticks, subtick)
end

#<=>(other) ⇒ Object

The flow deliberately takes no part in comparison. Positions in different flows have always compared equal here, and Voice#placement_at guards with exactly that comparison -- narrowing it would silently change note lookup.



61
62
63
64
# File 'lib/head_music/content/position.rb', line 61

def <=>(other)
  other = self.class.new(flow, other) if other.is_a?(String) && other =~ /\D/
  to_a <=> other.to_a
end

#bar_numberObject



39
40
41
# File 'lib/head_music/content/position.rb', line 39

def bar_number
  value.bar
end

#carried_countsObject (private)

The count the subticks and ticks carry up to, which may be more than a bar holds. Any bars normalize! carried into are folded back into the count, so that the walk above -- not the divmod -- decides which bar this lands in.



130
131
132
133
# File 'lib/head_music/content/position.rb', line 130

def carried_counts
  carried = HeadMusic::Time::MusicalPosition.new(1, value.count, value.tick, value.subtick).normalize!(meter)
  [(carried.bar - 1) * meter.counts_per_bar + carried.count, carried.tick, carried.subtick]
end

#codeObject

Subticks are emitted only when there are any, so the everyday "bar:count:tick" form is unchanged and stays parseable.



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

def code
  base = [bar_number, count, tick.to_s.rjust(3, "0")].join(":")
  subtick.zero? ? base : "#{base}:#{subtick.to_s.rjust(3, "0")}"
end

#eql?(other) ⇒ Boolean

Returns:

  • (Boolean)


66
67
68
# File 'lib/head_music/content/position.rb', line 66

def eql?(other)
  other.is_a?(self.class) && to_a == other.to_a
end

#hashObject



70
71
72
# File 'lib/head_music/content/position.rb', line 70

def hash
  to_a.hash
end

#meterObject



43
44
45
# File 'lib/head_music/content/position.rb', line 43

def meter
  flow.meter_at(bar_number)
end

#normalizeObject (private)

Normalizing a flow-bound position is not one carry but two jobs, because the bars it crosses need not share a meter.

MusicalPosition#normalize! does the radix carry -- subticks into ticks, ticks into counts -- under a single meter, which is right, because those ratios are read from the bar the position starts in. But its counts-into- bars step is a single divmod, and that silently assumes every bar it crosses has the same number of counts. So the bar carry is walked here one bar at a time, asking the flow for each bar's meter as it goes.

And the radix carry was done under the origin bar's meter, so a position that lands in a bar with a different count unit is carried again under that bar's, until it lands in a bar it was carried under. One instant then has one spelling, which is what equality and hashing depend on.



113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/head_music/content/position.rb', line 113

def normalize
  loop do
    origin_bar = value.bar
    counts, tick, subtick = carried_counts
    bar = origin_bar
    while counts > flow.meter_at(bar).counts_per_bar
      counts -= flow.meter_at(bar).counts_per_bar
      bar += 1
    end
    @value = HeadMusic::Time::MusicalPosition.new(bar, counts, tick, subtick)
    break if bar == origin_bar
  end
end

#start_of_next_barObject



91
92
93
# File 'lib/head_music/content/position.rb', line 91

def start_of_next_bar
  self.class.new(flow, bar_number + 1, 1, 0)
end

#strengthObject



74
75
76
# File 'lib/head_music/content/position.rb', line 74

def strength
  meter.beat_strength(count, tick: tick)
end

#strong?Boolean

Returns:

  • (Boolean)


78
79
80
# File 'lib/head_music/content/position.rb', line 78

def strong?
  strength >= 80
end

#weak?Boolean

Returns:

  • (Boolean)


82
83
84
# File 'lib/head_music/content/position.rb', line 82

def weak?
  !strong?
end

#within_placement?(placement) ⇒ Boolean

Returns:

  • (Boolean)


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

def within_placement?(placement)
  placement.position <= self && placement.next_position > self
end