Skip to content

Commit 583225f

Browse files
fhirschmannclaude
andcommitted
feat: fade in audiobook resume to mask the cold-start glitch
Continuing an audiobook from a saved position stutters for the first ~2 s: opening the file, decoding the first MP3 frames and seeking to the resume position saturate the 1-bit SD/CPU while I2S is already playing, so the input buffer underruns until it catches up. Hide it elegantly instead of fighting the hardware: on resume, rewind a few seconds before the saved position and start muted, then ramp the audio-lib volume back up over the fade duration. The glitch now lands on already-heard audio, so no content is lost and playback sounds clean from where you stopped. Only the audio-lib gain is ramped; the UI/MQTT volume is untouched, and a manual volume change cancels an active fade. Tunable via RESUME_FADEIN_DURATION_MS / RESUME_FADEIN_REWIND_S in settings.h (duration 0 disables the feature). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 6abf0fd commit 583225f

3 files changed

Lines changed: 43 additions & 1 deletion

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ neon logo that doubles as the SVG favicon ([`7be5254`](../../commit/7be5254)):
206206
| Unlocking controls via button press while locked | [`d83e15f`](../../commit/d83e15f) |
207207
| Support for a 6th button | [`b116151`](../../commit/b116151) |
208208
| OLED display support (SH1106/SSD1306 128×64 over I2C): boot splash, idle screen with IP + READY, now-playing title (up to 3 lines, scrolling) with battery/time/wifi status bar, and a volume bar | [`8ce8104`](../../commit/8ce8104) |
209+
| Audiobook resume fade-in: continuing a saved position briefly stutters in the first ~2 s (file open + MP3 header decode + seek-flush saturate the 1-bit SD/CPU while I2S already plays). The resume now rewinds a few seconds and fades the volume up over that span, so the glitch lands on already-heard audio — no content lost, clean sound from where you stopped. Tunable / disable via `RESUME_FADEIN_DURATION_MS` and `RESUME_FADEIN_REWIND_S` in `settings.h` | [`PENDINGHASH`](../../commit/PENDINGHASH) |
209210

210211
### Virtual RFID cards
211212

‎src/AudioPlayer.cpp‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ BaseType_t trackQStatus = pdFAIL;
9898
uint8_t trackCommand = NO_ACTION;
9999
bool audioReturnCode;
100100
uint32_t AudioPlayer_LastPlaytimeStatsTimestamp = 0u;
101+
// Resume fade-in (see settings.h RESUME_FADEIN_*): millis() when the fade started after a
102+
// rewound audiobook-resume; 0 = no fade active. Output starts muted and is ramped up so the
103+
// brief cold-start glitch is masked and lands on the rewound (already-heard) audio.
104+
uint32_t AudioPlayer_ResumeFadeStartMs = 0u;
101105
Playlist *newPlayList = nullptr;
102106
bool newPlayListAvailable = false;
103107

