Auto commit Thu Mar 19 08:01:01 PM CDT 2026
This commit is contained in:
@@ -29,12 +29,19 @@ data class UiState(
|
||||
val presets: List<SessionPreset> = Presets.builtIn,
|
||||
val selectedPreset: SessionPreset = Presets.builtIn.first(),
|
||||
val config: SessionConfig = Presets.builtIn.first().toConfig(),
|
||||
|
||||
// Timeline program (curves) used for both editing + runtime modulation.
|
||||
val timeline: TimelineEditorState = TimelineProgramFactory.fromPreset(Presets.builtIn.first()),
|
||||
|
||||
val runtimeState: RuntimeState = RuntimeState.IDLE,
|
||||
val error: String? = null,
|
||||
val remainingSec: Int = 0,
|
||||
val countdownSec: Int = 0,
|
||||
val endedEarly: Boolean = false,
|
||||
val interruptionReason: String? = null,
|
||||
|
||||
// Current runtime-evaluated parameters (updated while running).
|
||||
val runtimeParams: RuntimeParams = RuntimeParams(),
|
||||
)
|
||||
|
||||
class MainViewModel(
|
||||
@@ -62,11 +69,37 @@ class MainViewModel(
|
||||
fun choosePreset(id: String) = viewModelScope.launch {
|
||||
val preset = _ui.value.presets.first { it.id == id }
|
||||
settingsRepository.updateLastPreset(id)
|
||||
_ui.update { it.copy(selectedPreset = preset, config = preset.toConfig(it.settings.defaultModePreference), error = null) }
|
||||
_ui.update {
|
||||
val cfg = preset.toConfig(it.settings.defaultModePreference)
|
||||
it.copy(
|
||||
selectedPreset = preset,
|
||||
config = cfg.copy(durationSec = cfg.durationSec.coerceIn(60, 8 * 60 * 60)),
|
||||
timeline = TimelineProgramFactory.fromPreset(preset),
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setMode(mode: SessionMode) = _ui.update { it.copy(config = it.config.copy(mode = mode), error = null) }
|
||||
fun setDurationMin(min: Int) = _ui.update { it.copy(config = it.config.copy(durationSec = (min.coerceIn(1, 30) * 60)), error = null) }
|
||||
|
||||
fun setDurationSec(seconds: Int) = _ui.update {
|
||||
val clamped = seconds.coerceIn(60, 8 * 60 * 60)
|
||||
it.copy(
|
||||
config = it.config.copy(durationSec = clamped),
|
||||
timeline = it.timeline.setDurationSec(clamped),
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
|
||||
fun updateTimeline(newState: TimelineEditorState) = _ui.update {
|
||||
it.copy(
|
||||
timeline = newState,
|
||||
config = it.config.copy(durationSec = newState.durationSec.coerceIn(60, 8 * 60 * 60)),
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
|
||||
// Legacy fixed-parameter setters kept for now but no longer used by Setup.
|
||||
fun setFlashIntervalMs(value: Int) = _ui.update { it.copy(config = it.config.copy(flashIntervalMs = value.coerceIn(50, 2000)), error = null) }
|
||||
fun setCarrier(value: Float) = _ui.update { it.copy(config = it.config.copy(carrierFrequencyHz = value.coerceIn(80f, 400f)), error = null) }
|
||||
fun setDifference(value: Float) = _ui.update { it.copy(config = it.config.copy(binauralDifferenceHz = value.coerceIn(0.5f, 20f)), error = null) }
|
||||
@@ -96,21 +129,54 @@ class MainViewModel(
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
_ui.update { it.copy(runtimeState = RuntimeState.RUNNING, remainingSec = state.config.durationSec, countdownSec = 0, error = null) }
|
||||
if (state.config.mode != SessionMode.VISUAL_ONLY) {
|
||||
audioEngine.start(state.config.carrierFrequencyHz, state.config.binauralDifferenceHz)
|
||||
// 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),
|
||||
)
|
||||
}
|
||||
var remaining = state.config.durationSec
|
||||
while (remaining > 0) {
|
||||
delay(1000)
|
||||
|
||||
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
|
||||
}
|
||||
remaining -= 1
|
||||
_ui.update { it.copy(remainingSec = remaining) }
|
||||
|
||||
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) }
|
||||
}
|
||||
@@ -137,7 +203,8 @@ class MainViewModel(
|
||||
}
|
||||
_ui.update { it.copy(runtimeState = RuntimeState.RUNNING, countdownSec = 0) }
|
||||
if (state.config.mode != SessionMode.VISUAL_ONLY) {
|
||||
audioEngine.start(state.config.carrierFrequencyHz, state.config.binauralDifferenceHz)
|
||||
val p = _ui.value.runtimeParams
|
||||
audioEngine.start(p.carrierHz, p.binauralHz)
|
||||
}
|
||||
var remaining = state.remainingSec
|
||||
while (remaining > 0) {
|
||||
|
||||
Reference in New Issue
Block a user