ESPHome  2024.12.2
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 
6 namespace esphome {
7 namespace rtttl {
8 
9 static const char *const TAG = "rtttl";
10 
11 static const uint32_t DOUBLE_NOTE_GAP_MS = 10;
12 
13 // These values can also be found as constants in the Tone library (Tone.h)
14 static 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 
19 static const uint16_t I2S_SPEED = 1000;
20 
21 #undef HALF_PI
22 static const double HALF_PI = 1.5707963267948966192313216916398;
23 
24 inline 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 
34 void Rtttl::play(std::string rtttl) {
35  if (this->state_ != State::STATE_STOPPED && this->state_ != State::STATE_STOPPING) {
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 
124 void Rtttl::stop() {
125 #ifdef USE_OUTPUT
126  if (this->output_ != nullptr) {
127  this->output_->set_level(0.0);
128  this->set_state_(STATE_STOPPED);
129  }
130 #endif
131 #ifdef USE_SPEAKER
132  if (this->speaker_ != nullptr) {
133  if (this->speaker_->is_running()) {
134  this->speaker_->stop();
135  }
136  this->set_state_(STATE_STOPPING);
137  }
138 #endif
139  this->note_duration_ = 0;
140 }
141 
142 void Rtttl::loop() {
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) {
309  this->output_->update_frequency(this->output_freq_);
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;
366  this->on_finished_playback_callback_.call();
367  ESP_LOGD(TAG, "Playback finished");
368 }
369 
370 static 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
uint16_t wholenote_
Definition: rtttl.h:68
bool is_running() const
Definition: speaker.h:61
const char * name
Definition: stm32flash.h:78
void play(std::string rtttl)
Definition: rtttl.cpp:34
uint16_t x
Definition: tt21100.cpp:17
num_t degrees(num_t rad)
Definition: sun.cpp:27
void dump_config() override
Definition: rtttl.cpp:29
virtual void finish()
Definition: speaker.h:57
void loop() override
Definition: rtttl.cpp:142
mopeka_std_values val[4]
CallbackManager< void()> on_finished_playback_callback_
Definition: rtttl.h:92
speaker::Speaker * speaker_
Definition: rtttl.h:83
bool is_stopped() const
Definition: speaker.h:62
uint32_t IRAM_ATTR HOT millis()
Definition: core.cpp:25
uint32_t output_freq_
Definition: rtttl.h:74
void set_level(float state)
Set the level of this float output, this is called from the front-end.
void set_state_(State state)
Definition: rtttl.cpp:387
uint8_t get_integer_()
Definition: rtttl.h:56
double deg2rad(double degrees)
Definition: rtttl.cpp:24
uint16_t note_duration_
Definition: rtttl.h:72
std::string rtttl_
Definition: rtttl.h:66
uint16_t default_duration_
Definition: rtttl.h:69
size_t position_
Definition: rtttl.h:67
output::FloatOutput * output_
Definition: rtttl.h:79
virtual void start()=0
uint16_t default_octave_
Definition: rtttl.h:70
Implementation of SPI Controller mode.
Definition: a01nyub.cpp:7
virtual size_t play(const uint8_t *data, size_t length, TickType_t ticks_to_wait)
Plays the provided audio data.
Definition: speaker.h:37
virtual void stop()=0
uint32_t last_note_
Definition: rtttl.h:71
virtual void update_frequency(float frequency)
Set the frequency of the output for PWM outputs.
Definition: float_output.h:67
bool state
Definition: fan.h:34
void IRAM_ATTR HOT delay(uint32_t ms)
Definition: core.cpp:26