mirror of
https://github.com/esphome/esphome.git
synced 2026-11-06 13:15:57 +00:00
[rtttl] Fade speaker notes in and out to stop clicks (#22075)
This commit is contained in:
1 parent
910c42a8bc
commit
fc921d0a96
3 files changed
+38
-1
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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});
|
||||
|
||||
Reference in new issue
Block a user