[rtttl] allow any control parameters order and default value fallback (#14438)

Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
Thomas Rupprecht
2026-04-20 15:10:05 +02:00
committed by GitHub
parent f0c21520aa
commit 7321e6e52f
2 changed files with 66 additions and 48 deletions
+50 -48
View File
@@ -294,57 +294,59 @@ void Rtttl::play(std::string rtttl) {
}
ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str());
// Get default duration
this->position_ = this->rtttl_.find("d=", this->position_);
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing 'd='");
return;
}
this->position_ += 2;
num = this->get_integer_();
if (num == 1 || num == 2 || num == 4 || num == 8 || num == 16 || num == 32) {
this->default_note_denominator_ = num;
} else {
ESP_LOGE(TAG, "Invalid default duration: %d", num);
return;
}
// Get default octave
this->position_ = this->rtttl_.find("o=", this->position_);
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing 'o=");
return;
}
this->position_ += 2;
num = this->get_integer_();
if (num >= MIN_OCTAVE && num <= MAX_OCTAVE) {
this->default_octave_ = num;
} else {
ESP_LOGE(TAG, "Invalid default octave: %d", num);
return;
}
// Get BPM
this->position_ = this->rtttl_.find("b=", this->position_);
if (this->position_ == std::string::npos) {
ESP_LOGE(TAG, "Missing b=");
return;
}
this->position_ += 2;
num = this->get_integer_();
if (num >= 4) { // Below 4 is not realistic and would cause a integer overflow
bpm = num;
} else {
ESP_LOGE(TAG, "Invalid BPM: %d", num);
return;
}
this->position_ = this->rtttl_.find(':', this->position_);
if (this->position_ == std::string::npos) {
size_t name_end_position = this->position_;
size_t control_end = this->rtttl_.find(':', name_end_position + 1);
if (control_end == std::string::npos) {
ESP_LOGE(TAG, "Missing second ':'");
return;
}
this->position_++;
// Get default duration
size_t pos = this->rtttl_.find("d=", name_end_position);
if (pos == std::string::npos || pos >= control_end) {
ESP_LOGW(TAG, "Missing 'd='; use default duration %d", this->default_note_denominator_);
} else {
this->position_ = pos + 2;
num = this->get_integer_();
if (num == 1 || num == 2 || num == 4 || num == 8 || num == 16 || num == 32) {
this->default_note_denominator_ = num;
} else {
ESP_LOGE(TAG, "Invalid default duration: %d", num);
return;
}
}
// Get default octave
pos = this->rtttl_.find("o=", name_end_position);
if (pos == std::string::npos || pos >= control_end) {
ESP_LOGW(TAG, "Missing 'o='; use default octave %d", this->default_octave_);
} else {
this->position_ = pos + 2;
num = this->get_integer_();
if (num >= MIN_OCTAVE && num <= MAX_OCTAVE) {
this->default_octave_ = num;
} else {
ESP_LOGE(TAG, "Invalid default octave: %d", num);
return;
}
}
// Get BPM
pos = this->rtttl_.find("b=", name_end_position);
if (pos == std::string::npos || pos >= control_end) {
ESP_LOGW(TAG, "Missing 'b='; use default BPM %d", bpm);
} else {
this->position_ = pos + 2;
num = this->get_integer_();
if (num >= 4) { // Below 4 is not realistic and would cause a integer overflow
bpm = num;
} else {
ESP_LOGE(TAG, "Invalid BPM: %d", num);
return;
}
}
this->position_ = control_end + 1;
// BPM usually expresses the number of quarter notes per minute
this->wholenote_duration_ = 60 * 1000L * 4 / bpm; // This is the time for whole note (in milliseconds)
+16
View File
@@ -3,6 +3,22 @@ esphome:
then:
- rtttl.play: 'siren:d=8,o=5,b=100:d,e,d,e,d,e,d,e'
- rtttl.stop
# Test all note features: all notes, denominators (1,2,4,8,16,32), sharp (#), octaves (4-7), dotted (.), note gap (c5,c5), pause (p)
- rtttl.play: 'special:d=4,o=5,b=120:1c4,2d#5,4e6.,8f#7,16g4,32a5,8a#5,4b6,8h5,c5,c5,8p,2c4'
# Different orders of control parameters
- rtttl.play: 'test_odb:o=5,d=8,b=100:c'
- rtttl.play: 'test_bod:b=100,o=5,d=8:c'
- rtttl.play: 'test_bdo:b=100,d=8,o=5:c'
- rtttl.play: 'test_obd:o=5,b=100,d=8:c'
- rtttl.play: 'test_dbo:d=8,b=100,o=5:c'
# Missing parameters (use defaults)
- rtttl.play: 'test_no_d:o=5,b=100:c'
- rtttl.play: 'test_no_o:d=8,b=100:c'
- rtttl.play: 'test_no_b:d=8,o=5:c'
- rtttl.play: 'test_only_d:d=8:c'
- rtttl.play: 'test_only_o:o=5:c'
- rtttl.play: 'test_only_b:b=100:c'
- rtttl.play: 'test_empty::c'
output:
- platform: ${output_platform}