mirror of
https://github.com/esphome/esphome.git
synced 2026-10-07 19:44:08 +00:00
[audio_file] Keep audio files in flash instead of building them in setup() (#20072)
This commit is contained in:
@@ -72,7 +72,7 @@ esp_err_t AudioReader::add_sink(const std::weak_ptr<ring_buffer::RingBuffer> &ou
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
esp_err_t AudioReader::start(AudioFile *audio_file, AudioFileType &file_type) {
|
||||
esp_err_t AudioReader::start(const AudioFile *audio_file, AudioFileType &file_type) {
|
||||
file_type = AudioFileType::NONE;
|
||||
|
||||
this->current_audio_file_ = audio_file;
|
||||
|
||||
@@ -47,7 +47,7 @@ class AudioReader {
|
||||
/// @param audio_file AudioFile struct containing the file.
|
||||
/// @param file_type AudioFileType variable passed-by-reference indicating the type of file being read.
|
||||
/// @return ESP_OK
|
||||
esp_err_t start(AudioFile *audio_file, AudioFileType &file_type);
|
||||
esp_err_t start(const AudioFile *audio_file, AudioFileType &file_type);
|
||||
|
||||
/// @brief Reads new file data from the source and sends to the ring buffer sink.
|
||||
/// @return AudioReaderState
|
||||
@@ -69,7 +69,7 @@ class AudioReader {
|
||||
|
||||
esp_http_client_handle_t client_{nullptr};
|
||||
|
||||
AudioFile *current_audio_file_{nullptr};
|
||||
const AudioFile *current_audio_file_{nullptr};
|
||||
AudioFileType audio_file_type_{AudioFileType::NONE};
|
||||
const uint8_t *file_current_{nullptr};
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ from esphome.const import (
|
||||
CONF_URL,
|
||||
)
|
||||
from esphome.core import CORE, ID, HexInt
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.cpp_generator import MockObj, ProgmemAssignmentExpression
|
||||
from esphome.external_files import download_web_files_in_config
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -151,11 +151,14 @@ TYPED_FILE_SCHEMA = cv.typed_schema(
|
||||
)
|
||||
|
||||
|
||||
CONF_FILE_DATA_ID = "file_data_id"
|
||||
|
||||
MEDIA_FILE_TYPE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(audio.AudioFile),
|
||||
cv.Required(CONF_FILE): _file_schema,
|
||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||
cv.GenerateID(CONF_FILE_DATA_ID): cv.declare_id(audio.AudioFile),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -217,9 +220,9 @@ def audio_files_schema() -> cv.All:
|
||||
|
||||
|
||||
def generate_audio_file_code(file_config: ConfigType) -> MockObj:
|
||||
"""Generate the progmem data, AudioFile struct, and Pvariable for one file.
|
||||
"""Generate the progmem data and a flash AudioFile for one file.
|
||||
|
||||
Returns the created Pvariable. Caller is responsible for any further
|
||||
Returns a const pointer to the AudioFile. Caller is responsible for any further
|
||||
registration (the audio_file component additionally registers each file in
|
||||
its named C++ registry; other consumers may skip that).
|
||||
"""
|
||||
@@ -230,17 +233,33 @@ def generate_audio_file_code(file_config: ConfigType) -> MockObj:
|
||||
else:
|
||||
data, media_file_type = read_audio_file_and_type(file_config)
|
||||
|
||||
rhs = [HexInt(x) for x in data]
|
||||
prog_arr = cg.progmem_array(file_config[CONF_RAW_DATA_ID], rhs)
|
||||
|
||||
media_files_struct = cg.StructInitializer(
|
||||
# Global constants so the AudioFile lives in flash; the id stays a plain pointer
|
||||
# because actions render id arguments as ``::<id>``.
|
||||
data_id = file_config[CONF_RAW_DATA_ID]
|
||||
cg.add_global(
|
||||
ProgmemAssignmentExpression(
|
||||
data_id.type, data_id, cg.safe_exp([HexInt(x) for x in data])
|
||||
)
|
||||
)
|
||||
media_file = cg.StructInitializer(
|
||||
audio.AudioFile,
|
||||
("data", prog_arr),
|
||||
("length", len(rhs)),
|
||||
("data", MockObj(data_id, ".")),
|
||||
("length", len(data)),
|
||||
("file_type", media_file_type),
|
||||
)
|
||||
|
||||
return cg.new_Pvariable(file_config[CONF_ID], media_files_struct)
|
||||
file_var_id = file_config[CONF_ID]
|
||||
storage = file_config[CONF_FILE_DATA_ID]
|
||||
cg.add_global(
|
||||
cg.RawStatement(f"static constexpr {storage.type} {storage} = {media_file};")
|
||||
)
|
||||
cg.add_global(
|
||||
cg.RawStatement(
|
||||
f"static const {storage.type} *const {file_var_id} = &{storage};"
|
||||
)
|
||||
)
|
||||
var = MockObj(file_var_id, "->")
|
||||
CORE.register_variable(file_var_id, var)
|
||||
return var
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
namespace esphome::audio_file {
|
||||
|
||||
struct NamedAudioFile {
|
||||
audio::AudioFile *file;
|
||||
const audio::AudioFile *file;
|
||||
const char *file_id;
|
||||
};
|
||||
|
||||
inline StaticVector<NamedAudioFile, AUDIO_FILE_MAX_FILES>
|
||||
named_audio_files; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
inline void add_named_audio_file(audio::AudioFile *file, const char *file_id) {
|
||||
inline void add_named_audio_file(const audio::AudioFile *file, const char *file_id) {
|
||||
named_audio_files.push_back({file, file_id});
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ class AudioFileMediaSource final : public Component,
|
||||
protected:
|
||||
std::unique_ptr<micro_decoder::DecoderSource> decoder_;
|
||||
audio::AudioStreamInfo stream_info_;
|
||||
audio::AudioFile *current_file_{nullptr};
|
||||
const audio::AudioFile *current_file_{nullptr};
|
||||
|
||||
// Written from the main loop in handle_command(), read from the decoder task in
|
||||
// on_audio_write(). Must be atomic to avoid a data race.
|
||||
|
||||
@@ -251,7 +251,7 @@ automation.register_apply_action(
|
||||
# so a trigger arg with the same name cannot shadow it.
|
||||
(
|
||||
CONF_MEDIA_FILE,
|
||||
audio.AudioFile.operator("ptr"),
|
||||
audio.AudioFile.operator("const").operator("ptr"),
|
||||
lambda _, value: f"::{value}",
|
||||
),
|
||||
(CONF_ANNOUNCEMENT, cg.bool_),
|
||||
|
||||
@@ -66,7 +66,7 @@ void AudioPipeline::start_url(const std::string &uri) {
|
||||
this->pending_url_ = true;
|
||||
}
|
||||
|
||||
void AudioPipeline::start_file(audio::AudioFile *audio_file) {
|
||||
void AudioPipeline::start_file(const audio::AudioFile *audio_file) {
|
||||
if (this->is_playing_) {
|
||||
xEventGroupSetBits(this->event_group_, PIPELINE_COMMAND_STOP);
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ class AudioPipeline final {
|
||||
/// @brief Starts an audio pipeline given a AudioFile pointer
|
||||
/// @param audio_file pointer to an AudioFile object
|
||||
/// @return ESP_OK if successful or an appropriate error if not
|
||||
void start_file(audio::AudioFile *audio_file);
|
||||
void start_file(const audio::AudioFile *audio_file);
|
||||
|
||||
/// @brief Stops the pipeline. Sends a stop signal to each task (if running) and clears the ring buffers.
|
||||
/// @return ESP_OK if successful or ESP_ERR_TIMEOUT if the tasks did not indicate they stopped
|
||||
@@ -121,7 +121,7 @@ class AudioPipeline final {
|
||||
speaker::Speaker *speaker_{nullptr};
|
||||
|
||||
std::string current_uri_{};
|
||||
audio::AudioFile *current_audio_file_{nullptr};
|
||||
const audio::AudioFile *current_audio_file_{nullptr};
|
||||
|
||||
audio::AudioFileType current_audio_file_type_;
|
||||
audio::AudioStreamInfo current_audio_stream_info_;
|
||||
|
||||
@@ -493,7 +493,7 @@ void SpeakerMediaPlayer::loop() {
|
||||
#endif
|
||||
}
|
||||
|
||||
void SpeakerMediaPlayer::play_file(audio::AudioFile *media_file, bool announcement, bool enqueue) {
|
||||
void SpeakerMediaPlayer::play_file(const audio::AudioFile *media_file, bool announcement, bool enqueue) {
|
||||
if (!this->is_ready()) {
|
||||
// Ignore any commands sent before the media player is setup
|
||||
return;
|
||||
|
||||
@@ -28,13 +28,13 @@ struct MediaCallCommand {
|
||||
optional<float> volume;
|
||||
optional<bool> announce;
|
||||
optional<std::string *> url; // Must be manually deleted after receiving this struct from a queue
|
||||
optional<audio::AudioFile *> file;
|
||||
optional<const audio::AudioFile *> file;
|
||||
optional<bool> enqueue;
|
||||
};
|
||||
|
||||
struct PlaylistItem {
|
||||
optional<std::string> url;
|
||||
optional<audio::AudioFile *> file;
|
||||
optional<const audio::AudioFile *> file;
|
||||
};
|
||||
|
||||
struct VolumeRestoreState {
|
||||
@@ -87,7 +87,7 @@ class SpeakerMediaPlayer final : public Component,
|
||||
Trigger<> *get_unmute_trigger() { return &this->unmute_trigger_; }
|
||||
Trigger<float> *get_volume_trigger() { return &this->volume_trigger_; }
|
||||
|
||||
void play_file(audio::AudioFile *media_file, bool announcement, bool enqueue);
|
||||
void play_file(const audio::AudioFile *media_file, bool announcement, bool enqueue);
|
||||
|
||||
void set_playlist_delay_ms(AudioPipelineType pipeline_type, uint32_t delay_ms);
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
audio_file:
|
||||
- id: chime
|
||||
file:
|
||||
type: local
|
||||
path: ../../../components/audio_file/test.wav
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Tests for the audio_file component codegen."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_audio_file_is_constant_in_flash(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""The AudioFile is a global constant behind a const pointer, not a placement new."""
|
||||
main_cpp = generate_main(component_config_path("audio_file.yaml"))
|
||||
|
||||
assert (
|
||||
"static constexpr audio::AudioFile audio_audiofile_id = audio::AudioFile{"
|
||||
in main_cpp
|
||||
)
|
||||
assert (
|
||||
"static const audio::AudioFile *const chime = &audio_audiofile_id;" in main_cpp
|
||||
)
|
||||
assert "new(chime)" not in main_cpp
|
||||
assert 'audio_file::add_named_audio_file(chime, "chime");' in main_cpp
|
||||
Reference in New Issue
Block a user