fix(sleep): звук сна вплывает в конце таймера, а не в первые 90 секунд
Было: при таймере со звуком радио кроссфейдилось в шум в НАЧАЛЕ (CROSSFADE_MS=90с), и для 15-мин таймера уже через ~1.5 мин играл полный белый шум всё оставшееся время. Стало: радио играет почти весь таймер; в последние SOUND_OUTRO_MS (3 мин, но не больше половины таймера) включается звук сна — радио кроссфейдится в шум, шум держится, в самом конце затухает в тишину. Генератор шума стартует лениво (только в аутро, не молотит весь таймер). Засыпаешь под радио, а не под резкий шум сразу.
This commit is contained in:
@@ -86,8 +86,9 @@ class PlayerController @Inject constructor(
|
|||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val FADE_MS = 20_000L // длительность плавного затухания в конце таймера
|
const val FADE_MS = 20_000L // длительность плавного затухания в конце таймера
|
||||||
const val CROSSFADE_MS = 90_000L // переход радио → звук для сна
|
const val CROSSFADE_MS = 90_000L // переход радио → звук для сна (внутри аутро)
|
||||||
const val SOUND_VOL = 0.6f // комфортная громкость шума
|
const val SOUND_VOL = 0.6f // комфортная громкость шума
|
||||||
|
const val SOUND_OUTRO_MS = 180_000L // финальное окно со звуком сна (последние ~3 мин)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
@@ -253,8 +254,10 @@ class PlayerController @Inject constructor(
|
|||||||
/**
|
/**
|
||||||
* Запустить таймер сна на [durationMs] мс.
|
* Запустить таймер сна на [durationMs] мс.
|
||||||
* Без [sound]: в последние FADE_MS радио экспоненциально затухает, затем пауза.
|
* Без [sound]: в последние FADE_MS радио экспоненциально затухает, затем пауза.
|
||||||
* Со [sound] («Smart Sleep Fade»): в начале радио кроссфейдится в звук для сна
|
* Со [sound]: радио играет почти весь таймер; в последние SOUND_OUTRO_MS (не больше
|
||||||
* (радио ↓, шум ↑), затем шум играет, в конце затухает — как в спеке.
|
* половины таймера) включается звук для сна — радио кроссфейдится в шум (радио ↓,
|
||||||
|
* шум ↑), шум держится, в самом конце затухает в тишину. Засыпаешь под радио, а не
|
||||||
|
* под резкий белый шум в первые же полторы минуты.
|
||||||
*/
|
*/
|
||||||
fun startSleepTimer(durationMs: Long, sound: SleepSound? = null) {
|
fun startSleepTimer(durationMs: Long, sound: SleepSound? = null) {
|
||||||
sleepJob?.cancel()
|
sleepJob?.cancel()
|
||||||
@@ -263,41 +266,52 @@ class PlayerController @Inject constructor(
|
|||||||
sleepJob = timerScope.launch {
|
sleepJob = timerScope.launch {
|
||||||
val start = SystemClock.elapsedRealtime()
|
val start = SystemClock.elapsedRealtime()
|
||||||
val end = start + durationMs
|
val end = start + durationMs
|
||||||
// Кроссфейд не длиннее трети таймера (для коротких интервалов).
|
// Финальное окно со звуком — не длиннее половины таймера (для коротких).
|
||||||
val crossfade = if (sound != null) CROSSFADE_MS.coerceAtMost(durationMs / 3) else 0L
|
val outro = if (sound != null) SOUND_OUTRO_MS.coerceAtMost(durationMs / 2) else 0L
|
||||||
if (sound != null) sleepSoundPlayer.start(sound)
|
// Кроссфейд радио→шум занимает первую половину аутро.
|
||||||
|
val crossfade = CROSSFADE_MS.coerceAtMost(outro / 2).coerceAtLeast(1L)
|
||||||
|
var soundStarted = false
|
||||||
while (true) {
|
while (true) {
|
||||||
val now = SystemClock.elapsedRealtime()
|
val now = SystemClock.elapsedRealtime()
|
||||||
val remaining = end - now
|
val remaining = end - now
|
||||||
if (remaining <= 0L) break
|
if (remaining <= 0L) break
|
||||||
_sleepRemainingMs.value = remaining
|
_sleepRemainingMs.value = remaining
|
||||||
if (sound != null) {
|
|
||||||
val elapsed = now - start
|
|
||||||
when {
|
when {
|
||||||
elapsed < crossfade -> {
|
sound != null && remaining <= outro -> {
|
||||||
|
// Генератор шума стартуем лениво — только в аутро, не весь таймер.
|
||||||
|
if (!soundStarted) {
|
||||||
|
sleepSoundPlayer.start(sound)
|
||||||
|
soundStarted = true
|
||||||
|
}
|
||||||
|
val outroElapsed = outro - remaining
|
||||||
|
when {
|
||||||
|
outroElapsed < crossfade -> {
|
||||||
// Кроссфейд: радио вниз, шум вверх.
|
// Кроссфейд: радио вниз, шум вверх.
|
||||||
val f = elapsed.toFloat() / crossfade
|
val f = outroElapsed.toFloat() / crossfade
|
||||||
exoPlayer.volume = (1f - f).coerceIn(0f, 1f)
|
exoPlayer.volume = (1f - f).coerceIn(0f, 1f)
|
||||||
sleepSoundPlayer.setVolume(f * SOUND_VOL)
|
sleepSoundPlayer.setVolume(f * SOUND_VOL)
|
||||||
}
|
}
|
||||||
remaining <= FADE_MS -> {
|
remaining <= FADE_MS -> {
|
||||||
// Финальное затухание шума.
|
// Финальное затухание шума в тишину.
|
||||||
val frac = remaining.toFloat() / FADE_MS
|
val frac = remaining.toFloat() / FADE_MS
|
||||||
sleepSoundPlayer.setVolume((frac * frac) * SOUND_VOL)
|
sleepSoundPlayer.setVolume((frac * frac) * SOUND_VOL)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
// Радио отыграло — на паузу, шум на комфортной громкости.
|
// Радио отыграло — пауза, шум на комфортной громкости.
|
||||||
if (exoPlayer.isPlaying) exoPlayer.pause()
|
if (exoPlayer.isPlaying) exoPlayer.pause()
|
||||||
sleepSoundPlayer.setVolume(SOUND_VOL)
|
sleepSoundPlayer.setVolume(SOUND_VOL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delay(150)
|
delay(150)
|
||||||
} else {
|
}
|
||||||
if (remaining <= FADE_MS) {
|
sound == null && remaining <= FADE_MS -> {
|
||||||
|
// Без звука: экспоненциальное затухание радио в конце.
|
||||||
val frac = remaining.toFloat() / FADE_MS
|
val frac = remaining.toFloat() / FADE_MS
|
||||||
exoPlayer.volume = (frac * frac).coerceIn(0f, 1f)
|
exoPlayer.volume = (frac * frac).coerceIn(0f, 1f)
|
||||||
delay(150)
|
delay(150)
|
||||||
} else {
|
}
|
||||||
|
else -> {
|
||||||
|
// Основная фаза: радио играет как обычно.
|
||||||
delay(1_000)
|
delay(1_000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user