Auto commit Thu Mar 19 09:01:01 PM CDT 2026
This commit is contained in:
@@ -53,6 +53,7 @@ class MainViewModel(
|
||||
val ui: StateFlow<UiState> = _ui.asStateFlow()
|
||||
|
||||
private var runJob: Job? = null
|
||||
private var elapsedBeforePauseSec: Float = 0f
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
@@ -78,6 +79,7 @@ class MainViewModel(
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
elapsedBeforePauseSec = 0f
|
||||
}
|
||||
|
||||
fun setMode(mode: SessionMode) = _ui.update { it.copy(config = it.config.copy(mode = mode), error = null) }
|
||||
@@ -120,73 +122,22 @@ class MainViewModel(
|
||||
_ui.update { it.copy(error = validation) }
|
||||
return
|
||||
}
|
||||
runJob?.cancel()
|
||||
runJob = viewModelScope.launch {
|
||||
val count = state.settings.countdownPreference.seconds
|
||||
if (count > 0) {
|
||||
for (i in count downTo 1) {
|
||||
_ui.update { it.copy(runtimeState = RuntimeState.COUNTDOWN, countdownSec = i, remainingSec = state.config.durationSec, endedEarly = false, interruptionReason = null) }
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
// Use the timeline curves as the actual runtime program.
|
||||
val program = _ui.value.timeline
|
||||
val durationSec = program.durationSec
|
||||
|
||||
_ui.update {
|
||||
it.copy(
|
||||
runtimeState = RuntimeState.RUNNING,
|
||||
remainingSec = durationSec,
|
||||
countdownSec = 0,
|
||||
error = null,
|
||||
runtimeParams = TimelineRuntimeEvaluator.evaluate(program, 0f),
|
||||
)
|
||||
}
|
||||
|
||||
if (state.config.mode != SessionMode.VISUAL_ONLY) {
|
||||
val p0 = TimelineRuntimeEvaluator.evaluate(program, 0f)
|
||||
audioEngine.start(p0.carrierHz, p0.binauralHz)
|
||||
}
|
||||
|
||||
val tStart = System.currentTimeMillis()
|
||||
var lastWholeSec = durationSec
|
||||
|
||||
while (true) {
|
||||
delay(50)
|
||||
|
||||
val elapsedSec = (System.currentTimeMillis() - tStart) / 1000f
|
||||
val remaining = (durationSec - elapsedSec.toInt()).coerceAtLeast(0)
|
||||
|
||||
if (state.config.mode != SessionMode.VISUAL_ONLY && !headsetMonitor.isStereoHeadsetAvailable()) {
|
||||
audioEngine.stop()
|
||||
_ui.update { it.copy(runtimeState = RuntimeState.INTERRUPTED, interruptionReason = "Headphones disconnected. Session paused.") }
|
||||
return@launch
|
||||
}
|
||||
|
||||
val params = TimelineRuntimeEvaluator.evaluate(program, elapsedSec)
|
||||
_ui.update {
|
||||
val nextRemaining = if (remaining != lastWholeSec) remaining else it.remainingSec
|
||||
it.copy(runtimeParams = params, remainingSec = nextRemaining)
|
||||
}
|
||||
lastWholeSec = remaining
|
||||
|
||||
if (state.config.mode != SessionMode.VISUAL_ONLY) {
|
||||
audioEngine.setFrequencies(params.carrierHz, params.binauralHz)
|
||||
}
|
||||
|
||||
if (elapsedSec >= durationSec.toFloat()) break
|
||||
}
|
||||
|
||||
audioEngine.stop()
|
||||
_ui.update { it.copy(runtimeState = RuntimeState.COMPLETED, endedEarly = false) }
|
||||
}
|
||||
elapsedBeforePauseSec = 0f
|
||||
runProgram(startElapsedSec = 0f, includeCountdown = true)
|
||||
}
|
||||
|
||||
fun pause(reason: String? = null) {
|
||||
if (_ui.value.runtimeState != RuntimeState.RUNNING) return
|
||||
elapsedBeforePauseSec = currentElapsedSec()
|
||||
runJob?.cancel()
|
||||
audioEngine.stop()
|
||||
_ui.update { it.copy(runtimeState = if (reason == null) RuntimeState.PAUSED else RuntimeState.INTERRUPTED, interruptionReason = reason) }
|
||||
_ui.update {
|
||||
it.copy(
|
||||
runtimeState = if (reason == null) RuntimeState.PAUSED else RuntimeState.INTERRUPTED,
|
||||
interruptionReason = reason,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun resume() {
|
||||
@@ -196,35 +147,13 @@ class MainViewModel(
|
||||
_ui.update { it.copy(error = "Headphones are required to resume audio mode.") }
|
||||
return
|
||||
}
|
||||
runJob = viewModelScope.launch {
|
||||
for (i in 3 downTo 1) {
|
||||
_ui.update { it.copy(runtimeState = RuntimeState.COUNTDOWN, countdownSec = i) }
|
||||
delay(1000)
|
||||
}
|
||||
_ui.update { it.copy(runtimeState = RuntimeState.RUNNING, countdownSec = 0) }
|
||||
if (state.config.mode != SessionMode.VISUAL_ONLY) {
|
||||
val p = _ui.value.runtimeParams
|
||||
audioEngine.start(p.carrierHz, p.binauralHz)
|
||||
}
|
||||
var remaining = state.remainingSec
|
||||
while (remaining > 0) {
|
||||
delay(1000)
|
||||
if (state.config.mode != SessionMode.VISUAL_ONLY && !headsetMonitor.isStereoHeadsetAvailable()) {
|
||||
audioEngine.stop()
|
||||
_ui.update { it.copy(runtimeState = RuntimeState.INTERRUPTED, interruptionReason = "Headphones disconnected. Session paused.") }
|
||||
return@launch
|
||||
}
|
||||
remaining -= 1
|
||||
_ui.update { it.copy(remainingSec = remaining) }
|
||||
}
|
||||
audioEngine.stop()
|
||||
_ui.update { it.copy(runtimeState = RuntimeState.COMPLETED, endedEarly = false) }
|
||||
}
|
||||
runProgram(startElapsedSec = elapsedBeforePauseSec, includeCountdown = true)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
runJob?.cancel()
|
||||
audioEngine.stop()
|
||||
elapsedBeforePauseSec = 0f
|
||||
_ui.update { it.copy(runtimeState = RuntimeState.STOPPED, endedEarly = true) }
|
||||
}
|
||||
|
||||
@@ -238,6 +167,91 @@ class MainViewModel(
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
private fun runProgram(startElapsedSec: Float, includeCountdown: Boolean) {
|
||||
runJob?.cancel()
|
||||
runJob = viewModelScope.launch {
|
||||
val program = _ui.value.timeline
|
||||
val durationSec = program.durationSec
|
||||
val clampedStartSec = startElapsedSec.coerceIn(0f, durationSec.toFloat())
|
||||
|
||||
if (includeCountdown) {
|
||||
val count = _ui.value.settings.countdownPreference.seconds
|
||||
if (count > 0) {
|
||||
for (i in count downTo 1) {
|
||||
_ui.update {
|
||||
it.copy(
|
||||
runtimeState = RuntimeState.COUNTDOWN,
|
||||
countdownSec = i,
|
||||
remainingSec = (durationSec - clampedStartSec.toInt()).coerceAtLeast(0),
|
||||
endedEarly = false,
|
||||
interruptionReason = null,
|
||||
runtimeParams = TimelineRuntimeEvaluator.evaluate(program, clampedStartSec),
|
||||
)
|
||||
}
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val initialParams = TimelineRuntimeEvaluator.evaluate(program, clampedStartSec)
|
||||
_ui.update {
|
||||
it.copy(
|
||||
runtimeState = RuntimeState.RUNNING,
|
||||
remainingSec = (durationSec - clampedStartSec.toInt()).coerceAtLeast(0),
|
||||
countdownSec = 0,
|
||||
error = null,
|
||||
endedEarly = false,
|
||||
interruptionReason = null,
|
||||
runtimeParams = initialParams,
|
||||
)
|
||||
}
|
||||
|
||||
if (_ui.value.config.mode != SessionMode.VISUAL_ONLY) {
|
||||
audioEngine.start(initialParams.carrierHz, initialParams.binauralHz)
|
||||
}
|
||||
|
||||
val tStart = System.currentTimeMillis()
|
||||
while (true) {
|
||||
delay(50)
|
||||
|
||||
val elapsedSec = clampedStartSec + (System.currentTimeMillis() - tStart) / 1000f
|
||||
elapsedBeforePauseSec = elapsedSec.coerceAtMost(durationSec.toFloat())
|
||||
val remaining = (durationSec - kotlin.math.ceil(elapsedBeforePauseSec).toInt()).coerceAtLeast(0)
|
||||
val currentUi = _ui.value
|
||||
|
||||
if (currentUi.config.mode != SessionMode.VISUAL_ONLY && !headsetMonitor.isStereoHeadsetAvailable()) {
|
||||
audioEngine.stop()
|
||||
_ui.update {
|
||||
it.copy(
|
||||
runtimeState = RuntimeState.INTERRUPTED,
|
||||
interruptionReason = "Headphones disconnected. Session paused.",
|
||||
)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
|
||||
val params = TimelineRuntimeEvaluator.evaluate(program, elapsedBeforePauseSec)
|
||||
_ui.update { it.copy(runtimeParams = params, remainingSec = remaining) }
|
||||
|
||||
if (currentUi.config.mode != SessionMode.VISUAL_ONLY) {
|
||||
audioEngine.setFrequencies(params.carrierHz, params.binauralHz)
|
||||
}
|
||||
|
||||
if (elapsedBeforePauseSec >= durationSec.toFloat()) break
|
||||
}
|
||||
|
||||
audioEngine.stop()
|
||||
elapsedBeforePauseSec = 0f
|
||||
_ui.update { it.copy(runtimeState = RuntimeState.COMPLETED, endedEarly = false, remainingSec = 0) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun currentElapsedSec(): Float {
|
||||
val state = _ui.value
|
||||
val total = state.timeline.durationSec.toFloat().coerceAtLeast(1f)
|
||||
return (total - state.remainingSec).coerceIn(0f, total)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val headsetMonitor: HeadsetMonitor,
|
||||
|
||||
Reference in New Issue
Block a user