mirror of
https://github.com/esphome/esphome.git
synced 2024-12-22 13:34:54 +01: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:
parent
4996967c79
commit
f6e3070dd8
8 changed files with 356 additions and 6 deletions
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue