Skip to content

Audio Streaming

Audio RX/TX via the Icom audio UDP port (default 50003).

Naming Map

Low-level Opus methods are now explicitly suffixed with _opus. High-level PCM APIs are available for both RX and TX.

Scope Preferred method names
Low-level Opus (current) start_audio_rx_opus, stop_audio_rx_opus, start_audio_tx_opus, push_audio_tx_opus, stop_audio_tx_opus, start_audio_opus, stop_audio_opus
High-level PCM start_audio_rx_pcm, stop_audio_rx_pcm, start_audio_tx_pcm, push_audio_tx_pcm, stop_audio_tx_pcm

Deprecated aliases still work during the deprecation window (two minor releases): start_audio_rx, stop_audio_rx, start_audio_tx, push_audio_tx, stop_audio_tx, start_audio, stop_audio.

AudioStream

icom_lan.audio.AudioStream

Manages audio RX/TX on the Icom audio UDP port.

Uses an :class:IcomTransport for the underlying UDP communication (discovery, pings, retransmit). Audio-specific packet framing is handled here.

Parameters:

Name Type Description Default
transport IcomTransport

Connected IcomTransport for the audio port.

required

Example::

stream = AudioStream(audio_transport)
await stream.start_rx(my_callback)
# ... later
await stream.stop_rx()

state property

Current audio stream state.

transport property

Underlying transport.

get_audio_stats()

Return runtime audio stats for the current stream.

Metrics and units:

  • rx_packets_received / rx_packets_delivered / tx_packets_sent: packet counters (>= 0).
  • packets_lost: inferred missing RX packets (>= 0).
  • packet_loss_percent: percentage in [0.0, 100.0].
  • jitter_ms / jitter_max_ms: sequence-jitter estimates in ms (>= 0.0).
  • underrun_count / overrun_count: jitter-buffer event counters (>= 0).
  • estimated_latency_ms: current buffering latency estimate in ms (>= 0.0).
  • jitter_buffer_depth_packets / jitter_buffer_pending_packets: packet counts (>= 0).

push_tx(opus_data) async

Send an Opus-encoded audio frame to the radio.

Parameters:

Name Type Description Default
opus_data bytes

Opus-encoded audio data.

required

Raises:

Type Description
RuntimeError

If not in transmitting state.

start_rx(callback, *, jitter_depth=None) async

Start receiving audio from the radio.

Parameters:

Name Type Description Default
callback Callable[[AudioPacket | None], None]

Called with each decoded :class:AudioPacket. When jitter buffering is enabled, None may be passed for gap placeholders (missing packets).

required
jitter_depth int | None

Override jitter buffer depth (0 to disable). Defaults to the value set at construction time.

None

Raises:

Type Description
RuntimeError

If already receiving or transmitting.

start_tx() async

Start transmitting audio to the radio.

Can be called while already receiving (full-duplex).

Raises:

Type Description
RuntimeError

If already transmitting.

stop_rx() async

Stop receiving audio and flush remaining buffered packets.

stop_tx() async

Stop transmitting audio.

If RX is still active, state reverts to RECEIVING.

Runtime Audio Stats (get_audio_stats)

Use get_audio_stats() on AudioStream or IcomRadio to retrieve a JSON-friendly snapshot of live stream quality metrics.

stats = radio.get_audio_stats()
print(stats["packet_loss_percent"], stats["jitter_ms"])

Metrics, Units, Bounds

