[rtttl] Fade speaker notes in and out to stop clicks (#22075)

This commit is contained in:
Clyde Stubbs authored and GitHub committed 2026-11-01 11:25:09 +11:00
1 parent 910c42a8bc
commit fc921d0a96
3 files changed
+38 -1

No files matched your search

+13 -1
View File
@@ -27,6 +27,9 @@ static constexpr uint8_t NOTES_COUNT = static_cast<uint8_t>(sizeof(NOTES) / size
static constexpr uint8_t REPEATING_NOTE_GAP_MS = 10;
// Each note fades in and out over 2^5 samples (2 ms at 16 kHz)
static constexpr uint8_t NOTE_FADE_BITS = 5;
// The first quarter of a sine wave at full scale: round(32767 * sin(i * pi / 128))
static constexpr uint8_t QUARTER_SINE_BITS = 6;
static constexpr uint16_t QUARTER_SINE[(1 << QUARTER_SINE_BITS) + 1] PROGMEM = {
@@ -131,7 +134,9 @@ void Rtttl::loop() {
// Try and send out the remainder of the existing note, one per `loop()`
if (this->phase_step_ != 0 && this->samples_sent_ >= this->samples_gap_) { // Play note
// The multiplication wraps at exactly one full cycle of the phase
sample[sample_index] = sine_sample(this->samples_sent_ * this->phase_step_);
sample[sample_index] =
fade_sample(sine_sample(this->samples_sent_ * this->phase_step_),
this->samples_sent_ - this->samples_gap_, this->samples_count_ - 1 - this->samples_sent_);
} else {
sample[sample_index] = 0;
}
@@ -282,6 +287,13 @@ int16_t Rtttl::sine_sample(uint32_t phase) {
return static_cast<int16_t>((phase >> 31) != 0 ? -value : value);
}
int16_t Rtttl::fade_sample(int16_t value, uint32_t from_start, uint32_t to_end) {
const uint32_t edge = std::min(from_start, to_end);
if (edge >= (1U << NOTE_FADE_BITS))
return value;
return static_cast<int16_t>((value * static_cast<int32_t>(edge)) >> NOTE_FADE_BITS);
}
void Rtttl::play(std::string rtttl) {
if (this->is_busy_()) {
return;
+3
View File
@@ -102,6 +102,9 @@ class Rtttl final : public Component {
void set_state_(State state);
/// The value of a full scale sine wave at phase, where the full uint32_t range is one cycle.
static int16_t sine_sample(uint32_t phase);
/// Scales a sample near either end of a note, so the note does not click when the wave starts or stops away from
/// zero. from_start and to_end count the samples between this one and each end of the note.
static int16_t fade_sample(int16_t value, uint32_t from_start, uint32_t to_end);
/// The song being played; points at owned_song_ or at static storage.
const char *song_{nullptr};
@@ -21,6 +21,9 @@ class RtttlParserAccess {
static char at(const Rtttl &p, size_t pos) { return p.song_at_(pos); }
static void set_running(Rtttl &p) { p.state_ = State::RUNNING; }
static int16_t sine(uint32_t phase) { return Rtttl::sine_sample(phase); }
static int16_t fade(int16_t value, uint32_t from_start, uint32_t to_end) {
return Rtttl::fade_sample(value, from_start, to_end);
}
};
using Access = RtttlParserAccess;
@@ -45,6 +48,25 @@ TEST(RtttlSine, HitsTheQuarterPointsOfTheWave) {
EXPECT_NEAR(Access::sine(0xFFFFFFFF), 0, 1);
}
TEST(RtttlFade, FirstAndLastSamplesOfANoteAreSilent) {
EXPECT_EQ(Access::fade(INT16_MAX, 0, 1000), 0);
EXPECT_EQ(Access::fade(INT16_MIN, 1000, 0), 0);
}
TEST(RtttlFade, RampsAcrossTheFade) {
EXPECT_EQ(Access::fade(3200, 16, 1000), 1600);
EXPECT_EQ(Access::fade(3200, 1000, 16), 1600);
EXPECT_EQ(Access::fade(-3200, 16, 1000), -1600);
EXPECT_EQ(Access::fade(3200, 31, 1000), 3100);
}
TEST(RtttlFade, LeavesTheMiddleOfANoteUnchanged) {
EXPECT_EQ(Access::fade(INT16_MAX, 32, 32), INT16_MAX);
EXPECT_EQ(Access::fade(INT16_MIN, 1000, 1000), INT16_MIN);
}
TEST(RtttlFade, UsesTheNearerEndOfAVeryShortNote) { EXPECT_EQ(Access::fade(3200, 16, 8), 800); }
TEST(RtttlParser, StaticSongParsesTheHeader) {
Rtttl player;
player.play(StaticSong{SONG});