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



157
158
159
# File 'lib/head_music/time/musical_position.rb', line 157

def <=>(other)
  to_total_subticks <=> other.to_total_subticks
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
137
138
139
140
141
142
143
144
145
146
147
148
# 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 subticks into ticks
  if subtick >= HeadMusic::Time::SUBTICKS_PER_TICK || subtick.negative?
    tick_delta, @subtick = subtick.divmod(HeadMusic::Time::SUBTICKS_PER_TICK)
    @tick += tick_delta
  end

  # Carry ticks into beats
  if tick >= meter.ticks_per_count || tick.negative?
    beat_delta, @tick = tick.divmod(meter.ticks_per_count)
    @beat += beat_delta
  end

  # Carry beats into bars
  if beat >= meter.counts_per_bar || beat.negative?
    bar_delta, @beat = beat.divmod(meter.counts_per_bar)
    @bar += bar_delta
  end

  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



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/head_music/time/musical_position.rb', line 165

def to_total_subticks
  return @total_subticks if @total_subticks

  # Calculate based on the structure
  # 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

  total = 0
  total += (bar - 1) * counts_per_bar * ticks_per_count * HeadMusic::Time::SUBTICKS_PER_TICK
  total += (beat - 1) * ticks_per_count * HeadMusic::Time::SUBTICKS_PER_TICK
  total += tick * HeadMusic::Time::SUBTICKS_PER_TICK
  total += subtick

  @total_subticks = total
end