Field Unit Bounds Notes
active boolean true/false Whether stream state is not idle
state string idle / receiving / transmitting Current stream state
rx_packets_received packets >= 0 Parsed RX audio packets
rx_packets_delivered packets >= 0 RX packets delivered to callback
tx_packets_sent packets >= 0 TX packets sent
packets_lost packets >= 0 Inferred missing RX packets
packet_loss_percent percent 0.0..100.0 packets_lost / (delivered + lost)
jitter_ms milliseconds >= 0.0 Smoothed sequence-jitter estimate
jitter_max_ms milliseconds >= 0.0 Peak observed jitter estimate
underrun_count events >= 0 Jitter-buffer underrun events
overrun_count events >= 0 Jitter-buffer overrun events
estimated_latency_ms milliseconds >= 0.0 Estimated buffering delay
jitter_buffer_depth_packets packets >= 0 Configured jitter depth (0 when disabled)
jitter_buffer_pending_packets packets >= 0 Currently buffered packets
duplicates_dropped packets >= 0 Duplicate RX packets dropped
stale_packets_dropped packets >= 0 Stale/old RX packets dropped
out_of_order_packets packets >= 0 RX packets observed out of sequence

AudioPacket

icom_lan.audio.AudioPacket dataclass

Parsed audio packet.

Attributes:

Name Type Description
ident int

Audio stream identifier (0x0080 for TX, varies for RX).

send_seq int

Audio-level sequence number.

data bytes

Opus-encoded audio data (raw bytes after header).

AudioState

icom_lan.audio.AudioState

Bases: StrEnum

Audio stream state.

JitterBuffer

icom_lan.audio.JitterBuffer

Reorder-and-delay buffer for incoming audio packets.

Collects packets and delivers them in sequence-number order after a configurable depth of buffering. Handles out-of-order packets, duplicates, and gaps (delivering None for missing packets).

Parameters:

Name Type Description Default
depth int

Number of packets to buffer before delivery (default 5, which is ~100 ms at 20 ms/packet).

5

Example::

jb = JitterBuffer(depth=5)
for pkt in jb.push(audio_packet):
    if pkt is None:
        # gap — insert silence
        ...
    else:
        play(pkt.data)

depth property

Configured buffer depth (number of packets).

duplicate_count property

Count of duplicate packets dropped.

gap_count property

Count of inferred missing packets (gap placeholders).

overrun_count property

Count of jitter-buffer overrun events.

pending property

Number of packets currently held in the buffer.

stale_count property

Count of stale/old packets dropped.

underrun_count property

Count of jitter-buffer underrun events.

flush()

Flush all buffered packets in order (for stream end).

Returns:

Type Description
list[AudioPacket | None]

Remaining packets in order (None for gaps).

push(packet)

Insert a packet and return any packets ready for delivery.

Packets are delivered in order. If a gap is detected (missing sequence number), None is yielded in its place.

Parameters:

Name Type Description Default
packet AudioPacket

Incoming audio packet.

required

Returns:

Type Description
list[AudioPacket | None]

List of packets (or None for gaps) ready for playback.

list[AudioPacket | None]

May be empty if more buffering is needed.

Packet Functions

icom_lan.audio.parse_audio_packet(data)

Parse a raw UDP audio packet into an :class:AudioPacket.

Parameters:

Name Type Description Default
data bytes

Raw UDP packet bytes (must be > 0x18 bytes).

required

Returns:

Type Description
AudioPacket | None

Parsed AudioPacket, or None if the packet is too short or

AudioPacket | None

is a control/retransmit packet (type != DATA).

icom_lan.audio.build_audio_packet(opus_data, *, sender_id, receiver_id, send_seq, ident=TX_IDENT)

Build a raw UDP audio packet from Opus data.

Parameters:

Name Type Description Default
opus_data bytes

Opus-encoded audio frame.

required
sender_id int

Our connection ID.

required
receiver_id int

Radio's connection ID.

required
send_seq int

Audio-level sequence number.

required
ident int

Audio ident field (default TX_IDENT=0x0080).

TX_IDENT

Returns:

Type Description
bytes

Complete UDP packet bytes ready to send.

Internal Transcoder Layer

icom_lan now includes an internal PCM<->Opus transcoder foundation used for future high-level PCM APIs.

  • Module: icom_lan._audio_transcoder (internal, no stability guarantee yet)
  • Backend: optional opuslib (pip install icom-lan[audio])
  • Typed failures:
  • AudioCodecBackendError for missing backend
  • AudioFormatError for invalid PCM/Opus frame formats
  • AudioTranscodeError for codec encode/decode failures

