Class: HeadMusic::Time::MusicalPosition

Inherits:
Object
  • Object
show all
Includes:
Comparable
Defined in:
lib/head_music/time/musical_position.rb

Overview

Representation of a musical position in bars:beats:ticks:subticks notation

A MusicalPosition represents a point in musical time using a hierarchical structure: - bar: the measure number (1-indexed) - beat: the beat within the bar (1-indexed) - tick: subdivision of a beat (0-indexed, 960 ticks per quarter note) - subtick: finest resolution (0-indexed, 240 subticks per tick)

The position can be normalized according to a meter, which handles overflow by carrying excess values to higher levels (e.g., excess ticks become beats, excess beats become bars).

Examples:

Creating a position at bar 1, beat 1

position = HeadMusic::Time::MusicalPosition.new
position.to_s # => "1:1:0:0"

Parsing from a string

position = HeadMusic::Time::MusicalPosition.parse("2:3:480:0")
position.bar # => 2
position.beat # => 3

Normalizing with overflow

meter = HeadMusic::Rudiment::Meter.get("4/4")
position = HeadMusic::Time::MusicalPosition.new(1, 1, 960, 0)
position.normalize!(meter)
position.to_s # => "1:2:0:0" (ticks carried into beats)

Comparing positions

pos1 = HeadMusic::Time::MusicalPosition.new(1, 1, 0, 0)
pos2 = HeadMusic::Time::MusicalPosition.new(1, 2, 0, 0)
meter = HeadMusic::Rudiment::Meter.get("4/4")
pos1.normalize!(meter)
pos2.normalize!(meter)
pos1 < pos2 # => true

Constant Summary collapse

DEFAULT_FIRST_BAR =

Default starting bar number

1
FIRST_BEAT =

First beat in a bar

1
FIRST_TICK =

First tick in a beat

0
FIRST_SUBTICK =

First subtick in a tick

0

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(bar = DEFAULT_FIRST_BAR, beat = FIRST_BEAT, tick = FIRST_TICK, subtick = FIRST_SUBTICK) ⇒ MusicalPosition

Create a new musical position

Parameters:

  • bar (Integer, String) (defaults to: DEFAULT_FIRST_BAR)

    the bar number (default: 1)

  • beat (Integer, String) (defaults to: FIRST_BEAT)

    the beat number (default: 1)

  • tick (Integer, String) (defaults to: FIRST_TICK)

    the tick number (default: 0)

  • subtick (Integer, String) (defaults to: FIRST_SUBTICK)

    the subtick number (default: 0)



83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/head_music/time/musical_position.rb', line 83

def initialize(
  bar = DEFAULT_FIRST_BAR,
  beat = FIRST_BEAT,
  tick = FIRST_TICK,
  subtick = FIRST_SUBTICK
)
  @bar = bar.to_i
  @beat = beat.to_i
  @tick = tick.to_i
  @subtick = subtick.to_i
  @meter = nil
  @total_subticks = nil
end

Instance Attribute Details

#barInteger (readonly)

Returns the bar (measure) number (1-indexed).

Returns:

  • (Integer)

    the bar (measure) number (1-indexed)



44
45
46
# File 'lib/head_music/time/musical_position.rb', line 44

def bar
  @bar
end

#beatInteger (readonly)

Returns the beat within the bar (1-indexed).

Returns:

  • (Integer)

    the beat within the bar (1-indexed)



47
48
49
# File 'lib/head_music/time/musical_position.rb', line 47

def beat
  @beat
end

#subtickInteger (readonly)

Returns the subtick within the tick (0-indexed).

Returns:

  • (Integer)

    the subtick within the tick (0-indexed)



53
54
55
# File 'lib/head_music/time/musical_position.rb', line 53

def subtick
  @subtick
end

#tickInteger (readonly)

Returns the tick within the beat (0-indexed).

Returns:

  • (Integer)

    the tick within the beat (0-indexed)



