[rtttl] Fix speaker playback bugs#14280
Conversation
- Fix finish_() reading past SpeakerSample array (8 bytes for 4 byte buffer) - Fix play() size mismatch (compared against x*4 instead of x*sizeof(SpeakerSample)) - Fix note duration divisor (1600 should be 1000, notes played 37.5% too short) - Fix NOTES array bounds check using sizeof bytes instead of element count - Fix get_integer_() overflow for BPM > 255 (RTTTL spec allows up to 900) - Initialize output_ pointer to nullptr Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
To use the changes from this PR as an external component, add the following to your ESPHome configuration YAML file: external_components:
- source: github://pr#14280
components: [rtttl]
refresh: 1h(Added by the PR bot) |
|
👋 Hi there! I've automatically requested reviews from codeowners based on the files changed in this PR. @glmnet - You've been requested to review this PR as codeowner(s) of 2 file(s) that were modified. Thanks for your time! 🙏 |
There was a problem hiding this comment.
Pull request overview
This PR fixes multiple critical bugs in the RTTTL speaker playback path that caused buffer overruns, timing errors, and potential undefined behavior. The changes address fundamental issues with buffer size calculations, note timing, array bounds checking, and integer overflow handling.
Changes:
- Fixed buffer size calculations in speaker playback code to use
sizeof()correctly - Corrected note timing divisor from /1600 to /1000 for accurate tempo
- Fixed NOTES array bounds check to use element count instead of byte size
- Changed
get_integer_()return type to uint16_t to support BPM values > 255 - Initialized
output_pointer to nullptr to prevent undefined behavior
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| esphome/components/rtttl/rtttl.h | Changed get_integer_() return type to uint16_t and initialized output_ pointer to nullptr |
| esphome/components/rtttl/rtttl.cpp | Fixed buffer size calculations, note timing divisor, NOTES array bounds check, and BPM variable type |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev #14280 +/- ##
=======================================
Coverage 74.28% 74.28%
=======================================
Files 55 55
Lines 11595 11595
Branches 1583 1583
=======================================
Hits 8613 8613
Misses 2578 2578
Partials 404 404 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Memory Impact AnalysisComponents:
📊 Component Memory Breakdown
🔍 Symbol-Level Changes (click to expand)Changed Symbols
This analysis runs automatically when components change. Memory usage is measured from a representative test configuration. |
Skip the inter-note gap when the note duration is too short to accommodate it, preventing uint16_t underflow on note_duration_. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ximex
left a comment
There was a problem hiding this comment.
what i understood, looks good
| this->samples_count_ = (this->sample_rate_ * this->note_duration_) / 1600; //(ms); | ||
| this->samples_count_ = (this->sample_rate_ * this->note_duration_) / 1000; | ||
| if (need_note_gap) { | ||
| this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1600; //(ms); | ||
| this->samples_gap_ = (this->sample_rate_ * DOUBLE_NOTE_GAP_MS) / 1000; |
There was a problem hiding this comment.
Trying to figure out why this was 1600 before
There was a problem hiding this comment.
samples per sec * note duration / 1000 = samples..
but why was it 1600
There was a problem hiding this comment.
I guess someone assumed the sample rate was always 16000 ?
There was a problem hiding this comment.
but we are converting ms to s here so 1000 seems right
There was a problem hiding this comment.
Yeah I dont get it either. Need to divide by 1000 to convert to ms, not sure where 1600 came from
|
Thanks |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
What does this implement/fix?
Fixes multiple bugs in the RTTTL speaker playback path:
finish_() buffer overread: passed 8 bytes to
speaker_->play()butsamplearray is onlysizeof(SpeakerSample) * 2 = 4bytes, reading past the array (UB). Fixed to usesizeof(sample).play() size mismatch:
play()was called withx * 2bytes but the completion check compared againstx * 4. Fixed both to usex * sizeof(SpeakerSample).Note timing divisor:
samples_count_andsamples_gap_were calculated with/1600instead of/1000, causing notes to play at 62.5% of their intended duration.sample_rate_is in samples/sec andnote_duration_is in milliseconds, so dividing by 1000 converts ms to seconds:With
/1600, a 300ms note at 16kHz produces 3000 samples (187.5ms) instead of the correct 4800 samples (300ms).NOTES array bounds check: used
sizeof(NOTES)(98 bytes for 49 uint16_t elements) instead of element count. Fixed tosizeof(NOTES) / sizeof(NOTES[0]).get_integer_() overflow: returned
uint8_t, so BPM values > 255 would silently wrap. RTTTL spec allows BPM up to 900. Changed touint16_t.Uninitialized output_ pointer:
output::FloatOutput *output_was not initialized, could cause UB ifUSE_OUTPUTis defined but no output is set. Initialized tonullptr.Note gap underflow guard: the output path subtracts
DOUBLE_NOTE_GAP_MSfromnote_duration_without checking the duration is large enough. With theuint16_tfix (Remove sleeps. #5) allowing higher BPMs and short note durations (e.g. 32nd note at BPM 900 = 8.3ms < 10ms gap), this could underflow theuint16_t. Added a guard to skip the gap when the note is too short.Testing
Tested on Waveshare ESP32-P4 Nano with ES8311 codec and I2S speaker output.
Mario theme (
mario:d=4,o=5,b=100:16e6,16e6,32p,8e6,...):/1600): 4.72s — notes played at 62.5% of intended duration/1000): 7.53s — correct duration (6.9s + speaker start/stop overhead)Types of changes
Related issue or feature (if applicable):
N/A
Pull request in esphome-docs with documentation (if applicable):
N/A
Test Environment
Example entry for
config.yaml:Checklist:
tests/folder).