ESPHome 2026.2.3
Loading...
Searching...
No Matches
rtttl.cpp
Go to the documentation of this file.
1#include "rtttl.h"
2#include <cmath>
3#include "esphome/core/hal.h"
4#include "esphome/core/log.h"
6
7namespace esphome::rtttl {
8
9static const char *const TAG = "rtttl";
10
11// These values can also be found as constants in the Tone library (Tone.h)
12static const uint16_t NOTES[] = {0, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494,
13 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047,
14 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217,
15 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951};
16
17#if defined(USE_OUTPUT) || defined(USE_SPEAKER)
18static const uint32_t DOUBLE_NOTE_GAP_MS = 10;
19#endif // USE_OUTPUT || USE_SPEAKER
20
21#ifdef USE_SPEAKER
22static const size_t SAMPLE_BUFFER_SIZE = 2048;
23
24struct SpeakerSample {
25 int8_t left{0};
26 int8_t right{0};
27};
28
29inline double deg2rad(double degrees) {
30 static const double PI_ON_180 = 4.0 * atan(1.0) / 180.0;
31 return degrees * PI_ON_180;
32}
33#endif // USE_SPEAKER
34
35#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
36// RTTTL state strings indexed by State enum (0-4): STOPPED, INIT, STARTING, RUNNING, STOPPING, plus UNKNOWN fallback
37PROGMEM_STRING_TABLE(RtttlStateStrings, "State::STOPPED", "State::INIT", "State::STARTING", "State::RUNNING",
38 "State::STOPPING", "UNKNOWN");
39
40static const LogString *state_to_string(State state) {
41 return RtttlStateStrings::get_log_str(static_cast<uint8_t>(state), RtttlStateStrings::LAST_INDEX);
42}
43#endif // ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
44
45static uint8_t note_index_from_char(char note) {
46 switch (note) {
47 case 'c':
48 return 1;
49 // 'c#': 2
50 case 'd':
51 return 3;
52 // 'd#': 4
53 case 'e':
54 return 5;
55 case 'f':
56 return 6;
57 // 'f#': 7
58 case 'g':
59 return 8;
60 // 'g#': 9
61 case 'a':
62 return 10;
63 // 'a#': 11
64 // Support both 'b' (English notation for B natural) and 'h' (German notation for B natural)
65 case 'b':
66 case 'h':
67 return 12;
68 case 'p':
69 default:
70 return 0;
71 }
72}
73
75 ESP_LOGCONFIG(TAG,
76 "Rtttl:\n"
77 " Gain: %f",
78 this->gain_);
79}
80
82 if (this->state_ == State::STOPPED) {
83 this->disable_loop();
84 return;
85 }
86
87#ifdef USE_OUTPUT
88 if (this->output_ != nullptr && millis() - this->last_note_ < this->note_duration_) {
89 return;
90 }
91#endif // USE_OUTPUT
92
93#ifdef USE_SPEAKER
94 if (this->speaker_ != nullptr) {
95 if (this->state_ == State::STOPPING) {
96 if (this->speaker_->is_stopped()) {
98 } else {
99 return;
100 }
101 } else if (this->state_ == State::INIT) {
102 if (this->speaker_->is_stopped()) {
103 this->speaker_->start();
105 }
106 } else if (this->state_ == State::STARTING) {
107 if (this->speaker_->is_running()) {
109 }
110 }
111 if (!this->speaker_->is_running()) {
112 return;
113 }
114 if (this->samples_sent_ != this->samples_count_) {
115 SpeakerSample sample[SAMPLE_BUFFER_SIZE + 2];
116 int x = 0;
117 double rem = 0.0;
118
119 while (true) {
120 // Try and send out the remainder of the existing note, one per loop()
121
122 if (this->samples_per_wave_ != 0 && this->samples_sent_ >= this->samples_gap_) { // Play note//
123 rem = ((this->samples_sent_ << 10) % this->samples_per_wave_) * (360.0 / this->samples_per_wave_);
124
125 int16_t val = (127 * this->gain_) * sin(deg2rad(rem)); // 16bit = 49152
126
127 sample[x].left = val;
128 sample[x].right = val;
129
130 } else {
131 sample[x].left = 0;
132 sample[x].right = 0;
133 }
134
135 if (static_cast<size_t>(x) >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) {
136 break;
137 }
138 this->samples_sent_++;
139 x++;
140 }
141 if (x > 0) {
142 size_t bytes_to_send = x * sizeof(SpeakerSample);
143 size_t send = this->speaker_->play((uint8_t *) (&sample), bytes_to_send);
144 if (send != bytes_to_send) {
145 this->samples_sent_ -= (x - (send / sizeof(SpeakerSample)));
146 }
147 return;
148 }
149 }
150 }
151#endif // USE_SPEAKER
152
153 if (this->position_ >= this->rtttl_.length()) {
154 this->finish_();
155 return;
156 }
157
158 // align to note: most rtttl's out there does not add and space after the ',' separator but just in case...
159 while (this->rtttl_[this->position_] == ',' || this->rtttl_[this->position_] == ' ') {
160 this->position_++;
161 }
162
163 // first, get note duration, if available
164 uint8_t num = this->get_integer_();
165
166 if (num) {
167 this->note_duration_ = this->wholenote_ / num;
168 } else {
169 this->note_duration_ =
170 this->wholenote_ / this->default_duration_; // we will need to check if we are a dotted note after
171 }
172
173 uint8_t note = note_index_from_char(this->rtttl_[this->position_]);
174
175 this->position_++;
176
177 // now, get optional '#' sharp
178 if (this->rtttl_[this->position_] == '#') {
179 note++;
180 this->position_++;
181 }
182
183 // now, get scale
184 uint8_t scale = this->get_integer_();
185 if (scale == 0) {
186 scale = this->default_octave_;
187 }
188
189 if (scale < 4 || scale > 7) {
190 ESP_LOGE(TAG, "Octave must be between 4 and 7 (it is %d)", scale);
191 this->finish_();
192 return;
193 }
194
195 // now, get optional '.' dotted note
196 if (this->rtttl_[this->position_] == '.') {
197 this->note_duration_ += this->note_duration_ / 2;
198 this->position_++;
199 }
200
201 // Now play the note
202 bool need_note_gap = false;
203 if (note) {
204 auto note_index = (scale - 4) * 12 + note;
205 if (note_index < 0 || note_index >= (int) (sizeof(NOTES) / sizeof(NOTES[0]))) {
206 ESP_LOGE(TAG, "Note out of range (note: %d, scale: %d, index: %d, max: %d)", note, scale, note_index,
207 (int) (sizeof(NOTES) / sizeof(NOTES[0])));
208 this->finish_();
209 return;
210 }
211 auto freq = NOTES[note_index];
212 need_note_gap = freq == this->output_freq_;
213
214 // Add small silence gap between same note
215 this->output_freq_ = freq;
216
217 ESP_LOGVV(TAG, "playing note: %d for %dms", note, this->note_duration_);
218 } else {
219 ESP_LOGVV(TAG, "waiting: %dms", this->note_duration_);
220 this->output_freq_ = 0;
221 }
222
223#ifdef USE_OUTPUT
224 if (this->output_ != nullptr) {
225 if (need_note_gap && this->note_duration_ > DOUBLE_NOTE_GAP_MS) {
226 this->output_->set_level(0.0);
227 delay(DOUBLE_NOTE_GAP_MS);
228 this->note_duration_ -= DOUBLE_NOTE_GAP_MS;
229 }
230 if (this->output_freq_ != 0) {
232 this->output_->set_level(this->gain_);
233 } else {
234 this->output_->set_level(0.0);
235 }
236 }
237#endif // USE_OUTPUT
238
239#ifdef USE_SPEAKER
240 if (this->speaker_ != nullptr) {
241 this->samples_sent_ = 0;
242 this->samples_gap_ = 0;
243 this->samples_per_wave_ = 0;
244 this->samples_count_ = (this->sample_rate_ * this->note_duration_) / 1000;
245 if (need_note_gap) {
246 this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1000;
247 }
248 if (this->output_freq_ != 0) {
249 // make sure there is enough samples to add a full last sinus.
250
251 uint16_t samples_wish = this->samples_count_;
252 this->samples_per_wave_ = (this->sample_rate_ << 10) / this->output_freq_;
253
254 uint16_t division = ((this->samples_count_ << 10) / this->samples_per_wave_) + 1;
255
256 this->samples_count_ = (division * this->samples_per_wave_);
257 this->samples_count_ = this->samples_count_ >> 10;
258 ESP_LOGVV(TAG, "- Calc play time: wish: %d gets: %d (div: %d spw: %d)", samples_wish, this->samples_count_,
259 division, this->samples_per_wave_);
260 }
261 // Convert from frequency in Hz to high and low samples in fixed point
262 }
263#endif // USE_SPEAKER
264
265 this->last_note_ = millis();
266}
267
268void Rtttl::play(std::string rtttl) {
269 if (this->state_ != State::STOPPED && this->state_ != State::STOPPING) {
270 size_t pos = this->rtttl_.find(':');
271 size_t len = (pos != std::string::npos) ? pos : this->rtttl_.length();
272 ESP_LOGW(TAG, "Already playing: %.*s", (int) len, this->rtttl_.c_str());
273 return;
274 }
275
276 this->rtttl_ = std::move(rtttl);
277
278 this->default_duration_ = 4;
279 this->default_octave_ = 6;
280 this->note_duration_ = 0;
281
282 int bpm = 63;
283 uint16_t num;
284
285 // Get name
286 this->position_ = this->rtttl_.find(':');
287
288 // it's somewhat documented to be up to 10 characters but let's be a bit flexible here
289 if (this->position_ == std::string::npos || this->position_ > 15) {
290 ESP_LOGE(TAG, "Unable to determine name; missing ':'");
291 return;
292 }
293
294 ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str());
295
296 // get default duration
297 this->position_ = this->rtttl_.find("d=", this->position_);
298 if (this->position_ == std::string::npos) {
299 ESP_LOGE(TAG, "Missing 'd='");
300 return;
301 }
302 this->position_ += 2;
303 num = this->get_integer_();
304 if (num > 0) {
305 this->default_duration_ = num;
306 }
307
308 // get default octave
309 this->position_ = this->rtttl_.find("o=", this->position_);
310 if (this->position_ == std::string::npos) {
311 ESP_LOGE(TAG, "Missing 'o=");
312 return;
313 }
314 this->position_ += 2;
315 num = this->get_integer_();
316 if (num >= 3 && num <= 7) {
317 this->default_octave_ = num;
318 }
319
320 // get BPM
321 this->position_ = this->rtttl_.find("b=", this->position_);
322 if (this->position_ == std::string::npos) {
323 ESP_LOGE(TAG, "Missing b=");
324 return;
325 }
326 this->position_ += 2;
327 num = this->get_integer_();
328 if (num != 0) {
329 bpm = num;
330 }
331
332 this->position_ = this->rtttl_.find(':', this->position_);
333 if (this->position_ == std::string::npos) {
334 ESP_LOGE(TAG, "Missing second ':'");
335 return;
336 }
337 this->position_++;
338
339 // BPM usually expresses the number of quarter notes per minute
340 this->wholenote_ = 60 * 1000L * 4 / bpm; // this is the time for whole note (in milliseconds)
341
342 this->output_freq_ = 0;
343 this->last_note_ = millis();
344 this->note_duration_ = 1;
345
346#ifdef USE_OUTPUT
347 if (this->output_ != nullptr) {
349 }
350#endif // USE_OUTPUT
351
352#ifdef USE_SPEAKER
353 if (this->speaker_ != nullptr) {
354 this->set_state_(State::INIT);
355 this->samples_sent_ = 0;
356 this->samples_count_ = 0;
357 }
358#endif // USE_SPEAKER
359}
360
362#ifdef USE_OUTPUT
363 if (this->output_ != nullptr) {
364 this->output_->set_level(0.0);
366 }
367#endif // USE_OUTPUT
368
369#ifdef USE_SPEAKER
370 if (this->speaker_ != nullptr) {
371 if (this->speaker_->is_running()) {
372 this->speaker_->stop();
373 }
375 }
376#endif // USE_SPEAKER
377
378 this->position_ = this->rtttl_.length();
379 this->note_duration_ = 0;
380}
381
383 ESP_LOGV(TAG, "Rtttl::finish_()");
384
385#ifdef USE_OUTPUT
386 if (this->output_ != nullptr) {
387 this->output_->set_level(0.0);
389 }
390#endif // USE_OUTPUT
391
392#ifdef USE_SPEAKER
393 if (this->speaker_ != nullptr) {
394 SpeakerSample sample[2];
395 sample[0].left = 0;
396 sample[0].right = 0;
397 sample[1].left = 0;
398 sample[1].right = 0;
399 this->speaker_->play((uint8_t *) (&sample), sizeof(sample));
400 this->speaker_->finish();
402 }
403#endif // USE_SPEAKER
404
405 // Ensure no more notes are played in case finish_() is called for an error.
406 this->position_ = this->rtttl_.length();
407 this->note_duration_ = 0;
408}
409
411 State old_state = this->state_;
412 this->state_ = state;
413 ESP_LOGV(TAG, "State changed from %s to %s", LOG_STR_ARG(state_to_string(old_state)),
414 LOG_STR_ARG(state_to_string(state)));
415
416 // Clear loop_done when transitioning from `State::STOPPED` to any other state
417 if (state == State::STOPPED) {
418 this->disable_loop();
420 ESP_LOGD(TAG, "Playback finished");
421 } else if (old_state == State::STOPPED) {
422 this->enable_loop();
423 }
424}
425
426} // namespace esphome::rtttl
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.
int samples_per_wave_
The number of samples for one full cycle of a note's waveform, in Q10 fixed-point format.
Definition rtttl.h:99
uint16_t wholenote_
The duration of a whole note in milliseconds.
Definition rtttl.h:71
uint32_t last_note_
The time the last note was started.
Definition rtttl.h:77
int samples_sent_
The number of samples sent.
Definition rtttl.h:101
uint16_t note_duration_
The duration of the current note in milliseconds.
Definition rtttl.h:79
int samples_gap_
The number of samples for the gap between notes.
Definition rtttl.h:105
int sample_rate_
The sample rate of the speaker.
Definition rtttl.h:97
output::FloatOutput * output_
The output to write the sound to.
Definition rtttl.h:90
uint16_t get_integer_()
Definition rtttl.h:49
void dump_config() override
Definition rtttl.cpp:74
void set_state_(State state)
Definition rtttl.cpp:410
void finish_()
Finalizes the playback of the RTTTL string.
Definition rtttl.cpp:382
float gain_
The gain of the output.
Definition rtttl.h:84
uint32_t output_freq_
The frequency of the current note in Hz.
Definition rtttl.h:82
void loop() override
Definition rtttl.cpp:81
uint16_t default_duration_
The default duration of a note (e.g. 4 for a quarter note).
Definition rtttl.h:73
size_t position_
The current position in the RTTTL string.
Definition rtttl.h:69
uint16_t default_octave_
The default octave for a note.
Definition rtttl.h:75
State state_
The current state of the RTTTL player.
Definition rtttl.h:86
int samples_count_
The total number of samples to send.
Definition rtttl.h:103
speaker::Speaker * speaker_
The speaker to write the sound to.
Definition rtttl.h:95
CallbackManager< void()> on_finished_playback_callback_
The callback to call when playback is finished.
Definition rtttl.h:109
void play(std::string rtttl)
Definition rtttl.cpp:268
std::string rtttl_
The RTTTL string to play.
Definition rtttl.h:67
virtual size_t play(const uint8_t *data, size_t length)=0
Plays the provided audio data.
bool is_running() const
Definition speaker.h:66
virtual void start()=0
virtual void finish()
Definition speaker.h:58
bool is_stopped() const
Definition speaker.h:67
virtual void stop()=0
bool state
Definition fan.h:2
mopeka_std_values val[4]
double deg2rad(double degrees)
Definition rtttl.cpp:29
PROGMEM_STRING_TABLE(RtttlStateStrings, "State::STOPPED", "State::INIT", "State::STARTING", "State::RUNNING", "State::STOPPING", "UNKNOWN")
std::string size_t len
Definition helpers.h:692
size_t size_t pos
Definition helpers.h:729
void IRAM_ATTR HOT delay(uint32_t ms)
Definition core.cpp:26
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:25
uint16_t x
Definition tt21100.cpp:5