ESPHome 2025.10.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 int pos = this->rtttl_.find(':');
39 auto name = this->rtttl_.substr(0, pos);
40 ESP_LOGW(TAG, "Already playing: %s", name.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 auto name = this->rtttl_.substr(0, this->position_);
63 ESP_LOGD(TAG, "Playing song %s", name.c_str());
64
65 // get default duration
66 this->position_ = this->rtttl_.find("d=", this->position_);
67 if (this->position_ == std::string::npos) {
68 ESP_LOGE(TAG, "Missing 'd='");
69 return;
70 }
71 this->position_ += 2;
72 num = this->get_integer_();
73 if (num > 0)
74 this->default_duration_ = num;
75
76 // get default octave
77 this->position_ = this->rtttl_.find("o=", this->position_);
78 if (this->position_ == std::string::npos) {
79 ESP_LOGE(TAG, "Missing 'o=");
80 return;
81 }
82 this->position_ += 2;
83 num = get_integer_();
84 if (num >= 3 && num <= 7)
85 this->default_octave_ = num;
86
87 // get BPM
88 this->position_ = this->rtttl_.find("b=", this->position_);
89 if (this->position_ == std::string::npos) {
90 ESP_LOGE(TAG, "Missing b=");
91 return;
92 }
93 this->position_ += 2;
94 num = get_integer_();
95 if (num != 0)
96 bpm = num;
97
98 this->position_ = this->rtttl_.find(':', this->position_);
99 if (this->position_ == std::string::npos) {
100 ESP_LOGE(TAG, "Missing second ':'");
101 return;
102 }
103 this->position_++;
104
105 // BPM usually expresses the number of quarter notes per minute
106 this->wholenote_ = 60 * 1000L * 4 / bpm; // this is the time for whole note (in milliseconds)
107
108 this->output_freq_ = 0;
109 this->last_note_ = millis();
110 this->note_duration_ = 1;
111
112#ifdef USE_SPEAKER
113 if (this->speaker_ != nullptr) {
115 this->samples_sent_ = 0;
116 this->samples_count_ = 0;
117 }
118#endif
119#ifdef USE_OUTPUT
120 if (this->output_ != nullptr) {
122 }
123#endif
124}
125
127#ifdef USE_OUTPUT
128 if (this->output_ != nullptr) {
129 this->output_->set_level(0.0);
131 }
132#endif
133#ifdef USE_SPEAKER
134 if (this->speaker_ != nullptr) {
135 if (this->speaker_->is_running()) {
136 this->speaker_->stop();
137 }
139 }
140#endif
141 this->position_ = this->rtttl_.length();
142 this->note_duration_ = 0;
143}
144
146 ESP_LOGV(TAG, "Rtttl::finish_()");
147#ifdef USE_OUTPUT
148 if (this->output_ != nullptr) {
149 this->output_->set_level(0.0);
151 }
152#endif
153#ifdef USE_SPEAKER
154 if (this->speaker_ != nullptr) {
155 SpeakerSample sample[2];
156 sample[0].left = 0;
157 sample[0].right = 0;
158 sample[1].left = 0;
159 sample[1].right = 0;
160 this->speaker_->play((uint8_t *) (&sample), 8);
161 this->speaker_->finish();
163 }
164#endif
165 // Ensure no more notes are played in case finish_() is called for an error.
166 this->position_ = this->rtttl_.length();
167 this->note_duration_ = 0;
168}
169
171 if (this->state_ == State::STATE_STOPPED) {
172 this->disable_loop();
173 return;
174 }
175
176#ifdef USE_SPEAKER
177 if (this->speaker_ != nullptr) {
178 if (this->state_ == State::STATE_STOPPING) {
179 if (this->speaker_->is_stopped()) {
181 } else {
182 return;
183 }
184 } else if (this->state_ == State::STATE_INIT) {
185 if (this->speaker_->is_stopped()) {
186 this->speaker_->start();
188 }
189 } else if (this->state_ == State::STATE_STARTING) {
190 if (this->speaker_->is_running()) {
192 }
193 }
194 if (!this->speaker_->is_running()) {
195 return;
196 }
197 if (this->samples_sent_ != this->samples_count_) {
198 SpeakerSample sample[SAMPLE_BUFFER_SIZE + 2];
199 int x = 0;
200 double rem = 0.0;
201
202 while (true) {
203 // Try and send out the remainder of the existing note, one per loop()
204
205 if (this->samples_per_wave_ != 0 && this->samples_sent_ >= this->samples_gap_) { // Play note//
206 rem = ((this->samples_sent_ << 10) % this->samples_per_wave_) * (360.0 / this->samples_per_wave_);
207
208 int16_t val = (127 * this->gain_) * sin(deg2rad(rem)); // 16bit = 49152
209
210 sample[x].left = val;
211 sample[x].right = val;
212
213 } else {
214 sample[x].left = 0;
215 sample[x].right = 0;
216 }
217
218 if (x >= SAMPLE_BUFFER_SIZE || this->samples_sent_ >= this->samples_count_) {
219 break;
220 }
221 this->samples_sent_++;
222 x++;
223 }
224 if (x > 0) {
225 int send = this->speaker_->play((uint8_t *) (&sample), x * 2);
226 if (send != x * 4) {
227 this->samples_sent_ -= (x - (send / 2));
228 }
229 return;
230 }
231 }
232 }
233#endif
234#ifdef USE_OUTPUT
235 if (this->output_ != nullptr && millis() - this->last_note_ < this->note_duration_)
236 return;
237#endif
238 if (this->position_ >= this->rtttl_.length()) {
239 this->finish_();
240 return;
241 }
242
243 // align to note: most rtttl's out there does not add and space after the ',' separator but just in case...
244 while (this->rtttl_[this->position_] == ',' || this->rtttl_[this->position_] == ' ')
245 this->position_++;
246
247 // first, get note duration, if available
248 uint8_t num = this->get_integer_();
249
250 if (num) {
251 this->note_duration_ = this->wholenote_ / num;
252 } else {
253 this->note_duration_ =
254 this->wholenote_ / this->default_duration_; // we will need to check if we are a dotted note after
255 }
256
257 uint8_t note;
258
259 switch (this->rtttl_[this->position_]) {
260 case 'c':
261 note = 1;
262 break;
263 case 'd':
264 note = 3;
265 break;
266 case 'e':
267 note = 5;
268 break;
269 case 'f':
270 note = 6;
271 break;
272 case 'g':
273 note = 8;
274 break;
275 case 'a':
276 note = 10;
277 break;
278 case 'h':
279 case 'b':
280 note = 12;
281 break;
282 case 'p':
283 default:
284 note = 0;
285 }
286 this->position_++;
287
288 // now, get optional '#' sharp
289 if (this->rtttl_[this->position_] == '#') {
290 note++;
291 this->position_++;
292 }
293
294 // now, get optional '.' dotted note
295 if (this->rtttl_[this->position_] == '.') {
296 this->note_duration_ += this->note_duration_ / 2;
297 this->position_++;
298 }
299
300 // now, get scale
301 uint8_t scale = get_integer_();
302 if (scale == 0)
303 scale = this->default_octave_;
304
305 if (scale < 4 || scale > 7) {
306 ESP_LOGE(TAG, "Octave must be between 4 and 7 (it is %d)", scale);
307 this->finish_();
308 return;
309 }
310 bool need_note_gap = false;
311
312 // Now play the note
313 if (note) {
314 auto note_index = (scale - 4) * 12 + note;
315 if (note_index < 0 || note_index >= (int) sizeof(NOTES)) {
316 ESP_LOGE(TAG, "Note out of range (note: %d, scale: %d, index: %d, max: %d)", note, scale, note_index,
317 (int) sizeof(NOTES));
318 this->finish_();
319 return;
320 }
321 auto freq = NOTES[note_index];
322 need_note_gap = freq == this->output_freq_;
323
324 // Add small silence gap between same note
325 this->output_freq_ = freq;
326
327 ESP_LOGVV(TAG, "playing note: %d for %dms", note, this->note_duration_);
328 } else {
329 ESP_LOGVV(TAG, "waiting: %dms", this->note_duration_);
330 this->output_freq_ = 0;
331 }
332
333#ifdef USE_OUTPUT
334 if (this->output_ != nullptr) {
335 if (need_note_gap) {
336 this->output_->set_level(0.0);
337 delay(DOUBLE_NOTE_GAP_MS);
338 this->note_duration_ -= DOUBLE_NOTE_GAP_MS;
339 }
340 if (this->output_freq_ != 0) {
342 this->output_->set_level(this->gain_);
343 } else {
344 this->output_->set_level(0.0);
345 }
346 }
347#endif
348#ifdef USE_SPEAKER
349 if (this->speaker_ != nullptr) {
350 this->samples_sent_ = 0;
351 this->samples_gap_ = 0;
352 this->samples_per_wave_ = 0;
353 this->samples_count_ = (this->sample_rate_ * this->note_duration_) / 1600; //(ms);
354 if (need_note_gap) {
355 this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1600; //(ms);
356 }
357 if (this->output_freq_ != 0) {
358 // make sure there is enough samples to add a full last sinus.
359
360 uint16_t samples_wish = this->samples_count_;
361 this->samples_per_wave_ = (this->sample_rate_ << 10) / this->output_freq_;
362
363 uint16_t division = ((this->samples_count_ << 10) / this->samples_per_wave_) + 1;
364
365 this->samples_count_ = (division * this->samples_per_wave_);
366 this->samples_count_ = this->samples_count_ >> 10;
367 ESP_LOGVV(TAG, "- Calc play time: wish: %d gets: %d (div: %d spw: %d)", samples_wish, this->samples_count_,
368 division, this->samples_per_wave_);
369 }
370 // Convert from frequency in Hz to high and low samples in fixed point
371 }
372#endif
373
374 this->last_note_ = millis();
375}
376
377#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
378static const LogString *state_to_string(State state) {
379 switch (state) {
380 case STATE_STOPPED:
381 return LOG_STR("STATE_STOPPED");
382 case STATE_STARTING:
383 return LOG_STR("STATE_STARTING");
384 case STATE_RUNNING:
385 return LOG_STR("STATE_RUNNING");
386 case STATE_STOPPING:
387 return LOG_STR("STATE_STOPPING");
388 case STATE_INIT:
389 return LOG_STR("STATE_INIT");
390 default:
391 return LOG_STR("UNKNOWN");
392 }
393};
394#endif
395
397 State old_state = this->state_;
398 this->state_ = state;
399 ESP_LOGV(TAG, "State changed from %s to %s", LOG_STR_ARG(state_to_string(old_state)),
400 LOG_STR_ARG(state_to_string(state)));
401
402 // Clear loop_done when transitioning from STOPPED to any other state
404 this->disable_loop();
406 ESP_LOGD(TAG, "Playback finished");
407 } else if (old_state == State::STATE_STOPPED) {
408 this->enable_loop();
409 }
410}
411
412} // namespace rtttl
413} // 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:396
void finish_()
Finalizes the playback of the RTTTL string.
Definition rtttl.cpp:145
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:170
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
void IRAM_ATTR HOT delay(uint32_t ms)
Definition core.cpp:29
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:28
uint16_t x
Definition tt21100.cpp:5