mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	rtttl player (#1171)
* rtttl player * fixes * Cleanup, add action, condition, etc. * add test * updates * fixes * Add better error messages * lint
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							4996967c79
						
					
				
				
					commit
					f6e3070dd8
				
			| @@ -13,7 +13,7 @@ class ESP8266PWM : public output::FloatOutput, public Component { | |||||||
|   void set_pin(GPIOPin *pin) { pin_ = pin; } |   void set_pin(GPIOPin *pin) { pin_ = pin; } | ||||||
|   void set_frequency(float frequency) { this->frequency_ = frequency; } |   void set_frequency(float frequency) { this->frequency_ = frequency; } | ||||||
|   /// Dynamically update frequency |   /// Dynamically update frequency | ||||||
|   void update_frequency(float frequency) { |   void update_frequency(float frequency) override { | ||||||
|     this->set_frequency(frequency); |     this->set_frequency(frequency); | ||||||
|     this->write_state(this->last_output_); |     this->write_state(this->last_output_); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ void LEDCOutput::write_state(float state) { | |||||||
| } | } | ||||||
|  |  | ||||||
| void LEDCOutput::setup() { | void LEDCOutput::setup() { | ||||||
|   this->apply_frequency(this->frequency_); |   this->update_frequency(this->frequency_); | ||||||
|   this->turn_off(); |   this->turn_off(); | ||||||
|   // Attach pin after setting default value |   // Attach pin after setting default value | ||||||
|   ledcAttachPin(this->pin_->get_pin(), this->channel_); |   ledcAttachPin(this->pin_->get_pin(), this->channel_); | ||||||
| @@ -50,7 +50,7 @@ optional<uint8_t> ledc_bit_depth_for_frequency(float frequency) { | |||||||
|   return {}; |   return {}; | ||||||
| } | } | ||||||
|  |  | ||||||
| void LEDCOutput::apply_frequency(float frequency) { | void LEDCOutput::update_frequency(float frequency) { | ||||||
|   auto bit_depth_opt = ledc_bit_depth_for_frequency(frequency); |   auto bit_depth_opt = ledc_bit_depth_for_frequency(frequency); | ||||||
|   if (!bit_depth_opt.has_value()) { |   if (!bit_depth_opt.has_value()) { | ||||||
|     ESP_LOGW(TAG, "Frequency %f can't be achieved with any bit depth", frequency); |     ESP_LOGW(TAG, "Frequency %f can't be achieved with any bit depth", frequency); | ||||||
|   | |||||||
| @@ -19,7 +19,7 @@ class LEDCOutput : public output::FloatOutput, public Component { | |||||||
|   void set_channel(uint8_t channel) { this->channel_ = channel; } |   void set_channel(uint8_t channel) { this->channel_ = channel; } | ||||||
|   void set_frequency(float frequency) { this->frequency_ = frequency; } |   void set_frequency(float frequency) { this->frequency_ = frequency; } | ||||||
|   /// Dynamically change frequency at runtime |   /// Dynamically change frequency at runtime | ||||||
|   void apply_frequency(float frequency); |   void update_frequency(float frequency) override; | ||||||
|  |  | ||||||
|   /// Setup LEDC. |   /// Setup LEDC. | ||||||
|   void setup() override; |   void setup() override; | ||||||
| @@ -45,7 +45,7 @@ template<typename... Ts> class SetFrequencyAction : public Action<Ts...> { | |||||||
|  |  | ||||||
|   void play(Ts... x) { |   void play(Ts... x) { | ||||||
|     float freq = this->frequency_.value(x...); |     float freq = this->frequency_.value(x...); | ||||||
|     this->parent_->apply_frequency(freq); |     this->parent_->update_frequency(freq); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   | |||||||
| @@ -46,9 +46,20 @@ class FloatOutput : public BinaryOutput { | |||||||
|    */ |    */ | ||||||
|   void set_min_power(float min_power); |   void set_min_power(float min_power); | ||||||
|  |  | ||||||
|   /// Set the level of this float output, this is called from the front-end. |   /** Set the level of this float output, this is called from the front-end. | ||||||
|  |    * | ||||||
|  |    * @param state The new state. | ||||||
|  |    */ | ||||||
|   void set_level(float state); |   void set_level(float state); | ||||||
|  |  | ||||||
|  |   /** Set the frequency of the output for PWM outputs. | ||||||
|  |    * | ||||||
|  |    * Implemented only by components which can set the output PWM frequency. | ||||||
|  |    * | ||||||
|  |    * @param frequence The new frequency. | ||||||
|  |    */ | ||||||
|  |   virtual void update_frequency(float frequency) {} | ||||||
|  |  | ||||||
|   // ========== INTERNAL METHODS ========== |   // ========== INTERNAL METHODS ========== | ||||||
|   // (In most use cases you won't need these) |   // (In most use cases you won't need these) | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										69
									
								
								esphome/components/rtttl/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								esphome/components/rtttl/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome import automation | ||||||
|  | from esphome.components.output import FloatOutput | ||||||
|  | from esphome.const import CONF_ID, CONF_OUTPUT, CONF_TRIGGER_ID | ||||||
|  |  | ||||||
|  | CONF_RTTTL = 'rtttl' | ||||||
|  | CONF_ON_FINISHED_PLAYBACK = 'on_finished_playback' | ||||||
|  |  | ||||||
|  | rtttl_ns = cg.esphome_ns.namespace('rtttl') | ||||||
|  |  | ||||||
|  | Rtttl = rtttl_ns .class_('Rtttl', cg.Component) | ||||||
|  | PlayAction = rtttl_ns.class_('PlayAction', automation.Action) | ||||||
|  | StopAction = rtttl_ns.class_('StopAction', automation.Action) | ||||||
|  | FinishedPlaybackTrigger = rtttl_ns.class_('FinishedPlaybackTrigger', | ||||||
|  |                                           automation.Trigger.template()) | ||||||
|  | IsPlayingCondition = rtttl_ns.class_('IsPlayingCondition', automation.Condition) | ||||||
|  |  | ||||||
|  | MULTI_CONF = True | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.Schema({ | ||||||
|  |     cv.GenerateID(CONF_ID): cv.declare_id(Rtttl), | ||||||
|  |     cv.Required(CONF_OUTPUT): cv.use_id(FloatOutput), | ||||||
|  |     cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation({ | ||||||
|  |         cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FinishedPlaybackTrigger), | ||||||
|  |     }), | ||||||
|  | }).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     yield cg.register_component(var, config) | ||||||
|  |  | ||||||
|  |     out = yield cg.get_variable(config[CONF_OUTPUT]) | ||||||
|  |     cg.add(var.set_output(out)) | ||||||
|  |  | ||||||
|  |     for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []): | ||||||
|  |         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||||
|  |         yield automation.build_automation(trigger, [], conf) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @automation.register_action('rtttl.play', PlayAction, cv.maybe_simple_value({ | ||||||
|  |     cv.GenerateID(CONF_ID): cv.use_id(Rtttl), | ||||||
|  |     cv.Required(CONF_RTTTL): cv.templatable(cv.string) | ||||||
|  | }, key=CONF_RTTTL)) | ||||||
|  | def rtttl_play_to_code(config, action_id, template_arg, args): | ||||||
|  |     paren = yield cg.get_variable(config[CONF_ID]) | ||||||
|  |     var = cg.new_Pvariable(action_id, template_arg, paren) | ||||||
|  |     template_ = yield cg.templatable(config[CONF_RTTTL], args, cg.std_string) | ||||||
|  |     cg.add(var.set_value(template_)) | ||||||
|  |     yield var | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @automation.register_action('rtttl.stop', StopAction, cv.Schema({ | ||||||
|  |     cv.GenerateID(): cv.use_id(Rtttl), | ||||||
|  | })) | ||||||
|  | def rtttl_stop_to_code(config, action_id, template_arg, args): | ||||||
|  |     var = cg.new_Pvariable(action_id, template_arg) | ||||||
|  |     yield cg.register_parented(var, config[CONF_ID]) | ||||||
|  |     yield var | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @automation.register_condition('rtttl.is_playing', IsPlayingCondition, cv.Schema({ | ||||||
|  |     cv.GenerateID(): cv.use_id(Rtttl), | ||||||
|  | })) | ||||||
|  | def rtttl_is_playing_to_code(config, condition_id, template_arg, args): | ||||||
|  |     var = cg.new_Pvariable(condition_id, template_arg) | ||||||
|  |     yield cg.register_parented(var, config[CONF_ID]) | ||||||
|  |     yield var | ||||||
							
								
								
									
										186
									
								
								esphome/components/rtttl/rtttl.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								esphome/components/rtttl/rtttl.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,186 @@ | |||||||
|  | #include "rtttl.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace rtttl { | ||||||
|  |  | ||||||
|  | static const char* TAG = "rtttl"; | ||||||
|  |  | ||||||
|  | static const uint32_t DOUBLE_NOTE_GAP_MS = 10; | ||||||
|  |  | ||||||
|  | // These values can also be found as constants in the Tone library (Tone.h) | ||||||
|  | static const uint16_t NOTES[] = {0,    262,  277,  294,  311,  330,  349,  370,  392,  415,  440,  466,  494, | ||||||
|  |                                  523,  554,  587,  622,  659,  698,  740,  784,  831,  880,  932,  988,  1047, | ||||||
|  |                                  1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, | ||||||
|  |                                  2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951}; | ||||||
|  |  | ||||||
|  | void Rtttl::dump_config() { ESP_LOGCONFIG(TAG, "Rtttl"); } | ||||||
|  |  | ||||||
|  | void Rtttl::play(std::string rtttl) { | ||||||
|  |   rtttl_ = std::move(rtttl); | ||||||
|  |  | ||||||
|  |   default_duration_ = 4; | ||||||
|  |   default_octave_ = 6; | ||||||
|  |   int bpm = 63; | ||||||
|  |   uint8_t num; | ||||||
|  |  | ||||||
|  |   // Get name | ||||||
|  |   position_ = rtttl_.find(':'); | ||||||
|  |  | ||||||
|  |   // it's somewhat documented to be up to 10 characters but let's be a bit flexible here | ||||||
|  |   if (position_ == std::string::npos || position_ > 15) { | ||||||
|  |     ESP_LOGE(TAG, "Missing ':' when looking for name."); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   auto name = this->rtttl_.substr(0, position_); | ||||||
|  |   ESP_LOGD(TAG, "Playing song %s", name.c_str()); | ||||||
|  |  | ||||||
|  |   // get default duration | ||||||
|  |   position_ = this->rtttl_.find("d=", position_); | ||||||
|  |   if (position_ == std::string::npos) { | ||||||
|  |     ESP_LOGE(TAG, "Missing 'd='"); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   position_ += 2; | ||||||
|  |   num = this->get_integer_(); | ||||||
|  |   if (num > 0) | ||||||
|  |     default_duration_ = num; | ||||||
|  |  | ||||||
|  |   // get default octave | ||||||
|  |   position_ = rtttl_.find("o=", position_); | ||||||
|  |   if (position_ == std::string::npos) { | ||||||
|  |     ESP_LOGE(TAG, "Missing 'o="); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   position_ += 2; | ||||||
|  |   num = get_integer_(); | ||||||
|  |   if (num >= 3 && num <= 7) | ||||||
|  |     default_octave_ = num; | ||||||
|  |  | ||||||
|  |   // get BPM | ||||||
|  |   position_ = rtttl_.find("b=", position_); | ||||||
|  |   if (position_ == std::string::npos) { | ||||||
|  |     ESP_LOGE(TAG, "Missing b="); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   position_ += 2; | ||||||
|  |   num = get_integer_(); | ||||||
|  |   if (num != 0) | ||||||
|  |     bpm = num; | ||||||
|  |  | ||||||
|  |   position_ = rtttl_.find(':', position_); | ||||||
|  |   if (position_ == std::string::npos) { | ||||||
|  |     ESP_LOGE(TAG, "Missing second ':'"); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   position_++; | ||||||
|  |  | ||||||
|  |   // BPM usually expresses the number of quarter notes per minute | ||||||
|  |   wholenote_ = 60 * 1000L * 4 / bpm;  // this is the time for whole note (in milliseconds) | ||||||
|  |  | ||||||
|  |   output_freq_ = 0; | ||||||
|  |   last_note_ = millis(); | ||||||
|  |   note_duration_ = 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Rtttl::loop() { | ||||||
|  |   if (note_duration_ == 0 || millis() - last_note_ < note_duration_) | ||||||
|  |     return; | ||||||
|  |  | ||||||
|  |   if (!rtttl_[position_]) { | ||||||
|  |     output_->set_level(0.0); | ||||||
|  |     ESP_LOGD(TAG, "Playback finished"); | ||||||
|  |     this->on_finished_playback_callback_.call(); | ||||||
|  |     note_duration_ = 0; | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // align to note: most rtttl's out there does not add and space after the ',' separator but just in case... | ||||||
|  |   while (rtttl_[position_] == ',' || rtttl_[position_] == ' ') | ||||||
|  |     position_++; | ||||||
|  |  | ||||||
|  |   // first, get note duration, if available | ||||||
|  |   uint8_t num = this->get_integer_(); | ||||||
|  |  | ||||||
|  |   if (num) | ||||||
|  |     note_duration_ = wholenote_ / num; | ||||||
|  |   else | ||||||
|  |     note_duration_ = wholenote_ / default_duration_;  // we will need to check if we are a dotted note after | ||||||
|  |  | ||||||
|  |   uint8_t note; | ||||||
|  |  | ||||||
|  |   switch (rtttl_[position_]) { | ||||||
|  |     case 'c': | ||||||
|  |       note = 1; | ||||||
|  |       break; | ||||||
|  |     case 'd': | ||||||
|  |       note = 3; | ||||||
|  |       break; | ||||||
|  |     case 'e': | ||||||
|  |       note = 5; | ||||||
|  |       break; | ||||||
|  |     case 'f': | ||||||
|  |       note = 6; | ||||||
|  |       break; | ||||||
|  |     case 'g': | ||||||
|  |       note = 8; | ||||||
|  |       break; | ||||||
|  |     case 'a': | ||||||
|  |       note = 10; | ||||||
|  |       break; | ||||||
|  |     case 'b': | ||||||
|  |       note = 12; | ||||||
|  |       break; | ||||||
|  |     case 'p': | ||||||
|  |     default: | ||||||
|  |       note = 0; | ||||||
|  |   } | ||||||
|  |   position_++; | ||||||
|  |  | ||||||
|  |   // now, get optional '#' sharp | ||||||
|  |   if (rtttl_[position_] == '#') { | ||||||
|  |     note++; | ||||||
|  |     position_++; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // now, get optional '.' dotted note | ||||||
|  |   if (rtttl_[position_] == '.') { | ||||||
|  |     note_duration_ += note_duration_ / 2; | ||||||
|  |     position_++; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // now, get scale | ||||||
|  |   uint8_t scale = get_integer_(); | ||||||
|  |   if (scale == 0) | ||||||
|  |     scale = default_octave_; | ||||||
|  |  | ||||||
|  |   // Now play the note | ||||||
|  |   if (note) { | ||||||
|  |     auto note_index = (scale - 4) * 12 + note; | ||||||
|  |     if (note_index < 0 || note_index >= sizeof(NOTES)) { | ||||||
|  |       ESP_LOGE(TAG, "Note out of valid range"); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     auto freq = NOTES[note_index]; | ||||||
|  |  | ||||||
|  |     if (freq == output_freq_) { | ||||||
|  |       // Add small silence gap between same note | ||||||
|  |       output_->set_level(0.0); | ||||||
|  |       delay(DOUBLE_NOTE_GAP_MS); | ||||||
|  |       note_duration_ -= DOUBLE_NOTE_GAP_MS; | ||||||
|  |     } | ||||||
|  |     output_freq_ = freq; | ||||||
|  |  | ||||||
|  |     ESP_LOGVV(TAG, "playing note: %d for %dms", note, note_duration_); | ||||||
|  |     output_->update_frequency(freq); | ||||||
|  |     output_->set_level(0.5); | ||||||
|  |   } else { | ||||||
|  |     ESP_LOGVV(TAG, "waiting: %dms", note_duration_); | ||||||
|  |     output_->set_level(0.0); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   last_note_ = millis(); | ||||||
|  | } | ||||||
|  | }  // namespace rtttl | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										81
									
								
								esphome/components/rtttl/rtttl.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								esphome/components/rtttl/rtttl.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/core/automation.h" | ||||||
|  | #include "esphome/components/output/float_output.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace rtttl { | ||||||
|  |  | ||||||
|  | extern uint32_t global_rtttl_id; | ||||||
|  |  | ||||||
|  | class Rtttl : public Component { | ||||||
|  |  public: | ||||||
|  |   void set_output(output::FloatOutput *output) { output_ = output; } | ||||||
|  |   void play(std::string rtttl); | ||||||
|  |   void stop() { | ||||||
|  |     note_duration_ = 0; | ||||||
|  |     output_->set_level(0.0); | ||||||
|  |   } | ||||||
|  |   void dump_config() override; | ||||||
|  |  | ||||||
|  |   bool is_playing() { return note_duration_ != 0; } | ||||||
|  |   void loop() override; | ||||||
|  |  | ||||||
|  |   void add_on_finished_playback_callback(std::function<void()> callback) { | ||||||
|  |     this->on_finished_playback_callback_.add(std::move(callback)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   inline uint8_t get_integer_() { | ||||||
|  |     uint8_t ret = 0; | ||||||
|  |     while (isdigit(rtttl_[position_])) { | ||||||
|  |       ret = (ret * 10) + (rtttl_[position_++] - '0'); | ||||||
|  |     } | ||||||
|  |     return ret; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   std::string rtttl_; | ||||||
|  |   size_t position_; | ||||||
|  |   uint16_t wholenote_; | ||||||
|  |   uint16_t default_duration_; | ||||||
|  |   uint16_t default_octave_; | ||||||
|  |   uint32_t last_note_; | ||||||
|  |   uint16_t note_duration_; | ||||||
|  |  | ||||||
|  |   uint32_t output_freq_; | ||||||
|  |   output::FloatOutput *output_; | ||||||
|  |  | ||||||
|  |   CallbackManager<void()> on_finished_playback_callback_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class PlayAction : public Action<Ts...> { | ||||||
|  |  public: | ||||||
|  |   PlayAction(Rtttl *rtttl) : rtttl_(rtttl) {} | ||||||
|  |   TEMPLATABLE_VALUE(std::string, value) | ||||||
|  |  | ||||||
|  |   void play(Ts... x) override { this->rtttl_->play(this->value_.value(x...)); } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   Rtttl *rtttl_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class StopAction : public Action<Ts...>, public Parented<Rtttl> { | ||||||
|  |  public: | ||||||
|  |   void play(Ts... x) override { this->parent_->stop(); } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class IsPlayingCondition : public Condition<Ts...>, public Parented<Rtttl> { | ||||||
|  |  public: | ||||||
|  |   bool check(Ts... x) override { return this->parent_->is_playing(); } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class FinishedPlaybackTrigger : public Trigger<> { | ||||||
|  |  public: | ||||||
|  |   explicit FinishedPlaybackTrigger(Rtttl *parent) { | ||||||
|  |     parent->add_on_finished_playback_callback([this]() { this->trigger(); }); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace rtttl | ||||||
|  | }  // namespace esphome | ||||||
| @@ -1773,3 +1773,6 @@ sn74hc595: | |||||||
|     latch_pin: GPIO22 |     latch_pin: GPIO22 | ||||||
|     oe_pin: GPIO32 |     oe_pin: GPIO32 | ||||||
|     sr_count: 2 |     sr_count: 2 | ||||||
|  |  | ||||||
|  | rtttl: | ||||||
|  |   output: gpio_19 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user