mirror of
https://github.com/esphome/esphome.git
synced 2026-10-22 22:06:12 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb034d38ea | ||
|
|
a49805ddc7 | ||
|
|
a240f89a91 | ||
|
|
58c0683f3b | ||
|
|
cf2131a367 |
@@ -1785,6 +1785,8 @@ message MediaPlayerSupportedFormat {
|
||||
uint32 num_channels = 3;
|
||||
MediaPlayerFormatPurpose purpose = 4;
|
||||
uint32 sample_bytes = 5;
|
||||
// Constant bitrate in kbps for lossy formats; 0 means no preference
|
||||
uint32 bitrate = 6;
|
||||
}
|
||||
message ListEntitiesMediaPlayerResponse {
|
||||
option (id) = 63;
|
||||
|
||||
@@ -1131,6 +1131,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec
|
||||
media_format.num_channels = supported_format.num_channels;
|
||||
media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose);
|
||||
media_format.sample_bytes = supported_format.sample_bytes;
|
||||
media_format.bitrate = supported_format.bitrate;
|
||||
}
|
||||
return fill_and_encode_entity_info(media_player, msg, conn, remaining_size);
|
||||
}
|
||||
|
||||
@@ -2373,6 +2373,7 @@ uint8_t *MediaPlayerSupportedFormat::encode_msg(const void *self, uint8_t *__res
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.num_channels);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, static_cast<uint32_t>(msg.purpose));
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.sample_bytes);
|
||||
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.bitrate);
|
||||
return pos;
|
||||
}
|
||||
uint32_t MediaPlayerSupportedFormat::calc_size_msg(const void *self) {
|
||||
@@ -2383,6 +2384,7 @@ uint32_t MediaPlayerSupportedFormat::calc_size_msg(const void *self) {
|
||||
size += ProtoSize::calc_uint32(1, msg.num_channels);
|
||||
size += msg.purpose ? 2 : 0;
|
||||
size += ProtoSize::calc_uint32(1, msg.sample_bytes);
|
||||
size += ProtoSize::calc_uint32(1, msg.bitrate);
|
||||
return size;
|
||||
}
|
||||
uint8_t *ListEntitiesMediaPlayerResponse::encode_msg(const void *self,
|
||||
|
||||
@@ -2263,6 +2263,7 @@ class MediaPlayerSupportedFormat final : public ProtoMessage {
|
||||
uint32_t num_channels{0};
|
||||
enums::MediaPlayerFormatPurpose purpose{};
|
||||
uint32_t sample_bytes{0};
|
||||
uint32_t bitrate{0};
|
||||
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
|
||||
|
||||
@@ -2099,6 +2099,7 @@ const char *MediaPlayerSupportedFormat::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, ESPHOME_PSTR("num_channels"), this->num_channels);
|
||||
dump_field(out, ESPHOME_PSTR("purpose"), static_cast<enums::MediaPlayerFormatPurpose>(this->purpose));
|
||||
dump_field(out, ESPHOME_PSTR("sample_bytes"), this->sample_bytes);
|
||||
dump_field(out, ESPHOME_PSTR("bitrate"), this->bitrate);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ListEntitiesMediaPlayerResponse::dump_to(DumpBuffer &out) const {
|
||||
|
||||
@@ -3,6 +3,7 @@ from collections.abc import Callable
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import audio
|
||||
from esphome.components.const import CONF_BITRATE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ENTITY_CATEGORY,
|
||||
@@ -55,6 +56,29 @@ FORMAT_MAPPING = {
|
||||
}
|
||||
|
||||
|
||||
# MP3 constant bitrates in kbps; the encoder rounds anything else to the nearest one
|
||||
MP3_BITRATES = (
|
||||
8,
|
||||
16,
|
||||
24,
|
||||
32,
|
||||
40,
|
||||
48,
|
||||
56,
|
||||
64,
|
||||
80,
|
||||
96,
|
||||
112,
|
||||
128,
|
||||
160,
|
||||
192,
|
||||
224,
|
||||
256,
|
||||
320,
|
||||
)
|
||||
validate_mp3_bitrate = cv.one_of(*MP3_BITRATES, int=True)
|
||||
|
||||
|
||||
def build_supported_format_struct(
|
||||
format_config: ConfigType, purpose: MockObj
|
||||
) -> cg.StructInitializer:
|
||||
@@ -75,6 +99,10 @@ def build_supported_format_struct(
|
||||
if format_config[CONF_FORMAT] != "MP3":
|
||||
args.append(("sample_bytes", 2))
|
||||
|
||||
# Omitted means no preference, and the client keeps encoding as it always has.
|
||||
if bitrate := format_config.get(CONF_BITRATE):
|
||||
args.append(("bitrate", bitrate))
|
||||
|
||||
return cg.StructInitializer(*args)
|
||||
|
||||
|
||||
@@ -91,6 +119,14 @@ def validate_preferred_format(
|
||||
inherit_property_from(CONF_NUM_CHANNELS, audio_device_key)(config)
|
||||
inherit_property_from(CONF_SAMPLE_RATE, audio_device_key)(config)
|
||||
|
||||
# A constant bitrate is only meaningful for the one lossy format whose
|
||||
# encoder Home Assistant can be told to fix
|
||||
if CONF_BITRATE in config and config.get(CONF_FORMAT) != "MP3":
|
||||
raise cv.Invalid(
|
||||
f"{CONF_BITRATE} is only supported for the MP3 format",
|
||||
path=[CONF_BITRATE],
|
||||
)
|
||||
|
||||
# Opus only supports 48 kHz
|
||||
if config.get(CONF_FORMAT) == "OPUS" and config.get(CONF_SAMPLE_RATE) != 48000:
|
||||
raise cv.Invalid("Opus only supports a sample rate of 48000 Hz")
|
||||
|
||||
@@ -77,6 +77,7 @@ struct MediaPlayerSupportedFormat {
|
||||
uint32_t num_channels;
|
||||
MediaPlayerFormatPurpose purpose;
|
||||
uint32_t sample_bytes;
|
||||
uint32_t bitrate;
|
||||
};
|
||||
|
||||
// Base features always reported for all media players
|
||||
|
||||
@@ -13,6 +13,7 @@ from esphome.components import (
|
||||
)
|
||||
from esphome.components.const import (
|
||||
CONF_ANNOUNCEMENT_VOLUME,
|
||||
CONF_BITRATE,
|
||||
CONF_VOLUME_INCREMENT,
|
||||
CONF_VOLUME_INITIAL,
|
||||
CONF_VOLUME_MAX,
|
||||
@@ -107,6 +108,7 @@ PIPELINE_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_FORMAT, default="FLAC"): cv.enum(audio.AUDIO_FILE_TYPE_ENUM),
|
||||
cv.Optional(CONF_SAMPLE_RATE): cv.int_range(min=1),
|
||||
cv.Optional(CONF_NUM_CHANNELS): cv.int_range(1, 2),
|
||||
cv.Optional(CONF_BITRATE): media_player.validate_mp3_bitrate,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import audio, media_player, media_source, speaker
|
||||
from esphome.components.const import (
|
||||
CONF_ANNOUNCEMENT_VOLUME,
|
||||
CONF_BITRATE,
|
||||
CONF_SOURCES,
|
||||
CONF_VOLUME_INCREMENT,
|
||||
CONF_VOLUME_INITIAL,
|
||||
@@ -78,6 +79,7 @@ PIPELINE_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_FORMAT, default="FLAC"): cv.enum(audio.AUDIO_FILE_TYPE_ENUM),
|
||||
cv.Optional(CONF_SAMPLE_RATE): cv.int_range(min=1),
|
||||
cv.Optional(CONF_NUM_CHANNELS): cv.int_range(1, 2),
|
||||
cv.Optional(CONF_BITRATE): media_player.validate_mp3_bitrate,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
"""Tests for the speaker_source media player announcement volume validation."""
|
||||
"""Tests for the speaker_source media player pipeline validation."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.const import CONF_ANNOUNCEMENT_VOLUME
|
||||
from esphome.components import audio
|
||||
from esphome.components.const import CONF_ANNOUNCEMENT_VOLUME, CONF_BITRATE
|
||||
from esphome.components.speaker_source.media_player import (
|
||||
CONF_ANNOUNCEMENT_PIPELINE,
|
||||
CONF_MEDIA_PIPELINE,
|
||||
PIPELINE_SCHEMA,
|
||||
_validate_announcement_volume,
|
||||
_validate_pipeline,
|
||||
)
|
||||
from esphome.const import CONF_FORMAT, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE
|
||||
|
||||
PIPELINE = {"speaker": "spk", "sources": ["src"]}
|
||||
|
||||
|
||||
def test_announcement_volume_with_both_pipelines() -> None:
|
||||
@@ -29,3 +37,49 @@ def test_no_announcement_volume_with_one_pipeline() -> None:
|
||||
def test_announcement_volume_needs_both_pipelines(pipeline: str) -> None:
|
||||
with pytest.raises(cv.Invalid, match="requires both"):
|
||||
_validate_announcement_volume({CONF_ANNOUNCEMENT_VOLUME: 0.6, pipeline: {}})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bitrate", [8, 48, 320])
|
||||
def test_pipeline_accepts_mp3_bitrates(bitrate: int) -> None:
|
||||
config = PIPELINE_SCHEMA({**PIPELINE, CONF_FORMAT: "MP3", CONF_BITRATE: bitrate})
|
||||
assert config[CONF_BITRATE] == bitrate
|
||||
|
||||
|
||||
def test_pipeline_without_bitrate() -> None:
|
||||
assert CONF_BITRATE not in PIPELINE_SCHEMA(PIPELINE)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bitrate", [0, -48, 50, 48000])
|
||||
def test_pipeline_rejects_bitrates_the_encoder_cannot_use(bitrate: int) -> None:
|
||||
with pytest.raises(cv.Invalid, match="Unknown value"):
|
||||
PIPELINE_SCHEMA({**PIPELINE, CONF_FORMAT: "MP3", CONF_BITRATE: bitrate})
|
||||
|
||||
|
||||
def _final_validate(monkeypatch: pytest.MonkeyPatch, **overrides: Any) -> dict:
|
||||
"""Run the pipeline's final validator with the speaker checks isolated."""
|
||||
monkeypatch.setattr(
|
||||
audio, "final_validate_audio_schema", lambda *args, **kwargs: lambda c: c
|
||||
)
|
||||
# Both audio properties are set, so nothing is inherited from the speaker.
|
||||
config = PIPELINE_SCHEMA(
|
||||
{**PIPELINE, CONF_SAMPLE_RATE: 48000, CONF_NUM_CHANNELS: 1, **overrides}
|
||||
)
|
||||
return _validate_pipeline(config)
|
||||
|
||||
|
||||
def test_final_validation_keeps_mp3_bitrate(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
config = _final_validate(monkeypatch, **{CONF_FORMAT: "MP3", CONF_BITRATE: 48})
|
||||
assert config[CONF_BITRATE] == 48
|
||||
|
||||
|
||||
def test_final_validation_without_bitrate(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
assert CONF_BITRATE not in _final_validate(monkeypatch, **{CONF_FORMAT: "FLAC"})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("format_", ["FLAC", "NONE", "WAV"])
|
||||
def test_final_validation_rejects_bitrate_for_other_formats(
|
||||
format_: str, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
with pytest.raises(cv.Invalid, match="only supported for the MP3 format") as exc:
|
||||
_final_validate(monkeypatch, **{CONF_FORMAT: format_, CONF_BITRATE: 48})
|
||||
assert exc.value.path == [CONF_BITRATE]
|
||||
|
||||
@@ -35,7 +35,8 @@ media_player:
|
||||
announcement_volume: 60%
|
||||
announcement_pipeline:
|
||||
speaker: announcement_mixer_speaker_id
|
||||
format: FLAC
|
||||
format: MP3
|
||||
bitrate: 48
|
||||
num_channels: 1
|
||||
sources:
|
||||
- announcement_audio_file_source
|
||||
|
||||
Reference in New Issue
Block a user