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