@@ -691,6 +695,21 @@ void AudioPlayer_HeadphoneVolumeManager(void) {
691695

692696
// Function to play music as task
693697
void AudioPlayer_Loop() {
698+
// Resume fade-in: ramp the output volume from 0 back to the user's volume over
699+
// RESUME_FADEIN_DURATION_MS after a rewound audiobook-resume, so the brief cold-start
700+
// glitch is masked. Only the audio-lib gain is ramped; AudioPlayer_CurrentVolume (and
701+
// thus the UI/MQTT-reported volume) is left untouched.
702+
if (AudioPlayer_ResumeFadeStartMs && (RESUME_FADEIN_DURATION_MS > 0)) {
703+
const uint8_t target = AudioPlayer_GetCurrentVolume();
704+
const uint32_t elapsed = millis() - AudioPlayer_ResumeFadeStartMs;
705+
if (elapsed >= RESUME_FADEIN_DURATION_MS) {
706+
audio->setVolume(target);
707+
AudioPlayer_ResumeFadeStartMs = 0;
708+
} else {
709+
audio->setVolume((uint8_t) ((uint32_t) target * elapsed / RESUME_FADEIN_DURATION_MS));
710+
}
711+
}
712+
694713
// Update playtime stats every 250 ms
695714
if ((millis() - AudioPlayer_LastPlaytimeStatsTimestamp) > 250) {
696715
AudioPlayer_LastPlaytimeStatsTimestamp = millis();
@@ -1084,6 +1103,7 @@ void AudioPlayer_Loop() {
10841103
}
10851104
gPlayProperties.currentRelPos = 0;
10861105
audioReturnCode = false;
1106+
bool resumeFadeWanted = false; // set when a rewound audiobook-resume should fade in (see AudioPlayer_Loop)
10871107

10881108
if (gPlayProperties.playMode == WEBSTREAM || (gPlayProperties.playMode == LOCAL_M3U && gPlayProperties.isWebstream)) { // Webstream
10891109
audioReturnCode = audio->connecttohost(gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber));
@@ -1099,7 +1119,13 @@ void AudioPlayer_Loop() {
10991119
int32_t fileStartTime = -1;
11001120
if (gPlayProperties.startAtFilePos > 0) {
11011121
fileStartTime = gPlayProperties.startAtFilePos;
1102-
Log_Printf(LOGLEVEL_NOTICE, trackStartatPos, gPlayProperties.startAtFilePos);
1122+
if (RESUME_FADEIN_DURATION_MS > 0) {
1123+
// Rewind a few seconds so the cold-start glitch (file open + header
1124+
// decode + seek-flush) lands on already-heard audio, then fade in below.
1125+
fileStartTime = (fileStartTime > (int32_t) RESUME_FADEIN_REWIND_S) ? (fileStartTime - (int32_t) RESUME_FADEIN_REWIND_S) : 1;
1126+
resumeFadeWanted = true;
1127+
}
1128+
Log_Printf(LOGLEVEL_NOTICE, trackStartatPos, fileStartTime);
11031129
gPlayProperties.startAtFilePos = 0;
11041130
}
11051131
String pathToTrack = gFSystem.rawPath(gPlayProperties.playlist->at(gPlayProperties.currentTrackNumber));
@@ -1116,6 +1142,11 @@ void AudioPlayer_Loop() {
11161142
gPlayProperties.trackFinished = true;
11171143
return;
11181144
} else {
1145+
if (resumeFadeWanted) {
1146+
// Start the (rewound) resume muted; AudioPlayer_Loop ramps the volume back up.
1147+
audio->setVolume(0);
1148+
AudioPlayer_ResumeFadeStartMs = millis();
1149+
}
11191150
if (gPlayProperties.currentTrackNumber) {
11201151
Led_Indicate(LedIndicatorType::PlaylistProgress);
11211152
}
@@ -1298,6 +1329,7 @@ void AudioPlayer_SetVolume(const int32_t _newVolume) {
12981329
AudioPlayer_SetCurrentVolume(_volume);
12991330

13001331
Log_Printf(LOGLEVEL_INFO, newLoudnessReceived, _volume);
1332+
AudioPlayer_ResumeFadeStartMs = 0; // a manual volume change cancels an active resume fade-in
13011333
audio->setVolume(_volume);
13021334
Web_SendWebsocketData(0, WebsocketCodeType::Volume);
13031335
#ifdef MQTT_ENABLE

‎src/settings.h‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@
5959
//#define HALLEFFECT_SENSOR_ENABLE // Support for hallsensor. For fine-tuning please adjust HallEffectSensor.h Please note: only user-support provided (https://forum.espuino.de/t/magnetische-hockey-tags/1449/35)
6060
//#define RTC_ENABLE // Battery-backed real-time-clock (DS3231) on i2cBusTwo. Keeps the time correct without WiFi/NTP. RTC stores UTC; timezone is applied on top.
6161

62+
//################## resume fade-in (audiobook) #############################
63+
// When continuing an audiobook from a saved position, opening the file + decoding the
64+
// first MP3 frames + seeking briefly saturates SD/CPU, so the first ~2 s can stutter.
65+
// To hide it elegantly: rewind a few seconds before the saved position and fade the
66+
// volume in over that span. The glitch then lands on already-heard audio -> no content
67+
// is lost, and you hear clean sound from where you stopped. Set duration to 0 to disable.
68+
#define RESUME_FADEIN_DURATION_MS 2500u // fade-in length in ms (0 = feature off)
69+
#define RESUME_FADEIN_REWIND_S 3u // seconds to rewind before the saved resume-position
70+
6271
//################## set PAUSE_WHEN_RFID_REMOVED behaviour #############################
6372
#ifdef PAUSE_WHEN_RFID_REMOVED
6473
#define ACCEPT_SAME_RFID_AFTER_TRACK_END // Accepts same RFID after playback has ended (https://forum.espuino.de/t/neues-feature-pausieren-wenn-rfid-karte-entfernt-wurde/541/2)

0 commit comments

Comments
 (0)