Skip to content

Commit ba3628e

Browse files
fhirschmannclaude
andcommitted
feat(audio): smart forward/backward seek with press-coalescing
Replace the ad-hoc 5-minute seek that commit 76e1535 bolted onto the next/previous-track buttons (it overshot on VBR/FLAC, silently jumped to the next track near a file boundary, and re-synced the decoder on every single press so it felt sluggish and dropped presses). - Revert NEXTTRACK/PREVIOUSTRACK to upstream behavior (incl. the "previous track only when <5s in, else restart" logic). - Add CMD_SMART_FORWARDS/BACKWARDS (action codes 190/191) plus track-control codes SMARTFORWARD/SMARTBACKWARD (10/11). On a single long file they seek inside the file; on a multi-file playlist they fall back to next/previous track. - Coalesce rapid presses: the offset accumulates and is applied once presses settle (smartSeekCoalesceMs), so the decoder re-syncs once instead of once per press. - Step configurable in the web UI (General -> options -> "Smart seek step (sec.)", NVS key seekStep, default 300), applied live without a reboot. de/en/fr locales, Swagger general schema, and the GitHub-Pages demo mock updated. - MQTT: smart seek works via the existing track-control topic (codes 10/11); web/REST via the controls "action" command. Built for the complete env and flashed/verified on device (the new seekStep setting round-trips through /settings). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 30abd44 commit ba3628e

14 files changed

