[audio_file] Keep audio files in flash instead of building them in setup() (#20072)

This commit is contained in:
J. Nick Koston
2026-10-05 12:41:33 -04:00
committed by GitHub
parent 8735aee847
commit 4a47a984a0
13 changed files with 80 additions and 25 deletions
+1 -1
View File
@@ -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;
+2 -2
View 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};
};
+30 -11
View File
@@ -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(
+2 -2
View File
@@ -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