Add support for fading out on sleep timer end

Playback will start to fadeout during last 60 seconds of the sleep timer. Once faded out, playback will be paused, volume reset, and playback seeked to start of fadeout.
This commit is contained in:
Adam Traeger 2025-03-09 13:14:15 -05:00
parent 769ce0ade9
commit 33c738873f
No known key found for this signature in database
GPG key ID: 136E380CBA630639
6 changed files with 75 additions and 5 deletions

View file

@ -14,7 +14,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// Override point for customization after application launch. // Override point for customization after application launch.
let configuration = Realm.Configuration( let configuration = Realm.Configuration(
schemaVersion: 18, schemaVersion: 19,
migrationBlock: { [weak self] migration, oldSchemaVersion in migrationBlock: { [weak self] migration, oldSchemaVersion in
if (oldSchemaVersion < 1) { if (oldSchemaVersion < 1) {
self?.logger.log("Realm schema version was \(oldSchemaVersion)") self?.logger.log("Realm schema version was \(oldSchemaVersion)")
@ -61,6 +61,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
newObject?["streamingUsingCellular"] = "ALWAYS" newObject?["streamingUsingCellular"] = "ALWAYS"
} }
} }
if (oldSchemaVersion < 18) {
self?.logger.log("Realm schema version was \(oldSchemaVersion)... Adding disableSleepTimerFadeOut settings")
migration.enumerateObjects(ofType: PlayerSettings.className()) { oldObject, newObject in
newObject?["disableSleepTimerFadeOut"] = false
}
}
} }
) )

View file

@ -246,6 +246,7 @@ public class AbsDatabase: CAPPlugin {
let languageCode = call.getString("languageCode") ?? "en-us" let languageCode = call.getString("languageCode") ?? "en-us"
let downloadUsingCellular = call.getString("downloadUsingCellular") ?? "ALWAYS" let downloadUsingCellular = call.getString("downloadUsingCellular") ?? "ALWAYS"
let streamingUsingCellular = call.getString("streamingUsingCellular") ?? "ALWAYS" let streamingUsingCellular = call.getString("streamingUsingCellular") ?? "ALWAYS"
let disableSleepTimerFadeOut = call.getBool("disableSleepTimerFadeOut") ?? false
let settings = DeviceSettings() let settings = DeviceSettings()
settings.disableAutoRewind = disableAutoRewind settings.disableAutoRewind = disableAutoRewind
settings.enableAltView = enableAltView settings.enableAltView = enableAltView
@ -257,6 +258,7 @@ public class AbsDatabase: CAPPlugin {
settings.languageCode = languageCode settings.languageCode = languageCode
settings.downloadUsingCellular = downloadUsingCellular settings.downloadUsingCellular = downloadUsingCellular
settings.streamingUsingCellular = streamingUsingCellular settings.streamingUsingCellular = streamingUsingCellular
settings.disableSleepTimerFadeOut = disableSleepTimerFadeOut
Database.shared.setDeviceSettings(deviceSettings: settings) Database.shared.setDeviceSettings(deviceSettings: settings)

View file

@ -19,6 +19,7 @@ class DeviceSettings: Object {
@Persisted var languageCode: String = "en-us" @Persisted var languageCode: String = "en-us"
@Persisted var downloadUsingCellular: String = "ALWAYS" @Persisted var downloadUsingCellular: String = "ALWAYS"
@Persisted var streamingUsingCellular: String = "ALWAYS" @Persisted var streamingUsingCellular: String = "ALWAYS"
@Persisted var disableSleepTimerFadeOut: Bool = false
} }
func getDefaultDeviceSettings() -> DeviceSettings { func getDefaultDeviceSettings() -> DeviceSettings {
@ -36,6 +37,7 @@ func deviceSettingsToJSON(settings: DeviceSettings) -> Dictionary<String, Any> {
"hapticFeedback": settings.hapticFeedback, "hapticFeedback": settings.hapticFeedback,
"languageCode": settings.languageCode, "languageCode": settings.languageCode,
"downloadUsingCellular": settings.downloadUsingCellular, "downloadUsingCellular": settings.downloadUsingCellular,
"streamingUsingCellular": settings.streamingUsingCellular "streamingUsingCellular": settings.streamingUsingCellular,
"disableSleepTimerFadeOut": settings.disableSleepTimerFadeOut
] ]
} }

View file

