14static constexpr uint8_t SONG_NAME_LENGTH_LIMIT = 64;
15static constexpr uint8_t SEMITONES_IN_OCTAVE = 12;
17static constexpr uint8_t MIN_OCTAVE = 4;
18static constexpr uint8_t MAX_OCTAVE = 7;
20static constexpr uint8_t DEFAULT_BPM = 63;
23static constexpr uint16_t NOTES[] = {0, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494,
24 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047,
25 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217,
26 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951};
27static constexpr uint8_t NOTES_COUNT =
static_cast<uint8_t
>(
sizeof(NOTES) /
sizeof(NOTES[0]));
29static constexpr uint8_t REPEATING_NOTE_GAP_MS = 10;
32static constexpr uint16_t SAMPLE_BUFFER_SIZE = 2048;
33static constexpr uint16_t SAMPLE_RATE = 16000;
36 static constexpr double PI_ON_180 = M_PI / 180.0;
37 return degrees * PI_ON_180;
41#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
43PROGMEM_STRING_TABLE(RtttlStateStrings,
"State::STOPPED",
"State::INIT",
"State::STARTING",
"State::RUNNING",
44 "State::STOPPING",
"UNKNOWN");
46static const LogString *state_to_string(
State state) {
47 return RtttlStateStrings::get_log_str(
static_cast<uint8_t
>(
state), RtttlStateStrings::LAST_INDEX);
51static uint8_t note_index_from_char(
char note) {
124 int16_t sample[SAMPLE_BUFFER_SIZE];
125 uint16_t sample_index = 0;
132 sample[sample_index] = INT16_MAX * sin(
deg2rad(rem));
134 sample[sample_index] = 0;
139 if (sample_index > 0) {
140 size_t bytes = sample_index *
sizeof(int16_t);
141 size_t sent_bytes = this->
speaker_->
play((uint8_t *) (&sample), bytes);
142 size_t samples_sent = sent_bytes /
sizeof(int16_t);
143 if (samples_sent != sample_index) {
155 if (c !=
',' && c !=
' ')
160 if (this->
position_ >= this->song_len_) {
168 if (note_denominator) {
175 uint8_t note_index_in_octave = note_index_from_char(this->
song_at_(this->
position_));
181 note_index_in_octave++;
191 if (scale < MIN_OCTAVE || scale > MAX_OCTAVE) {
192 ESP_LOGE(TAG,
"Octave must be between %d and %d (it is %d)", MIN_OCTAVE, MAX_OCTAVE, scale);
199 this->note_duration_ += this->note_duration_ / 2;
203 bool need_note_gap =
false;
206 if (note_index_in_octave == 0) {
208 ESP_LOGVV(TAG,
"Waiting: %dms", this->note_duration_);
210 uint8_t note_index = (scale - MIN_OCTAVE) * SEMITONES_IN_OCTAVE + note_index_in_octave;
211 if (note_index >= NOTES_COUNT) {
212 ESP_LOGE(TAG,
"Note out of range (note: %d, scale: %d, index: %d, max: %d)", note_index_in_octave, scale,
213 note_index, NOTES_COUNT);
217 uint16_t freq = NOTES[note_index];
223 ESP_LOGVV(TAG,
"Playing note: %d for %dms", note_index_in_octave, this->note_duration_);
227 if (this->
output_ !=
nullptr) {
231 if (need_note_gap && this->note_duration_ > REPEATING_NOTE_GAP_MS) {
233 delay(REPEATING_NOTE_GAP_MS);
234 this->note_duration_ -= REPEATING_NOTE_GAP_MS;
249 this->
samples_gap_ = (SAMPLE_RATE * REPEATING_NOTE_GAP_MS) / 1000;
259 ESP_LOGVV(TAG,
"Calc play time: wish: %" PRIu32
" gets: %" PRIu32
" (div: %d spw: %" PRIu32
")", samples_wish,
284 const char *data =
reinterpret_cast<const char *
>(song.
rtttl);
285 this->
set_song_(data, ESPHOME_strlen_P(data));
292 this->
song_len_ =
static_cast<uint16_t
>(std::min<size_t>(
len, std::numeric_limits<uint16_t>::max()));
299 char name[SONG_NAME_LENGTH_LIMIT + 1];
301 ESP_LOGW(TAG,
"Already playing: %s", name);
324 size_t len = std::min(name_len,
size - 1);
336 uint16_t bpm = DEFAULT_BPM;
343 ESP_LOGE(TAG,
"Unable to determine name; missing ':'");
346 if (this->
position_ >= SONG_NAME_LENGTH_LIMIT) {
347 ESP_LOGE(TAG,
"Name is too long: length=%u, limit=%u",
static_cast<unsigned>(this->
position_),
348 static_cast<unsigned>(SONG_NAME_LENGTH_LIMIT));
351 char name[SONG_NAME_LENGTH_LIMIT + 1];
353 ESP_LOGD(TAG,
"Playing song %s", name);
355 size_t name_end_position = this->
position_;
358 ESP_LOGE(TAG,
"Missing second ':'");
364 if (
pos == control_end) {
369 if (num == 1 || num == 2 || num == 4 || num == 8 || num == 16 || num == 32) {
372 ESP_LOGE(TAG,
"Invalid default duration: %d", num);
379 if (
pos == control_end) {
380 ESP_LOGW(TAG,
"Missing 'o='; use default octave %d", this->
default_octave_);
384 if (num >= MIN_OCTAVE && num <= MAX_OCTAVE) {
387 ESP_LOGE(TAG,
"Invalid default octave: %d", num);
394 if (
pos == control_end) {
395 ESP_LOGW(TAG,
"Missing 'b='; use default BPM %d", bpm);
402 ESP_LOGE(TAG,
"Invalid BPM: %d", num);
417 if (this->
output_ !=
nullptr) {
433 if (this->
output_ !=
nullptr) {
453 ESP_LOGV(TAG,
"Rtttl::finish_()");
456 if (this->
output_ !=
nullptr) {
464 int16_t sample[2] = {0, 0};
465 this->
speaker_->
play((uint8_t *) (&sample),
sizeof(sample));
479 ESP_LOGV(TAG,
"State changed from %s to %s", LOG_STR_ARG(state_to_string(old_state)),
480 LOG_STR_ARG(state_to_string(
state)));
485#ifdef USE_RTTTL_FINISHED_PLAYBACK_CALLBACK
488 ESP_LOGD(TAG,
"Playback finished");
void enable_loop()
Enable this component's loop.
void disable_loop()
Disable this component's loop.
void set_level(float state)
Set the level of this float output, this is called from the front-end.
virtual void update_frequency(float frequency)
Set the frequency of the output for PWM outputs.
uint32_t samples_per_wave_
The number of samples for one full cycle of a note's waveform, in Q10 fixed-point format.
std::string owned_song_
Backing store for play(std::string).
size_t song_find_(char c, size_t from, size_t end) const
Index of the first c in [from, end), or end when absent.
uint32_t last_note_start_time_
The time in milliseconds since microcontroller boot when the last note was started.
uint8_t default_note_denominator_
The default duration of a note (e.g. 4 for a quarter note).
uint16_t position_
The current position in the song.
bool is_busy_() const
Logs and returns true when a song is still playing, so a new one must wait.
uint16_t note_duration_
The duration of the current note in milliseconds.
uint16_t song_len_
Length of the song in bytes; songs are a few hundred bytes, so 16 bits is ample.
void start_()
Parses the header and starts playback of the song the view points at.
uint32_t samples_sent_
The number of samples sent.
output::FloatOutput * output_
The output to write the sound to.
void dump_config() override
void set_state_(State state)
void finish_()
Finalizes the playback of the RTTTL string.
float gain_
The gain of the output.
uint8_t default_octave_
The default octave for a note.
uint32_t output_freq_
The frequency of the current note in Hz.
uint32_t samples_gap_
The number of samples for the gap between notes.
char song_at_(size_t pos) const
The byte of the song at pos, or NUL past its end; the read is flash safe for a PROGMEM song on ESP826...
uint32_t samples_count_
The total number of samples to send.
uint16_t wholenote_duration_
The duration of a whole note in milliseconds.
const char * song_
The song being played; points at owned_song_ or at static storage.
size_t song_find_control_(char key, size_t from, size_t end) const
Index of key followed by '=' in [from, end), or end when absent.
void song_name_(char *buf, size_t size, size_t name_len) const
Copies the first name_len bytes of the song, its name, into buf for logging.
State state_
The current state of the RTTTL player.
speaker::Speaker * speaker_
The speaker to write the sound to.
CallbackManager< void()> on_finished_playback_callback_
The callback to call when playback is finished.
void set_song_(const char *song, size_t len)
Points the view at a song, clamped to what song_len_ can hold.
void play(std::string rtttl)
Play a song; the string is copied so it may come from anywhere.
virtual size_t play(const uint8_t *data, size_t length)=0
Plays the provided audio data.
virtual void set_volume(float volume)
void set_audio_stream_info(const audio::AudioStreamInfo &audio_stream_info)
double deg2rad(double degrees)
constexpr uint8_t DEFAULT_OCTAVE
constexpr uint8_t DEFAULT_NOTE_DENOMINATOR
ESPHOME_LOG_TAG(TAG, "rtttl")
PROGMEM_STRING_TABLE(RtttlStateStrings, "State::STOPPED", "State::INIT", "State::STARTING", "State::RUNNING", "State::STOPPING", "UNKNOWN")
void HOT delay(uint32_t ms)
uint32_t IRAM_ATTR HOT millis()
A song in storage that outlives playback, such as a literal from generated code; PROGMEM on ESP8266.