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