From 015977cfdfcf99e781af4d31e964c67d508dbc3a Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Wed, 27 Aug 2025 21:53:57 -0400 Subject: [PATCH] [rtttl] Fix RTTTL for speakers (#10381) --- esphome/components/rtttl/rtttl.cpp | 63 +++++++++++++++++------------- esphome/components/rtttl/rtttl.h | 25 ++++++++++++ 2 files changed, 60 insertions(+), 28 deletions(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 65a3af1bbc..5aedc74489 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -138,11 +138,37 @@ void Rtttl::stop() { this->set_state_(STATE_STOPPING); } #endif + this->position_ = this->rtttl_.length(); + this->note_duration_ = 0; +} + +void Rtttl::finish_() { + ESP_LOGV(TAG, "Rtttl::finish_()"); +#ifdef USE_OUTPUT + if (this->output_ != nullptr) { + this->output_->set_level(0.0); + this->set_state_(State::STATE_STOPPED); + } +#endif +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + SpeakerSample sample[2]; + sample[0].left = 0; + sample[0].right = 0; + sample[1].left = 0; + sample[1].right = 0; + this->speaker_->play((uint8_t *) (&sample), 8); + this->speaker_->finish(); + this->set_state_(State::STATE_STOPPING); + } +#endif + // Ensure no more notes are played in case finish_() is called for an error. + this->position_ = this->rtttl_.length(); this->note_duration_ = 0; } void Rtttl::loop() { - if (this->note_duration_ == 0 || this->state_ == State::STATE_STOPPED) { + if (this->state_ == State::STATE_STOPPED) { this->disable_loop(); return; } @@ -152,6 +178,8 @@ void Rtttl::loop() { if (this->state_ == State::STATE_STOPPING) { if (this->speaker_->is_stopped()) { this->set_state_(State::STATE_STOPPED); + } else { + return; } } else if (this->state_ == State::STATE_INIT) { if (this->speaker_->is_stopped()) { @@ -207,7 +235,7 @@ void Rtttl::loop() { if (this->output_ != nullptr && millis() - this->last_note_ < this->note_duration_) return; #endif - if (!this->rtttl_[this->position_]) { + if (this->position_ >= this->rtttl_.length()) { this->finish_(); return; } @@ -346,31 +374,6 @@ void Rtttl::loop() { this->last_note_ = millis(); } -void Rtttl::finish_() { -#ifdef USE_OUTPUT - if (this->output_ != nullptr) { - this->output_->set_level(0.0); - this->set_state_(State::STATE_STOPPED); - } -#endif -#ifdef USE_SPEAKER - if (this->speaker_ != nullptr) { - SpeakerSample sample[2]; - sample[0].left = 0; - sample[0].right = 0; - sample[1].left = 0; - sample[1].right = 0; - this->speaker_->play((uint8_t *) (&sample), 8); - - this->speaker_->finish(); - this->set_state_(State::STATE_STOPPING); - } -#endif - this->note_duration_ = 0; - this->on_finished_playback_callback_.call(); - ESP_LOGD(TAG, "Playback finished"); -} - #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG static const LogString *state_to_string(State state) { switch (state) { @@ -397,7 +400,11 @@ void Rtttl::set_state_(State state) { LOG_STR_ARG(state_to_string(state))); // Clear loop_done when transitioning from STOPPED to any other state - if (old_state == State::STATE_STOPPED && state != State::STATE_STOPPED) { + if (state == State::STATE_STOPPED) { + this->disable_loop(); + this->on_finished_playback_callback_.call(); + ESP_LOGD(TAG, "Playback finished"); + } else if (old_state == State::STATE_STOPPED) { this->enable_loop(); } } diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index 420948bfbf..d536c6c08e 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -60,35 +60,60 @@ class Rtttl : public Component { } return ret; } + /** + * @brief Finalizes the playback of the RTTTL string. + * + * This method is called internally when the end of the RTTTL string is reached + * or when a parsing error occurs. It stops the output, sets the component state, + * and triggers the on_finished_playback_callback_. + */ void finish_(); void set_state_(State state); + /// The RTTTL string to play. std::string rtttl_{""}; + /// The current position in the RTTTL string. size_t position_{0}; + /// The duration of a whole note in milliseconds. uint16_t wholenote_; + /// The default duration of a note (e.g. 4 for a quarter note). uint16_t default_duration_; + /// The default octave for a note. uint16_t default_octave_; + /// The time the last note was started. uint32_t last_note_; + /// The duration of the current note in milliseconds. uint16_t note_duration_; + /// The frequency of the current note in Hz. uint32_t output_freq_; + /// The gain of the output. float gain_{0.6f}; + /// The current state of the RTTTL player. State state_{State::STATE_STOPPED}; #ifdef USE_OUTPUT + /// The output to write the sound to. output::FloatOutput *output_; #endif #ifdef USE_SPEAKER + /// The speaker to write the sound to. speaker::Speaker *speaker_{nullptr}; + /// The sample rate of the speaker. int sample_rate_{16000}; + /// The number of samples for one full cycle of a note's waveform, in Q10 fixed-point format. int samples_per_wave_{0}; + /// The number of samples sent. int samples_sent_{0}; + /// The total number of samples to send. int samples_count_{0}; + /// The number of samples for the gap between notes. int samples_gap_{0}; #endif + /// The callback to call when playback is finished. CallbackManager on_finished_playback_callback_; };