Class: HeadMusic::Time::MusicalPosition
- Inherits:
-
Object
- Object
- HeadMusic::Time::MusicalPosition
- 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).
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
-
#bar ⇒ Integer
readonly
The bar (measure) number (1-indexed).
-
#beat ⇒ Integer
readonly
The beat within the bar (1-indexed).
-
#subtick ⇒ Integer
readonly
The subtick within the tick (0-indexed).
-
#tick ⇒ Integer
readonly
The tick within the beat (0-indexed).
Class Method Summary collapse
-
.parse(identifier) ⇒ MusicalPosition
Parse a position from a string representation.
Instance Method Summary collapse
-
#<=>(other) ⇒ Integer
Compare this position to another.
-
#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.
-
#initialize(bar = DEFAULT_FIRST_BAR, beat = FIRST_BEAT, tick = FIRST_TICK, subtick = FIRST_SUBTICK) ⇒ MusicalPosition
constructor
Create a new musical position.
-
#normalize!(meter) ⇒ self
Normalize the position according to a meter, handling overflow.
-
#to_a ⇒ Array<Integer>
Convert position to array format.
-
#to_s ⇒ String
Convert position to string format.
-
#to_total_subticks ⇒ Integer
(also: #to_i)
Convert position to total subticks for comparison and calculation.
Constructor Details
#initialize(bar = DEFAULT_FIRST_BAR, beat = FIRST_BEAT, tick = FIRST_TICK, subtick = FIRST_SUBTICK) ⇒ MusicalPosition
Create a new musical position
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( = DEFAULT_FIRST_BAR, beat = FIRST_BEAT, tick = FIRST_TICK, subtick = FIRST_SUBTICK ) @bar = .to_i @beat = beat.to_i @tick = tick.to_i @subtick = subtick.to_i @meter = nil @total_subticks = nil end |
Instance Attribute Details
#bar ⇒ Integer (readonly)
Returns the bar (measure) number (1-indexed).
44 45 46 |
# File 'lib/head_music/time/musical_position.rb', line 44 def @bar end |
#beat ⇒ Integer (readonly)
Returns the beat within the bar (1-indexed).
47 48 49 |
# File 'lib/head_music/time/musical_position.rb', line 47 def beat @beat end |
#subtick ⇒ Integer (readonly)
Returns the subtick within the tick (0-indexed).
53 54 55 |
# File 'lib/head_music/time/musical_position.rb', line 53 def subtick @subtick end |
#tick ⇒ Integer (readonly)
Returns 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
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.
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.
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.
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.) self end |
#to_a ⇒ Array<Integer>
Convert position to array format
100 101 102 |
# File 'lib/head_music/time/musical_position.rb', line 100 def to_a [, beat, tick, subtick] end |
#to_s ⇒ String
Convert position to string format
107 108 109 |
# File 'lib/head_music/time/musical_position.rb', line 107 def to_s "#{}:#{beat}:#{tick}:#{subtick}" end |
#to_total_subticks ⇒ Integer Also known as: to_i
This calculation assumes the position has been normalized
Convert position to total subticks for comparison and calculation
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 = @meter&. || 4 subticks_per_count = ticks_per_count * HeadMusic::Time::SUBTICKS_PER_TICK = * subticks_per_count @total_subticks = ( - 1) * + (beat - 1) * subticks_per_count + tick * HeadMusic::Time::SUBTICKS_PER_TICK + subtick end |