wavekat_sip/rtp/dtmf.rs
1//! RFC 4733 DTMF (`telephone-event`) packet construction and transmission.
2//!
3//! A DTMF "press" rides RTP as a sequence of small (4-byte payload)
4//! event packets. The first three carry the marker bit and a short
5//! initial duration; continuation packets follow every 20 ms with a
6//! growing duration field; three end packets with the `E` bit set
7//! close out the burst. The three-fold redundancy at the start and end
8//! protects against single-packet loss; without it, a dropped marker
9//! could silently swallow the digit, and a dropped end could leave it
10//! "stuck on" at the receiver.
11//!
12//! ## Why a separate SSRC
13//!
14//! RFC 4733 §2.6.2 explicitly allows telephone-event to ride either
15//! the audio SSRC (interleaved) or its own SSRC (a separate RTP
16//! stream). We pick the latter: it lets DTMF be sent without
17//! coordinating the sequence-number / timestamp counter with the
18//! audio send loop, keeps both code paths simple, and is what the
19//! receiver demuxes by payload type anyway. Pick a fresh random SSRC
20//! per call and keep it stable for that call's lifetime.
21//!
22//! ## Example
23//!
24//! ```no_run
25//! use std::net::SocketAddr;
26//! use std::sync::Arc;
27//! use tokio::net::UdpSocket;
28//! use wavekat_sip::rtp::dtmf::{send_dtmf_burst, DtmfBurstConfig, DtmfDigit};
29//!
30//! # async fn ex(socket: Arc<UdpSocket>, remote: SocketAddr) -> std::io::Result<()> {
31//! let mut seq = 1_000u16;
32//! let mut ts = 0_u32;
33//! let cfg = DtmfBurstConfig {
34//! payload_type: 101,
35//! ssrc: 0xDEAD_BEEF,
36//! initial_seq: seq,
37//! initial_timestamp: ts,
38//! hold_duration_ms: 160,
39//! volume_dbm0: 10,
40//! };
41//! let (next_seq, next_ts) =
42//! send_dtmf_burst(socket, remote, cfg, DtmfDigit::D5).await?;
43//! seq = next_seq;
44//! ts = next_ts;
45//! # Ok(())
46//! # }
47//! ```
48//!
49//! [`send_dtmf_burst`] is `async` and `await`s ~20 ms between
50//! continuation packets, so a press of `hold_duration_ms = 160`
51//! takes roughly 160 ms wall-clock to complete.
52
53use std::net::SocketAddr;
54use std::sync::Arc;
55
56use tokio::net::UdpSocket;
57use tokio::time::{sleep, Duration};
58use tracing::debug;
59
60/// Default volume (dBm0) advertised in each event packet.
61///
62/// 10 dBm0 is the recommended fall-back per RFC 4733 §2.5.2.3 and the
63/// value every major softphone defaults to. The valid range is 0-63
64/// (each unit is one dB below the reference; higher = quieter).
65pub const DEFAULT_VOLUME_DBM0: u8 = 10;
66
67/// 20 ms at an 8 kHz clock = 160 sample ticks. The RTP timestamp clock
68/// for `telephone-event/8000` is 8 kHz regardless of the audio codec.
69const TICKS_PER_PACKET: u16 = 160;
70
71/// Inter-packet gap for continuation packets.
72const PACKET_INTERVAL: Duration = Duration::from_millis(20);
73
74/// Recommended number of duplicate copies for the first and last
75/// packets of a burst (RFC 4733 §2.5.1.4 — redundancy against single
76/// packet loss).
77const REDUNDANCY: u8 = 3;
78
79/// A single DTMF digit per RFC 4733 §2.5.2.1 event codes 0-15.
80///
81/// `D0`-`D9`, `Star`, `Pound`, `A`-`D`. The letter variants are vestigial
82/// (US AUTOVON keypad) and almost never appear in practice; consumers
83/// should typically only expose the digits and `* #` in their UI.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
85pub enum DtmfDigit {
86 D0,
87 D1,
88 D2,
89 D3,
90 D4,
91 D5,
92 D6,
93 D7,
94 D8,
95 D9,
96 Star,
97 Pound,
98 A,
99 B,
100 C,
101 D,
102}
103
104impl DtmfDigit {
105 /// Parse a digit from one of `0`-`9`, `*`, `#`, or `A`-`D`
106 /// (case-insensitive for the letters). Returns `None` for anything
107 /// else, including whitespace.
108 pub fn from_char(c: char) -> Option<Self> {
109 Some(match c {
110 '0' => Self::D0,
111 '1' => Self::D1,
112 '2' => Self::D2,
113 '3' => Self::D3,
114 '4' => Self::D4,
115 '5' => Self::D5,
116 '6' => Self::D6,
117 '7' => Self::D7,
118 '8' => Self::D8,
119 '9' => Self::D9,
120 '*' => Self::Star,
121 '#' => Self::Pound,
122 'a' | 'A' => Self::A,
123 'b' | 'B' => Self::B,
124 'c' | 'C' => Self::C,
125 'd' | 'D' => Self::D,
126 _ => return None,
127 })
128 }
129
130 /// The canonical character for this digit, e.g. `5` for `D5`,
131 /// `*` for `Star`, `A` (uppercase) for `A`.
132 pub fn as_char(self) -> char {
133 match self {
134 Self::D0 => '0',
135 Self::D1 => '1',
136 Self::D2 => '2',
137 Self::D3 => '3',
138 Self::D4 => '4',
139 Self::D5 => '5',
140 Self::D6 => '6',
141 Self::D7 => '7',
142 Self::D8 => '8',
143 Self::D9 => '9',
144 Self::Star => '*',
145 Self::Pound => '#',
146 Self::A => 'A',
147 Self::B => 'B',
148 Self::C => 'C',
149 Self::D => 'D',
150 }
151 }
152
153 /// Map an RFC 4733 §2.5.2.1 event code back to a digit — the
154 /// inverse of [`DtmfDigit::event_code`]. Returns `None` for codes
155 /// ≥ 16 (flash-hook and the other non-DTMF telephone events).
156 pub fn from_event_code(code: u8) -> Option<Self> {
157 Some(match code {
158 0 => Self::D0,
159 1 => Self::D1,
160 2 => Self::D2,
161 3 => Self::D3,
162 4 => Self::D4,
163 5 => Self::D5,
164 6 => Self::D6,
165 7 => Self::D7,
166 8 => Self::D8,
167 9 => Self::D9,
168 10 => Self::Star,
169 11 => Self::Pound,
170 12 => Self::A,
171 13 => Self::B,
172 14 => Self::C,
173 15 => Self::D,
174 _ => return None,
175 })
176 }
177
178 /// The RFC 4733 §2.5.2.1 event code (0-15) for this digit.
179 pub fn event_code(self) -> u8 {
180 match self {
181 Self::D0 => 0,
182 Self::D1 => 1,
183 Self::D2 => 2,
184 Self::D3 => 3,
185 Self::D4 => 4,
186 Self::D5 => 5,
187 Self::D6 => 6,
188 Self::D7 => 7,
189 Self::D8 => 8,
190 Self::D9 => 9,
191 Self::Star => 10,
192 Self::Pound => 11,
193 Self::A => 12,
194 Self::B => 13,
195 Self::C => 14,
196 Self::D => 15,
197 }
198 }
199}
200
201/// Build the 4-byte RFC 4733 event payload.
202///
203/// Layout (RFC 4733 §2.5.2):
204///
205/// ```text
206/// 0 1 2 3
207/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
208/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
209/// | event |E|R| volume | duration |
210/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
211/// ```
212///
213/// - `event`: digit code 0-15 (see [`DtmfDigit::event_code`]).
214/// - `E`: end-of-event flag (set on the last packet of a burst).
215/// - `R`: reserved (always 0).
216/// - `volume`: power in dBm0 (0-63, lower magnitude = louder; see
217/// [`DEFAULT_VOLUME_DBM0`]). Clamped to 6 bits.
218/// - `duration`: how long the event has been going so far, in RTP
219/// timestamp ticks (8 kHz). Grows across continuation packets.
220pub fn build_event_payload(event: u8, end: bool, volume: u8, duration_ticks: u16) -> [u8; 4] {
221 let mut out = [0u8; 4];
222 out[0] = event;
223 // Bit 7 = E (end), bit 6 = R (reserved/0), bits 0-5 = volume.
224 let end_bit = if end { 0x80 } else { 0x00 };
225 out[1] = end_bit | (volume & 0x3F);
226 let dur = duration_ticks.to_be_bytes();
227 out[2] = dur[0];
228 out[3] = dur[1];
229 out
230}
231
232/// Settings for a single DTMF burst.
233///
234/// Each press of one digit is one burst. For multi-digit dialing,
235/// pace the calls to [`send_dtmf_burst`] yourself — typically with an
236/// inter-digit gap of ≥50 ms so the receiver can demarcate them.
237#[derive(Debug, Clone, Copy)]
238pub struct DtmfBurstConfig {
239 /// Negotiated `telephone-event` payload type — typically 101. Get
240 /// this from [`crate::RemoteMedia::dtmf_payload_type`]; if it's
241 /// `None`, the remote didn't agree to RFC 4733 and DTMF must be
242 /// sent via SIP INFO instead.
243 pub payload_type: u8,
244 /// SSRC for the DTMF stream. Pick a fresh random value per call
245 /// (distinct from the audio stream's SSRC, per the module docs).
246 pub ssrc: u32,
247 /// RTP sequence number for the first packet of this burst. The
248 /// returned `(next_seq, _)` tuple is the seq to pass on the next
249 /// burst — keep the counter monotonic for the lifetime of the
250 /// SSRC, per RFC 3550.
251 pub initial_seq: u16,
252 /// RTP timestamp for the *start* of the event. All packets in
253 /// this burst share this value (RFC 4733 §2.5.1.2 — the start
254 /// time of the event being marked, not the time of the packet).
255 /// Pass the previous burst's returned `next_ts` for back-to-back
256 /// digits — that step keeps the timeline monotonic without
257 /// running a clock between presses.
258 pub initial_timestamp: u32,
259 /// How long to hold the digit, in milliseconds. Typical: 100-200.
260 /// The actual on-wire duration field uses 8 kHz ticks; this is
261 /// rounded to the nearest whole packet (`PACKET_INTERVAL` = 20 ms).
262 pub hold_duration_ms: u32,
263 /// Volume in dBm0 (0-63). See [`DEFAULT_VOLUME_DBM0`].
264 pub volume_dbm0: u8,
265}
266
267/// Build the full RTP packet (12-byte header + 4-byte event payload).
268///
269/// Exposed for tests and for consumers that want to send DTMF via a
270/// transport other than [`send_dtmf_burst`] (for example, batching
271/// packets onto a shared send loop).
272pub fn build_rtp_dtmf_packet(
273 payload_type: u8,
274 marker: bool,
275 seq: u16,
276 timestamp: u32,
277 ssrc: u32,
278 event_payload: [u8; 4],
279) -> [u8; 16] {
280 let mut pkt = [0u8; 16];
281 // V=2, no padding, no extension, CSRC count = 0.
282 pkt[0] = 0x80;
283 // Marker bit + payload type.
284 let marker_bit = if marker { 0x80 } else { 0x00 };
285 pkt[1] = marker_bit | (payload_type & 0x7F);
286 pkt[2..4].copy_from_slice(&seq.to_be_bytes());
287 pkt[4..8].copy_from_slice(×tamp.to_be_bytes());
288 pkt[8..12].copy_from_slice(&ssrc.to_be_bytes());
289 pkt[12..16].copy_from_slice(&event_payload);
290 pkt
291}
292
293/// Send one digit as a burst of RFC 4733 event packets on the given
294/// socket, addressed to `remote`. Returns `(next_seq, next_timestamp)`
295/// — the values to thread into the next burst on the same SSRC.
296///
297/// `next_timestamp` is `initial_timestamp + final_duration_ticks`, so
298/// chaining bursts produces a monotonically increasing RTP timeline
299/// even when there's no audio on the same SSRC to fill the gaps.
300///
301/// Network policy: this function `await`s a UDP send per packet plus
302/// `sleep`s `PACKET_INTERVAL` between continuation packets. A digit
303/// with `hold_duration_ms = 160` therefore takes ≈ 160 ms wall-clock
304/// to finish. Cancel by dropping the returned future or by aborting
305/// the surrounding task — partial bursts are valid RTP (the receiver
306/// will see the missing end as packet loss and stop the tone).
307pub async fn send_dtmf_burst(
308 socket: Arc<UdpSocket>,
309 remote: SocketAddr,
310 config: DtmfBurstConfig,
311 digit: DtmfDigit,
312) -> Result<(u16, u32), std::io::Error> {
313 let event = digit.event_code();
314 let volume = config.volume_dbm0.min(0x3F);
315
316 debug!(
317 "DTMF burst '{}' → {remote} (PT={}, SSRC=0x{:08X}, hold={}ms)",
318 digit.as_char(),
319 config.payload_type,
320 config.ssrc,
321 config.hold_duration_ms,
322 );
323
324 // Total packets in the press: at least the 3 initial + 3 end.
325 // Continuation packets fill in the middle every 20 ms.
326 let total_packets = config.hold_duration_ms.div_ceil(20).max(1) as u16;
327 let total_duration_ticks = total_packets.saturating_mul(TICKS_PER_PACKET);
328
329 let mut seq = config.initial_seq;
330 let mut current_duration = TICKS_PER_PACKET;
331
332 // Marker bit + first three copies — `E=0`, duration = one tick.
333 // All three share the marker'd timestamp; the receiver de-dupes by
334 // seq/duration but treats the first arrival as the start of the
335 // event.
336 for i in 0..REDUNDANCY {
337 let payload = build_event_payload(event, false, volume, current_duration);
338 let marker = i == 0;
339 let pkt = build_rtp_dtmf_packet(
340 config.payload_type,
341 marker,
342 seq,
343 config.initial_timestamp,
344 config.ssrc,
345 payload,
346 );
347 socket.send_to(&pkt, remote).await?;
348 seq = seq.wrapping_add(1);
349 }
350
351 // Continuation packets — one per 20 ms tick, growing duration.
352 // `total_packets - 1` because the initial trio already covered the
353 // first tick.
354 for _ in 1..total_packets {
355 sleep(PACKET_INTERVAL).await;
356 current_duration = current_duration.saturating_add(TICKS_PER_PACKET);
357 let payload = build_event_payload(event, false, volume, current_duration);
358 let pkt = build_rtp_dtmf_packet(
359 config.payload_type,
360 false,
361 seq,
362 config.initial_timestamp,
363 config.ssrc,
364 payload,
365 );
366 socket.send_to(&pkt, remote).await?;
367 seq = seq.wrapping_add(1);
368 }
369
370 // Three end packets — `E=1`, final duration. Sent back-to-back at
371 // this tick boundary; no sleep between them.
372 for _ in 0..REDUNDANCY {
373 let payload = build_event_payload(event, true, volume, current_duration);
374 let pkt = build_rtp_dtmf_packet(
375 config.payload_type,
376 false,
377 seq,
378 config.initial_timestamp,
379 config.ssrc,
380 payload,
381 );
382 socket.send_to(&pkt, remote).await?;
383 seq = seq.wrapping_add(1);
384 }
385
386 let next_timestamp = config
387 .initial_timestamp
388 .wrapping_add(total_duration_ticks as u32);
389 Ok((seq, next_timestamp))
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 #[test]
397 fn digit_from_char_round_trips() {
398 for d in [
399 DtmfDigit::D0,
400 DtmfDigit::D1,
401 DtmfDigit::D2,
402 DtmfDigit::D3,
403 DtmfDigit::D4,
404 DtmfDigit::D5,
405 DtmfDigit::D6,
406 DtmfDigit::D7,
407 DtmfDigit::D8,
408 DtmfDigit::D9,
409 DtmfDigit::Star,
410 DtmfDigit::Pound,
411 DtmfDigit::A,
412 DtmfDigit::B,
413 DtmfDigit::C,
414 DtmfDigit::D,
415 ] {
416 assert_eq!(DtmfDigit::from_char(d.as_char()), Some(d), "digit {d:?}");
417 }
418 }
419
420 #[test]
421 fn digit_from_event_code_round_trips() {
422 for code in 0u8..16 {
423 let d = DtmfDigit::from_event_code(code).expect("codes 0-15 are digits");
424 assert_eq!(d.event_code(), code, "code {code}");
425 }
426 }
427
428 #[test]
429 fn digit_from_event_code_rejects_non_dtmf_events() {
430 // 16 = flash-hook, and everything above is fax/modem signaling
431 // per the RFC 4733 event registry — not keypad digits.
432 for code in [16u8, 17, 63, 255] {
433 assert_eq!(DtmfDigit::from_event_code(code), None, "code {code}");
434 }
435 }
436
437 #[test]
438 fn digit_event_codes_match_rfc_4733() {
439 // Spot-check the spec assignments — these are wire-visible and a
440 // typo would scramble every IVR's "you pressed 7" response.
441 assert_eq!(DtmfDigit::D0.event_code(), 0);
442 assert_eq!(DtmfDigit::D9.event_code(), 9);
443 assert_eq!(DtmfDigit::Star.event_code(), 10);
444 assert_eq!(DtmfDigit::Pound.event_code(), 11);
445 assert_eq!(DtmfDigit::A.event_code(), 12);
446 assert_eq!(DtmfDigit::D.event_code(), 15);
447 }
448
449 #[test]
450 fn digit_from_char_rejects_non_dtmf() {
451 for c in [' ', 'e', 'E', '+', '-', '\n'] {
452 assert_eq!(DtmfDigit::from_char(c), None, "should reject {c:?}");
453 }
454 }
455
456 #[test]
457 fn digit_from_char_accepts_letters_case_insensitive() {
458 assert_eq!(DtmfDigit::from_char('a'), Some(DtmfDigit::A));
459 assert_eq!(DtmfDigit::from_char('A'), Some(DtmfDigit::A));
460 assert_eq!(DtmfDigit::from_char('d'), Some(DtmfDigit::D));
461 assert_eq!(DtmfDigit::from_char('D'), Some(DtmfDigit::D));
462 }
463
464 #[test]
465 fn event_payload_byte_layout() {
466 // event=5, E=0, volume=10, duration=160 — typical first packet
467 // of pressing "5".
468 let p = build_event_payload(5, false, 10, 160);
469 assert_eq!(p[0], 5);
470 // 0x0A = 10; E and R both 0.
471 assert_eq!(p[1], 0x0A);
472 assert_eq!(p[2..4], 160u16.to_be_bytes());
473
474 // E bit set on the last packet of a burst.
475 let end = build_event_payload(5, true, 10, 1280);
476 // E bit (0x80) plus volume 10.
477 assert_eq!(end[1], 0x8A);
478 assert_eq!(end[2..4], 1280u16.to_be_bytes());
479 }
480
481 #[test]
482 fn event_payload_clamps_volume_to_6_bits() {
483 // A consumer passing volume=100 (out of the 0-63 range) must not
484 // bleed into the E or R bits — the volume field is 6 bits.
485 let p = build_event_payload(5, false, 0xFF, 160);
486 // Top two bits must be zero (E=0, R=0); bottom six = volume.
487 assert_eq!(p[1] & 0xC0, 0x00);
488 assert_eq!(p[1] & 0x3F, 0x3F);
489 }
490
491 #[test]
492 fn rtp_dtmf_packet_header_shape() {
493 // Single packet build: V=2, marker set, PT=101, seq/ts/ssrc as
494 // given, event payload appended verbatim.
495 let event = build_event_payload(5, false, 10, 160);
496 let pkt = build_rtp_dtmf_packet(101, true, 1000, 12345, 0xCAFE_BABE, event);
497
498 assert_eq!(pkt[0], 0x80); // V=2, P=0, X=0, CC=0
499 assert_eq!(pkt[1], 0x80 | 101); // marker | PT 101
500 assert_eq!(&pkt[2..4], &1000u16.to_be_bytes());
501 assert_eq!(&pkt[4..8], &12345u32.to_be_bytes());
502 assert_eq!(&pkt[8..12], &0xCAFE_BABEu32.to_be_bytes());
503 assert_eq!(&pkt[12..16], &event);
504 }
505
506 /// Bind a pair of UDP sockets on loopback. Returns (sender, receiver).
507 async fn loopback_pair() -> (Arc<UdpSocket>, UdpSocket) {
508 let a = UdpSocket::bind("127.0.0.1:0").await.unwrap();
509 let b = UdpSocket::bind("127.0.0.1:0").await.unwrap();
510 (Arc::new(a), b)
511 }
512
513 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
514 async fn burst_packet_count_and_durations() {
515 // A 100 ms press at 20 ms per tick is 5 ticks. With 3 initial
516 // duplicates and 3 end duplicates we expect:
517 // 3 (initial, dur=160)
518 // + 4 (continuation, dur=320, 480, 640, 800)
519 // + 3 (end, dur=800, E=1)
520 // = 10 packets. The continuation count is `total_packets - 1`
521 // because the initial trio already covered the first tick.
522 let (sender, receiver) = loopback_pair().await;
523 let remote = receiver.local_addr().unwrap();
524
525 let cfg = DtmfBurstConfig {
526 payload_type: 101,
527 ssrc: 0xDEAD_BEEF,
528 initial_seq: 100,
529 initial_timestamp: 5000,
530 hold_duration_ms: 100,
531 volume_dbm0: 10,
532 };
533
534 let handle = tokio::spawn(send_dtmf_burst(sender, remote, cfg, DtmfDigit::D5));
535
536 let mut buf = [0u8; 64];
537 let mut packets: Vec<[u8; 4]> = Vec::new();
538 let mut markers: Vec<bool> = Vec::new();
539 let mut seqs: Vec<u16> = Vec::new();
540 for _ in 0..10 {
541 let (n, _) =
542 tokio::time::timeout(Duration::from_millis(500), receiver.recv_from(&mut buf))
543 .await
544 .expect("packet arrived in time")
545 .expect("recv ok");
546 assert_eq!(n, 16, "DTMF packets are 12 + 4 bytes");
547 markers.push(buf[1] & 0x80 != 0);
548 seqs.push(u16::from_be_bytes([buf[2], buf[3]]));
549 let mut payload = [0u8; 4];
550 payload.copy_from_slice(&buf[12..16]);
551 packets.push(payload);
552 }
553
554 let (next_seq, next_ts) = handle.await.unwrap().unwrap();
555 assert_eq!(next_seq, 100 + 10);
556 // 5 ticks × 160 = 800
557 assert_eq!(next_ts, 5000 + 800);
558
559 // Sequence numbers strictly increasing by 1 from initial_seq.
560 for (i, s) in seqs.iter().enumerate() {
561 assert_eq!(*s, 100 + i as u16, "packet {i}");
562 }
563
564 // First packet has marker; nothing else does. (Per RFC 4733
565 // §2.5.1.1 the marker rides only on the first packet of the
566 // talkspurt.)
567 assert!(markers[0], "marker on first packet");
568 for (i, m) in markers.iter().enumerate().skip(1) {
569 assert!(!m, "no marker on packet {i}");
570 }
571
572 // Each payload's first byte = event code 5.
573 for p in &packets {
574 assert_eq!(p[0], 5);
575 }
576
577 // Initial three: E=0, dur=160.
578 for p in &packets[0..3] {
579 assert_eq!(p[1] & 0x80, 0, "E bit clear on initial");
580 assert_eq!(u16::from_be_bytes([p[2], p[3]]), 160);
581 }
582
583 // Continuation 4 packets: E=0, dur grows 320, 480, 640, 800.
584 let expected_durations = [320u16, 480, 640, 800];
585 for (p, &dur) in packets[3..7].iter().zip(expected_durations.iter()) {
586 assert_eq!(p[1] & 0x80, 0, "E bit clear on continuation");
587 assert_eq!(u16::from_be_bytes([p[2], p[3]]), dur);
588 }
589
590 // End three: E=1, dur=800 (the final tick).
591 for p in &packets[7..10] {
592 assert_eq!(p[1] & 0x80, 0x80, "E bit set on end packet");
593 assert_eq!(u16::from_be_bytes([p[2], p[3]]), 800);
594 }
595 }
596
597 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
598 async fn chained_bursts_keep_timestamp_monotonic() {
599 // Pressing "5" then "7" back-to-back must produce a continuous
600 // RTP timeline on the same SSRC, with the second burst's
601 // timestamps starting where the first left off.
602 let (sender, receiver) = loopback_pair().await;
603 let remote = receiver.local_addr().unwrap();
604
605 let cfg1 = DtmfBurstConfig {
606 payload_type: 101,
607 ssrc: 0xCAFE_F00D,
608 initial_seq: 0,
609 initial_timestamp: 0,
610 hold_duration_ms: 40, // 2 ticks → 3 init + 1 cont + 3 end = 7 packets
611 volume_dbm0: 10,
612 };
613
614 let receiver_task = tokio::spawn(async move {
615 let mut buf = [0u8; 64];
616 let mut count = 0;
617 while count < 14 {
618 let (_n, _) =
619 tokio::time::timeout(Duration::from_millis(500), receiver.recv_from(&mut buf))
620 .await
621 .unwrap()
622 .unwrap();
623 count += 1;
624 }
625 });
626
627 let (s1, t1) = send_dtmf_burst(sender.clone(), remote, cfg1, DtmfDigit::D5)
628 .await
629 .unwrap();
630 // 7 packets, 2 ticks × 160 = 320.
631 assert_eq!(s1, 7);
632 assert_eq!(t1, 320);
633
634 let cfg2 = DtmfBurstConfig {
635 initial_seq: s1,
636 initial_timestamp: t1,
637 ..cfg1
638 };
639 let (s2, t2) = send_dtmf_burst(sender, remote, cfg2, DtmfDigit::D7)
640 .await
641 .unwrap();
642 assert_eq!(s2, 14);
643 assert_eq!(t2, 640);
644
645 receiver_task.await.unwrap();
646 }
647}