ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
rtttl.cpp
Go to the documentation of this file.
1#include "rtttl.h"
2#include <algorithm>
3#include <cmath>
4#include <cstring>
5#include <limits>
6#include "esphome/core/hal.h"
7#include "esphome/core/log.h"
9
10namespace esphome::rtttl {
11
12ESPHOME_LOG_TAG(TAG, "rtttl");
13
14static constexpr uint8_t SONG_NAME_LENGTH_LIMIT = 64;
15static constexpr uint8_t SEMITONES_IN_OCTAVE = 12;
16
17static constexpr uint8_t MIN_OCTAVE = 4;
18static constexpr uint8_t MAX_OCTAVE = 7;
19
20static constexpr uint8_t DEFAULT_BPM = 63; // Default beats per minute
21
22// These values can also be found as constants in the Tone library (Tone.h)
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]));
28
29static constexpr uint8_t REPEATING_NOTE_GAP_MS = 10;
30
31#ifdef USE_SPEAKER
32static constexpr uint16_t SAMPLE_BUFFER_SIZE = 2048;
33static constexpr uint16_t SAMPLE_RATE = 16000;
34
35inline double deg2rad(double degrees) {
36 static constexpr double PI_ON_180 = M_PI / 180.0;
37 return degrees * PI_ON_180;
38}
39#endif // USE_SPEAKER
40
41#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
42// RTTTL state strings indexed by State enum (0-4): STOPPED, INIT, STARTING, RUNNING, STOPPING, plus UNKNOWN fallback
43PROGMEM_STRING_TABLE(RtttlStateStrings, "State::STOPPED", "State::INIT", "State::STARTING", "State::RUNNING",
44 "State::STOPPING", "UNKNOWN");
45
46static const LogString *state_to_string(State state) {
47 return RtttlStateStrings::get_log_str(static_cast<uint8_t>(state), RtttlStateStrings::LAST_INDEX);
48}
49#endif // ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
50
51static uint8_t note_index_from_char(char note) {
52 switch (note) {
53 case 'c':
54 return 1;
55 // 'c#': 2
56 case 'd':
57 return 3;
58 // 'd#': 4
59 case 'e':
60 return 5;
61 case 'f':
62 return 6;
63 // 'f#': 7
64 case 'g':
65 return 8;
66 // 'g#': 9
67 case 'a':
68 return 10;
69 // 'a#': 11
70 // Support both 'b' (English notation for B natural) and 'h' (German notation for B natural)
71 case 'b':
72 case 'h':
73 return 12;
74 case 'p':
75 default:
76 return 0;
77 }
78}
79
81 ESP_LOGCONFIG(TAG,
82 "Rtttl:\n"
83 " Gain: %f",
84 this->gain_);
85}
86
88 if (this->state_ == State::STOPPED) {
89 this->disable_loop();
90 return;
91 }
92
93#ifdef USE_OUTPUT
94 if (this->output_ != nullptr && millis() - this->last_note_start_time_ < this->note_duration_) {
95 return;
96 }
97#endif // USE_OUTPUT
98
99#ifdef USE_SPEAKER
100 if (this->speaker_ != nullptr) {
101 if (this->state_ == State::STOPPING) {
102 if (this->speaker_->is_stopped()) {
104 } else {
105 return;
106 }
107 } else if (this->state_ == State::INIT) {
108 if (this->speaker_->is_stopped()) {
109 audio::AudioStreamInfo audio_stream_info = audio::AudioStreamInfo(16, 1, SAMPLE_RATE);
110 this->speaker_->set_audio_stream_info(audio_stream_info);
111 this->speaker_->set_volume(this->gain_);
112 this->speaker_->start();
114 }
115 } else if (this->state_ == State::STARTING) {
116 if (this->speaker_->is_running()) {
118 }
119 }
120 if (!this->speaker_->is_running()) {
121 return;
122 }
123 if (this->samples_sent_ != this->samples_count_) {
124 int16_t sample[SAMPLE_BUFFER_SIZE];
125 uint16_t sample_index = 0;
126 double rem = 0.0;
127
128 while (sample_index < SAMPLE_BUFFER_SIZE && this->samples_sent_ < this->samples_count_) {
129 // Try and send out the remainder of the existing note, one per `loop()`
130 if (this->samples_per_wave_ != 0 && this->samples_sent_ >= this->samples_gap_) { // Play note
131 rem = ((this->samples_sent_ << 10) % this->samples_per_wave_) * (360.0 / this->samples_per_wave_);
132 sample[sample_index] = INT16_MAX * sin(deg2rad(rem));
133 } else {
134 sample[sample_index] = 0;
135 }
136 this->samples_sent_++;
137 sample_index++;
138 }
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) {
144 this->samples_sent_ -= (sample_index - samples_sent);
145 }
146 return;
147 }
148 }
149 }
150#endif // USE_SPEAKER
151
152 // Align to note: most rtttl's out there does not add any space after the ',' separator but just in case
153 while (this->position_ < this->song_len_) {
154 char c = this->song_at_(this->position_);
155 if (c != ',' && c != ' ')
156 break;
157 this->position_++;
158 }
159
160 if (this->position_ >= this->song_len_) {
161 this->finish_();
162 return;
163 }
164
165 // First, get note duration, if available
166 uint8_t note_denominator = this->get_integer_();
167
168 if (note_denominator) {
169 this->note_duration_ = this->wholenote_duration_ / note_denominator;
170 } else {
171 // We will need to check if we are a dotted note after
172 this->note_duration_ = this->wholenote_duration_ / this->default_note_denominator_;
173 }
174
175 uint8_t note_index_in_octave = note_index_from_char(this->song_at_(this->position_));
176
177 this->position_++;
178
179 // Now, get optional '#' sharp
180 if (this->song_at_(this->position_) == '#') {
181 note_index_in_octave++;
182 this->position_++;
183 }
184
185 // Now, get scale
186 uint8_t scale = this->get_integer_();
187 if (scale == 0) {
188 scale = this->default_octave_;
189 }
190
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);
193 this->finish_();
194 return;
195 }
196
197 // Now, get optional '.' dotted note
198 if (this->song_at_(this->position_) == '.') {
199 this->note_duration_ += this->note_duration_ / 2; // Duration +50%
200 this->position_++;
201 }
202
203 bool need_note_gap = false;
204
205 // Now play the note
206 if (note_index_in_octave == 0) {
207 this->output_freq_ = 0;
208 ESP_LOGVV(TAG, "Waiting: %dms", this->note_duration_);
209 } else {
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);
214 this->finish_();
215 return;
216 }
217 uint16_t freq = NOTES[note_index];
218 need_note_gap = freq == this->output_freq_;
219
220 // Add small silence gap between same note
221 this->output_freq_ = freq;
222
223 ESP_LOGVV(TAG, "Playing note: %d for %dms", note_index_in_octave, this->note_duration_);
224 }
225
226#ifdef USE_OUTPUT
227 if (this->output_ != nullptr) {
228 if (this->output_freq_ == 0) {
229 this->output_->set_level(0.0);
230 } else {
231 if (need_note_gap && this->note_duration_ > REPEATING_NOTE_GAP_MS) {
232 this->output_->set_level(0.0);
233 delay(REPEATING_NOTE_GAP_MS);
234 this->note_duration_ -= REPEATING_NOTE_GAP_MS;
235 }
237 this->output_->set_level(this->gain_);
238 }
239 }
240#endif // USE_OUTPUT
241
242#ifdef USE_SPEAKER
243 if (this->speaker_ != nullptr) {
244 this->samples_sent_ = 0;
245 this->samples_gap_ = 0;
246 this->samples_per_wave_ = 0;
247 this->samples_count_ = (SAMPLE_RATE * this->note_duration_) / 1000;
248 if (need_note_gap) {
249 this->samples_gap_ = (SAMPLE_RATE * REPEATING_NOTE_GAP_MS) / 1000;
250 }
251 if (this->output_freq_ != 0) {
252 // Make sure there is enough samples to add a full last sinus.
253 uint32_t samples_wish = this->samples_count_;
254 this->samples_per_wave_ = (SAMPLE_RATE << 10) / this->output_freq_;
255
256 uint16_t division = ((this->samples_count_ << 10) / this->samples_per_wave_) + 1;
257
258 this->samples_count_ = (division * this->samples_per_wave_) >> 10;
259 ESP_LOGVV(TAG, "Calc play time: wish: %" PRIu32 " gets: %" PRIu32 " (div: %d spw: %" PRIu32 ")", samples_wish,
260 this->samples_count_, division, this->samples_per_wave_);
261 }
262 // Convert from frequency in Hz to high and low samples in fixed point
263 }
264#endif // USE_SPEAKER
265
267}
268
269void Rtttl::play(std::string rtttl) {
270 if (this->is_busy_()) {
271 return;
272 }
273 this->owned_song_ = std::move(rtttl);
274 this->set_song_(this->owned_song_.data(), this->owned_song_.size());
275 this->start_();
276}
277
279 if (this->is_busy_()) {
280 return;
281 }
282 // Release a previously owned song; the static one needs no storage.
283 std::string().swap(this->owned_song_);
284 const char *data = reinterpret_cast<const char *>(song.rtttl);
285 this->set_song_(data, ESPHOME_strlen_P(data));
286 this->start_();
287}
288
289void Rtttl::set_song_(const char *song, size_t len) {
290 this->song_ = song;
291 // Anything past what the 16 bit length holds is dropped; the validator keeps configured songs shorter.
292 this->song_len_ = static_cast<uint16_t>(std::min<size_t>(len, std::numeric_limits<uint16_t>::max()));
293}
294
295bool Rtttl::is_busy_() const {
296 if (this->state_ == State::STOPPED || this->state_ == State::STOPPING) {
297 return false;
298 }
299 char name[SONG_NAME_LENGTH_LIMIT + 1];
300 this->song_name_(name, sizeof(name), this->song_find_(':', 0, this->song_len_));
301 ESP_LOGW(TAG, "Already playing: %s", name);
302 return true;
303}
304
305size_t Rtttl::song_find_(char c, size_t from, size_t end) const {
306 for (size_t pos = from; pos < end; pos++) {
307 if (this->song_at_(pos) == c)
308 return pos;
309 }
310 return end;
311}
312
313size_t Rtttl::song_find_control_(char key, size_t from, size_t end) const {
314 size_t pos = from;
315 while ((pos = this->song_find_(key, pos, end)) < end) {
316 if (this->song_at_(pos + 1) == '=')
317 return pos;
318 pos++;
319 }
320 return end;
321}
322
323void Rtttl::song_name_(char *buf, size_t size, size_t name_len) const {
324 size_t len = std::min(name_len, size - 1);
325 for (size_t pos = 0; pos < len; pos++) {
326 buf[pos] = this->song_at_(pos);
327 }
328 buf[len] = '\0';
329}
330
334 this->note_duration_ = 0;
335
336 uint16_t bpm = DEFAULT_BPM;
337 uint16_t num; // Used for: default note-denominator, default octave, BPM
338
339 // Get name
340 this->position_ = this->song_find_(':', 0, this->song_len_);
341
342 if (this->position_ == this->song_len_) {
343 ESP_LOGE(TAG, "Unable to determine name; missing ':'");
344 return;
345 }
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));
349 return;
350 }
351 char name[SONG_NAME_LENGTH_LIMIT + 1];
352 this->song_name_(name, sizeof(name), this->position_);
353 ESP_LOGD(TAG, "Playing song %s", name);
354
355 size_t name_end_position = this->position_;
356 size_t control_end = this->song_find_(':', name_end_position + 1, this->song_len_);
357 if (control_end == this->song_len_) {
358 ESP_LOGE(TAG, "Missing second ':'");
359 return;
360 }
361
362 // Get default duration
363 size_t pos = this->song_find_control_('d', name_end_position, control_end);
364 if (pos == control_end) {
365 ESP_LOGW(TAG, "Missing 'd='; use default duration %d", this->default_note_denominator_);
366 } else {
367 this->position_ = pos + 2;
368 num = this->get_integer_();
369 if (num == 1 || num == 2 || num == 4 || num == 8 || num == 16 || num == 32) {
370 this->default_note_denominator_ = num;
371 } else {
372 ESP_LOGE(TAG, "Invalid default duration: %d", num);
373 return;
374 }
375 }
376
377 // Get default octave
378 pos = this->song_find_control_('o', name_end_position, control_end);
379 if (pos == control_end) {
380 ESP_LOGW(TAG, "Missing 'o='; use default octave %d", this->default_octave_);
381 } else {
382 this->position_ = pos + 2;
383 num = this->get_integer_();
384 if (num >= MIN_OCTAVE && num <= MAX_OCTAVE) {
385 this->default_octave_ = num;
386 } else {
387 ESP_LOGE(TAG, "Invalid default octave: %d", num);
388 return;
389 }
390 }
391
392 // Get BPM
393 pos = this->song_find_control_('b', name_end_position, control_end);
394 if (pos == control_end) {
395 ESP_LOGW(TAG, "Missing 'b='; use default BPM %d", bpm);
396 } else {
397 this->position_ = pos + 2;
398 num = this->get_integer_();
399 if (num >= 4) { // Below 4 is not realistic and would cause a integer overflow
400 bpm = num;
401 } else {
402 ESP_LOGE(TAG, "Invalid BPM: %d", num);
403 return;
404 }
405 }
406
407 this->position_ = control_end + 1;
408
409 // BPM usually expresses the number of quarter notes per minute
410 this->wholenote_duration_ = 60 * 1000L * 4 / bpm; // This is the time for whole note (in milliseconds)
411
412 this->output_freq_ = 0;
414 this->note_duration_ = 1;
415
416#ifdef USE_OUTPUT
417 if (this->output_ != nullptr) {
419 }
420#endif // USE_OUTPUT
421
422#ifdef USE_SPEAKER
423 if (this->speaker_ != nullptr) {
424 this->set_state_(State::INIT);
425 this->samples_sent_ = 0;
426 this->samples_count_ = 0;
427 }
428#endif // USE_SPEAKER
429}
430
432#ifdef USE_OUTPUT
433 if (this->output_ != nullptr) {
434 this->output_->set_level(0.0);
436 }
437#endif // USE_OUTPUT
438
439#ifdef USE_SPEAKER
440 if (this->speaker_ != nullptr) {
441 if (this->speaker_->is_running()) {
442 this->speaker_->stop();
443 }
445 }
446#endif // USE_SPEAKER
447
448 this->position_ = this->song_len_;
449 this->note_duration_ = 0;
450}
451
453 ESP_LOGV(TAG, "Rtttl::finish_()");
454
455#ifdef USE_OUTPUT
456 if (this->output_ != nullptr) {
457 this->output_->set_level(0.0);
459 }
460#endif // USE_OUTPUT
461
462#ifdef USE_SPEAKER
463 if (this->speaker_ != nullptr) {
464 int16_t sample[2] = {0, 0};
465 this->speaker_->play((uint8_t *) (&sample), sizeof(sample));
466 this->speaker_->finish();
468 }
469#endif // USE_SPEAKER
470
471 // Ensure no more notes are played in case finish_() is called for an error.
472 this->position_ = this->song_len_;
473 this->note_duration_ = 0;
474}
475
477 State old_state = this->state_;
478 this->state_ = state;
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)));
481
482 // Clear loop_done when transitioning from `State::STOPPED` to any other state
483 if (state == State::STOPPED) {
484 this->disable_loop();
485#ifdef USE_RTTTL_FINISHED_PLAYBACK_CALLBACK
487#endif
488 ESP_LOGD(TAG, "Playback finished");
489 } else if (old_state == State::STOPPED) {
490 this->enable_loop();
491 }
492}
493
494} // namespace esphome::rtttl
void enable_loop()
Enable this component's loop.
Definition component.h:248
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.
Definition rtttl.h:138
std::string owned_song_
Backing store for play(std::string).
Definition rtttl.h:107
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.
Definition rtttl.cpp:305
uint32_t last_note_start_time_
The time in milliseconds since microcontroller boot when the last note was started.
Definition rtttl.h:109
uint8_t default_note_denominator_
The default duration of a note (e.g. 4 for a quarter note).
Definition rtttl.h:123
uint16_t position_
The current position in the song.
Definition rtttl.h:117
bool is_busy_() const
Logs and returns true when a song is still playing, so a new one must wait.
Definition rtttl.cpp:295
uint16_t note_duration_
The duration of the current note in milliseconds.
Definition rtttl.h:119
uint16_t song_len_
Length of the song in bytes; songs are a few hundred bytes, so 16 bits is ample.
Definition rtttl.h:115
void start_()
Parses the header and starts playback of the song the view points at.
Definition rtttl.cpp:331
uint32_t samples_sent_
The number of samples sent.
Definition rtttl.h:140
output::FloatOutput * output_
The output to write the sound to.
Definition rtttl.h:131
uint16_t get_integer_()
Definition rtttl.h:69
void dump_config() override
Definition rtttl.cpp:80
void set_state_(State state)
Definition rtttl.cpp:476
void finish_()
Finalizes the playback of the RTTTL string.
Definition rtttl.cpp:452
float gain_
The gain of the output.
Definition rtttl.h:113
uint8_t default_octave_
The default octave for a note.
Definition rtttl.h:125
uint32_t output_freq_
The frequency of the current note in Hz.
Definition rtttl.h:111
void loop() override
Definition rtttl.cpp:87
uint32_t samples_gap_
The number of samples for the gap between notes.
Definition rtttl.h:144
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...
Definition rtttl.h:77
uint32_t samples_count_
The total number of samples to send.
Definition rtttl.h:142
uint16_t wholenote_duration_
The duration of a whole note in milliseconds.
Definition rtttl.h:121
const char * song_
The song being played; points at owned_song_ or at static storage.
Definition rtttl.h:105
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.
Definition rtttl.cpp:313
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.
Definition rtttl.cpp:323
State state_
The current state of the RTTTL player.
Definition rtttl.h:127
speaker::Speaker * speaker_
The speaker to write the sound to.
Definition rtttl.h:136
CallbackManager< void()> on_finished_playback_callback_
The callback to call when playback is finished.
Definition rtttl.h:149
void set_song_(const char *song, size_t len)
Points the view at a song, clamped to what song_len_ can hold.
Definition rtttl.cpp:289
void play(std::string rtttl)
Play a song; the string is copied so it may come from anywhere.
Definition rtttl.cpp:269
virtual size_t play(const uint8_t *data, size_t length)=0
Plays the provided audio data.
bool is_running() const
Definition speaker.h:68
virtual void set_volume(float volume)
Definition speaker.h:74
void set_audio_stream_info(const audio::AudioStreamInfo &audio_stream_info)
Definition speaker.h:97
virtual void start()=0
virtual void finish()
Definition speaker.h:60
bool is_stopped() const
Definition speaker.h:69
virtual void stop()=0
const char * key
bool state
Definition fan.h:2
double deg2rad(double degrees)
Definition rtttl.cpp:35
constexpr uint8_t DEFAULT_OCTAVE
Definition rtttl.h:23
constexpr uint8_t DEFAULT_NOTE_DENOMINATOR
Definition rtttl.h:22
ESPHOME_LOG_TAG(TAG, "rtttl")
PROGMEM_STRING_TABLE(RtttlStateStrings, "State::STOPPED", "State::INIT", "State::STARTING", "State::RUNNING", "State::STOPPING", "UNKNOWN")
const void size_t len
Definition hal.h:68
uint16_t size
Definition helpers.cpp:34
size_t size_t pos
Definition helpers.h:1255
void HOT delay(uint32_t ms)
Definition hal.cpp:85
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
static void uint32_t
A song in storage that outlives playback, such as a literal from generated code; PROGMEM on ESP8266.
Definition rtttl.h:35
uint8_t end[39]
Definition sun_gtil2.cpp:17