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.
-
#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 ) = .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 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.
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.
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. || beat.negative? , @beat = beat.divmod(meter.) += end 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 "#{bar}:#{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
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 = @meter&. || 4 total = 0 total += ( - 1) * * 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 |