Auto commit Sat Mar 21 10:01:01 PM CDT 2026
This commit is contained in:
@@ -44,6 +44,7 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
|||||||
import androidx.compose.material3.Slider
|
import androidx.compose.material3.Slider
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -77,6 +78,7 @@ import androidx.navigation.compose.rememberNavController
|
|||||||
import com.mindmachine.mvp.audio.BinauralAudioEngine
|
import com.mindmachine.mvp.audio.BinauralAudioEngine
|
||||||
import com.mindmachine.mvp.audio.HeadsetMonitor
|
import com.mindmachine.mvp.audio.HeadsetMonitor
|
||||||
import com.mindmachine.mvp.data.SettingsRepository
|
import com.mindmachine.mvp.data.SettingsRepository
|
||||||
|
import com.mindmachine.mvp.data.UserProgramRepository
|
||||||
import com.mindmachine.mvp.domain.CountdownPreference
|
import com.mindmachine.mvp.domain.CountdownPreference
|
||||||
import com.mindmachine.mvp.domain.RuntimeState
|
import com.mindmachine.mvp.domain.RuntimeState
|
||||||
import com.mindmachine.mvp.domain.SessionMode
|
import com.mindmachine.mvp.domain.SessionMode
|
||||||
@@ -109,7 +111,12 @@ private val MindMachineColors = darkColorScheme(
|
|||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private val vm: MainViewModel by viewModels {
|
private val vm: MainViewModel by viewModels {
|
||||||
MainViewModel.Factory(SettingsRepository(applicationContext), HeadsetMonitor(applicationContext), BinauralAudioEngine())
|
MainViewModel.Factory(
|
||||||
|
SettingsRepository(applicationContext),
|
||||||
|
UserProgramRepository(applicationContext),
|
||||||
|
HeadsetMonitor(applicationContext),
|
||||||
|
BinauralAudioEngine(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -158,6 +165,10 @@ fun App(vm: MainViewModel = viewModel()) {
|
|||||||
composable("home") {
|
composable("home") {
|
||||||
Column(Modifier.fillMaxSize()) {
|
Column(Modifier.fillMaxSize()) {
|
||||||
TopAppBar(title = { Text("MindMachine") }, actions = {
|
TopAppBar(title = { Text("MindMachine") }, actions = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
vm.createNewProgramDraft()
|
||||||
|
nav.navigate("setup")
|
||||||
|
}) { Text("Add") }
|
||||||
TextButton(onClick = { nav.navigate("settings") }) { Text("Settings") }
|
TextButton(onClick = { nav.navigate("settings") }) { Text("Settings") }
|
||||||
})
|
})
|
||||||
LazyColumn(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
LazyColumn(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
@@ -175,10 +186,19 @@ fun App(vm: MainViewModel = viewModel()) {
|
|||||||
nav.navigate("setup")
|
nav.navigate("setup")
|
||||||
}.padding(4.dp)
|
}.padding(4.dp)
|
||||||
) {
|
) {
|
||||||
Column(Modifier.padding(12.dp)) {
|
Row(
|
||||||
Text(p.name, style = MaterialTheme.typography.titleMedium)
|
modifier = Modifier.fillMaxWidth().padding(12.dp),
|
||||||
Text(p.description)
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
Text("${p.defaultDurationSec / 60} min • ${p.visualPatternType} • binaural ${p.binauralDifferenceHz}")
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Text(p.name, style = MaterialTheme.typography.titleMedium)
|
||||||
|
Text(p.description)
|
||||||
|
Text("${p.defaultDurationSec / 60} min • ${p.visualPatternType} • binaural ${p.binauralDifferenceHz}")
|
||||||
|
}
|
||||||
|
if (p.isUserProgram) {
|
||||||
|
TextButton(onClick = { vm.deleteProgram(p.id) }) { Text("Delete") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,6 +290,8 @@ fun SetupScreen(
|
|||||||
.safeDrawingPadding()
|
.safeDrawingPadding()
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
|
|
||||||
|
var programName by remember(ui.selectedPreset.id) { mutableStateOf(ui.selectedPreset.name) }
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = containerModifier,
|
modifier = containerModifier,
|
||||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
@@ -280,6 +302,13 @@ fun SetupScreen(
|
|||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
value = programName,
|
||||||
|
onValueChange = { programName = it },
|
||||||
|
label = { Text("Program name") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
DurationSliderCard(
|
DurationSliderCard(
|
||||||
durationSec = ui.timeline.durationSec,
|
durationSec = ui.timeline.durationSec,
|
||||||
onDurationSecChanged = { vm.setDurationSec(it) },
|
onDurationSecChanged = { vm.setDurationSec(it) },
|
||||||
@@ -292,8 +321,13 @@ fun SetupScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
TextButton(onClick = onHolder) { Text("Holder Guidance") }
|
TextButton(onClick = onHolder) { Text("Holder Guidance") }
|
||||||
if (ui.error != null) Text(ui.error!!, color = MaterialTheme.colorScheme.error)
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth()) {
|
||||||
Button(onClick = onStart, modifier = Modifier.fillMaxWidth().height(52.dp)) { Text("Start") }
|
OutlinedButton(onClick = { vm.saveCurrentProgram(programName) }, modifier = Modifier.weight(1f).height(52.dp)) {
|
||||||
|
Text("Save")
|
||||||
|
}
|
||||||
|
Button(onClick = onStart, modifier = Modifier.weight(1f).height(52.dp)) { Text("Start") }
|
||||||
|
}
|
||||||
|
if (ui.error != null) Text(ui.error!!, color = if (ui.error!!.startsWith("Saved")) Color(0xFF72E39A) else MaterialTheme.colorScheme.error)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package com.mindmachine.mvp.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import com.mindmachine.mvp.session.TimelineCurve
|
||||||
|
import com.mindmachine.mvp.session.TimelineEditorState
|
||||||
|
import com.mindmachine.mvp.session.TimelinePoint
|
||||||
|
import com.mindmachine.mvp.session.TimelineSelection
|
||||||
|
import com.mindmachine.mvp.session.TimelineViewport
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
private val Context.programDataStore by preferencesDataStore("mindmachine_programs")
|
||||||
|
|
||||||
|
class UserProgramRepository(private val context: Context) {
|
||||||
|
private object Keys {
|
||||||
|
val programsJson = stringPreferencesKey("user_programs_json")
|
||||||
|
}
|
||||||
|
|
||||||
|
val userPrograms: Flow<List<UserProgramEntity>> = context.programDataStore.data.map { prefs ->
|
||||||
|
parsePrograms(prefs[Keys.programsJson])
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun upsert(program: UserProgramEntity) {
|
||||||
|
context.programDataStore.edit { prefs ->
|
||||||
|
val current = parsePrograms(prefs[Keys.programsJson]).toMutableList()
|
||||||
|
val idx = current.indexOfFirst { it.id == program.id }
|
||||||
|
if (idx >= 0) {
|
||||||
|
current[idx] = program
|
||||||
|
} else {
|
||||||
|
current += program
|
||||||
|
}
|
||||||
|
prefs[Keys.programsJson] = toJson(current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun delete(id: String) {
|
||||||
|
context.programDataStore.edit { prefs ->
|
||||||
|
val current = parsePrograms(prefs[Keys.programsJson]).filterNot { it.id == id }
|
||||||
|
prefs[Keys.programsJson] = toJson(current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parsePrograms(raw: String?): List<UserProgramEntity> {
|
||||||
|
if (raw.isNullOrBlank()) return emptyList()
|
||||||
|
return runCatching {
|
||||||
|
val arr = JSONArray(raw)
|
||||||
|
buildList {
|
||||||
|
for (i in 0 until arr.length()) {
|
||||||
|
val o = arr.getJSONObject(i)
|
||||||
|
add(
|
||||||
|
UserProgramEntity(
|
||||||
|
id = o.getString("id"),
|
||||||
|
name = o.getString("name"),
|
||||||
|
description = o.optString("description", "Custom program"),
|
||||||
|
timeline = parseTimeline(o.getJSONObject("timeline")),
|
||||||
|
sortOrder = o.optInt("sortOrder", i + 1000),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.getOrDefault(emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseTimeline(o: JSONObject): TimelineEditorState {
|
||||||
|
fun parseCurve(name: String): TimelineCurve {
|
||||||
|
val points = o.getJSONArray(name)
|
||||||
|
return TimelineCurve(
|
||||||
|
points = buildList {
|
||||||
|
for (i in 0 until points.length()) {
|
||||||
|
val p = points.getJSONObject(i)
|
||||||
|
add(TimelinePoint(tSec = p.getInt("t"), value01 = p.getDouble("v").toFloat()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return TimelineEditorState(
|
||||||
|
durationSec = o.getInt("durationSec"),
|
||||||
|
selection = TimelineSelection(
|
||||||
|
startSec = o.optInt("selectionStartSec", 0),
|
||||||
|
endSec = o.optInt("selectionEndSec", o.getInt("durationSec")),
|
||||||
|
),
|
||||||
|
viewport = TimelineViewport(
|
||||||
|
startSec = o.optDouble("viewportStartSec", 0.0).toFloat(),
|
||||||
|
secondsPerScreen = o.optDouble("viewportSecondsPerScreen", 600.0).toFloat(),
|
||||||
|
),
|
||||||
|
activeCurve = TimelineEditorState.ActiveCurve.valueOf(o.optString("activeCurve", TimelineEditorState.ActiveCurve.VISUAL_LEFT.name)),
|
||||||
|
visualLeft = parseCurve("visualLeft"),
|
||||||
|
visualRight = parseCurve("visualRight"),
|
||||||
|
audioLeft = parseCurve("audioLeft"),
|
||||||
|
audioRight = parseCurve("audioRight"),
|
||||||
|
isPlaying = false,
|
||||||
|
playheadSec = 0f,
|
||||||
|
).setDurationSec(o.getInt("durationSec"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toJson(programs: List<UserProgramEntity>): String {
|
||||||
|
val arr = JSONArray()
|
||||||
|
programs.sortedBy { it.sortOrder }.forEach { program ->
|
||||||
|
val o = JSONObject()
|
||||||
|
o.put("id", program.id)
|
||||||
|
o.put("name", program.name)
|
||||||
|
o.put("description", program.description)
|
||||||
|
o.put("sortOrder", program.sortOrder)
|
||||||
|
|
||||||
|
val timeline = JSONObject()
|
||||||
|
timeline.put("durationSec", program.timeline.durationSec)
|
||||||
|
timeline.put("selectionStartSec", program.timeline.selection.startSec)
|
||||||
|
timeline.put("selectionEndSec", program.timeline.selection.endSec)
|
||||||
|
timeline.put("viewportStartSec", program.timeline.viewport.startSec)
|
||||||
|
timeline.put("viewportSecondsPerScreen", program.timeline.viewport.secondsPerScreen)
|
||||||
|
timeline.put("activeCurve", program.timeline.activeCurve.name)
|
||||||
|
timeline.put("visualLeft", pointsArray(program.timeline.visualLeft))
|
||||||
|
timeline.put("visualRight", pointsArray(program.timeline.visualRight))
|
||||||
|
timeline.put("audioLeft", pointsArray(program.timeline.audioLeft))
|
||||||
|
timeline.put("audioRight", pointsArray(program.timeline.audioRight))
|
||||||
|
o.put("timeline", timeline)
|
||||||
|
|
||||||
|
arr.put(o)
|
||||||
|
}
|
||||||
|
return arr.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pointsArray(curve: TimelineCurve): JSONArray {
|
||||||
|
val arr = JSONArray()
|
||||||
|
curve.points.forEach { p ->
|
||||||
|
arr.put(JSONObject().put("t", p.tSec).put("v", p.value01.toDouble()))
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class UserProgramEntity(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val description: String,
|
||||||
|
val timeline: TimelineEditorState,
|
||||||
|
val sortOrder: Int,
|
||||||
|
)
|
||||||
@@ -17,6 +17,7 @@ data class SessionPreset(
|
|||||||
val binauralDifferenceHz: Float,
|
val binauralDifferenceHz: Float,
|
||||||
val cautionNote: String,
|
val cautionNote: String,
|
||||||
val sortOrder: Int,
|
val sortOrder: Int,
|
||||||
|
val isUserProgram: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class SessionConfig(
|
data class SessionConfig(
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import com.mindmachine.mvp.audio.BinauralAudioEngine
|
import com.mindmachine.mvp.audio.BinauralAudioEngine
|
||||||
import com.mindmachine.mvp.audio.HeadsetMonitor
|
import com.mindmachine.mvp.audio.HeadsetMonitor
|
||||||
import com.mindmachine.mvp.data.SettingsRepository
|
import com.mindmachine.mvp.data.SettingsRepository
|
||||||
|
import com.mindmachine.mvp.data.UserProgramEntity
|
||||||
|
import com.mindmachine.mvp.data.UserProgramRepository
|
||||||
import com.mindmachine.mvp.domain.AppSettings
|
import com.mindmachine.mvp.domain.AppSettings
|
||||||
import com.mindmachine.mvp.domain.CountdownPreference
|
import com.mindmachine.mvp.domain.CountdownPreference
|
||||||
import com.mindmachine.mvp.domain.Presets
|
import com.mindmachine.mvp.domain.Presets
|
||||||
@@ -19,8 +21,10 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
const val SAFETY_VERSION = 1
|
const val SAFETY_VERSION = 1
|
||||||
|
|
||||||
@@ -46,6 +50,7 @@ data class UiState(
|
|||||||
|
|
||||||
class MainViewModel(
|
class MainViewModel(
|
||||||
private val settingsRepository: SettingsRepository,
|
private val settingsRepository: SettingsRepository,
|
||||||
|
private val userProgramRepository: UserProgramRepository,
|
||||||
private val headsetMonitor: HeadsetMonitor,
|
private val headsetMonitor: HeadsetMonitor,
|
||||||
private val audioEngine: BinauralAudioEngine,
|
private val audioEngine: BinauralAudioEngine,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
@@ -54,34 +59,136 @@ class MainViewModel(
|
|||||||
|
|
||||||
private var runJob: Job? = null
|
private var runJob: Job? = null
|
||||||
private var elapsedBeforePauseSec: Float = 0f
|
private var elapsedBeforePauseSec: Float = 0f
|
||||||
|
private var cachedUserPrograms: List<UserProgramEntity> = emptyList()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
settingsRepository.settings.collect { s ->
|
combine(settingsRepository.settings, userProgramRepository.userPrograms) { settings, userPrograms ->
|
||||||
|
settings to userPrograms
|
||||||
|
}.collect { (settings, userPrograms) ->
|
||||||
|
cachedUserPrograms = userPrograms
|
||||||
|
val userPresets = userPrograms.map {
|
||||||
|
TimelineProgramFactory.toPreset(
|
||||||
|
id = it.id,
|
||||||
|
name = it.name,
|
||||||
|
description = it.description,
|
||||||
|
timeline = it.timeline,
|
||||||
|
sortOrder = it.sortOrder,
|
||||||
|
isUserProgram = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val allPresets = (Presets.builtIn + userPresets).sortedBy { it.sortOrder }
|
||||||
|
|
||||||
_ui.update { current ->
|
_ui.update { current ->
|
||||||
val preset = current.presets.find { it.id == (s.lastPresetId ?: current.selectedPreset.id) } ?: current.selectedPreset
|
val selectedId = settings.lastPresetId ?: current.selectedPreset.id
|
||||||
current.copy(settings = s, selectedPreset = preset, config = current.config.copy(mode = s.defaultModePreference))
|
val selectedPreset = allPresets.find { it.id == selectedId } ?: allPresets.first()
|
||||||
|
val sameSelection = selectedPreset.id == current.selectedPreset.id
|
||||||
|
current.copy(
|
||||||
|
settings = settings,
|
||||||
|
presets = allPresets,
|
||||||
|
selectedPreset = selectedPreset,
|
||||||
|
config = current.config.copy(mode = settings.defaultModePreference),
|
||||||
|
timeline = if (sameSelection) current.timeline else timelineForPreset(selectedPreset, userPrograms),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun acknowledgeSafety() = viewModelScope.launch { settingsRepository.acknowledgeSafety(SAFETY_VERSION) }
|
fun acknowledgeSafety() = viewModelScope.launch { settingsRepository.acknowledgeSafety(SAFETY_VERSION) }
|
||||||
|
|
||||||
fun choosePreset(id: String) = viewModelScope.launch {
|
fun choosePreset(id: String) = viewModelScope.launch {
|
||||||
val preset = _ui.value.presets.first { it.id == id }
|
val state = _ui.value
|
||||||
|
val preset = state.presets.first { it.id == id }
|
||||||
settingsRepository.updateLastPreset(id)
|
settingsRepository.updateLastPreset(id)
|
||||||
_ui.update {
|
_ui.update {
|
||||||
val cfg = preset.toConfig(it.settings.defaultModePreference)
|
val cfg = preset.toConfig(it.settings.defaultModePreference)
|
||||||
it.copy(
|
it.copy(
|
||||||
selectedPreset = preset,
|
selectedPreset = preset,
|
||||||
config = cfg.copy(durationSec = cfg.durationSec.coerceIn(60, 8 * 60 * 60)),
|
config = cfg.copy(durationSec = cfg.durationSec.coerceIn(60, 8 * 60 * 60)),
|
||||||
timeline = TimelineProgramFactory.fromPreset(preset),
|
timeline = timelineForPreset(preset, cachedUserPrograms),
|
||||||
error = null,
|
error = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
elapsedBeforePauseSec = 0f
|
elapsedBeforePauseSec = 0f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createNewProgramDraft() {
|
||||||
|
val draftId = "user-${UUID.randomUUID()}"
|
||||||
|
val displayName = nextUntitledName()
|
||||||
|
val timeline = TimelineEditorState.default(20 * 60)
|
||||||
|
val preset = TimelineProgramFactory.toPreset(
|
||||||
|
id = draftId,
|
||||||
|
name = displayName,
|
||||||
|
description = "Custom program",
|
||||||
|
timeline = timeline,
|
||||||
|
sortOrder = nextSortOrder(),
|
||||||
|
isUserProgram = true,
|
||||||
|
)
|
||||||
|
_ui.update {
|
||||||
|
it.copy(
|
||||||
|
selectedPreset = preset,
|
||||||
|
config = preset.toConfig(it.settings.defaultModePreference),
|
||||||
|
timeline = timeline,
|
||||||
|
error = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveCurrentProgram(name: String) = viewModelScope.launch {
|
||||||
|
val trimmed = name.trim()
|
||||||
|
if (trimmed.isBlank()) {
|
||||||
|
_ui.update { it.copy(error = "Enter a name before saving.") }
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val state = _ui.value
|
||||||
|
val id = if (state.selectedPreset.isUserProgram) state.selectedPreset.id else "user-${UUID.randomUUID()}"
|
||||||
|
val sortOrder = cachedUserPrograms.find { it.id == id }?.sortOrder ?: nextSortOrder()
|
||||||
|
|
||||||
|
userProgramRepository.upsert(
|
||||||
|
UserProgramEntity(
|
||||||
|
id = id,
|
||||||
|
name = trimmed,
|
||||||
|
description = "Custom program",
|
||||||
|
timeline = state.timeline,
|
||||||
|
sortOrder = sortOrder,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
settingsRepository.updateLastPreset(id)
|
||||||
|
|
||||||
|
_ui.update {
|
||||||
|
val savedPreset = TimelineProgramFactory.toPreset(
|
||||||
|
id = id,
|
||||||
|
name = trimmed,
|
||||||
|
description = "Custom program",
|
||||||
|
timeline = it.timeline,
|
||||||
|
sortOrder = sortOrder,
|
||||||
|
isUserProgram = true,
|
||||||
|
)
|
||||||
|
it.copy(
|
||||||
|
selectedPreset = savedPreset,
|
||||||
|
error = "Saved \"$trimmed\"",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteProgram(id: String) = viewModelScope.launch {
|
||||||
|
userProgramRepository.delete(id)
|
||||||
|
if (_ui.value.selectedPreset.id == id) {
|
||||||
|
val fallback = Presets.builtIn.first()
|
||||||
|
settingsRepository.updateLastPreset(fallback.id)
|
||||||
|
_ui.update {
|
||||||
|
it.copy(
|
||||||
|
selectedPreset = fallback,
|
||||||
|
config = fallback.toConfig(it.settings.defaultModePreference),
|
||||||
|
timeline = TimelineProgramFactory.fromPreset(fallback),
|
||||||
|
error = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setMode(mode: SessionMode) = _ui.update { it.copy(config = it.config.copy(mode = mode), error = null) }
|
fun setMode(mode: SessionMode) = _ui.update { it.copy(config = it.config.copy(mode = mode), error = null) }
|
||||||
|
|
||||||
fun setDurationSec(seconds: Int) = _ui.update {
|
fun setDurationSec(seconds: Int) = _ui.update {
|
||||||
@@ -101,7 +208,6 @@ class MainViewModel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 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 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) }
|
fun setDifference(value: Float) = _ui.update { it.copy(config = it.config.copy(binauralDifferenceHz = value.coerceIn(0.5f, 20f)), error = null) }
|
||||||
@@ -252,11 +358,29 @@ class MainViewModel(
|
|||||||
return (total - state.remainingSec).coerceIn(0f, total)
|
return (total - state.remainingSec).coerceIn(0f, total)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun timelineForPreset(preset: SessionPreset, userPrograms: List<UserProgramEntity>): TimelineEditorState {
|
||||||
|
return userPrograms.firstOrNull { it.id == preset.id }?.timeline ?: TimelineProgramFactory.fromPreset(preset)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun nextSortOrder(): Int = (cachedUserPrograms.maxOfOrNull { it.sortOrder } ?: 999) + 1
|
||||||
|
|
||||||
|
private fun nextUntitledName(): String {
|
||||||
|
val names = cachedUserPrograms.map { it.name.lowercase() }.toSet()
|
||||||
|
var i = 1
|
||||||
|
while (true) {
|
||||||
|
val candidate = "Custom $i"
|
||||||
|
if (candidate.lowercase() !in names) return candidate
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Factory(
|
class Factory(
|
||||||
private val settingsRepository: SettingsRepository,
|
private val settingsRepository: SettingsRepository,
|
||||||
|
private val userProgramRepository: UserProgramRepository,
|
||||||
private val headsetMonitor: HeadsetMonitor,
|
private val headsetMonitor: HeadsetMonitor,
|
||||||
private val audioEngine: BinauralAudioEngine,
|
private val audioEngine: BinauralAudioEngine,
|
||||||
) : ViewModelProvider.Factory {
|
) : ViewModelProvider.Factory {
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = MainViewModel(settingsRepository, headsetMonitor, audioEngine) as T
|
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
||||||
|
MainViewModel(settingsRepository, userProgramRepository, headsetMonitor, audioEngine) as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.weight
|
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -321,169 +320,167 @@ internal fun TimelineGraph(
|
|||||||
var curveRight = currentCurveRight
|
var curveRight = currentCurveRight
|
||||||
val w = size.width.toFloat()
|
val w = size.width.toFloat()
|
||||||
val h = size.height.toFloat()
|
val h = size.height.toFloat()
|
||||||
val startX = viewport.timeToX(selection.startSec, w)
|
|
||||||
val endX = viewport.timeToX(selection.endSec, w)
|
|
||||||
val hitPx = 32f
|
val hitPx = 32f
|
||||||
|
|
||||||
var activePointerId = down.id
|
var activePointerId = down.id
|
||||||
var lastPos = down.position
|
var lastPos = down.position
|
||||||
var isTransform = false
|
var isTransform = false
|
||||||
var moved = false
|
var moved = false
|
||||||
|
|
||||||
val hits = buildList {
|
val startX = viewport.timeToX(selection.startSec, w)
|
||||||
curveLeft.hitTestPoint(down.position, viewport, w, h)?.let { idx ->
|
val endX = viewport.timeToX(selection.endSec, w)
|
||||||
val point = curveLeft.points[idx]
|
val hits = buildList {
|
||||||
val x = viewport.timeToX(point.tSec, w)
|
curveLeft.hitTestPoint(down.position, viewport, w, h)?.let { idx ->
|
||||||
val y = valueToY(point.value01, h)
|
val point = curveLeft.points[idx]
|
||||||
val dx = down.position.x - x
|
val x = viewport.timeToX(point.tSec, w)
|
||||||
val dy = down.position.y - y
|
val y = valueToY(point.value01, h)
|
||||||
add(Triple(leftId, idx, dx * dx + dy * dy))
|
val dx = down.position.x - x
|
||||||
|
val dy = down.position.y - y
|
||||||
|
add(Triple(leftId, idx, dx * dx + dy * dy))
|
||||||
|
}
|
||||||
|
curveRight.hitTestPoint(down.position, viewport, w, h)?.let { idx ->
|
||||||
|
val point = curveRight.points[idx]
|
||||||
|
val x = viewport.timeToX(point.tSec, w)
|
||||||
|
val y = valueToY(point.value01, h)
|
||||||
|
val dx = down.position.x - x
|
||||||
|
val dy = down.position.y - y
|
||||||
|
add(Triple(rightId, idx, dx * dx + dy * dy))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
curveRight.hitTestPoint(down.position, viewport, w, h)?.let { idx ->
|
|
||||||
val point = curveRight.points[idx]
|
|
||||||
val x = viewport.timeToX(point.tSec, w)
|
|
||||||
val y = valueToY(point.value01, h)
|
|
||||||
val dx = down.position.x - x
|
|
||||||
val dy = down.position.y - y
|
|
||||||
add(Triple(rightId, idx, dx * dx + dy * dy))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val nearestHit = hits.minByOrNull { it.third }
|
val nearestHit = hits.minByOrNull { it.third }
|
||||||
if (nearestHit != null) {
|
if (nearestHit != null) {
|
||||||
draggingCurveId = nearestHit.first
|
draggingCurveId = nearestHit.first
|
||||||
draggingPointIndex = nearestHit.second
|
draggingPointIndex = nearestHit.second
|
||||||
dragMode = HandleDrag.NONE
|
dragMode = HandleDrag.NONE
|
||||||
currentOnTapSideSelect(if (nearestHit.first == leftId) Side.LEFT else Side.RIGHT)
|
currentOnTapSideSelect(if (nearestHit.first == leftId) Side.LEFT else Side.RIGHT)
|
||||||
} else {
|
} else {
|
||||||
draggingPointIndex = null
|
|
||||||
draggingCurveId = null
|
|
||||||
dragMode = when {
|
|
||||||
abs(down.position.x - startX) <= hitPx -> HandleDrag.START
|
|
||||||
abs(down.position.x - endX) <= hitPx -> HandleDrag.END
|
|
||||||
else -> HandleDrag.NONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
val event = awaitPointerEvent(pass = PointerEventPass.Initial)
|
|
||||||
val pressed = event.changes.filter { it.pressed }
|
|
||||||
if (pressed.isEmpty()) break
|
|
||||||
|
|
||||||
if (pressed.size >= 2) {
|
|
||||||
isTransform = true
|
|
||||||
draggingPointIndex = null
|
draggingPointIndex = null
|
||||||
draggingCurveId = null
|
draggingCurveId = null
|
||||||
dragMode = HandleDrag.NONE
|
dragMode = when {
|
||||||
} else if (pressed.size == 1 && isTransform) {
|
abs(down.position.x - startX) <= hitPx -> HandleDrag.START
|
||||||
// Single finger after multi-finger: treat as single-finger drag
|
abs(down.position.x - endX) <= hitPx -> HandleDrag.END
|
||||||
isTransform = false
|
else -> HandleDrag.NONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTransform) {
|
while (true) {
|
||||||
val zoom = event.calculateZoom()
|
val event = awaitPointerEvent(pass = PointerEventPass.Initial)
|
||||||
val pan = event.calculatePan()
|
val pressed = event.changes.filter { it.pressed }
|
||||||
val centroid = event.calculateCentroid(useCurrent = true)
|
if (pressed.isEmpty()) break
|
||||||
if (zoom != 1f || pan.x != 0f || pan.y != 0f) {
|
|
||||||
moved = true
|
if (pressed.size >= 2) {
|
||||||
val newViewport = viewport.applyZoomPan(
|
isTransform = true
|
||||||
zoomChange = zoom,
|
draggingPointIndex = null
|
||||||
panPx = pan.x,
|
draggingCurveId = null
|
||||||
widthPx = size.width.toFloat(),
|
dragMode = HandleDrag.NONE
|
||||||
centroidPx = centroid.x,
|
} else if (pressed.size == 1 && isTransform) {
|
||||||
)
|
isTransform = false
|
||||||
viewport = newViewport
|
lastPos = pressed.first().position
|
||||||
currentOnViewportChanged(newViewport)
|
|
||||||
}
|
}
|
||||||
event.changes.forEach { it.consume() }
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
val primary = pressed.firstOrNull { it.id == activePointerId } ?: pressed.first()
|
if (isTransform) {
|
||||||
activePointerId = primary.id
|
val zoom = event.calculateZoom()
|
||||||
val delta = primary.position - lastPos
|
val pan = event.calculatePan()
|
||||||
lastPos = primary.position
|
val centroid = event.calculateCentroid(useCurrent = true)
|
||||||
|
if (zoom != 1f || pan.x != 0f || pan.y != 0f) {
|
||||||
if (delta.getDistanceSquared() > 0.5f) moved = true
|
moved = true
|
||||||
|
|
||||||
val pointIndex = draggingPointIndex
|
|
||||||
val curveId = draggingCurveId
|
|
||||||
if (pointIndex != null && curveId != null) {
|
|
||||||
val targetCurve = if (curveId == leftId) curveLeft else curveRight
|
|
||||||
val updated = targetCurve.movePointVerticalWithPropagation(pointIndex, primary.position.y, h)
|
|
||||||
if (curveId == leftId) curveLeft = updated else curveRight = updated
|
|
||||||
currentOnUpdateCurve(curveId, updated)
|
|
||||||
onCurveEditedHaptic()
|
|
||||||
primary.consume()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
val t = viewport.xToTimeSec(primary.position.x, w).toInt().coerceAtLeast(0)
|
|
||||||
when (dragMode) {
|
|
||||||
HandleDrag.START -> {
|
|
||||||
val newSelection = TimelineSelection(startSec = min(t, selection.endSec - 60), endSec = selection.endSec)
|
|
||||||
selection = newSelection
|
|
||||||
currentOnSelectionChanged(newSelection)
|
|
||||||
primary.consume()
|
|
||||||
}
|
|
||||||
HandleDrag.END -> {
|
|
||||||
val newSelection = TimelineSelection(startSec = selection.startSec, endSec = max(t, selection.startSec + 60))
|
|
||||||
selection = newSelection
|
|
||||||
currentOnSelectionChanged(newSelection)
|
|
||||||
primary.consume()
|
|
||||||
}
|
|
||||||
HandleDrag.NONE -> {
|
|
||||||
if (delta.x != 0f) {
|
|
||||||
val newViewport = viewport.applyZoomPan(
|
val newViewport = viewport.applyZoomPan(
|
||||||
zoomChange = 1f,
|
zoomChange = zoom,
|
||||||
panPx = delta.x,
|
panPx = pan.x,
|
||||||
widthPx = w,
|
widthPx = w,
|
||||||
centroidPx = w / 2f,
|
centroidPx = centroid.x,
|
||||||
)
|
)
|
||||||
viewport = newViewport
|
viewport = newViewport
|
||||||
currentOnViewportChanged(newViewport)
|
currentOnViewportChanged(newViewport)
|
||||||
|
}
|
||||||
|
event.changes.forEach { it.consume() }
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val primary = pressed.firstOrNull { it.id == activePointerId } ?: pressed.first()
|
||||||
|
activePointerId = primary.id
|
||||||
|
val delta = primary.position - lastPos
|
||||||
|
lastPos = primary.position
|
||||||
|
|
||||||
|
if (delta.getDistanceSquared() > 0.5f) moved = true
|
||||||
|
|
||||||
|
val pointIndex = draggingPointIndex
|
||||||
|
val curveId = draggingCurveId
|
||||||
|
if (pointIndex != null && curveId != null) {
|
||||||
|
val targetCurve = if (curveId == leftId) curveLeft else curveRight
|
||||||
|
val updated = targetCurve.movePointVerticalWithPropagation(pointIndex, primary.position.y, h)
|
||||||
|
if (curveId == leftId) curveLeft = updated else curveRight = updated
|
||||||
|
currentOnUpdateCurve(curveId, updated)
|
||||||
|
onCurveEditedHaptic()
|
||||||
|
primary.consume()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val t = viewport.xToTimeSec(primary.position.x, w).toInt().coerceAtLeast(0)
|
||||||
|
when (dragMode) {
|
||||||
|
HandleDrag.START -> {
|
||||||
|
val newSelection = TimelineSelection(startSec = min(t, selection.endSec - 60), endSec = selection.endSec)
|
||||||
|
selection = newSelection
|
||||||
|
currentOnSelectionChanged(newSelection)
|
||||||
primary.consume()
|
primary.consume()
|
||||||
}
|
}
|
||||||
|
HandleDrag.END -> {
|
||||||
|
val newSelection = TimelineSelection(startSec = selection.startSec, endSec = max(t, selection.startSec + 60))
|
||||||
|
selection = newSelection
|
||||||
|
currentOnSelectionChanged(newSelection)
|
||||||
|
primary.consume()
|
||||||
|
}
|
||||||
|
HandleDrag.NONE -> {
|
||||||
|
if (delta.x != 0f) {
|
||||||
|
val newViewport = viewport.applyZoomPan(
|
||||||
|
zoomChange = 1f,
|
||||||
|
panPx = delta.x,
|
||||||
|
widthPx = w,
|
||||||
|
centroidPx = w / 2f,
|
||||||
|
)
|
||||||
|
viewport = newViewport
|
||||||
|
currentOnViewportChanged(newViewport)
|
||||||
|
primary.consume()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!moved && !isTransform) {
|
if (!moved && !isTransform) {
|
||||||
val side = if (down.position.x < size.width / 2f) Side.LEFT else Side.RIGHT
|
val side = if (down.position.x < size.width / 2f) Side.LEFT else Side.RIGHT
|
||||||
currentOnTapSideSelect(side)
|
currentOnTapSideSelect(side)
|
||||||
|
}
|
||||||
|
draggingPointIndex = null
|
||||||
|
draggingCurveId = null
|
||||||
|
dragMode = HandleDrag.NONE
|
||||||
}
|
}
|
||||||
draggingPointIndex = null
|
|
||||||
draggingCurveId = null
|
|
||||||
dragMode = HandleDrag.NONE
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
) {
|
||||||
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
|
drawGrid()
|
||||||
|
|
||||||
|
val startX = viewport.timeToX(selection.startSec, size.width)
|
||||||
|
val endX = viewport.timeToX(selection.endSec, size.width)
|
||||||
|
drawRect(
|
||||||
|
color = Color.White.copy(alpha = 0.06f),
|
||||||
|
topLeft = Offset(min(startX, endX), 0f),
|
||||||
|
size = androidx.compose.ui.geometry.Size(abs(endX - startX), size.height)
|
||||||
|
)
|
||||||
|
drawLine(Color.White.copy(alpha = 0.35f), Offset(startX, 0f), Offset(startX, size.height), strokeWidth = 3f)
|
||||||
|
drawLine(Color.White.copy(alpha = 0.35f), Offset(endX, 0f), Offset(endX, size.height), strokeWidth = 3f)
|
||||||
|
|
||||||
|
val handleColor = Color.White.copy(alpha = 0.9f)
|
||||||
|
drawCircle(handleColor, radius = 10f, center = Offset(startX, size.height - 14f))
|
||||||
|
drawCircle(handleColor, radius = 10f, center = Offset(endX, size.height - 14f))
|
||||||
|
|
||||||
|
val playX = viewport.timeToX(playheadSec.toInt(), size.width)
|
||||||
|
drawLine(Color(0xFFFFFFFF).copy(alpha = 0.6f), Offset(playX, 0f), Offset(playX, size.height), strokeWidth = 2f)
|
||||||
|
|
||||||
|
drawCurve(curveLeft, selected = selectedLeft, color = leftColor, viewport = viewport)
|
||||||
|
drawCurve(curveRight, selected = selectedRight, color = rightColor, viewport = viewport)
|
||||||
}
|
}
|
||||||
) {
|
|
||||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
|
||||||
drawGrid()
|
|
||||||
|
|
||||||
val startX = viewport.timeToX(selection.startSec, size.width)
|
|
||||||
val endX = viewport.timeToX(selection.endSec, size.width)
|
|
||||||
drawRect(
|
|
||||||
color = Color.White.copy(alpha = 0.06f),
|
|
||||||
topLeft = Offset(min(startX, endX), 0f),
|
|
||||||
size = androidx.compose.ui.geometry.Size(abs(endX - startX), size.height)
|
|
||||||
)
|
|
||||||
drawLine(Color.White.copy(alpha = 0.35f), Offset(startX, 0f), Offset(startX, size.height), strokeWidth = 3f)
|
|
||||||
drawLine(Color.White.copy(alpha = 0.35f), Offset(endX, 0f), Offset(endX, size.height), strokeWidth = 3f)
|
|
||||||
|
|
||||||
val handleColor = Color.White.copy(alpha = 0.9f)
|
|
||||||
drawCircle(handleColor, radius = 10f, center = Offset(startX, size.height - 14f))
|
|
||||||
drawCircle(handleColor, radius = 10f, center = Offset(endX, size.height - 14f))
|
|
||||||
|
|
||||||
val playX = viewport.timeToX(playheadSec.toInt(), size.width)
|
|
||||||
drawLine(Color(0xFFFFFFFF).copy(alpha = 0.6f), Offset(playX, 0f), Offset(playX, size.height), strokeWidth = 2f)
|
|
||||||
|
|
||||||
drawCurve(curveLeft, selected = selectedLeft, color = leftColor, viewport = viewport)
|
|
||||||
drawCurve(curveRight, selected = selectedRight, color = rightColor, viewport = viewport)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.mindmachine.mvp.session
|
package com.mindmachine.mvp.session
|
||||||
|
|
||||||
import com.mindmachine.mvp.domain.SessionPreset
|
import com.mindmachine.mvp.domain.SessionPreset
|
||||||
|
import com.mindmachine.mvp.domain.VisualPattern
|
||||||
|
|
||||||
object TimelineProgramFactory {
|
object TimelineProgramFactory {
|
||||||
fun fromPreset(preset: SessionPreset): TimelineEditorState {
|
fun fromPreset(preset: SessionPreset): TimelineEditorState {
|
||||||
@@ -36,4 +37,45 @@ object TimelineProgramFactory {
|
|||||||
playheadSec = 0f,
|
playheadSec = 0f,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toPreset(
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
timeline: TimelineEditorState,
|
||||||
|
sortOrder: Int,
|
||||||
|
isUserProgram: Boolean,
|
||||||
|
): SessionPreset {
|
||||||
|
val t0 = 0f
|
||||||
|
val flashMs = ParameterMapping.logMap01(
|
||||||
|
timeline.visualLeft.valueAt(t0),
|
||||||
|
ParameterRanges.FLASH_INTERVAL_MS_MIN,
|
||||||
|
ParameterRanges.FLASH_INTERVAL_MS_MAX,
|
||||||
|
).toInt().coerceIn(ParameterRanges.FLASH_INTERVAL_MS_MIN.toInt(), ParameterRanges.FLASH_INTERVAL_MS_MAX.toInt())
|
||||||
|
val carrierHz = ParameterMapping.logMap01(
|
||||||
|
timeline.audioLeft.valueAt(t0),
|
||||||
|
ParameterRanges.CARRIER_HZ_MIN,
|
||||||
|
ParameterRanges.CARRIER_HZ_MAX,
|
||||||
|
)
|
||||||
|
val binauralHz = ParameterMapping.logMap01(
|
||||||
|
timeline.audioRight.valueAt(t0),
|
||||||
|
ParameterRanges.BINAURAL_HZ_MIN,
|
||||||
|
ParameterRanges.BINAURAL_HZ_MAX,
|
||||||
|
)
|
||||||
|
|
||||||
|
return SessionPreset(
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
description = description,
|
||||||
|
defaultDurationSec = timeline.durationSec,
|
||||||
|
visualPatternType = VisualPattern.FLASH,
|
||||||
|
flashIntervalMs = flashMs,
|
||||||
|
intensityPercent = 60,
|
||||||
|
carrierFrequencyHz = carrierHz,
|
||||||
|
binauralDifferenceHz = binauralHz,
|
||||||
|
cautionNote = "Stop if discomfort occurs.",
|
||||||
|
sortOrder = sortOrder,
|
||||||
|
isUserProgram = isUserProgram,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user