Class: HeadMusic::Time::Conductor
- Inherits:
-
Object
- Object
- HeadMusic::Time::Conductor
- Defined in:
- lib/head_music/time/conductor.rb
Overview
Representation of a conductor track for musical material
The Conductor class synchronizes three different time representations:
- Clock time: elapsed nanoseconds (source of truth)
- Musical position: bars:beats:ticks:subticks notation
- SMPTE timecode: hours:minutes:seconds:frames
Each moment in a track corresponds to all three positions simultaneously. The conductor handles conversions between these representations based on the current tempo, meter, and framerate.
Instance Attribute Summary collapse
-
#framerate ⇒ Integer
Frames per second for SMPTE conversions.
-
#meter_map ⇒ MeterMap
readonly
The meter map for this conductor.
-
#starting_musical_position ⇒ MusicalPosition
The musical position at clock time 0.
-
#starting_smpte_timecode ⇒ SmpteTimecode
The SMPTE timecode at clock time 0.
-
#tempo_map ⇒ TempoMap
readonly
The tempo map for this conductor.
Instance Method Summary collapse
-
#clock_to_musical(clock_position) ⇒ MusicalPosition
Convert clock position to musical position.
-
#clock_to_smpte(clock_position) ⇒ SmpteTimecode
Convert clock position to SMPTE timecode.
-
#initialize(starting_musical_position: nil, starting_smpte_timecode: nil, framerate: SmpteTimecode::DEFAULT_FRAMERATE, starting_tempo: nil, starting_meter: nil, tempo_map: nil, meter_map: nil) ⇒ Conductor
constructor
Create a new conductor.
-
#musical_position_to_subticks(position, meter = nil) ⇒ Integer
private
Convert a musical position to total subticks for calculation.
-
#musical_to_clock(musical_position) ⇒ ClockPosition
Convert musical position to clock position.
-
#smpte_to_clock(smpte_timecode) ⇒ ClockPosition
Convert SMPTE timecode to clock position.
-
#starting_meter ⇒ HeadMusic::Rudiment::Meter
The initial meter (delegates to meter_map).
-
#starting_tempo ⇒ HeadMusic::Rudiment::Tempo
The initial tempo (delegates to tempo_map).
Constructor Details
#initialize(starting_musical_position: nil, starting_smpte_timecode: nil, framerate: SmpteTimecode::DEFAULT_FRAMERATE, starting_tempo: nil, starting_meter: nil, tempo_map: nil, meter_map: nil) ⇒ Conductor
Create a new conductor
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/head_music/time/conductor.rb', line 68 def initialize( starting_musical_position: nil, starting_smpte_timecode: nil, framerate: SmpteTimecode::DEFAULT_FRAMERATE, starting_tempo: nil, starting_meter: nil, tempo_map: nil, meter_map: nil ) @starting_musical_position = starting_musical_position || MusicalPosition.new @starting_smpte_timecode = starting_smpte_timecode || SmpteTimecode.new(0, 0, 0, 0, framerate: framerate) @framerate = framerate # Create or use provided maps @tempo_map = tempo_map || TempoMap.new( starting_tempo: starting_tempo || HeadMusic::Rudiment::Tempo.new("quarter", 120), starting_position: @starting_musical_position ) @meter_map = meter_map || MeterMap.new( starting_meter: starting_meter || "4/4", starting_position: @starting_musical_position ) # Link maps together for position normalization @tempo_map.meter = @meter_map.meter_at(@starting_musical_position) end |
Instance Attribute Details
#framerate ⇒ Integer
Returns frames per second for SMPTE conversions.
41 42 43 |
# File 'lib/head_music/time/conductor.rb', line 41 def framerate @framerate end |
#meter_map ⇒ MeterMap (readonly)
Returns the meter map for this conductor.
47 48 49 |
# File 'lib/head_music/time/conductor.rb', line 47 def meter_map @meter_map end |
#starting_musical_position ⇒ MusicalPosition
Returns the musical position at clock time 0.
35 36 37 |
# File 'lib/head_music/time/conductor.rb', line 35 def starting_musical_position @starting_musical_position end |
#starting_smpte_timecode ⇒ SmpteTimecode
Returns the SMPTE timecode at clock time 0.
38 39 40 |
# File 'lib/head_music/time/conductor.rb', line 38 def starting_smpte_timecode @starting_smpte_timecode end |
#tempo_map ⇒ TempoMap (readonly)
Returns the tempo map for this conductor.
44 45 46 |
# File 'lib/head_music/time/conductor.rb', line 44 def tempo_map @tempo_map end |
Instance Method Details
#clock_to_musical(clock_position) ⇒ MusicalPosition
Convert clock position to musical position
Uses the tempo map to determine how many beats have elapsed, accounting for tempo changes along the timeline.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 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 149 150 151 152 153 154 155 156 157 |
# File 'lib/head_music/time/conductor.rb', line 102 def clock_to_musical(clock_position) target_nanoseconds = clock_position.nanoseconds accumulated_nanoseconds = 0 current_position = starting_musical_position # We need an end position far enough to contain our target clock time # Start with a reasonable guess and extend if needed = starting_musical_position. + 1000 estimated_end = MusicalPosition.new(, 1, 0, 0) tempo_map.each_segment(starting_musical_position, estimated_end) do |start_pos, end_pos, tempo| meter = meter_map.meter_at(start_pos) # Calculate clock duration of this segment start_subticks = musical_position_to_subticks(start_pos, meter) end_subticks = musical_position_to_subticks(end_pos, meter) segment_subticks = end_subticks - start_subticks segment_ticks = segment_subticks / HeadMusic::Time::SUBTICKS_PER_TICK.to_f segment_nanoseconds = (segment_ticks * tempo.tick_duration_in_nanoseconds).round # Check if our target falls within this segment if accumulated_nanoseconds + segment_nanoseconds >= target_nanoseconds # Target is in this segment - calculate exact position remaining_nanoseconds = target_nanoseconds - accumulated_nanoseconds remaining_ticks = remaining_nanoseconds / tempo.tick_duration_in_nanoseconds.to_f remaining_subticks = (remaining_ticks * HeadMusic::Time::SUBTICKS_PER_TICK).round # Add to start position of this segment total_subticks = start_subticks + remaining_subticks # Convert to bar:beat:tick:subtick ticks_per_count = meter.ticks_per_count = meter. subticks_per_count = ticks_per_count * HeadMusic::Time::SUBTICKS_PER_TICK = * subticks_per_count = (total_subticks / ).floor remaining = total_subticks % beats = (remaining / subticks_per_count).floor remaining %= subticks_per_count ticks = (remaining / HeadMusic::Time::SUBTICKS_PER_TICK).floor subticks = remaining % HeadMusic::Time::SUBTICKS_PER_TICK position = MusicalPosition.new( + 1, beats + 1, ticks, subticks) return position.normalize!(meter) end accumulated_nanoseconds += segment_nanoseconds current_position = end_pos end # If we get here, return the last position (shouldn't normally happen) current_position end |
#clock_to_smpte(clock_position) ⇒ SmpteTimecode
Convert clock position to SMPTE timecode
Uses the framerate to determine the timecode.
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/head_music/time/conductor.rb', line 198 def clock_to_smpte(clock_position) # Calculate total frames from nanoseconds nanoseconds_per_second = 1_000_000_000.0 elapsed_seconds = clock_position.nanoseconds / nanoseconds_per_second total_frames = (elapsed_seconds * framerate).round # Add starting timecode frames starting_frames = starting_smpte_timecode.to_total_frames total_frames += starting_frames # Convert frames to HH:MM:SS:FF hours = total_frames / (framerate * 60 * 60) remaining = total_frames % (framerate * 60 * 60) minutes = remaining / (framerate * 60) remaining %= (framerate * 60) seconds = remaining / framerate frames = remaining % framerate timecode = SmpteTimecode.new(hours, minutes, seconds, frames, framerate: framerate) timecode.normalize! end |
#musical_position_to_subticks(position, meter = nil) ⇒ Integer (private)
Convert a musical position to total subticks for calculation
249 250 251 252 253 254 255 256 257 258 259 260 261 |
# File 'lib/head_music/time/conductor.rb', line 249 def musical_position_to_subticks(position, meter = nil) meter ||= meter_map.meter_at(position) ticks_per_count = meter.ticks_per_count = meter. total = 0 total += (position. - 1) * * ticks_per_count * HeadMusic::Time::SUBTICKS_PER_TICK total += (position.beat - 1) * ticks_per_count * HeadMusic::Time::SUBTICKS_PER_TICK total += position.tick * HeadMusic::Time::SUBTICKS_PER_TICK total += position.subtick total end |
#musical_to_clock(musical_position) ⇒ ClockPosition
Convert musical position to clock position
Uses the tempo map to determine how much clock time has elapsed based on the musical position, accounting for tempo changes.
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/head_music/time/conductor.rb', line 166 def musical_to_clock(musical_position) total_nanoseconds = 0 # Iterate through each tempo segment from start to target position tempo_map.each_segment(starting_musical_position, musical_position) do |start_pos, end_pos, tempo| # Get the meter for this segment to calculate subticks correctly meter = meter_map.meter_at(start_pos) # Calculate subticks in this segment start_subticks = musical_position_to_subticks(start_pos, meter) end_subticks = musical_position_to_subticks(end_pos, meter) segment_subticks = end_subticks - start_subticks # Convert subticks to ticks segment_ticks = segment_subticks / HeadMusic::Time::SUBTICKS_PER_TICK.to_f # Convert ticks to nanoseconds using this segment's tempo nanoseconds_per_tick = tempo.tick_duration_in_nanoseconds segment_nanoseconds = (segment_ticks * nanoseconds_per_tick).round total_nanoseconds += segment_nanoseconds end ClockPosition.new(total_nanoseconds) end |
#smpte_to_clock(smpte_timecode) ⇒ ClockPosition
Convert SMPTE timecode to clock position
Uses the framerate to determine the clock time.
228 229 230 231 232 233 234 235 236 237 238 239 240 |
# File 'lib/head_music/time/conductor.rb', line 228 def smpte_to_clock(smpte_timecode) # Calculate total frames total_frames = smpte_timecode.to_total_frames starting_frames = starting_smpte_timecode.to_total_frames elapsed_frames = total_frames - starting_frames # Convert frames to seconds, then to nanoseconds nanoseconds_per_second = 1_000_000_000.0 elapsed_seconds = elapsed_frames / framerate.to_f elapsed_nanoseconds = (elapsed_seconds * nanoseconds_per_second).round ClockPosition.new(elapsed_nanoseconds) end |
#starting_meter ⇒ HeadMusic::Rudiment::Meter
Returns the initial meter (delegates to meter_map).
55 56 57 |
# File 'lib/head_music/time/conductor.rb', line 55 def starting_meter meter_map.events.first.meter end |
#starting_tempo ⇒ HeadMusic::Rudiment::Tempo
Returns the initial tempo (delegates to tempo_map).
50 51 52 |
# File 'lib/head_music/time/conductor.rb', line 50 def starting_tempo tempo_map.events.first.tempo end |