@ -366,6 +366,56 @@ class AudioPlayer: NSObject {
updateNowPlaying() updateNowPlaying()
} }
public func startFadeOut() {
guard self.isInitialized() else { return }
guard let currentTime = self.getCurrentTime() else { return }
logger.log("fadeOut: Fading out playback")
// Define fade parameters.
let fadeDuration: Float = 60.0 // total fade duration in seconds
let interval: Float = 1.0 // timer interval in seconds
// Get the current volume.
let initialVolume = self.audioPlayer.volume
let targetVolume: Float = 0.0
// If the current volume is already at or below zero, just pause.
if initialVolume <= targetVolume {
self.pause()
return
}
// Calculate the volume change per timer tick.
// (targetVolume - initialVolume) is negative since target < initial.
let step = (targetVolume - initialVolume) * interval / fadeDuration
// Schedule a timer on the main queue to adjust the volume.
DispatchQueue.runOnMainQueue { [weak self] in
var timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(interval), repeats: true) { t in
guard let self = self else {
t.invalidate()
return
}
// Calculate the new volume.
let newVolume = self.audioPlayer.volume + step
// Check if the next step would go below zero.
if newVolume > targetVolume {
self.audioPlayer.volume = newVolume
} else {
// Ensure volume is exactly zero and end fade.
self.audioPlayer.volume = targetVolume
t.invalidate()
self.logger.log("Fadeout: Fade complete, pausing playback")
self.pause()
self.audioPlayer.volume = initialVolume
self.seek(currentTime, from: "fadeOut")
}
}
}
}
public func seek(_ to: Double, from: String) { public func seek(_ to: Double, from: String) {
logger.log("SEEK: Seek to \(to) from \(from)") logger.log("SEEK: Seek to \(to) from \(from)")

View file

@ -126,6 +126,10 @@ extension AudioPlayer {
sleepTimeRemaining -= 1 sleepTimeRemaining -= 1
self.sleepTimeRemaining = sleepTimeRemaining self.sleepTimeRemaining = sleepTimeRemaining
if sleepTimeRemaining == 60 && self.isSleepTimerFadeOutEnabled() {
self.startFadeOut()
}
// Handle the sleep if the timer has expired // Handle the sleep if the timer has expired
if sleepTimeRemaining <= 0 { if sleepTimeRemaining <= 0 {
self.handleSleepEnd() self.handleSleepEnd()
@ -155,4 +159,8 @@ extension AudioPlayer {
return self.sleepTimeChapterStopAt != nil return self.sleepTimeChapterStopAt != nil
} }
private func isSleepTimerFadeOutEnabled() -> Bool {
let deviceSettings = Database.shared.getDeviceSettings()
return !deviceSettings.disableSleepTimerFadeOut
}
} }

View file

@ -84,6 +84,7 @@
<ui-text-input :value="shakeSensitivityOption" readonly append-icon="expand_more" style="width: 145px; max-width: 145px" /> <ui-text-input :value="shakeSensitivityOption" readonly append-icon="expand_more" style="width: 145px; max-width: 145px" />
</div> </div>
</div> </div>
</template>
<div class="flex items-center py-3"> <div class="flex items-center py-3">
<div class="w-10 flex justify-center" @click="toggleDisableSleepTimerFadeOut"> <div class="w-10 flex justify-center" @click="toggleDisableSleepTimerFadeOut">
<ui-toggle-switch v-model="settings.disableSleepTimerFadeOut" @input="saveSettings" /> <ui-toggle-switch v-model="settings.disableSleepTimerFadeOut" @input="saveSettings" />
@ -91,6 +92,7 @@
<p class="pl-4">{{ $strings.LabelDisableAudioFadeOut }}</p> <p class="pl-4">{{ $strings.LabelDisableAudioFadeOut }}</p>
<span class="material-icons-outlined ml-2" @click.stop="showInfo('disableSleepTimerFadeOut')">info</span> <span class="material-icons-outlined ml-2" @click.stop="showInfo('disableSleepTimerFadeOut')">info</span>
</div> </div>
<template v-if="!isiOS">
<div class="flex items-center py-3"> <div class="flex items-center py-3">
<div class="w-10 flex justify-center" @click="toggleDisableSleepTimerResetFeedback"> <div class="w-10 flex justify-center" @click="toggleDisableSleepTimerResetFeedback">
<ui-toggle-switch v-model="settings.disableSleepTimerResetFeedback" @input="saveSettings" /> <ui-toggle-switch v-model="settings.disableSleepTimerResetFeedback" @input="saveSettings" />