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:
Guillermo Ruffino 2020-07-25 12:57:11 -03:00 committed by GitHub
parent 4996967c79
commit f6e3070dd8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 356 additions and 6 deletions

View file

@ -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_);
} }

View file

@ -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);

View file

@ -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:

View file

@ -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)

View 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

View 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

View 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

View file

@ -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