Usage

RX Audio (callback-based)

async with IcomRadio("192.168.1.100", username="u", password="p") as radio:
    received = []

    def on_audio(pkt):
        if pkt is not None:  # None = gap (missing packet)
            received.append(pkt.data)

    await radio.start_audio_rx_opus(on_audio)
    await asyncio.sleep(10)
    await radio.stop_audio_rx_opus()

RX Audio (high-level PCM)

async with IcomRadio("192.168.1.100", username="u", password="p") as radio:
    def on_pcm(frame: bytes | None) -> None:
        if frame is None:
            return  # gap placeholder from jitter buffer
        # frame is 16-bit little-endian PCM for configured format
        process_pcm(frame)

    await radio.start_audio_rx_pcm(
        on_pcm,
        sample_rate=48000,
        channels=1,
        frame_ms=20,
        jitter_depth=5,
    )
    await asyncio.sleep(10)
    await radio.stop_audio_rx_pcm()

TX Audio (push-based)

async with IcomRadio("192.168.1.100", username="u", password="p") as radio:
    await radio.start_audio_tx_opus()
    await radio.push_audio_tx_opus(opus_frame)
    await radio.stop_audio_tx_opus()

TX Audio (high-level PCM)

async with IcomRadio("192.168.1.100", username="u", password="p") as radio:
    await radio.start_audio_tx_pcm(sample_rate=48000, channels=1, frame_ms=20)
    await radio.push_audio_tx_pcm(pcm_frame)  # one 20ms PCM frame (1920 bytes)
    await radio.stop_audio_tx_pcm()

Full-Duplex

async with IcomRadio("192.168.1.100", username="u", password="p") as radio:
    await radio.start_audio_opus(rx_callback=on_audio, tx_enabled=True)
    # ... push TX frames, receive RX via callback ...
    await radio.stop_audio_opus()

Codec Selection

from icom_lan import IcomRadio, AudioCodec

radio = IcomRadio(
    "192.168.1.100",
    audio_codec=AudioCodec.PCM_1CH_16BIT,  # default
    audio_sample_rate=48000,
)

Capability Introspection

Use the capability API to inspect negotiated client-side audio options and defaults:

from icom_lan import IcomRadio

caps = IcomRadio.audio_capabilities()
print(caps.supported_codecs)
print(caps.supported_sample_rates_hz)
print(caps.supported_channels)
print(caps.default_codec, caps.default_sample_rate_hz, caps.default_channels)

Deterministic default selection rules:

  1. Codec: first supported codec in icom-lan preference order.
  2. Sample rate: highest supported sample rate.
  3. Channels: the channel count implied by default codec (fallback: minimum supported channels).

Opus codecs

OPUS_1CH (0x40) and OPUS_2CH (0x41) are only supported when the radio reports connection_type == "WFVIEW". Standard connections use LPCM16 (0x04).

Migration

Use the explicit _opus methods now:

Deprecated alias Replacement
start_audio_rx start_audio_rx_opus
stop_audio_rx stop_audio_rx_opus
start_audio_tx start_audio_tx_opus
push_audio_tx push_audio_tx_opus
stop_audio_tx stop_audio_tx_opus
start_audio start_audio_opus
stop_audio stop_audio_opus

For RX PCM, migrate callback-side decoding to the built-in API:

  • Before: start_audio_rx_opus() + manual Opus decode in callback.
  • Now: start_audio_rx_pcm() and receive bytes | None directly.

For TX PCM, migrate manual Opus encoding to the built-in API:

  • Before: encode PCM to Opus yourself, then push_audio_tx_opus().
  • Now: start_audio_tx_pcm() and push_audio_tx_pcm() with fixed-size PCM frames.