@@ -32,6 +32,7 @@ const LARGE_FORWARD_JUMP_SECS: f64 = 5.0;
3232struct AudioDriftTracker {
3333 baseline_offset_secs : Option < f64 > ,
3434 drift_warning_logged : bool ,
35+ first_frame_timestamp_secs : Option < f64 > ,
3536}
3637
3738const AUDIO_WALL_CLOCK_TOLERANCE_SECS : f64 = 0.1 ;
@@ -42,38 +43,48 @@ impl AudioDriftTracker {
4243 Self {
4344 baseline_offset_secs : None ,
4445 drift_warning_logged : false ,
46+ first_frame_timestamp_secs : None ,
4547 }
4648 }
4749
4850 fn calculate_timestamp (
4951 & mut self ,
50- samples_before_frame : u64 ,
51- sample_rate : u32 ,
52+ frame_timestamp_secs : f64 ,
5253 wall_clock_secs : f64 ,
53- total_input_duration_secs : f64 ,
5454 ) -> Option < Duration > {
55- let sample_time_secs = samples_before_frame as f64 / sample_rate as f64 ;
55+ if frame_timestamp_secs < 0.0 {
56+ return None ;
57+ }
58+
59+ let first_timestamp = * self
60+ . first_frame_timestamp_secs
61+ . get_or_insert ( frame_timestamp_secs) ;
62+ let frame_elapsed_secs = frame_timestamp_secs - first_timestamp;
63+
64+ if frame_elapsed_secs < 0.0 {
65+ return None ;
66+ }
5667
57- if sample_time_secs > wall_clock_secs + AUDIO_WALL_CLOCK_TOLERANCE_SECS {
68+ if frame_elapsed_secs > wall_clock_secs + AUDIO_WALL_CLOCK_TOLERANCE_SECS {
5869 return None ;
5970 }
6071
61- if wall_clock_secs >= 2.0 && total_input_duration_secs >= 2.0 {
72+ if wall_clock_secs >= 2.0 && frame_elapsed_secs >= 2.0 {
6273 if self . baseline_offset_secs . is_none ( ) {
63- let offset = total_input_duration_secs - wall_clock_secs;
74+ let offset = frame_elapsed_secs - wall_clock_secs;
6475 debug ! (
6576 wall_clock_secs,
66- total_input_duration_secs ,
77+ frame_elapsed_secs ,
6778 baseline_offset_secs = offset,
6879 "Capturing audio baseline offset after warmup"
6980 ) ;
7081 self . baseline_offset_secs = Some ( offset) ;
7182 }
7283
7384 let baseline = self . baseline_offset_secs . unwrap_or ( 0.0 ) ;
74- let adjusted_input_duration = total_input_duration_secs - baseline;
75- let drift_ratio = if adjusted_input_duration > 0.0 {
76- wall_clock_secs / adjusted_input_duration
85+ let adjusted_frame_elapsed = frame_elapsed_secs - baseline;
86+ let drift_ratio = if adjusted_frame_elapsed > 0.0 {
87+ wall_clock_secs / adjusted_frame_elapsed
7788 } else {
7889 1.0
7990 } ;
@@ -82,15 +93,18 @@ impl AudioDriftTracker {
8293 warn ! (
8394 drift_ratio,
8495 wall_clock_secs,
85- adjusted_input_duration ,
96+ adjusted_frame_elapsed ,
8697 baseline,
8798 "Significant audio clock drift detected"
8899 ) ;
89100 self . drift_warning_logged = true ;
90101 }
102+
103+ let corrected_secs = adjusted_frame_elapsed * drift_ratio;
104+ return Some ( Duration :: from_secs_f64 ( corrected_secs. max ( 0.0 ) ) ) ;
91105 }
92106
93- Some ( Duration :: from_secs_f64 ( sample_time_secs ) )
107+ Some ( Duration :: from_secs_f64 ( frame_elapsed_secs . max ( 0.0 ) ) )
94108 }
95109}
96110
@@ -1056,7 +1070,6 @@ impl PreparedAudioSources {
10561070 let _ = first_tx. send ( frame. timestamp ) ;
10571071 }
10581072
1059- let samples_before_frame = total_samples;
10601073 let frame_samples = frame. inner . samples ( ) as u64 ;
10611074 total_samples += frame_samples;
10621075
@@ -1066,34 +1079,33 @@ impl PreparedAudioSources {
10661079 let effective_wall_clock =
10671080 raw_wall_clock. saturating_sub ( total_pause_duration) ;
10681081 let wall_clock_secs = effective_wall_clock. as_secs_f64 ( ) ;
1069- let total_input_duration_secs =
1070- total_samples as f64 / sample_rate as f64 ;
1082+
1083+ let frame_timestamp_secs =
1084+ frame. timestamp . signed_duration_since_secs ( timestamps) ;
10711085
10721086 if wall_clock_secs >= 5.0 && ( wall_clock_secs as u64 ) . is_multiple_of ( 10 )
10731087 {
1074- let drift_ratio = if total_input_duration_secs > 0.0 {
1075- wall_clock_secs / total_input_duration_secs
1088+ let total_input_duration_secs =
1089+ total_samples as f64 / sample_rate as f64 ;
1090+ let drift_ratio = if frame_timestamp_secs > 0.0 {
1091+ wall_clock_secs / frame_timestamp_secs
10761092 } else {
10771093 1.0
10781094 } ;
10791095 debug ! (
10801096 wall_clock_secs,
10811097 total_input_duration_secs,
1098+ frame_timestamp_secs,
10821099 drift_ratio,
1083- samples_before_frame,
10841100 total_samples,
10851101 baseline_offset = drift_tracker. baseline_offset_secs,
10861102 total_pause_ms = total_pause_duration. as_millis( ) ,
10871103 "Audio drift correction status"
10881104 ) ;
10891105 }
10901106
1091- let timestamp = drift_tracker. calculate_timestamp (
1092- samples_before_frame,
1093- sample_rate,
1094- wall_clock_secs,
1095- total_input_duration_secs,
1096- ) ;
1107+ let timestamp = drift_tracker
1108+ . calculate_timestamp ( frame_timestamp_secs, wall_clock_secs) ;
10971109
10981110 if let Some ( timestamp) = timestamp
10991111 && let Err ( e) =
@@ -1483,24 +1495,18 @@ mod tests {
14831495 mod audio_drift_tracker {
14841496 use super :: * ;
14851497
1486- const SAMPLE_RATE : u32 = 48000 ;
1487-
1488- fn samples_for_duration ( duration_secs : f64 ) -> u64 {
1489- ( duration_secs * SAMPLE_RATE as f64 ) as u64
1490- }
1491-
14921498 #[ test]
1493- fn returns_sample_based_time_during_warmup ( ) {
1499+ fn returns_frame_based_time_during_warmup ( ) {
14941500 let mut tracker = AudioDriftTracker :: new ( ) ;
1495- let samples = samples_for_duration ( 1.0 ) ;
1501+ let frame_timestamp = 1.0 ;
1502+ let wall_clock = 1.5 ;
14961503 let result = tracker
1497- . calculate_timestamp ( samples , SAMPLE_RATE , 1.5 , 1.5 )
1498- . expect ( "Should not be capped when sample time < wall clock" ) ;
1499- let expected = Duration :: from_secs_f64 ( 1.0 ) ;
1504+ . calculate_timestamp ( frame_timestamp , wall_clock )
1505+ . expect ( "Should not be capped when frame time < wall clock" ) ;
1506+ let expected = Duration :: ZERO ;
15001507 assert ! (
15011508 ( result. as_secs_f64( ) - expected. as_secs_f64( ) ) . abs( ) < 0.001 ,
1502- "Expected ~{:.3}s, got {:.3}s" ,
1503- expected. as_secs_f64( ) ,
1509+ "First frame should have ~0s timestamp, got {:.3}s" ,
15041510 result. as_secs_f64( )
15051511 ) ;
15061512 assert ! (
@@ -1513,11 +1519,12 @@ mod tests {
15131519 fn captures_baseline_after_warmup ( ) {
15141520 let mut tracker = AudioDriftTracker :: new ( ) ;
15151521 let buffer_delay = 0.05 ;
1516- let wall_clock = 2.0 ;
1517- let input_duration = 2.0 + buffer_delay;
1518- let samples = samples_for_duration ( input_duration) ;
15191522
1520- tracker. calculate_timestamp ( samples, SAMPLE_RATE , wall_clock, input_duration) ;
1523+ tracker. calculate_timestamp ( 0.0 , 0.0 ) ;
1524+
1525+ let frame_timestamp = 2.0 + buffer_delay;
1526+ let wall_clock = 2.0 ;
1527+ tracker. calculate_timestamp ( frame_timestamp, wall_clock) ;
15211528
15221529 assert ! ( tracker. baseline_offset_secs. is_some( ) ) ;
15231530 let baseline = tracker. baseline_offset_secs . unwrap ( ) ;
@@ -1528,30 +1535,26 @@ mod tests {
15281535 }
15291536
15301537 #[ test]
1531- fn returns_sample_based_time_after_warmup ( ) {
1538+ fn applies_drift_correction_after_warmup ( ) {
15321539 let mut tracker = AudioDriftTracker :: new ( ) ;
15331540 let buffer_delay = 0.05 ;
15341541
1542+ tracker. calculate_timestamp ( 0.0 , 0.0 ) ;
1543+
1544+ let frame_timestamp_1 = 2.0 + buffer_delay;
15351545 let wall_clock_1 = 2.0 ;
1536- let input_duration_1 = 2.0 + buffer_delay;
1537- tracker. calculate_timestamp (
1538- samples_for_duration ( input_duration_1) ,
1539- SAMPLE_RATE ,
1540- wall_clock_1,
1541- input_duration_1,
1542- ) ;
1546+ tracker. calculate_timestamp ( frame_timestamp_1, wall_clock_1) ;
15431547
1548+ let frame_timestamp_2 = 10.0 + buffer_delay;
15441549 let wall_clock_2 = 10.0 ;
1545- let input_duration_2 = 10.0 + buffer_delay;
1546- let samples_2 = samples_for_duration ( input_duration_2) ;
15471550 let result = tracker
1548- . calculate_timestamp ( samples_2 , SAMPLE_RATE , wall_clock_2, input_duration_2 )
1551+ . calculate_timestamp ( frame_timestamp_2 , wall_clock_2)
15491552 . expect ( "Should not be capped" ) ;
15501553
1551- let expected = Duration :: from_secs_f64 ( input_duration_2 ) ;
1554+ let expected = Duration :: from_secs_f64 ( wall_clock_2 ) ;
15521555 assert ! (
1553- ( result. as_secs_f64( ) - expected. as_secs_f64( ) ) . abs( ) < 0.001 ,
1554- "Expected sample-based time ~{:.3}s, got {:.3}s" ,
1556+ ( result. as_secs_f64( ) - expected. as_secs_f64( ) ) . abs( ) < 0.1 ,
1557+ "Expected drift-corrected time ~{:.3}s, got {:.3}s" ,
15551558 expected. as_secs_f64( ) ,
15561559 result. as_secs_f64( )
15571560 ) ;
@@ -1561,14 +1564,11 @@ mod tests {
15611564 fn continuous_timestamps_no_gaps ( ) {
15621565 let mut tracker = AudioDriftTracker :: new ( ) ;
15631566
1564- let mut samples = 0u64 ;
15651567 let mut last_timestamp = Duration :: ZERO ;
15661568 for i in 0 ..100 {
1569+ let frame_timestamp = i as f64 * 0.02 ;
15671570 let wall_clock = i as f64 * 0.02 ;
1568- let input_duration = samples as f64 / SAMPLE_RATE as f64 ;
1569- if let Some ( result) =
1570- tracker. calculate_timestamp ( samples, SAMPLE_RATE , wall_clock, input_duration)
1571- {
1571+ if let Some ( result) = tracker. calculate_timestamp ( frame_timestamp, wall_clock) {
15721572 if i > 0 {
15731573 let gap = result. as_secs_f64 ( ) - last_timestamp. as_secs_f64 ( ) ;
15741574 assert ! (
@@ -1579,88 +1579,97 @@ mod tests {
15791579
15801580 last_timestamp = result;
15811581 }
1582- samples += 960 ;
15831582 }
15841583 }
15851584
15861585 #[ test]
15871586 fn continuous_across_warmup_boundary ( ) {
15881587 let mut tracker = AudioDriftTracker :: new ( ) ;
15891588
1590- let samples_at_2s = samples_for_duration ( 2.0 ) ;
1589+ tracker. calculate_timestamp ( 0.0 , 0.0 ) ;
1590+
1591+ let frame_timestamp_1 = 2.0 ;
1592+ let wall_clock_1 = 2.1 ;
15911593 let result1 = tracker
1592- . calculate_timestamp (
1593- samples_at_2s,
1594- SAMPLE_RATE ,
1595- 2.1 ,
1596- samples_at_2s as f64 / SAMPLE_RATE as f64 ,
1597- )
1594+ . calculate_timestamp ( frame_timestamp_1, wall_clock_1)
15981595 . expect ( "Should not be capped" ) ;
15991596
1600- let samples_after = samples_at_2s + 960 ;
1597+ let frame_timestamp_2 = 2.02 ;
1598+ let wall_clock_2 = 2.12 ;
16011599 let result2 = tracker
1602- . calculate_timestamp (
1603- samples_after,
1604- SAMPLE_RATE ,
1605- 2.2 ,
1606- samples_after as f64 / SAMPLE_RATE as f64 ,
1607- )
1600+ . calculate_timestamp ( frame_timestamp_2, wall_clock_2)
16081601 . expect ( "Should not be capped" ) ;
16091602
16101603 let gap = result2. as_secs_f64 ( ) - result1. as_secs_f64 ( ) ;
1611- let expected_gap = 960.0 / SAMPLE_RATE as f64 ;
1604+ let expected_gap = 0.02 ;
16121605 assert ! (
1613- ( gap - expected_gap) . abs( ) < 0.001 ,
1606+ ( gap - expected_gap) . abs( ) < 0.01 ,
16141607 "Gap across warmup boundary should be continuous: expected {expected_gap:.3}s, got {gap:.3}s"
16151608 ) ;
16161609 }
16171610
16181611 #[ test]
1619- fn simulates_real_world_scenario ( ) {
1612+ fn simulates_real_world_scenario_with_drift ( ) {
16201613 let mut tracker = AudioDriftTracker :: new ( ) ;
1621- let initial_buffer = 0.05 ;
1614+ let initial_offset = 0.05 ;
16221615 let drift_rate = 0.004 ;
16231616
1624- let mut total_audio = initial_buffer ;
1617+ let mut frame_time = initial_offset ;
16251618 let mut wall_time = 0.0 ;
16261619 let step = 0.5 ;
16271620
16281621 while wall_time < 60.0 {
1629- wall_time += step;
1630- total_audio += step * ( 1.0 + drift_rate) ;
1631-
1632- let samples = samples_for_duration ( total_audio) ;
1633- if let Some ( result) =
1634- tracker. calculate_timestamp ( samples, SAMPLE_RATE , wall_time, total_audio)
1635- {
1636- let expected = samples as f64 / SAMPLE_RATE as f64 ;
1637- let error = ( result. as_secs_f64 ( ) - expected) . abs ( ) ;
1638- assert ! (
1639- error < 0.001 ,
1640- "At wall_time={:.1}s: result {:.3}s should equal sample time {:.3}s" ,
1641- wall_time,
1642- result. as_secs_f64( ) ,
1643- expected
1644- ) ;
1622+ if let Some ( result) = tracker. calculate_timestamp ( frame_time, wall_time) {
1623+ if wall_time >= 2.0 {
1624+ let error = ( result. as_secs_f64 ( ) - wall_time) . abs ( ) ;
1625+ assert ! (
1626+ error < 0.5 ,
1627+ "At wall_time={:.1}s: result {:.3}s should be close to wall clock" ,
1628+ wall_time,
1629+ result. as_secs_f64( )
1630+ ) ;
1631+ }
16451632 }
1633+
1634+ wall_time += step;
1635+ frame_time += step * ( 1.0 + drift_rate) ;
16461636 }
16471637 }
16481638
16491639 #[ test]
16501640 fn preserves_baseline_across_multiple_calls ( ) {
16511641 let mut tracker = AudioDriftTracker :: new ( ) ;
16521642
1653- tracker. calculate_timestamp ( samples_for_duration ( 2.1 ) , SAMPLE_RATE , 2.0 , 2.1 ) ;
1643+ tracker. calculate_timestamp ( 0.0 , 0.0 ) ;
1644+ tracker. calculate_timestamp ( 2.1 , 2.0 ) ;
16541645
16551646 let first_baseline = tracker. baseline_offset_secs ;
16561647
1657- tracker. calculate_timestamp ( samples_for_duration ( 10.1 ) , SAMPLE_RATE , 10.0 , 10.1 ) ;
1648+ tracker. calculate_timestamp ( 10.1 , 10.0 ) ;
16581649
16591650 assert_eq ! (
16601651 first_baseline, tracker. baseline_offset_secs,
16611652 "Baseline should not change after initial capture"
16621653 ) ;
16631654 }
1655+
1656+ #[ test]
1657+ fn rejects_negative_timestamps ( ) {
1658+ let mut tracker = AudioDriftTracker :: new ( ) ;
1659+ let result = tracker. calculate_timestamp ( -1.0 , 1.0 ) ;
1660+ assert ! ( result. is_none( ) , "Negative timestamps should be rejected" ) ;
1661+ }
1662+
1663+ #[ test]
1664+ fn rejects_timestamps_too_far_ahead_of_wall_clock ( ) {
1665+ let mut tracker = AudioDriftTracker :: new ( ) ;
1666+ tracker. calculate_timestamp ( 0.0 , 0.0 ) ;
1667+ let result = tracker. calculate_timestamp ( 5.0 , 1.0 ) ;
1668+ assert ! (
1669+ result. is_none( ) ,
1670+ "Timestamps too far ahead of wall clock should be rejected"
1671+ ) ;
1672+ }
16641673 }
16651674
16661675 mod video_drift_tracker {
0 commit comments