Implement smart B placementfor A-B looping based measure - windows
This commit is contained in:
parent
a628984986
commit
60c11b7ac8
6 changed files with 205 additions and 59 deletions
|
|
@ -17,8 +17,8 @@ expect class FMediaPlayer(filename: String, sharedScreenModel: SharedScreenModel
|
|||
fun seekTo(position: Long)
|
||||
fun seekToGrid(gridIndex: Int)
|
||||
fun setVolume(level: Float)
|
||||
fun setPointA()
|
||||
fun setPointB()
|
||||
fun setPointA(grid: Int)
|
||||
fun setPointB(grid: Int)
|
||||
fun clearLoop()
|
||||
fun setTempo(bpm: Float)
|
||||
fun getCurrentBPM(): Float
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import androidx.compose.ui.window.Popup
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import cafe.adriel.voyager.core.annotation.InternalVoyagerApi
|
||||
import cafe.adriel.voyager.navigator.internal.BackHandler
|
||||
import com.russhwolf.settings.Settings
|
||||
import feufaro.composeapp.generated.resources.Emmentaler
|
||||
import feufaro.composeapp.generated.resources.PTSerif_Bold
|
||||
import feufaro.composeapp.generated.resources.PT_Serif_Bold_Italic
|
||||
|
|
@ -59,6 +60,7 @@ import mg.dot.feufaro.TUODetailDialog
|
|||
import mg.dot.feufaro.data.GridTUOData
|
||||
import mg.dot.feufaro.getPathValidator
|
||||
import mg.dot.feufaro.getPlatform
|
||||
import mg.dot.feufaro.provideSettings
|
||||
import mg.dot.feufaro.ui.rememberFileSaveLauncher
|
||||
import mg.dot.feufaro.viewmodel.MidiMarkers
|
||||
import mg.dot.feufaro.viewmodel.SolfaScreenModel
|
||||
|
|
@ -283,18 +285,19 @@ fun TimeUnitComposable(
|
|||
isCurrentBlock: Boolean = false,
|
||||
parentFocusRequester: FocusRequester? = null,
|
||||
transpositionInterval: Int,
|
||||
hairpinRange: HairpinRange? = null
|
||||
hairpinRange: HairpinRange? = null,
|
||||
isInABrange: Boolean = false
|
||||
) {
|
||||
val col = if (tuo.getNum() % 2 == 0) MaterialTheme.colorScheme.tertiary.copy(alpha = 0.05f) else MaterialTheme.colorScheme.error.copy(alpha = 0.05f)
|
||||
val currentDensity = LocalDensity.current
|
||||
val colorUnderline = MaterialTheme.colorScheme.onSecondary
|
||||
val animatedColor = if (gridActive) {
|
||||
animateColorAsState(
|
||||
val animatedColor = when {
|
||||
gridActive -> animateColorAsState(
|
||||
targetValue = MaterialTheme.colorScheme.secondary.copy(alpha = 1f),
|
||||
animationSpec = tween(durationMillis = 100)
|
||||
).value
|
||||
} else {
|
||||
col
|
||||
isInABrange -> MaterialTheme.colorScheme.tertiary.copy(alpha = 0.25f)
|
||||
else -> col
|
||||
}
|
||||
|
||||
val focusRequesters = remember { List(4) { FocusRequester() } }
|
||||
|
|
@ -636,6 +639,15 @@ fun LazyVerticalGridTUO(
|
|||
|
||||
val platform = getPlatform()
|
||||
val isAndroid = platform.name.startsWith("Android")
|
||||
val loopingState by sharedScreenModel.loopingState.collectAsState()
|
||||
val activeIndex by sharedScreenModel.activeIndex.collectAsState()
|
||||
val loopState by produceState(Triple(-1L, -1L, false), sharedScreenModel.mediaPlayer) {
|
||||
while (true) {
|
||||
value = sharedScreenModel.mediaPlayer?.getLoopState() ?: Triple(-1L, -1L, false)
|
||||
kotlinx.coroutines.delay(100)
|
||||
}
|
||||
}
|
||||
val prefs: Settings = provideSettings()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -999,6 +1011,17 @@ fun LazyVerticalGridTUO(
|
|||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
measureTUOs.forEachIndexed { indexInMeasure, oneTUO ->
|
||||
val globalIndex = (measureIndex * gridColumnCount) + indexInMeasure
|
||||
val pointA = loopingState.first
|
||||
val pointB = loopingState.second
|
||||
|
||||
val isInABrange = if (pointA != -1 && pointB != -1) {
|
||||
val min = minOf(pointA, pointB)
|
||||
val max = maxOf(pointA, pointB)
|
||||
globalIndex in min..max
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
val isActive = (globalIndex == activeRowIndex)
|
||||
val isSelectedByKeyboard = (globalIndex == selectedGridIndex)
|
||||
|
||||
|
|
@ -1084,7 +1107,21 @@ fun LazyVerticalGridTUO(
|
|||
selectedIndex = globalIndex
|
||||
showEditDialog = true
|
||||
} else {
|
||||
sharedScreenModel.seekToGrid(globalIndex)
|
||||
val isAtStart = loopState.first == -1L
|
||||
val isWaitingForB = loopState.first != -1L && !loopState.third
|
||||
|
||||
if (isWaitingForB) {
|
||||
val enableSmartB = prefs.getBoolean("smartBpoint", true)
|
||||
val smartIndex = if (enableSmartB) {
|
||||
sharedScreenModel.calculateSmartPointB(globalIndex, isZeroIndexed = true)
|
||||
} else {
|
||||
globalIndex
|
||||
}
|
||||
sharedScreenModel.mediaPlayer?.setPointB(smartIndex)
|
||||
} else {
|
||||
sharedScreenModel.seekToGrid(globalIndex)
|
||||
sharedScreenModel.updateActiveIndexByIndex(globalIndex)
|
||||
}
|
||||
}
|
||||
},
|
||||
onDoubleTap = { offset ->
|
||||
|
|
@ -1108,7 +1145,8 @@ fun LazyVerticalGridTUO(
|
|||
isCurrentBlock = isSelectedByKeyboard && editMode,
|
||||
parentFocusRequester = focusRequester,
|
||||
transpositionInterval = currentInterval,
|
||||
hairpinRange = hairpinByEndBlock[oneTUO.numBlock]
|
||||
hairpinRange = hairpinByEndBlock[oneTUO.numBlock],
|
||||
isInABrange = isInABrange
|
||||
)
|
||||
|
||||
if (showContextualMenu && selectedIndex == globalIndex) {
|
||||
|
|
|
|||
|
|
@ -368,8 +368,8 @@ fun MidiControlPanel(
|
|||
OutlinedButton(
|
||||
onClick = {
|
||||
when {
|
||||
isAtStart -> mediaPlayer.setPointA()
|
||||
isWaitingForB -> mediaPlayer.setPointB()
|
||||
isAtStart -> mediaPlayer.setPointA(sharedScreenModel.activeIndex.value)
|
||||
isWaitingForB -> mediaPlayer.setPointB(sharedScreenModel.activeIndex.value)
|
||||
isLooping -> {
|
||||
mediaPlayer.clearLoop()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ fun Settings(
|
|||
) {
|
||||
val prefs: Settings = provideSettings()
|
||||
var hasCountIn by remember { mutableStateOf(prefs.getBoolean("isCountIn", true)) }
|
||||
var isSmartPointBLoop by remember { mutableStateOf(prefs.getBoolean("smartBpoint", true)) }
|
||||
var isDynamicEnabled by remember { mutableStateOf(Dynamic.isGloballyEnabled) }
|
||||
var refreshTrigger by remember { mutableStateOf(0) }
|
||||
val isFullScreenEnabled by sharedScreenModel.isFullScreen.collectAsState()
|
||||
|
|
@ -698,46 +699,94 @@ fun Settings(
|
|||
color = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Décompte avant la lecture ",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
Column {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if(!isAndroid) {
|
||||
val countInStat = if (hasCountIn) "Désactiver" else "Activer"
|
||||
Text(
|
||||
text = countInStat,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
Text(
|
||||
text = "Décompte avant la lecture ",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
if (!isAndroid) {
|
||||
val countInStat = if (hasCountIn) "Désactiver" else "Activer"
|
||||
Text(
|
||||
text = countInStat,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = hasCountIn,
|
||||
onCheckedChange = { enabled ->
|
||||
hasCountIn = enabled
|
||||
prefs.putBoolean("isCountIn", enabled)
|
||||
},
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
uncheckedBorderColor = MaterialTheme.colorScheme.outline
|
||||
),
|
||||
modifier = Modifier.size(45.dp)
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = hasCountIn,
|
||||
onCheckedChange = { enabled ->
|
||||
hasCountIn = enabled
|
||||
prefs.putBoolean("isCountIn", enabled)
|
||||
},
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
uncheckedBorderColor = MaterialTheme.colorScheme.outline
|
||||
),
|
||||
modifier = Modifier.size(45.dp)
|
||||
}
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f),
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "A-B lopping intelligent",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
if(!isAndroid) {
|
||||
val stat = if (isSmartPointBLoop) "Désactiver" else "Activer"
|
||||
Text(
|
||||
text = stat,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = isSmartPointBLoop,
|
||||
onCheckedChange = { enabled ->
|
||||
isSmartPointBLoop = enabled
|
||||
prefs.putBoolean("smartBpoint", enabled)
|
||||
},
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
uncheckedBorderColor = MaterialTheme.colorScheme.outline
|
||||
),
|
||||
modifier = Modifier.size(45.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -444,6 +444,36 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
|||
_activeIndex.value = clamped
|
||||
}
|
||||
}
|
||||
|
||||
private val _loopingState = MutableStateFlow(Pair(-1, -1))
|
||||
val loopingState: StateFlow<Pair<Int, Int>> = _loopingState.asStateFlow()
|
||||
|
||||
fun updateLoopingState(a: Int, b: Int) {
|
||||
_loopingState.value = Pair(a, b)
|
||||
}
|
||||
|
||||
fun calculateSmartPointB(rawPointB: Int, isZeroIndexed: Boolean = false): Int {
|
||||
val currentMeasure = _measure.value
|
||||
val numerator = currentMeasure.substringBefore("/").toIntOrNull() ?: 4
|
||||
|
||||
val multiple = when (numerator) {
|
||||
2, 4, 8, 10 -> 4
|
||||
3, 6, 9, 12 -> 3
|
||||
else -> 2
|
||||
}
|
||||
|
||||
val remainder = rawPointB % multiple
|
||||
var finalPointB = if (remainder == 0) {
|
||||
rawPointB
|
||||
} else {
|
||||
rawPointB + (multiple - remainder)
|
||||
}
|
||||
if (isZeroIndexed) {
|
||||
finalPointB -= 1
|
||||
}
|
||||
|
||||
return finalPointB
|
||||
}
|
||||
private val _gridCount = MutableStateFlow(0)
|
||||
val gridCount: StateFlow<Int> = _gridCount.asStateFlow()
|
||||
|
||||
|
|
@ -681,7 +711,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
|||
_isPos.value = false
|
||||
player.play()
|
||||
player.setVolume(_volumeLevel.value)
|
||||
if(currentPos.value == 0f) {
|
||||
if (currentPos.value == 0f && _loopingState.value.first == -1) {
|
||||
player.seekTo(0)
|
||||
}
|
||||
}
|
||||
|
|
@ -749,6 +779,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
|||
setTranspositionInterval(0)
|
||||
setTranspToC(0)
|
||||
setSilentDurBefr(0)
|
||||
updateLoopingState(0, 0)
|
||||
_midiMarkersList.value = emptyList()
|
||||
_tuoTimestamps.value = emptyList()
|
||||
_searchTitle.value = ""
|
||||
|
|
|
|||
|
|
@ -639,7 +639,7 @@ actual class FMediaPlayer actual constructor(
|
|||
|
||||
//Soufflet
|
||||
step.hairPin != null -> {
|
||||
val resolution = sequencer?.sequence?.resolution?.toLong() ?: 480L
|
||||
val resolution = sequencer?.sequence?.resolution?.toLong() ?: 60L
|
||||
val gridDistance = (step.hairPinEndGrid - step.gridIndex).coerceAtLeast(1)
|
||||
val beatDurationMs = (60_000L / targetBpm).toLong()
|
||||
val durationMs = (gridDistance * beatDurationMs).coerceIn(200L, 5000L)
|
||||
|
|
@ -717,8 +717,8 @@ actual class FMediaPlayer actual constructor(
|
|||
syncJob = playerScope.launch(Dispatchers.Default) {
|
||||
while (isActive) {
|
||||
if (sequencer?.isRunning == true) {
|
||||
val resolution = sequencer?.sequence?.resolution?.toLong() ?: 480L
|
||||
val gridIndex = (sequencer!!.tickPosition / resolution).toInt()
|
||||
val gridStep = 60L
|
||||
val gridIndex = (sequencer!!.tickPosition / gridStep).toInt()
|
||||
sharedScreenModel.updateActiveIndexByIndex(gridIndex)
|
||||
}
|
||||
delay(20)
|
||||
|
|
@ -726,8 +726,8 @@ actual class FMediaPlayer actual constructor(
|
|||
}
|
||||
}
|
||||
actual fun seekToGrid(gridIndex: Int) {
|
||||
val resolution = sequencer?.sequence?.resolution?.toDouble() ?: 480.0
|
||||
val tick = (gridIndex.toDouble() * resolution).toLong()
|
||||
val gridStep = 60.0
|
||||
val tick = (gridIndex.toDouble() * gridStep).toLong()
|
||||
sequencer?.tickPosition = tick
|
||||
|
||||
applyBpm()
|
||||
|
|
@ -746,7 +746,17 @@ actual class FMediaPlayer actual constructor(
|
|||
}
|
||||
actual fun play(){
|
||||
if (sequencer?.isOpen == true) {
|
||||
val currentTick = sequencer?.tickPosition ?: 0L
|
||||
var currentTick = sequencer?.tickPosition ?: 0L
|
||||
|
||||
if (isLoopingAB && pointA != -1L) {
|
||||
if (currentTick < pointA || currentTick >= pointB) {
|
||||
val gridStep = 60.0
|
||||
val gridIndexA = (pointA / gridStep).toInt()
|
||||
seekToGrid(gridIndexA)
|
||||
currentTick = sequencer?.tickPosition ?: pointA
|
||||
}
|
||||
}
|
||||
|
||||
if (currentTick == 0L) {
|
||||
resetNavigationFlags()
|
||||
/*targetBpm = initialBpm
|
||||
|
|
@ -754,7 +764,11 @@ actual class FMediaPlayer actual constructor(
|
|||
boundModel?.setBpmFlow(initialBpm)*/
|
||||
}
|
||||
val countInEnable = prefs.getBoolean("isCountIn", true)
|
||||
if (currentTick == 0L && countInEnable) {
|
||||
|
||||
val isStartingFromBeginning = (currentTick == 0L)
|
||||
val isStartingFromLoopA = (isLoopingAB && currentTick == pointA)
|
||||
|
||||
if ((isStartingFromBeginning || isStartingFromLoopA) && countInEnable) {
|
||||
playCountIn()
|
||||
} else {
|
||||
startMidiPlayback()
|
||||
|
|
@ -928,12 +942,24 @@ actual class FMediaPlayer actual constructor(
|
|||
// println("la volume $level")
|
||||
}
|
||||
|
||||
actual fun setPointA() {
|
||||
pointA = sequencer?.tickPosition ?: 0L
|
||||
}
|
||||
actual fun setPointA(grid: Int) {
|
||||
val gridStep = 60.0
|
||||
val tick = (grid.toDouble() * gridStep).toLong()
|
||||
pointA = tick
|
||||
|
||||
val currentB = sharedScreenModel.loopingState.value.second
|
||||
sharedScreenModel.updateLoopingState(grid, currentB)
|
||||
}
|
||||
|
||||
actual fun setPointB(grid: Int) {
|
||||
val gridStep = 60.0
|
||||
val tick = (((grid + 1).toDouble() * gridStep) - 1.0).toLong()
|
||||
pointB = tick.coerceAtLeast(0L)
|
||||
|
||||
val currentA = sharedScreenModel.loopingState.value.first
|
||||
sharedScreenModel.updateLoopingState(currentA, grid)
|
||||
seekToGrid(currentA)
|
||||
|
||||
actual fun setPointB() {
|
||||
pointB = sequencer?.tickPosition ?: 0L
|
||||
if (pointB > pointA && pointA != -1L) {
|
||||
isLoopingAB = true
|
||||
startABMonitor()
|
||||
|
|
@ -942,6 +968,8 @@ actual class FMediaPlayer actual constructor(
|
|||
|
||||
actual fun clearLoop() {
|
||||
isLoopingAB = false
|
||||
abJob?.cancel()
|
||||
sharedScreenModel.updateLoopingState(-1, -1)
|
||||
pointA = -1L
|
||||
pointB = -1L
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue