Skip to content

Commit 553c1bf

Browse files
committed
Fix realtime PCM duration calculation (#2010)
1 parent 73e7843 commit 553c1bf

File tree

4 files changed

+35
-18
lines changed

4 files changed

+35
-18
lines changed

src/agents/realtime/_util.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,19 @@
22

33
from .config import RealtimeAudioFormat
44

5+
PCM16_SAMPLE_RATE_HZ = 24_000
6+
PCM16_SAMPLE_WIDTH_BYTES = 2
7+
G711_SAMPLE_RATE_HZ = 8_000
8+
59

610
def calculate_audio_length_ms(format: RealtimeAudioFormat | None, audio_bytes: bytes) -> float:
7-
if format and isinstance(format, str) and format.startswith("g711"):
8-
return (len(audio_bytes) / 8000) * 1000
9-
return (len(audio_bytes) / 24 / 2) * 1000
11+
if not audio_bytes:
12+
return 0.0
13+
14+
normalized_format = format.lower() if isinstance(format, str) else None
15+
16+
if normalized_format and normalized_format.startswith("g711"):
17+
return (len(audio_bytes) / G711_SAMPLE_RATE_HZ) * 1000
18+
19+
samples = len(audio_bytes) / PCM16_SAMPLE_WIDTH_BYTES
20+
return (samples / PCM16_SAMPLE_RATE_HZ) * 1000

tests/realtime/test_openai_realtime.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -698,23 +698,27 @@ async def test_audio_timing_calculation_accuracy(self, model):
698698
for event in audio_deltas:
699699
await model._handle_ws_event(event)
700700

701-
# Should accumulate audio length: 8 bytes / 24 / 2 * 1000 = milliseconds
702-
# Total: 8 bytes / 24 / 2 * 1000
703-
expected_length = (8 / 24 / 2) * 1000
701+
# Should accumulate audio length: 8 bytes -> 4 samples -> (4 / 24000) * 1000 ≈ 0.167 ms
702+
expected_length = (8 / (24_000 * 2)) * 1000
704703

705704
# Test through the actual audio state tracker
706705
audio_state = model._audio_state_tracker.get_state("item_1", 0)
707706
assert audio_state is not None
708-
assert abs(audio_state.audio_length_ms - expected_length) < 0.001
707+
assert audio_state.audio_length_ms == pytest.approx(expected_length, rel=0, abs=1e-6)
709708

710709
def test_calculate_audio_length_ms_pure_function(self, model):
711710
"""Test the pure audio length calculation function."""
712711
from agents.realtime._util import calculate_audio_length_ms
713712

714713
# Test various audio buffer sizes for pcm16 format
715-
assert calculate_audio_length_ms("pcm16", b"test") == (4 / 24 / 2) * 1000 # 4 bytes
714+
expected_pcm = (len(b"test") / (24_000 * 2)) * 1000
715+
assert calculate_audio_length_ms("pcm16", b"test") == pytest.approx(
716+
expected_pcm, rel=0, abs=1e-6
717+
) # 4 bytes
716718
assert calculate_audio_length_ms("pcm16", b"") == 0 # empty
717-
assert calculate_audio_length_ms("pcm16", b"a" * 48) == 1000.0 # exactly 1000ms worth
719+
assert calculate_audio_length_ms("pcm16", b"a" * 48) == pytest.approx(
720+
(48 / (24_000 * 2)) * 1000, rel=0, abs=1e-6
721+
) # exactly 1ms worth
718722

719723
# Test g711 format
720724
assert calculate_audio_length_ms("g711_ulaw", b"test") == (4 / 8000) * 1000 # 4 bytes
@@ -741,7 +745,8 @@ async def test_handle_audio_delta_state_management(self, model):
741745
# Test that audio state is tracked correctly
742746
audio_state = model._audio_state_tracker.get_state("test_item", 5)
743747
assert audio_state is not None
744-
assert audio_state.audio_length_ms == (4 / 24 / 2) * 1000 # 4 bytes in milliseconds
748+
expected_ms = (len(b"test") / (24_000 * 2)) * 1000
749+
assert audio_state.audio_length_ms == pytest.approx(expected_ms, rel=0, abs=1e-6)
745750

746751
# Test that last audio item is tracked
747752
last_item = model._audio_state_tracker.get_last_audio_item()

tests/realtime/test_playback_tracker.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ def test_audio_state_accumulation_across_deltas(self):
6464

6565
state = tracker.get_state("item_1", 0)
6666
assert state is not None
67-
# Should accumulate: 8 bytes / 24 / 2 * 1000 = 166.67ms
68-
expected_length = (8 / 24 / 2) * 1000
69-
assert abs(state.audio_length_ms - expected_length) < 0.01
67+
# Should accumulate: 8 bytes -> 4 samples -> (4 / 24000) * 1000 ≈ 0.167ms
68+
expected_length = (8 / (24_000 * 2)) * 1000
69+
assert state.audio_length_ms == pytest.approx(expected_length, rel=0, abs=1e-6)
7070

7171
def test_state_cleanup_on_interruption(self):
7272
"""Test both trackers properly reset state on interruption."""
@@ -105,8 +105,9 @@ def test_audio_length_calculation_with_different_formats(self):
105105
# Test PCM format (24kHz, default)
106106
pcm_bytes = b"test" # 4 bytes
107107
pcm_length = calculate_audio_length_ms("pcm16", pcm_bytes)
108-
assert pcm_length == (4 / 24 / 2) * 1000 # ~83.33ms
108+
expected_pcm = (len(pcm_bytes) / (24_000 * 2)) * 1000
109+
assert pcm_length == pytest.approx(expected_pcm, rel=0, abs=1e-6)
109110

110111
# Test None format (defaults to PCM)
111112
none_length = calculate_audio_length_ms(None, pcm_bytes)
112-
assert none_length == pcm_length
113+
assert none_length == pytest.approx(expected_pcm, rel=0, abs=1e-6)

tests/realtime/test_playback_tracker_manual_unit.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@ def test_playback_tracker_on_play_bytes_and_state():
55
tr = RealtimePlaybackTracker()
66
tr.set_audio_format("pcm16") # PCM path
77

8-
# 48k bytes -> (48000 / 24 / 2) * 1000 = 1,000,000ms per current util
8+
# 48k bytes -> (48000 / (24000 * 2)) * 1000 = 1_000ms
99
tr.on_play_bytes("item1", 0, b"x" * 48000)
1010
st = tr.get_state()
1111
assert st["current_item_id"] == "item1"
12-
assert st["elapsed_ms"] and abs(st["elapsed_ms"] - 1_000_000.0) < 1e-6
12+
assert st["elapsed_ms"] and abs(st["elapsed_ms"] - 1_000.0) < 1e-6
1313

1414
# Subsequent play on same item accumulates
1515
tr.on_play_ms("item1", 0, 500.0)
1616
st2 = tr.get_state()
17-
assert st2["elapsed_ms"] and abs(st2["elapsed_ms"] - 1_000_500.0) < 1e-6
17+
assert st2["elapsed_ms"] and abs(st2["elapsed_ms"] - 1_500.0) < 1e-6
1818

1919
# Interruption clears state
2020
tr.on_interrupted()

0 commit comments

Comments
 (0)