mirror of
https://github.com/esphome/esphome.git
synced 2026-10-22 05:46:12 +00:00
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: J. Nick Koston <nick@koston.org>
133 lines
4.5 KiB
Python
133 lines
4.5 KiB
Python
"""Tests for the usb_audio buffer floors, the one-platform-per-hub check and the USB FIFO requests."""
|
|
|
|
import pytest
|
|
|
|
from esphome import config_validation as cv
|
|
from esphome.components import usb_audio, usb_host
|
|
from esphome.components.usb_audio import (
|
|
CONF_USB_AUDIO_ID,
|
|
microphone as usb_microphone,
|
|
speaker as usb_speaker,
|
|
)
|
|
from esphome.config import Config
|
|
from esphome.const import (
|
|
CONF_BITS_PER_SAMPLE,
|
|
CONF_BUFFER_SIZE,
|
|
CONF_NUM_CHANNELS,
|
|
CONF_PLATFORM,
|
|
CONF_SAMPLE_RATE,
|
|
PlatformFramework,
|
|
)
|
|
from esphome.core import CORE, ID
|
|
from esphome.types import ConfigType
|
|
from tests.component_tests.types import SetCoreConfigCallable
|
|
|
|
|
|
def _stream(sample_rate: int, channels: int, bits: int, buffer_size: int) -> ConfigType:
|
|
return {
|
|
CONF_SAMPLE_RATE: sample_rate,
|
|
CONF_NUM_CHANNELS: channels,
|
|
CONF_BITS_PER_SAMPLE: float(bits),
|
|
CONF_BUFFER_SIZE: buffer_size,
|
|
}
|
|
|
|
|
|
def _entry(hub: str, platform: str = "usb_audio") -> ConfigType:
|
|
return {CONF_PLATFORM: platform, CONF_USB_AUDIO_ID: ID(hub)}
|
|
|
|
|
|
def _set_full_config(
|
|
set_core_config: SetCoreConfigCallable, **domains: list[ConfigType]
|
|
) -> None:
|
|
full = Config()
|
|
full.update(domains)
|
|
set_core_config(PlatformFramework.ESP32_IDF, full_config=full)
|
|
|
|
|
|
def test_speaker_buffer_floor() -> None:
|
|
# Two transfers of 3 packets x 48 frames x 4 bytes at 48 kHz 16-bit stereo.
|
|
usb_speaker.validate_buffer_size(_stream(48000, 2, 16, 1152))
|
|
with pytest.raises(cv.Invalid, match="at least 1152 bytes"):
|
|
usb_speaker.validate_buffer_size(_stream(48000, 2, 16, 1151))
|
|
|
|
|
|
def test_speaker_floor_rounds_up_fractional_rates() -> None:
|
|
# 44.1 kHz packets carry up to 45 frames.
|
|
with pytest.raises(cv.Invalid, match="at least 1080 bytes"):
|
|
usb_speaker.validate_buffer_size(_stream(44100, 2, 16, 1079))
|
|
|
|
|
|
def test_speaker_default_buffer_meets_floor() -> None:
|
|
usb_speaker.validate_buffer_size(_stream(96000, 2, 32, 6400))
|
|
|
|
|
|
def test_microphone_buffer_floor() -> None:
|
|
# 16 kHz 16-bit mono is 32000 bytes/s, so two read periods (40 ms) are 1280 bytes.
|
|
usb_microphone.validate_buffer_size(_stream(16000, 1, 16, 1280))
|
|
with pytest.raises(cv.Invalid, match="at least 1280 bytes"):
|
|
usb_microphone.validate_buffer_size(_stream(16000, 1, 16, 1279))
|
|
|
|
|
|
def _unset_buffer(sample_rate: int, channels: int, bits: int) -> ConfigType:
|
|
config = _stream(sample_rate, channels, bits, 0)
|
|
del config[CONF_BUFFER_SIZE]
|
|
return config
|
|
|
|
|
|
def test_default_buffer_size() -> None:
|
|
assert (
|
|
usb_speaker.validate_buffer_size(_unset_buffer(48000, 2, 16))[CONF_BUFFER_SIZE]
|
|
== 6400
|
|
)
|
|
assert (
|
|
usb_microphone.validate_buffer_size(_unset_buffer(16000, 1, 16))[
|
|
CONF_BUFFER_SIZE
|
|
]
|
|
== 6400
|
|
)
|
|
|
|
|
|
def test_default_buffer_size_grows_to_the_floor() -> None:
|
|
# 48 kHz 16-bit stereo is 192000 bytes/s, so 40 ms need 7680 bytes, more than the 6400 default.
|
|
assert (
|
|
usb_microphone.validate_buffer_size(_unset_buffer(48000, 2, 16))[
|
|
CONF_BUFFER_SIZE
|
|
]
|
|
== 7680
|
|
)
|
|
|
|
|
|
def test_one_speaker_per_hub_accepted(set_core_config: SetCoreConfigCallable) -> None:
|
|
entries = [_entry("hub_a"), _entry("hub_b"), _entry("hub_a", "i2s_audio")]
|
|
_set_full_config(set_core_config, speaker=entries)
|
|
usb_speaker.FINAL_VALIDATE_SCHEMA(entries[0])
|
|
usb_speaker.FINAL_VALIDATE_SCHEMA(entries[1])
|
|
|
|
|
|
def test_two_speakers_on_one_hub_rejected(
|
|
set_core_config: SetCoreConfigCallable,
|
|
) -> None:
|
|
entries = [_entry("hub_a"), _entry("hub_a")]
|
|
_set_full_config(set_core_config, speaker=entries)
|
|
with pytest.raises(cv.Invalid, match="Only one usb_audio speaker"):
|
|
usb_speaker.FINAL_VALIDATE_SCHEMA(entries[0])
|
|
|
|
|
|
def test_two_microphones_on_one_hub_rejected(
|
|
set_core_config: SetCoreConfigCallable,
|
|
) -> None:
|
|
entries = [_entry("hub_a"), _entry("hub_a")]
|
|
_set_full_config(set_core_config, microphone=entries, speaker=[_entry("hub_a")])
|
|
with pytest.raises(cv.Invalid, match="Only one usb_audio microphone"):
|
|
usb_microphone.FINAL_VALIDATE_SCHEMA(entries[0])
|
|
|
|
|
|
def test_platforms_request_usb_fifo_space(
|
|
set_core_config: SetCoreConfigCallable,
|
|
) -> None:
|
|
# 48 kHz 16-bit stereo packets carry up to 49 frames of 4 bytes; 16 kHz 16-bit mono up to 17 frames of 2 bytes.
|
|
set_core_config(PlatformFramework.ESP32_IDF)
|
|
usb_audio.request_speaker_fifo(_stream(48000, 2, 16, 6400))
|
|
usb_audio.request_microphone_fifo(_stream(16000, 1, 16, 6400))
|
|
assert CORE.data[usb_host.DOMAIN][usb_host.KEY_PERIODIC_FIFO] == [34, 196]
|