Lines changed: 132 additions & 22 deletions

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,13 +237,14 @@ neon logo that doubles as the SVG favicon ([`7be5254`](../../commit/7be5254)):
237237
| Ready sound on cold start | [`c051c40`](../../commit/c051c40) |
238238
| Cyberpunk "Data Drop" idle LED animation | [`f20b111`](../../commit/f20b111) |
239239
| Selectable idle animation (standard idle dots or cyberpunk "Data Drop") in the LED settings; defaults to standard | [`f6c3f4e`](../../commit/f6c3f4e) |
240-
| Improved button responsiveness and track navigation seek options | [`76e1535`](../../commit/76e1535) |
240+
| Improved button responsiveness (faster debounce). The original 5-minute seek hack on the next/previous-track buttons has since been reverted to upstream behavior and replaced by dedicated smart-seek commands (see below) | [`76e1535`](../../commit/76e1535) |
241241
| Unlocking controls via button press while locked | [`d83e15f`](../../commit/d83e15f) |
242242
| Support for a 6th button | [`b116151`](../../commit/b116151) |
243243
| 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) |
244244
| Web-configurable OLED (new **OLED** settings tab): enable/disable the display, pick the startup animation (none / boot terminal / login splash / full), set the idle header text, toggle the battery/time/wifi/volume fields, and flip the panel 180°. Also a `CMD_TOGGLE_OLED` command for keybindings to blank the screen (e.g. at night) | [`7d86c3d`](../../commit/7d86c3d) |
245245
| 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` | [`583225f`](../../commit/583225f) |
246246
| Audiobooks restart from the beginning after a long pause: if a position-saving audiobook card has not been played for a configurable span (new **"Restart audiobook after (hrs)"** general setting, default 24 h, 0 = off), the next tap starts the book over instead of resuming mid-chapter — handy for kids who no longer remember where they were. The per-card last-seen timestamp lives in its own NVS namespace and is only consulted when the clock is valid (NTP/RTC) | [`445e290`](../../commit/445e290) |
247+
| **Smart forward/backward seek with press-coalescing** (`CMD_SMART_FORWARDS` / `CMD_SMART_BACKWARDS`, action codes 190/191; also track-control codes 10/11 for MQTT): on an audiobook that is a single long file these commands seek *inside* the file; on a playlist of several files they fall back to next/previous track. Rapid presses are bundled into one jump so the decoder re-syncs once instead of once per press (the old per-press seek felt sluggish on FLAC and dropped presses during the multi-second resync). The step is configurable in the web UI (General → options → **"Smart seek step (sec.)"**, NVS key `seekStep`, default 300 s = 5 min). Bind them to physical buttons in the **Buttons** settings tab. The plain `CMD_SEEK_FORWARDS`/`BACKWARDS` 30 s jump (`jumpOffset`) and the upstream next/previous-track behavior are both unchanged | [`PENDING`](../../commit/PENDING) |
247248

248249
### Virtual RFID cards
249250

html/locales/de.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,8 @@
694694
"savePlayPosPeriodicExp": "Während ein Hörbuch läuft, wird die Position alle ~30 s im Flash gesichert. So gehen bei einem unerwarteten Stromausfall (leerer Akku, harter Aus-Schalter) nur die letzten Sekunden verloren statt bis zum Anfang des Tracks zurückzuspringen. Fork-Funktion.",
695695
"restartFreshHrs": "Hörbuch von vorn nach (Std.)",
696696
"restartFreshHrsExp": "Wird ein Hörbuch länger als die angegebene Stundenzahl nicht abgespielt, startet es beim nächsten Auflegen wieder von vorn statt an der gespeicherten Stelle. Praktisch für Kinder, die sich nach einem Tag Pause nicht mehr erinnern, wo sie waren. 0 = aus (immer fortsetzen). Standard: 24. Benötigt eine gültige Uhrzeit (NTP/RTC). Fork-Funktion.",
697+
"seekStep": "Smart-Spulen Schritt (Sek.)",
698+
"seekStepExp": "Sprungweite in Sekunden für die Befehle „Smart vor/zurück\" (CMD_SMART_FORWARDS/BACKWARDS). Bei einem Hörbuch, das aus einer einzigen langen Datei besteht, spulen diese Tasten innerhalb der Datei; bei einer Playlist aus mehreren Dateien wechseln sie stattdessen den Track. Mehrere schnelle Tastendrücke werden zu einem Sprung gebündelt (nur ein Neu-Puffern statt vieler). Standard: 300 (5 Min.). Fork-Funktion.",
697699
"pauseIfRfidRemoved": "Pause wenn RFID-Tag entfernt",
698700
"pauseIfRfidRemovedExp": "Pausieren wenn RFID-Tag entfernt wird. Mehr Infos <a href='https://forum.espuino.de/t/welche-optionen-beim-kompilieren-gibt-es/120' target='_blank'>hier</a> unter <code>PAUSE_WHEN_RFID_REMOVED</code>.",
699701
"stopIfRfidRemoved": "Bei Entfernen stoppen statt pausieren",

html/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,8 @@
694694
"savePlayPosPeriodicExp": "While an audio book plays, the position is saved to flash every ~30 s. This way an unexpected power loss (dead battery, hard power-off) only loses the last few seconds instead of falling back to the start of the track. Fork feature.",
695695
"restartFreshHrs": "Restart audiobook after (hrs)",
696696
"restartFreshHrsExp": "If an audiobook has not been played for more than the given number of hours, it starts over from the beginning the next time the card is placed, instead of resuming the saved position. Handy for kids who, after a day's break, no longer remember where they were. 0 = off (always resume). Default: 24. Requires a valid clock (NTP/RTC). Fork feature.",
697+
"seekStep": "Smart seek step (sec.)",
698+
"seekStepExp": "Jump distance in seconds for the \"smart forward/backward\" commands (CMD_SMART_FORWARDS/BACKWARDS). On an audiobook that is a single long file these buttons seek within the file; on a playlist of several files they change tracks instead. Several quick presses are coalesced into one jump (a single re-buffer instead of many). Default: 300 (5 min). Fork feature.",
697699
"pauseIfRfidRemoved": "Pause when RFID tag is removed",
698700
"pauseIfRfidRemovedExp": "Playback is paused when the RFID tag is removed. More information <a href='https://forum.espuino.de/t/welche-optionen-beim-kompilieren-gibt-es/120' target='_blank'>here</a> under <code>PAUSE_IF_RFID_REMOVED</code>.",
699701
"stopIfRfidRemoved": "Stop instead of pause on removal",

html/locales/fr.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,8 @@
683683
"savePlayPosPeriodicExp": "Pendant la lecture d'un livre audio, la position est enregistrée dans la mémoire flash toutes les ~30 s. Ainsi, une coupure de courant inattendue (batterie vide, arrêt brutal) ne perd que les dernières secondes au lieu de revenir au début de la piste. Fonction du fork.",
684684
"restartFreshHrs": "Redémarrer le livre audio après (h)",
685685
"restartFreshHrsExp": "Si un livre audio n'a pas été lu depuis plus que le nombre d'heures indiqué, il recommence depuis le début lors de la prochaine pose de la carte, au lieu de reprendre à la position enregistrée. Pratique pour les enfants qui, après une journée de pause, ne se souviennent plus où ils en étaient. 0 = désactivé (toujours reprendre). Par défaut : 24. Nécessite une horloge valide (NTP/RTC). Fonction du fork.",
686+
"seekStep": "Pas d'avance rapide intelligente (s)",
687+
"seekStepExp": "Distance de saut en secondes pour les commandes « avance/recul intelligent » (CMD_SMART_FORWARDS/BACKWARDS). Pour un livre audio constitué d'un seul fichier long, ces touches avancent à l'intérieur du fichier ; pour une liste de plusieurs fichiers, elles changent de piste. Plusieurs appuis rapides sont regroupés en un seul saut (une seule remise en mémoire tampon au lieu de plusieurs). Par défaut : 300 (5 min). Fonction du fork.",
686688
"pauseIfRfidRemoved": "Mettre en pause si la balise RFID est retirée",
687689
"pauseIfRfidRemovedExp": "Mettre en pause la lecture si la balise RFID est retirée. Plus d'informations <a href='https://forum.espuino.de/t/welche-optionen-beim-kompilieren-gibt-es/120' target='_blank'>ici</a> sous <code>PAUSE_IF_RFID_REMOVED</code>.",
688690
"stopIfRfidRemoved": "Arrêter au lieu de mettre en pause",

html/management.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2686,6 +2686,14 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
26862686
data-i18n="[data-bs-content]general.options.restartFreshHrsExp" tabindex="0"><i
26872687
class="fas fa-circle-question"></i></a>
26882688
</div>
2689+
<div class="col-12 col-md-6 d-flex gap-2 align-items-center mb-1">
2690+
<label for="seekStep" data-i18n="general.options.seekStep"></label>
2691+
<input type="number" class="form-control" id="seekStep" name="seekStep"
2692+
min="5" max="3600" step="5" style="max-width:7em">
2693+
<a href="#" class="link-secondary" data-bs-toggle="popover"
2694+
data-i18n="[data-bs-content]general.options.seekStepExp" tabindex="0"><i
2695+
class="fas fa-circle-question"></i></a>
2696+
</div>
26892697
<div class="col-12 col-md-6 d-flex gap-2 align-items-center mb-1">
26902698
<input type="checkbox" id="playLastRfidOnReboot" name="playLastRfidOnReboot"
26912699
value="false">
@@ -5416,6 +5424,9 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
54165424
if (genSettings.restartFreshHrs !== undefined) {
54175425
$('#restartFreshHrs').val(genSettings.restartFreshHrs);
54185426
}
5427+
if (genSettings.seekStep !== undefined) {
5428+
$('#seekStep').val(genSettings.seekStep);
5429+
}
54195430
$('#playLastRfidOnReboot').prop('checked', genSettings.playLastRfidOnReboot);
54205431
$('#pauseIfRfidRemoved').prop('checked', genSettings.pauseIfRfidRemoved);
54215432
$('#stopIfRfidRemoved').prop('checked', genSettings.stopIfRfidRemoved);
@@ -6083,6 +6094,7 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
60836094
savePosRfidChge: $('#savePlayPosRfidChange').prop('checked'),
60846095
savePosPeriodic: $('#savePlayPosPeriodic').prop('checked'),
60856096
restartFreshHrs: Number($('#restartFreshHrs').val()),
6097+
seekStep: Number($('#seekStep').val()),
60866098
playLastRfidOnReboot: $('#playLastRfidOnReboot').prop('checked'),
60876099
pauseIfRfidRemoved: $('#pauseIfRfidRemoved').prop('checked'),
60886100
stopIfRfidRemoved: $('#stopIfRfidRemoved').prop('checked'),

src/AudioPlayer.cpp

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,18 @@ void AudioPlayer_SetSavePosPeriodic(bool enabled) {
310310
gSavePosPeriodic = enabled;
311311
}
312312

313+
// Step (in seconds) used by CMD_SMART_FORWARDS/BACKWARDS for in-file seeking. Cached from the
314+
// "seekStep" web setting so smart-seek presses don't hit NVS. Default see settings.h (seekStepDefault).
315+
static uint16_t gSeekStep = seekStepDefault;
316+
317+
void AudioPlayer_SetSeekStep(uint16_t seconds) {
318+
gSeekStep = (seconds == 0) ? seekStepDefault : seconds;
319+
}
320+
321+
uint16_t AudioPlayer_GetSeekStep(void) {
322+
return gSeekStep;
323+
}
324+
313325
void AudioPlayer_Init(void) {
314326
// create audio object
315327
audio = new AudioCustom();
@@ -391,6 +403,7 @@ void AudioPlayer_Init(void) {
391403
gPlayProperties.playlist = allocatePlaylist();
392404
gPlayProperties.SavePlayPosRfidChange = gPrefsSettings.getBool("savePosRfidChge", false); // SAVE_PLAYPOS_WHEN_RFID_CHANGE
393405
gSavePosPeriodic = gPrefsSettings.getBool("savePosPeriodic", true); // periodic audiobook play-position checkpoint
406+
gSeekStep = gPrefsSettings.getUInt("seekStep", seekStepDefault); // step for smart forward/backward in-file seeking
394407
gPlayProperties.pauseOnMinVolume = gPrefsSettings.getBool("pauseOnMinVol", false); // PAUSE_ON_MIN_VOLUME
395408
#ifdef PAUSE_WHEN_RFID_REMOVED
396409
gPlayProperties.pauseIfRfidRemoved = gPrefsSettings.getBool("pauseRfidRem", true);
@@ -769,6 +782,7 @@ void AudioPlayer_Loop() {
769782
gPlayProperties.pausePlay = false;
770783
gPlayProperties.trackFinished = false;
771784
gPlayProperties.playlistFinished = false;
785+
gPlayProperties.smartSeekPendingSec = 0; // drop any not-yet-applied smart-seek from the previous track
772786
#ifdef MQTT_ENABLE
773787
publishMqtt(topicPausePlay, "play", false);
774788
publishMqtt(topicPlaymode, static_cast<uint32_t>(gPlayProperties.playMode), false);
@@ -807,6 +821,29 @@ void AudioPlayer_Loop() {
807821
}
808822
}
809823

824+
// Resolve smart-seek (SMARTFORWARD/SMARTBACKWARD): on a single long file it becomes a coalesced
825+
// in-file seek (rapid presses accumulate into one jump -> one resync), otherwise it falls back to
826+
// a normal next/previous track change. Reads the cached duration so it works from any caller context.
827+
if (trackCommand == SMARTFORWARD || trackCommand == SMARTBACKWARD) {
828+
const bool forward = (trackCommand == SMARTFORWARD);
829+
const uint16_t step = AudioPlayer_GetSeekStep();
830+
const bool singleLongFile = (gPlayProperties.playlist != nullptr) && (gPlayProperties.playlist->size() == 1) && (gPlayProperties.audioFileDuration > step);
831+
if (singleLongFile) {
832+
if (gPlayProperties.pausePlay) { // resume before seeking (mirrors the next/previous-track behavior)
833+
audio->pauseResume();
834+
gPlayProperties.pausePlay = false;
835+
#ifdef MQTT_ENABLE
836+
publishMqtt(topicPausePlay, "play", false);
837+
#endif
838+
}
839+
gPlayProperties.smartSeekPendingSec += forward ? (int32_t) step : -(int32_t) step;
840+
gPlayProperties.smartSeekRequestMs = millis();
841+
trackCommand = NO_ACTION; // applied later (coalesced) in the seek-handling section
842+
} else {
843+
trackCommand = forward ? NEXTTRACK : PREVIOUSTRACK;
844+
}
845+
}
846+
810847
if (gPlayProperties.playlistFinished && trackCommand != NO_ACTION) {
811848
if (gPlayProperties.playMode != BUSY) { // Prevents from staying in mode BUSY forever when error occured (e.g. directory empty that should be played)
812849
Log_Println(noPlaymodeChangeIfIdle, LOGLEVEL_NOTICE);
@@ -890,16 +927,9 @@ void AudioPlayer_Loop() {
890927
audio->stopSong();
891928
}
892929
} else {
893-
if (gPlayProperties.playlist->size() == 1 && audio->getAudioFileDuration() > 300) {
894-
audio->setTimeOffset(300);
895-
Log_Printf(LOGLEVEL_NOTICE, secondsJumpForward, 300);
896-
trackCommand = NO_ACTION;
897-
return;
898-
} else {
899-
Log_Println(lastTrackAlreadyActive, LOGLEVEL_NOTICE);
900-
System_IndicateError();
901-
return;
902-
}
930+
Log_Println(lastTrackAlreadyActive, LOGLEVEL_NOTICE);
931+
System_IndicateError();
932+
return;
903933
}
904934
break;
905935

@@ -932,10 +962,12 @@ void AudioPlayer_Loop() {
932962
}
933963
} else {
934964
if (gPlayProperties.currentTrackNumber > 0 || gPlayProperties.repeatPlaylist) {
935-
if (gPlayProperties.currentTrackNumber == 0 && gPlayProperties.repeatPlaylist) {
936-
gPlayProperties.currentTrackNumber = gPlayProperties.playlist->size() - 1; // Go back to last track in loop-mode when first track is played
937-
} else {
938-
gPlayProperties.currentTrackNumber--;
965+
if (audio->getAudioCurrentTime() < 5) { // play previous track when current track time is small, else play current track again
966+
if (gPlayProperties.currentTrackNumber == 0 && gPlayProperties.repeatPlaylist) {
967+
gPlayProperties.currentTrackNumber = gPlayProperties.playlist->size() - 1; // Go back to last track in loop-mode when first track is played
968+
} else {
969+
gPlayProperties.currentTrackNumber--;
970+
}
939971
}
940972

941973
if (gPlayProperties.saveLastPlayPosition) {
@@ -948,12 +980,6 @@ void AudioPlayer_Loop() {
948980
audio->stopSong();
949981
}
950982
} else {
951-
if (gPlayProperties.playlist->size() == 1 && audio->getAudioFileDuration() > 300) {
952-
audio->setTimeOffset(-300);
953-
Log_Printf(LOGLEVEL_NOTICE, secondsJumpBackward, 300);
954-
trackCommand = NO_ACTION;
955-
return;
956-
}
957983
if (gPlayProperties.saveLastPlayPosition) {
958984
AudioPlayer_NvsRfidWriteWrapper(gPlayProperties.playRfidTag, 0, gPlayProperties.playMode, gPlayProperties.currentTrackNumber);
959985
}
@@ -1181,6 +1207,23 @@ void AudioPlayer_Loop() {
11811207
}
11821208
}
11831209

1210+
// Apply a coalesced smart-seek once the rapid presses have settled. Bundling several presses into a
1211+
// single setTimeOffset() means one buffer-flush/resync instead of one per press (which felt sluggish,
1212+
// and dropped presses while the decoder was re-syncing - especially on FLAC).
1213+
if (gPlayProperties.smartSeekPendingSec != 0 && (millis() - gPlayProperties.smartSeekRequestMs >= smartSeekCoalesceMs)) {
1214+
const int32_t offset = gPlayProperties.smartSeekPendingSec;
1215+
gPlayProperties.smartSeekPendingSec = 0;
1216+
if (audio->setTimeOffset(offset)) {
1217+
if (offset >= 0) {
1218+
Log_Printf(LOGLEVEL_NOTICE, secondsJumpForward, offset);
1219+
} else {
1220+
Log_Printf(LOGLEVEL_NOTICE, secondsJumpBackward, -offset);
1221+
}
1222+
} else {
1223+
System_IndicateError();
1224+
}
1225+
}
1226+
11841227
// Handle seekmodes
11851228
if (gPlayProperties.seekmode != SEEK_NORMAL) {
11861229
if (gPlayProperties.seekmode == SEEK_FORWARDS) {
@@ -1787,6 +1830,8 @@ void AudioPlayer_SetTrackControl(const uint8_t new_trackCommand) {
17871830
const bool wantsPlayback = (new_trackCommand == PLAY)
17881831
|| (new_trackCommand == NEXTTRACK)
17891832
|| (new_trackCommand == PREVIOUSTRACK)
1833+
|| (new_trackCommand == SMARTFORWARD)
1834+
|| (new_trackCommand == SMARTBACKWARD)
17901835
|| ((new_trackCommand == PAUSEPLAY) && gPlayProperties.pausePlay); // PAUSEPLAY while paused == resume
17911836
if (wantsPlayback) {
17921837
Sync_Cancel();

src/AudioPlayer.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ typedef struct { // Bit field
5151
bool stopIfRfidRemoved : 1; // When pauseIfRfidRemoved is active: stop playback instead of pausing when RFID is removed.
5252
bool dontAcceptRfidTwice : 1; // RFID-reader doesn't accept the same RFID-tag twice in a row (unless it's a modification-card or RFID-tag is unknown in NVS). Flag will be ignored silently if PAUSE_WHEN_RFID_REMOVED is active. (https://forum.espuino.de/t/neues-feature-dont-accept-same-rfid-twice/1247)
5353
int16_t jumpToFolderTrack = -1; // track to jump to
54+
int32_t smartSeekPendingSec = 0; // accumulated smart-seek offset (seconds, signed) waiting to be applied; coalesces rapid presses
55+
uint32_t smartSeekRequestMs = 0; // millis() of the last smart-seek press; the offset is applied once presses settle
5456
int8_t gainLowPass = 0; // Low Pass for EQ Control
5557
int8_t gainBandPass = 0; // Band Pass for EQ Control
5658
int8_t gainHighPass = 0; // High Pass for EQ Control
@@ -67,6 +69,8 @@ void AudioPlayer_NotifyUploadEnd(void);
6769
void AudioPlayer_Init(void);
6870
void AudioPlayer_Exit(void);
6971
void AudioPlayer_SetSavePosPeriodic(bool enabled);
72+
void AudioPlayer_SetSeekStep(uint16_t seconds); // step (in seconds) for CMD_SMART_FORWARDS/BACKWARDS in-file seeking
73+
uint16_t AudioPlayer_GetSeekStep(void);
7074
void AudioPlayer_Cyclic(void);
7175
void AudioPlayer_Loop(void);
7276
uint8_t AudioPlayer_GetRepeatMode(void);

src/Cmd.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,26 @@ void Cmd_Action(const uint16_t mod) {
477477
break;
478478
}
479479

480+
case CMD_SMART_FORWARDS: {
481+
// Smart forward: in-file seek on a single long file, else next track (decision + coalescing in AudioPlayer_Loop)
482+
if ((OPMODE_NORMAL == System_GetOperationMode()) || (OPMODE_BLUETOOTH_SOURCE == System_GetOperationMode())) {
483+
AudioPlayer_SetTrackControl(SMARTFORWARD);
484+
} else {
485+
Bluetooth_NextTrack();
486+
}
487+
break;
488+
}
489+
490+
case CMD_SMART_BACKWARDS: {
491+
// Smart backward: in-file seek on a single long file, else previous track (decision + coalescing in AudioPlayer_Loop)
492+
if ((OPMODE_NORMAL == System_GetOperationMode()) || (OPMODE_BLUETOOTH_SOURCE == System_GetOperationMode())) {
493+
AudioPlayer_SetTrackControl(SMARTBACKWARD);
494+
} else {
495+
Bluetooth_PreviousTrack();
496+
}
497+
break;
498+
}
499+
480500
case CMD_STOP: {
481501
AudioPlayer_SetTrackControl(STOP);
482502
break;

src/Web.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,11 @@ WebsocketCodeType JSONToSettings(JsonObject doc) {
10811081
if (!generalObj["restartFreshHrs"].isNull()) { // restart audiobooks from start after this idle gap (0 = off)
10821082
gPrefsSettings.putUInt("freshAfterHrs", generalObj["restartFreshHrs"].as<uint32_t>());
10831083
}
1084+
if (!generalObj["seekStep"].isNull()) { // step (s) for smart forward/backward in-file seeking
1085+
uint16_t seekStep = generalObj["seekStep"].as<uint16_t>();
1086+
gPrefsSettings.putUInt("seekStep", seekStep);
1087+
AudioPlayer_SetSeekStep(seekStep); // apply without reboot
1088+
}
10841089
success = success && (gPrefsSettings.putBool("playStartupSnd", generalObj["playStartupSnd"].as<bool>()) != 0);
10851090
success = success && (gPrefsSettings.putBool("playLastOnBoot", generalObj["playLastRfidOnReboot"].as<bool>()) != 0);
10861091
success = success && (gPrefsSettings.putBool("pauseRfidRem", generalObj["pauseIfRfidRemoved"].as<bool>()) != 0);

0 commit comments

Comments
 (0)