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