ESPHome  2023.5.5
rtttl.cpp
Go to the documentation of this file.
1 #include "rtttl.h"
2 #include "esphome/core/hal.h"
3 #include "esphome/core/log.h"
4 
5 namespace esphome {
6 namespace rtttl {
7 
8 static const char *const TAG = "rtttl";
9 
10 static const uint32_t DOUBLE_NOTE_GAP_MS = 10;
11 
12 // These values can also be found as constants in the Tone library (Tone.h)
13 static const uint16_t NOTES[] = {0, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494,
14  523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047,
15  1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217,
16  2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951};
17 
18 void Rtttl::dump_config() { ESP_LOGCONFIG(TAG, "Rtttl"); }
19 
20 void Rtttl::play(std::string rtttl) {
21  rtttl_ = std::move(rtttl);
22 
24  default_octave_ = 6;
25  int bpm = 63;
26  uint8_t num;
27 
28  // Get name
29  position_ = rtttl_.find(':');
30 
31  // it's somewhat documented to be up to 10 characters but let's be a bit flexible here
32  if (position_ == std::string::npos || position_ > 15) {
33  ESP_LOGE(TAG, "Missing ':' when looking for name.");
34  return;
35  }
36 
37  auto name = this->rtttl_.substr(0, position_);
38  ESP_LOGD(TAG, "Playing song %s", name.c_str());
39 
40  // get default duration
41  position_ = this->rtttl_.find("d=", position_);
42  if (position_ == std::string::npos) {
43  ESP_LOGE(TAG, "Missing 'd='");
44  return;
45  }
46  position_ += 2;
47  num = this->get_integer_();
48  if (num > 0)
49  default_duration_ = num;
50 
51  // get default octave
52  position_ = rtttl_.find("o=", position_);
53  if (position_ == std::string::npos) {
54  ESP_LOGE(TAG, "Missing 'o=");
55  return;
56  }
57  position_ += 2;
58  num = get_integer_();
59  if (num >= 3 && num <= 7)
60  default_octave_ = num;
61 
62  // get BPM
63  position_ = rtttl_.find("b=", position_);
64  if (position_ == std::string::npos) {
65  ESP_LOGE(TAG, "Missing b=");
66  return;
67  }
68  position_ += 2;
69  num = get_integer_();
70  if (num != 0)
71  bpm = num;
72 
73  position_ = rtttl_.find(':', position_);
74  if (position_ == std::string::npos) {
75  ESP_LOGE(TAG, "Missing second ':'");
76  return;
77  }
78  position_++;
79 
80  // BPM usually expresses the number of quarter notes per minute
81  wholenote_ = 60 * 1000L * 4 / bpm; // this is the time for whole note (in milliseconds)
82 
83  output_freq_ = 0;
84  last_note_ = millis();
85  note_duration_ = 1;
86 }
87 
88 void Rtttl::loop() {
90  return;
91 
92  if (!rtttl_[position_]) {
93  output_->set_level(0.0);
94  ESP_LOGD(TAG, "Playback finished");
95  this->on_finished_playback_callback_.call();
96  note_duration_ = 0;
97  return;
98  }
99 
100  // align to note: most rtttl's out there does not add and space after the ',' separator but just in case...
101  while (rtttl_[position_] == ',' || rtttl_[position_] == ' ')
102  position_++;
103 
104  // first, get note duration, if available
105  uint8_t num = this->get_integer_();
106 
107  if (num) {
108  note_duration_ = wholenote_ / num;
109  } else {
110  note_duration_ = wholenote_ / default_duration_; // we will need to check if we are a dotted note after
111  }
112 
113  uint8_t note;
114 
115  switch (rtttl_[position_]) {
116  case 'c':
117  note = 1;
118  break;
119  case 'd':
120  note = 3;
121  break;
122  case 'e':
123  note = 5;
124  break;
125  case 'f':
126  note = 6;
127  break;
128  case 'g':
129  note = 8;
130  break;
131  case 'a':
132  note = 10;
133  break;
134  case 'b':
135  note = 12;
136  break;
137  case 'p':
138  default:
139  note = 0;
140  }
141  position_++;
142 
143  // now, get optional '#' sharp
144  if (rtttl_[position_] == '#') {
145  note++;
146  position_++;
147  }
148 
149  // now, get optional '.' dotted note
150  if (rtttl_[position_] == '.') {
152  position_++;
153  }
154 
155  // now, get scale
156  uint8_t scale = get_integer_();
157  if (scale == 0)
158  scale = default_octave_;
159 
160  // Now play the note
161  if (note) {
162  auto note_index = (scale - 4) * 12 + note;
163  if (note_index < 0 || note_index >= (int) sizeof(NOTES)) {
164  ESP_LOGE(TAG, "Note out of valid range");
165  return;
166  }
167  auto freq = NOTES[note_index];
168 
169  if (freq == output_freq_) {
170  // Add small silence gap between same note
171  output_->set_level(0.0);
172  delay(DOUBLE_NOTE_GAP_MS);
173  note_duration_ -= DOUBLE_NOTE_GAP_MS;
174  }
175  output_freq_ = freq;
176 
177  ESP_LOGVV(TAG, "playing note: %d for %dms", note, note_duration_);
178  output_->update_frequency(freq);
179  output_->set_level(0.5);
180  } else {
181  ESP_LOGVV(TAG, "waiting: %dms", note_duration_);
182  output_->set_level(0.0);
183  }
184 
185  last_note_ = millis();
186 }
187 } // namespace rtttl
188 } // namespace esphome
uint16_t wholenote_
Definition: rtttl.h:38
const char * name
Definition: stm32flash.h:78
void play(std::string rtttl)
Definition: rtttl.cpp:20
void dump_config() override
Definition: rtttl.cpp:18
void loop() override
Definition: rtttl.cpp:88
CallbackManager< void()> on_finished_playback_callback_
Definition: rtttl.h:47
uint32_t IRAM_ATTR HOT millis()
Definition: core.cpp:27
uint32_t output_freq_
Definition: rtttl.h:44
void set_level(float state)
Set the level of this float output, this is called from the front-end.
uint8_t get_integer_()
Definition: rtttl.h:28
uint16_t note_duration_
Definition: rtttl.h:42
std::string rtttl_
Definition: rtttl.h:36
uint16_t default_duration_
Definition: rtttl.h:39
size_t position_
Definition: rtttl.h:37
output::FloatOutput * output_
Definition: rtttl.h:45
uint16_t default_octave_
Definition: rtttl.h:40
Definition: a4988.cpp:4
uint32_t last_note_
Definition: rtttl.h:41
virtual void update_frequency(float frequency)
Set the frequency of the output for PWM outputs.
Definition: float_output.h:67
void IRAM_ATTR HOT delay(uint32_t ms)
Definition: core.cpp:28