Auto commit (MindMachine) Mon Apr 13 07:01:01 PM CDT 2026
This commit is contained in:
@@ -519,6 +519,14 @@ fun SetupScreen(
|
|||||||
onCurveGranularityChanged = vm::setCurveGranularitySec,
|
onCurveGranularityChanged = vm::setCurveGranularitySec,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Checkbox(
|
||||||
|
checked = ui.config.monochromeFlashMode,
|
||||||
|
onCheckedChange = vm::setMonochromeFlashMode,
|
||||||
|
)
|
||||||
|
Text("Use black/white flashes for this session", color = MaterialTheme.colorScheme.onBackground)
|
||||||
|
}
|
||||||
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
|
||||||
if (hasUnsavedChanges) {
|
if (hasUnsavedChanges) {
|
||||||
OutlinedButton(onClick = { vm.saveCurrentProgram(programName) }, modifier = Modifier.weight(1f).height(52.dp)) {
|
OutlinedButton(onClick = { vm.saveCurrentProgram(programName) }, modifier = Modifier.weight(1f).height(52.dp)) {
|
||||||
@@ -587,6 +595,7 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToEntry:
|
|||||||
}
|
}
|
||||||
var brightnessOverlayVisible by remember { mutableStateOf(false) }
|
var brightnessOverlayVisible by remember { mutableStateOf(false) }
|
||||||
var brightnessOverlayToken by remember { mutableIntStateOf(0) }
|
var brightnessOverlayToken by remember { mutableIntStateOf(0) }
|
||||||
|
val timingLogTag = "MindMachineTiming"
|
||||||
|
|
||||||
LaunchedEffect(brightnessOverlayToken) {
|
LaunchedEffect(brightnessOverlayToken) {
|
||||||
if (!brightnessOverlayVisible) return@LaunchedEffect
|
if (!brightnessOverlayVisible) return@LaunchedEffect
|
||||||
@@ -594,6 +603,17 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToEntry:
|
|||||||
brightnessOverlayVisible = false
|
brightnessOverlayVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(ui.runtimeState) {
|
||||||
|
when (ui.runtimeState) {
|
||||||
|
RuntimeState.COUNTDOWN -> Log.d(timingLogTag, "COUNTDOWN_START")
|
||||||
|
RuntimeState.RUNNING -> Log.d(timingLogTag, "IMMERSIVE_START")
|
||||||
|
RuntimeState.STOPPED, RuntimeState.COMPLETED, RuntimeState.PAUSED, RuntimeState.INTERRUPTED, RuntimeState.ERROR -> {
|
||||||
|
Log.d(timingLogTag, "IMMERSIVE_STOP state=${ui.runtimeState}")
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ApplyKeepScreenOn(enabled = true)
|
ApplyKeepScreenOn(enabled = true)
|
||||||
|
|
||||||
ApplyScreenBrightnessOverride(
|
ApplyScreenBrightnessOverride(
|
||||||
@@ -601,6 +621,11 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToEntry:
|
|||||||
brightnessPercent = ui.settings.immersiveBrightnessPercent,
|
brightnessPercent = ui.settings.immersiveBrightnessPercent,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ApplyPreferredRefreshRate(
|
||||||
|
enabled = true,
|
||||||
|
preferLowest = ui.settings.preferLowestRefreshRate,
|
||||||
|
)
|
||||||
|
|
||||||
val progress = sessionProgressFraction(
|
val progress = sessionProgressFraction(
|
||||||
durationSec = ui.timeline.durationSec,
|
durationSec = ui.timeline.durationSec,
|
||||||
remainingSec = ui.remainingSec,
|
remainingSec = ui.remainingSec,
|
||||||
@@ -628,10 +653,12 @@ fun ActiveSessionScreen(vm: MainViewModel, onFinish: () -> Unit, onBackToEntry:
|
|||||||
SplitFlashRenderView(ctx).apply {
|
SplitFlashRenderView(ctx).apply {
|
||||||
setRunning(ui.runtimeState == RuntimeState.RUNNING)
|
setRunning(ui.runtimeState == RuntimeState.RUNNING)
|
||||||
updateIntervals(ui.runtimeParams.flashOnMs, ui.runtimeParams.flashOffMs)
|
updateIntervals(ui.runtimeParams.flashOnMs, ui.runtimeParams.flashOffMs)
|
||||||
|
setMonochrome(ui.config.monochromeFlashMode)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
update = { view ->
|
update = { view ->
|
||||||
view.updateIntervals(ui.runtimeParams.flashOnMs, ui.runtimeParams.flashOffMs)
|
view.updateIntervals(ui.runtimeParams.flashOnMs, ui.runtimeParams.flashOffMs)
|
||||||
|
view.setMonochrome(ui.config.monochromeFlashMode)
|
||||||
view.setRunning(ui.runtimeState == RuntimeState.RUNNING)
|
view.setRunning(ui.runtimeState == RuntimeState.RUNNING)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -759,6 +786,40 @@ private fun ApplyScreenBrightnessOverride(enabled: Boolean, brightnessPercent: I
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ApplyPreferredRefreshRate(enabled: Boolean, preferLowest: Boolean) {
|
||||||
|
val activity = LocalContext.current as? Activity ?: return
|
||||||
|
val window = activity.window
|
||||||
|
val timingLogTag = "MindMachineTiming"
|
||||||
|
|
||||||
|
DisposableEffect(window) {
|
||||||
|
onDispose {
|
||||||
|
val params = window.attributes
|
||||||
|
params.preferredRefreshRate = 0f
|
||||||
|
params.preferredDisplayModeId = 0
|
||||||
|
window.attributes = params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(enabled, preferLowest) {
|
||||||
|
val params = window.attributes
|
||||||
|
if (enabled && preferLowest) {
|
||||||
|
val display = activity.display
|
||||||
|
val chosenMode = display?.supportedModes?.minByOrNull { it.refreshRate }
|
||||||
|
params.preferredDisplayModeId = chosenMode?.modeId ?: 0
|
||||||
|
params.preferredRefreshRate = chosenMode?.refreshRate ?: 0f
|
||||||
|
Log.d(
|
||||||
|
timingLogTag,
|
||||||
|
"DISPLAY_MODE_REQUEST modeId=${chosenMode?.modeId ?: 0} refreshHz=${chosenMode?.refreshRate ?: 0f} supported=${display?.supportedModes?.joinToString { "${it.modeId}:${it.refreshRate}" }}"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
params.preferredDisplayModeId = 0
|
||||||
|
params.preferredRefreshRate = 0f
|
||||||
|
}
|
||||||
|
window.attributes = params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ApplyImmersiveMode(enabled: Boolean, revealSignal: Int) {
|
private fun ApplyImmersiveMode(enabled: Boolean, revealSignal: Int) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -806,6 +867,7 @@ private class SplitFlashRenderView(context: Context) : View(context) {
|
|||||||
private var rawOffMs = 167L
|
private var rawOffMs = 167L
|
||||||
private var onMs = 167L
|
private var onMs = 167L
|
||||||
private var offMs = 167L
|
private var offMs = 167L
|
||||||
|
private var monochrome = false
|
||||||
private var started = false
|
private var started = false
|
||||||
private var startUptimeMs = 0L
|
private var startUptimeMs = 0L
|
||||||
private var nextToggleUptimeMs = 0L
|
private var nextToggleUptimeMs = 0L
|
||||||
@@ -855,10 +917,12 @@ private class SplitFlashRenderView(context: Context) : View(context) {
|
|||||||
onMs = snapToFrameMs(rawOnMs, frameMs)
|
onMs = snapToFrameMs(rawOnMs, frameMs)
|
||||||
offMs = snapToFrameMs(rawOffMs, frameMs)
|
offMs = snapToFrameMs(rawOffMs, frameMs)
|
||||||
|
|
||||||
|
if (isRunning) {
|
||||||
Log.d(
|
Log.d(
|
||||||
timingLogTag,
|
timingLogTag,
|
||||||
"config onMs=$rawOnMs offMs=$rawOffMs refreshHz=${"%.2f".format(refreshRate())} frameMs=$frameMs snappedOnMs=$onMs snappedOffMs=$offMs"
|
"config onMs=$rawOnMs offMs=$rawOffMs refreshHz=${"%.2f".format(refreshRate())} frameMs=$frameMs snappedOnMs=$onMs snappedOffMs=$offMs"
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (isRunning && started) {
|
if (isRunning && started) {
|
||||||
scheduleFromNow()
|
scheduleFromNow()
|
||||||
@@ -886,12 +950,18 @@ private class SplitFlashRenderView(context: Context) : View(context) {
|
|||||||
maxLatenessMs = 0L
|
maxLatenessMs = 0L
|
||||||
Log.d(
|
Log.d(
|
||||||
timingLogTag,
|
timingLogTag,
|
||||||
"session-start phase=${phaseName(phase)} refreshHz=${"%.2f".format(refreshRate())} frameMs=${framePeriodMs()} nowMs=$startUptimeMs"
|
"session-start phase=${phaseName(phase)} modeId=${display?.mode?.modeId ?: 0} refreshHz=${"%.2f".format(refreshRate())} frameMs=${framePeriodMs()} nowMs=$startUptimeMs"
|
||||||
)
|
)
|
||||||
invalidate()
|
invalidate()
|
||||||
scheduleNext()
|
scheduleNext()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setMonochrome(enabled: Boolean) {
|
||||||
|
if (monochrome == enabled) return
|
||||||
|
monochrome = enabled
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDetachedFromWindow() {
|
override fun onDetachedFromWindow() {
|
||||||
handler.removeCallbacks(toggleRunnable)
|
handler.removeCallbacks(toggleRunnable)
|
||||||
super.onDetachedFromWindow()
|
super.onDetachedFromWindow()
|
||||||
@@ -919,8 +989,8 @@ private class SplitFlashRenderView(context: Context) : View(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun colorsForPhase(phase: Int): Pair<Int, Int> = when (phase) {
|
private fun colorsForPhase(phase: Int): Pair<Int, Int> = when (phase) {
|
||||||
0 -> Pair(android.graphics.Color.RED, android.graphics.Color.GREEN)
|
0 -> if (monochrome) Pair(android.graphics.Color.WHITE, android.graphics.Color.WHITE) else Pair(android.graphics.Color.RED, android.graphics.Color.GREEN)
|
||||||
2 -> Pair(android.graphics.Color.GREEN, android.graphics.Color.RED)
|
2 -> if (monochrome) Pair(android.graphics.Color.WHITE, android.graphics.Color.WHITE) else Pair(android.graphics.Color.GREEN, android.graphics.Color.RED)
|
||||||
else -> Pair(android.graphics.Color.BLACK, android.graphics.Color.BLACK)
|
else -> Pair(android.graphics.Color.BLACK, android.graphics.Color.BLACK)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1121,6 +1191,11 @@ fun SettingsScreen(vm: MainViewModel, onRemoveAds: () -> Unit) {
|
|||||||
Text("Show immersive bottom progress bar", color = MaterialTheme.colorScheme.onBackground)
|
Text("Show immersive bottom progress bar", color = MaterialTheme.colorScheme.onBackground)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Checkbox(checked = ui.settings.preferLowestRefreshRate, onCheckedChange = vm::updatePreferLowestRefreshRate)
|
||||||
|
Text("Prefer lowest display refresh rate during sessions", color = MaterialTheme.colorScheme.onBackground)
|
||||||
|
}
|
||||||
|
|
||||||
Button(onClick = onRemoveAds) {
|
Button(onClick = onRemoveAds) {
|
||||||
Text(if (ui.settings.noAds) "Ads removed" else "Remove ads")
|
Text(if (ui.settings.noAds) "Ads removed" else "Remove ads")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ class SettingsRepository(private val context: Context) {
|
|||||||
val countdownSeconds = intPreferencesKey("countdown_seconds")
|
val countdownSeconds = intPreferencesKey("countdown_seconds")
|
||||||
val forceAudioWithoutHeadphones = booleanPreferencesKey("force_audio_without_headphones")
|
val forceAudioWithoutHeadphones = booleanPreferencesKey("force_audio_without_headphones")
|
||||||
val showImmersiveProgressBar = booleanPreferencesKey("show_immersive_progress_bar")
|
val showImmersiveProgressBar = booleanPreferencesKey("show_immersive_progress_bar")
|
||||||
|
val preferLowestRefreshRate = booleanPreferencesKey("prefer_lowest_refresh_rate")
|
||||||
|
val monochromeFlashMode = booleanPreferencesKey("monochrome_flash_mode")
|
||||||
val immersiveBrightnessPercent = intPreferencesKey("immersive_brightness_percent")
|
val immersiveBrightnessPercent = intPreferencesKey("immersive_brightness_percent")
|
||||||
val lastPresetId = stringPreferencesKey("last_preset")
|
val lastPresetId = stringPreferencesKey("last_preset")
|
||||||
val noAds = booleanPreferencesKey("no_ads")
|
val noAds = booleanPreferencesKey("no_ads")
|
||||||
@@ -34,6 +36,8 @@ class SettingsRepository(private val context: Context) {
|
|||||||
?: 5,
|
?: 5,
|
||||||
forceAudioWithoutHeadphones = p[Keys.forceAudioWithoutHeadphones] ?: false,
|
forceAudioWithoutHeadphones = p[Keys.forceAudioWithoutHeadphones] ?: false,
|
||||||
showImmersiveProgressBar = p[Keys.showImmersiveProgressBar] ?: true,
|
showImmersiveProgressBar = p[Keys.showImmersiveProgressBar] ?: true,
|
||||||
|
preferLowestRefreshRate = p[Keys.preferLowestRefreshRate] ?: true,
|
||||||
|
monochromeFlashMode = p[Keys.monochromeFlashMode] ?: false,
|
||||||
immersiveBrightnessPercent = (p[Keys.immersiveBrightnessPercent] ?: 50).coerceIn(5, 100),
|
immersiveBrightnessPercent = (p[Keys.immersiveBrightnessPercent] ?: 50).coerceIn(5, 100),
|
||||||
lastPresetId = p[Keys.lastPresetId],
|
lastPresetId = p[Keys.lastPresetId],
|
||||||
noAds = p[Keys.noAds] ?: false,
|
noAds = p[Keys.noAds] ?: false,
|
||||||
@@ -51,6 +55,9 @@ class SettingsRepository(private val context: Context) {
|
|||||||
if (prefs[Keys.immersiveBrightnessPercent] == null) {
|
if (prefs[Keys.immersiveBrightnessPercent] == null) {
|
||||||
prefs[Keys.immersiveBrightnessPercent] = 50
|
prefs[Keys.immersiveBrightnessPercent] = 50
|
||||||
}
|
}
|
||||||
|
if (prefs[Keys.preferLowestRefreshRate] == null) {
|
||||||
|
prefs[Keys.preferLowestRefreshRate] = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +80,14 @@ class SettingsRepository(private val context: Context) {
|
|||||||
it[Keys.showImmersiveProgressBar] = value
|
it[Keys.showImmersiveProgressBar] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun updatePreferLowestRefreshRate(value: Boolean) = context.dataStore.edit {
|
||||||
|
it[Keys.preferLowestRefreshRate] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateMonochromeFlashMode(value: Boolean) = context.dataStore.edit {
|
||||||
|
it[Keys.monochromeFlashMode] = value
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun updateImmersiveBrightnessPercent(value: Int) = context.dataStore.edit {
|
suspend fun updateImmersiveBrightnessPercent(value: Int) = context.dataStore.edit {
|
||||||
it[Keys.immersiveBrightnessPercent] = value.coerceIn(5, 100)
|
it[Keys.immersiveBrightnessPercent] = value.coerceIn(5, 100)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ data class SessionConfig(
|
|||||||
val mode: SessionMode,
|
val mode: SessionMode,
|
||||||
val visualPatternType: VisualPattern,
|
val visualPatternType: VisualPattern,
|
||||||
val flashIntervalMs: Int,
|
val flashIntervalMs: Int,
|
||||||
|
val monochromeFlashMode: Boolean = false,
|
||||||
val intensityPercent: Int,
|
val intensityPercent: Int,
|
||||||
val carrierFrequencyHz: Float,
|
val carrierFrequencyHz: Float,
|
||||||
val binauralDifferenceHz: Float,
|
val binauralDifferenceHz: Float,
|
||||||
@@ -36,6 +37,8 @@ data class AppSettings(
|
|||||||
val countdownSeconds: Int = 5,
|
val countdownSeconds: Int = 5,
|
||||||
val forceAudioWithoutHeadphones: Boolean = false,
|
val forceAudioWithoutHeadphones: Boolean = false,
|
||||||
val showImmersiveProgressBar: Boolean = true,
|
val showImmersiveProgressBar: Boolean = true,
|
||||||
|
val preferLowestRefreshRate: Boolean = true,
|
||||||
|
val monochromeFlashMode: Boolean = false,
|
||||||
// Default used only before DataStore emits (and as a fallback).
|
// Default used only before DataStore emits (and as a fallback).
|
||||||
// We want the first-run experience to be less blinding.
|
// We want the first-run experience to be less blinding.
|
||||||
val immersiveBrightnessPercent: Int = 50,
|
val immersiveBrightnessPercent: Int = 50,
|
||||||
@@ -94,6 +97,7 @@ fun SessionPreset.toConfig(defaultMode: SessionMode = SessionMode.AUDIO_VISUAL)
|
|||||||
mode = defaultMode,
|
mode = defaultMode,
|
||||||
visualPatternType = visualPatternType,
|
visualPatternType = visualPatternType,
|
||||||
flashIntervalMs = flashIntervalMs,
|
flashIntervalMs = flashIntervalMs,
|
||||||
|
monochromeFlashMode = false,
|
||||||
intensityPercent = intensityPercent,
|
intensityPercent = intensityPercent,
|
||||||
carrierFrequencyHz = carrierFrequencyHz,
|
carrierFrequencyHz = carrierFrequencyHz,
|
||||||
binauralDifferenceHz = binauralDifferenceHz,
|
binauralDifferenceHz = binauralDifferenceHz,
|
||||||
|
|||||||
@@ -280,6 +280,7 @@ class MainViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setFlashIntervalMs(value: Int) = _ui.update { it.copy(config = it.config.copy(flashIntervalMs = value.coerceIn(50, 2000)), error = null) }
|
fun setFlashIntervalMs(value: Int) = _ui.update { it.copy(config = it.config.copy(flashIntervalMs = value.coerceIn(50, 2000)), error = null) }
|
||||||
|
fun setMonochromeFlashMode(value: Boolean) = _ui.update { it.copy(config = it.config.copy(monochromeFlashMode = value), error = null) }
|
||||||
fun setCarrier(value: Float) = _ui.update {
|
fun setCarrier(value: Float) = _ui.update {
|
||||||
val stableCarrier = value.coerceIn(ParameterRanges.CARRIER_HZ_MIN, ParameterRanges.CARRIER_HZ_MAX)
|
val stableCarrier = value.coerceIn(ParameterRanges.CARRIER_HZ_MIN, ParameterRanges.CARRIER_HZ_MAX)
|
||||||
it.copy(config = it.config.copy(carrierFrequencyHz = stableCarrier), error = null)
|
it.copy(config = it.config.copy(carrierFrequencyHz = stableCarrier), error = null)
|
||||||
@@ -291,6 +292,8 @@ class MainViewModel(
|
|||||||
settingsRepository.updateForceAudioWithoutHeadphones(value)
|
settingsRepository.updateForceAudioWithoutHeadphones(value)
|
||||||
}
|
}
|
||||||
fun updateShowImmersiveProgressBar(value: Boolean) = viewModelScope.launch { settingsRepository.updateShowImmersiveProgressBar(value) }
|
fun updateShowImmersiveProgressBar(value: Boolean) = viewModelScope.launch { settingsRepository.updateShowImmersiveProgressBar(value) }
|
||||||
|
fun updatePreferLowestRefreshRate(value: Boolean) = viewModelScope.launch { settingsRepository.updatePreferLowestRefreshRate(value) }
|
||||||
|
fun updateMonochromeFlashMode(value: Boolean) = viewModelScope.launch { settingsRepository.updateMonochromeFlashMode(value) }
|
||||||
fun updateImmersiveBrightnessPercent(value: Int) = viewModelScope.launch {
|
fun updateImmersiveBrightnessPercent(value: Int) = viewModelScope.launch {
|
||||||
settingsRepository.updateImmersiveBrightnessPercent(value)
|
settingsRepository.updateImmersiveBrightnessPercent(value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ data class TimelineModel(
|
|||||||
mode = mode,
|
mode = mode,
|
||||||
visualPatternType = preset.visualPatternType,
|
visualPatternType = preset.visualPatternType,
|
||||||
flashIntervalMs = flashIntervalMs,
|
flashIntervalMs = flashIntervalMs,
|
||||||
|
monochromeFlashMode = false,
|
||||||
intensityPercent = preset.intensityPercent,
|
intensityPercent = preset.intensityPercent,
|
||||||
carrierFrequencyHz = carrier,
|
carrierFrequencyHz = carrier,
|
||||||
binauralDifferenceHz = diff
|
binauralDifferenceHz = diff
|
||||||
|
|||||||
Reference in New Issue
Block a user