50
51
52
# File 'lib/head_music/time/musical_position.rb', line 50

def tick
  @tick
end

Class Method Details

.parse(identifier) ⇒ MusicalPosition

Parse a position from a string representation

Examples:

MusicalPosition.parse("2:3:480:120")

Parameters:

  • identifier (String)

    position in “bar:beat:tick:subtick” format

Returns:



73
74
75
# File 'lib/head_music/time/musical_position.rb', line 73

def self.parse(identifier)
  new(*identifier.scan(/\d+/)[0..3])
end

Instance Method Details

#<=>(other) ⇒ Integer

Compare this position to another

Note: For accurate comparison, both positions should be normalized with the same meter first.

Parameters:

Returns:

  • (Integer)

    -1 if less than, 0 if equal, 1 if greater than



145
146
147
# File 'lib/head_music/time/musical_position.rb', line 145

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

#carry(component, radix) ⇒ Integer (private)

Divide the named component by its radix, store the remainder back, and return the amount to carry into the next-higher component.

Returns:

  • (Integer)

    the carry (may be negative when borrowing)



180
181
182
183
184
185
# File 'lib/head_music/time/musical_position.rb', line 180

def carry(component, radix)
  ivar = :"@#{component}"
  delta, remainder = instance_variable_get(ivar).divmod(radix)
  instance_variable_set(ivar, remainder)
  delta
end

#normalize!(meter) ⇒ self

Normalize the position according to a meter, handling overflow

This method modifies the position in place, carrying excess values from lower levels to higher levels (subticks → ticks → beats → bars). Also handles negative values by borrowing from higher levels.

Examples:

meter = HeadMusic::Rudiment::Meter.get("4/4")
position = MusicalPosition.new(1, 1, 960, 240)
position.normalize!(meter) # => "1:2:1:0"

Parameters:

Returns:

  • (self)

    returns self for method chaining



123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/head_music/time/musical_position.rb', line 123

def normalize!(meter)
  return self unless meter

  @meter = meter
  @total_subticks = nil # Invalidate cached value

  # Carry overflow (and borrow underflow) up through each level.
  # divmod handles both in-range and out-of-range values uniformly.
  @tick += carry(:subtick, HeadMusic::Time::SUBTICKS_PER_TICK)
  @beat += carry(:tick, meter.ticks_per_count)
  @bar += carry(:beat, meter.counts_per_bar)

  self
end

#to_aArray<Integer>

Convert position to array format

Returns:

  • (Array<Integer>)

    [bar, beat, tick, subtick]



100
101
102
# File 'lib/head_music/time/musical_position.rb', line 100

def to_a
  [bar, beat, tick, subtick]
end

#to_sString

Convert position to string format

Returns:

  • (String)

    position in “bar:beat:tick:subtick” format



107
108
109
# File 'lib/head_music/time/musical_position.rb', line 107

def to_s
  "#{bar}:#{beat}:#{tick}:#{subtick}"
end

#to_total_subticksInteger Also known as: to_i

Note:

This calculation assumes the position has been normalized

Convert position to total subticks for comparison and calculation

Returns:

  • (Integer)

    total subticks from the beginning



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/head_music/time/musical_position.rb', line 153

def to_total_subticks
  return @total_subticks if @total_subticks

  # Note: This is a simplified calculation that assumes consistent meter
  ticks_per_count = @meter&.ticks_per_count || HeadMusic::Time::PPQN
  counts_per_bar = @meter&.counts_per_bar || 4
  subticks_per_count = ticks_per_count * HeadMusic::Time::SUBTICKS_PER_TICK
  subticks_per_bar = counts_per_bar * subticks_per_count

  @total_subticks =
    (bar - 1) * subticks_per_bar +
    (beat - 1) * subticks_per_count +
    tick * HeadMusic::Time::SUBTICKS_PER_TICK +
    subtick
end