ESPHome  2025.2.2
All Data Structures Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Modules Pages
audio_reader.cpp
Go to the documentation of this file.
1 #include "audio_reader.h"
2 
3 #ifdef USE_ESP_IDF
4 
5 #include "esphome/core/defines.h"
6 #include "esphome/core/hal.h"
7 #include "esphome/core/helpers.h"
8 
9 #if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
10 #include "esp_crt_bundle.h"
11 #endif
12 
13 namespace esphome {
14 namespace audio {
15 
16 static const uint32_t READ_WRITE_TIMEOUT_MS = 20;
17 
18 static const uint32_t CONNECTION_TIMEOUT_MS = 5000;
19 
20 // The number of times the http read times out with no data before throwing an error
21 static const uint32_t ERROR_COUNT_NO_DATA_READ_TIMEOUT = 100;
22 
23 static const size_t HTTP_STREAM_BUFFER_SIZE = 2048;
24 
25 static const uint8_t MAX_REDIRECTION = 5;
26 
27 // Some common HTTP status codes - borrowed from http_request component accessed 20241224
28 enum HttpStatus {
32 
33  /* 3xx - Redirection */
41 
42  /* 4XX - CLIENT ERROR */
50 
51  /* 5xx - Server Error */
53 };
54 
56 
57 esp_err_t AudioReader::add_sink(const std::weak_ptr<RingBuffer> &output_ring_buffer) {
58  if (current_audio_file_ != nullptr) {
59  // A transfer buffer isn't ncessary for a local file
60  this->file_ring_buffer_ = output_ring_buffer.lock();
61  return ESP_OK;
62  }
63 
64  if (this->output_transfer_buffer_ != nullptr) {
65  this->output_transfer_buffer_->set_sink(output_ring_buffer);
66  return ESP_OK;
67  }
68 
69  return ESP_ERR_INVALID_STATE;
70 }
71 
72 esp_err_t AudioReader::start(AudioFile *audio_file, AudioFileType &file_type) {
73  file_type = AudioFileType::NONE;
74 
75  this->current_audio_file_ = audio_file;
76 
77  this->file_current_ = audio_file->data;
78  file_type = audio_file->file_type;
79 
80  return ESP_OK;
81 }
82 
83 esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
84  file_type = AudioFileType::NONE;
85 
86  this->cleanup_connection_();
87 
88  if (uri.empty()) {
89  return ESP_ERR_INVALID_ARG;
90  }
91 
92  esp_http_client_config_t client_config = {};
93 
94  client_config.url = uri.c_str();
95  client_config.cert_pem = nullptr;
96  client_config.disable_auto_redirect = false;
97  client_config.max_redirection_count = 10;
98  client_config.event_handler = http_event_handler;
99  client_config.user_data = this;
100  client_config.buffer_size = HTTP_STREAM_BUFFER_SIZE;
101  client_config.keep_alive_enable = true;
102  client_config.timeout_ms = CONNECTION_TIMEOUT_MS; // Shouldn't trigger watchdog resets if caller runs in a task
103 
104 #if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
105  if (uri.find("https:") != std::string::npos) {
106  client_config.crt_bundle_attach = esp_crt_bundle_attach;
107  }
108 #endif
109 
110  this->client_ = esp_http_client_init(&client_config);
111 
112  if (this->client_ == nullptr) {
113  return ESP_FAIL;
114  }
115 
116  esp_err_t err = esp_http_client_open(this->client_, 0);
117 
118  if (err != ESP_OK) {
119  this->cleanup_connection_();
120  return err;
121  }
122 
123  int64_t header_length = esp_http_client_fetch_headers(this->client_);
124  if (header_length < 0) {
125  this->cleanup_connection_();
126  return ESP_FAIL;
127  }
128 
129  int status_code = esp_http_client_get_status_code(this->client_);
130 
131  if ((status_code < HTTP_STATUS_OK) || (status_code > HTTP_STATUS_PERMANENT_REDIRECT)) {
132  this->cleanup_connection_();
133  return ESP_FAIL;
134  }
135 
136  ssize_t redirect_count = 0;
137 
138  while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTION)) {
139  err = esp_http_client_open(this->client_, 0);
140  if (err != ESP_OK) {
141  this->cleanup_connection_();
142  return ESP_FAIL;
143  }
144 
145  header_length = esp_http_client_fetch_headers(this->client_);
146  if (header_length < 0) {
147  this->cleanup_connection_();
148  return ESP_FAIL;
149  }
150 
151  status_code = esp_http_client_get_status_code(this->client_);
152 
153  if ((status_code < HTTP_STATUS_OK) || (status_code > HTTP_STATUS_PERMANENT_REDIRECT)) {
154  this->cleanup_connection_();
155  return ESP_FAIL;
156  }
157 
158  ++redirect_count;
159  }
160 
161  if (this->audio_file_type_ == AudioFileType::NONE) {
162  // Failed to determine the file type from the header, fallback to using the url
163  char url[500];
164  err = esp_http_client_get_url(this->client_, url, 500);
165  if (err != ESP_OK) {
166  this->cleanup_connection_();
167  return err;
168  }
169 
170  std::string url_string = str_lower_case(url);
171 
172  if (str_endswith(url_string, ".wav")) {
173  file_type = AudioFileType::WAV;
174  }
175 #ifdef USE_AUDIO_MP3_SUPPORT
176  else if (str_endswith(url_string, ".mp3")) {
177  file_type = AudioFileType::MP3;
178  }
179 #endif
180 #ifdef USE_AUDIO_FLAC_SUPPORT
181  else if (str_endswith(url_string, ".flac")) {
182  file_type = AudioFileType::FLAC;
183  }
184 #endif
185  else {
186  file_type = AudioFileType::NONE;
187  this->cleanup_connection_();
188  return ESP_ERR_NOT_SUPPORTED;
189  }
190  } else {
191  file_type = this->audio_file_type_;
192  }
193 
194  this->last_data_read_ms_ = millis();
195 
197  if (this->output_transfer_buffer_ == nullptr) {
198  return ESP_ERR_NO_MEM;
199  }
200 
201  return ESP_OK;
202 }
203 
205  if (this->client_ != nullptr) {
206  return this->http_read_();
207  } else if (this->current_audio_file_ != nullptr) {
208  return this->file_read_();
209  }
210 
212 }
213 
214 AudioFileType AudioReader::get_audio_type(const char *content_type) {
215 #ifdef USE_AUDIO_MP3_SUPPORT
216  if (strcasecmp(content_type, "mp3") == 0 || strcasecmp(content_type, "audio/mp3") == 0 ||
217  strcasecmp(content_type, "audio/mpeg") == 0) {
218  return AudioFileType::MP3;
219  }
220 #endif
221  if (strcasecmp(content_type, "audio/wav") == 0) {
222  return AudioFileType::WAV;
223  }
224 #ifdef USE_AUDIO_FLAC_SUPPORT
225  if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) {
226  return AudioFileType::FLAC;
227  }
228 #endif
229  return AudioFileType::NONE;
230 }
231 
232 esp_err_t AudioReader::http_event_handler(esp_http_client_event_t *evt) {
233  // Based on https://github.com/maroc81/WeatherLily/tree/main/main/net accessed 20241224
234  AudioReader *this_reader = (AudioReader *) evt->user_data;
235 
236  switch (evt->event_id) {
237  case HTTP_EVENT_ON_HEADER:
238  if (strcasecmp(evt->header_key, "Content-Type") == 0) {
239  this_reader->audio_file_type_ = get_audio_type(evt->header_value);
240  }
241  break;
242  default:
243  break;
244  }
245  return ESP_OK;
246 }
247 
249  size_t remaining_bytes = this->current_audio_file_->length - (this->file_current_ - this->current_audio_file_->data);
250  if (remaining_bytes > 0) {
251  size_t bytes_written = this->file_ring_buffer_->write_without_replacement(this->file_current_, remaining_bytes,
252  pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
253  this->file_current_ += bytes_written;
254 
256  }
257 
259 }
260 
262  this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
263 
264  if (esp_http_client_is_complete_data_received(this->client_)) {
265  if (this->output_transfer_buffer_->available() == 0) {
266  this->cleanup_connection_();
268  }
269  } else {
270  size_t bytes_to_read = this->output_transfer_buffer_->free();
271  int received_len =
272  esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), bytes_to_read);
273 
274  if (received_len > 0) {
275  this->output_transfer_buffer_->increase_buffer_length(received_len);
276  this->last_data_read_ms_ = millis();
277  } else if (received_len < 0) {
278  // HTTP read error
279  this->cleanup_connection_();
281  } else {
282  if (bytes_to_read > 0) {
283  // Read timed out
284  if ((millis() - this->last_data_read_ms_) > CONNECTION_TIMEOUT_MS) {
285  this->cleanup_connection_();
287  }
288 
289  delay(READ_WRITE_TIMEOUT_MS);
290  }
291  }
292  }
293 
295 }
296 
298  if (this->client_ != nullptr) {
299  esp_http_client_close(this->client_);
300  esp_http_client_cleanup(this->client_);
301  this->client_ = nullptr;
302  }
303 }
304 
305 } // namespace audio
306 } // namespace esphome
307 
308 #endif
std::unique_ptr< AudioSinkTransferBuffer > output_transfer_buffer_
Definition: audio_reader.h:70
static std::unique_ptr< AudioSinkTransferBuffer > create(size_t buffer_size)
Creates a new sink transfer buffer.
const uint8_t * data
Definition: audio.h:120
AudioFileType audio_file_type_
Definition: audio_reader.h:79
uint32_t IRAM_ATTR HOT millis()
Definition: core.cpp:25
std::string str_lower_case(const std::string &str)
Convert the string to lower case.
Definition: helpers.cpp:291
AudioReaderState file_read_()
esp_err_t add_sink(const std::weak_ptr< RingBuffer > &output_ring_buffer)
Adds a sink ring buffer for audio data.
const uint8_t * file_current_
Definition: audio_reader.h:80
AudioFileType file_type
Definition: audio.h:122
bool str_endswith(const std::string &str, const std::string &end)
Check whether a string ends with a value.
Definition: helpers.cpp:268
AudioReaderState read()
Reads new file data from the source and sends to the ring buffer sink.
esp_err_t start(const std::string &uri, AudioFileType &file_type)
Starts reading an audio file from an http source.
static AudioFileType get_audio_type(const char *content_type)
Determines the audio file type from the http header&#39;s Content-Type key.
std::shared_ptr< RingBuffer > file_ring_buffer_
Definition: audio_reader.h:69
Implementation of SPI Controller mode.
Definition: a01nyub.cpp:7
static esp_err_t http_event_handler(esp_http_client_event_t *evt)
Monitors the http client events to attempt determining the file type from the Content-Type header...
esp_http_client_handle_t client_
Definition: audio_reader.h:76
AudioReaderState http_read_()
void IRAM_ATTR HOT delay(uint32_t ms)
Definition: core.cpp:26