Add metronome count-in before play midi file on desktop & android
This commit is contained in:
parent
ca2e10b9b2
commit
491f502daa
5 changed files with 160 additions and 5 deletions
|
|
@ -11,6 +11,7 @@ import java.io.RandomAccessFile
|
|||
|
||||
actual class FMediaPlayer actual constructor(
|
||||
private val filename: String,
|
||||
private val sharedScreenModel: SharedScreenModel,
|
||||
private val onFinished: () -> Unit
|
||||
): KoinComponent {
|
||||
private data class MidiEvent(
|
||||
|
|
@ -643,17 +644,68 @@ actual class FMediaPlayer actual constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private var countInJob: Job? = null
|
||||
|
||||
private fun getCountInBeats(measureStr: String): Int {
|
||||
val firstNumeratorChar = measureStr.substringBefore('/').trim()
|
||||
return firstNumeratorChar.toIntOrNull() ?: 4
|
||||
}
|
||||
actual fun play() {
|
||||
if (sequence == null) return
|
||||
applyVoiceStates()
|
||||
startPlaybackLoop()
|
||||
println("Android MIDI play — tick=$currentTickPos bpm=$targetBpm")
|
||||
if(currentTickPos == 0L) {
|
||||
playCountIn()
|
||||
} else {
|
||||
applyVoiceStates()
|
||||
startPlaybackLoop()
|
||||
println("Android MIDI play — tick=$currentTickPos bpm=$targetBpm")
|
||||
}
|
||||
}
|
||||
|
||||
private fun playCountIn() {
|
||||
countInJob?.cancel()
|
||||
countInJob = playerScope.launch(Dispatchers.Default) {
|
||||
val bpm = targetBpm
|
||||
val beatDurationMs = (60_000L / bpm).toLong()
|
||||
|
||||
val noteEvent = 42 // 39:Hand clap
|
||||
val velocity = 100
|
||||
val originalMeasure = sharedScreenModel.measureInt.value
|
||||
val nbSilent = sharedScreenModel.silentDurationBefore.value
|
||||
|
||||
val adjustedMeasure = when {
|
||||
originalMeasure == 6 -> 3
|
||||
originalMeasure % 2 != 0 && originalMeasure > 3 -> 3
|
||||
originalMeasure % 2 == 0 && originalMeasure > 4 -> 4
|
||||
else -> originalMeasure
|
||||
}
|
||||
val anacrouseBeats = nbSilent / 4
|
||||
val totalBeats = if (anacrouseBeats > 5) anacrouseBeats else adjustedMeasure + anacrouseBeats
|
||||
// println("Android Décompte total : $totalBeats bips ($adjustedMeasure de préparation + $anacrouseBeats d'anacrouse) à $bpm BPM")
|
||||
|
||||
val noteOnEvent = byteArrayOf(0x99.toByte(), noteEvent.toByte(), velocity.toByte())
|
||||
val noteOffEvent = byteArrayOf(0x89.toByte(), noteEvent.toByte(), 0.toByte())
|
||||
|
||||
for (i in 1..totalBeats) {
|
||||
midiDriver.write(noteOnEvent)
|
||||
delay(100)
|
||||
midiDriver.write(noteOffEvent)
|
||||
|
||||
delay((beatDurationMs - 100).coerceAtLeast(0L))
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
applyVoiceStates()
|
||||
startPlaybackLoop()
|
||||
}
|
||||
}
|
||||
}
|
||||
actual fun pause() {
|
||||
countInJob?.cancel()
|
||||
isRunning = false; isHolding = false
|
||||
playJob?.cancel(); allNotesOff()
|
||||
}
|
||||
actual fun stop() {
|
||||
countInJob?.cancel()
|
||||
isRunning = false; isHolding = false
|
||||
playJob?.cancel(); navigationJob?.cancel()
|
||||
allNotesOff(); currentTickPos = 0L
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import SharedScreenModel
|
|||
import mg.dot.feufaro.FileRepository
|
||||
import mg.dot.feufaro.viewmodel.MidiMarkers
|
||||
|
||||
expect class FMediaPlayer(filename: String, onFinished: () -> Unit) {
|
||||
expect class FMediaPlayer(filename: String, sharedScreenModel: SharedScreenModel, onFinished: () -> Unit) {
|
||||
fun play()
|
||||
fun pause()
|
||||
fun stop()
|
||||
|
|
|
|||
|
|
@ -1991,12 +1991,41 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
|||
T.add(newTemplateItem)
|
||||
}
|
||||
|
||||
private fun decodeDuration(duration: String): Int {
|
||||
return when (duration) {
|
||||
"0" -> 0
|
||||
"1" -> 1
|
||||
"2" -> 2
|
||||
"3" -> 3
|
||||
"4" -> 4
|
||||
"6" -> 6
|
||||
"8" -> 8
|
||||
"A" -> 10
|
||||
"B" -> 11
|
||||
"C" -> 12
|
||||
"E" -> 14
|
||||
"G" -> 16
|
||||
"K" -> 20
|
||||
"O" -> 24
|
||||
"S" -> 28
|
||||
"W" -> 32
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
fun loadU(line: String) {
|
||||
val measureLong = meta.getOrElse("m") { "4/4" }
|
||||
val measure = measureLong.split("/")[0].toIntOrNull() ?: 4
|
||||
val uObject = ParseULine(line, measure)
|
||||
val parsedULine = uObject.parsed()
|
||||
loadT(parsedULine)
|
||||
|
||||
val regex = Regex("""z([^:]):""")
|
||||
val match = regex.find(line)
|
||||
val silentDuration = match?.groupValues?.get(1) ?: "0"
|
||||
|
||||
sharedScreenModel.setMeasureInt(measure)
|
||||
sharedScreenModel.setSilentDurBefr(decodeDuration(silentDuration))
|
||||
// println(silentDuration)
|
||||
}
|
||||
|
||||
fun loadO(index: Int, line: String) {
|
||||
|
|
|
|||
|
|
@ -88,6 +88,19 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
|||
}
|
||||
.stateIn(screenModelScope, SharingStarted.Lazily, emptyList())
|
||||
|
||||
|
||||
private val _measureInt = MutableStateFlow<Int>(4)
|
||||
val measureInt: StateFlow<Int> = _measureInt.asStateFlow()
|
||||
fun setMeasureInt(measure: Int) {
|
||||
_measureInt.value = measure
|
||||
}
|
||||
|
||||
private val _silentDurationBefore = MutableStateFlow<Int>(0)
|
||||
val silentDurationBefore: StateFlow<Int> = _silentDurationBefore.asStateFlow()
|
||||
fun setSilentDurBefr(silent: Int) {
|
||||
_silentDurationBefore.value = silent
|
||||
}
|
||||
|
||||
private val playlistFilename = "playlist.json"
|
||||
private val _playlistPaths = MutableStateFlow<List<String>>(emptyList())
|
||||
val playlistItems: StateFlow<List<DrawerItem>> = combine(_playlistPaths, _drawerItems) { paths, allItems ->
|
||||
|
|
@ -567,7 +580,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
|||
try {
|
||||
val midiFileName = fileRepository.getFileName(newMidiFile)
|
||||
println("Opening xx129 $midiFileName")
|
||||
_mediaPlayer = FMediaPlayer(filename = midiFileName, onFinished = {
|
||||
_mediaPlayer = FMediaPlayer(filename = midiFileName, sharedScreenModel = this, onFinished = {
|
||||
// _isPos.value = true
|
||||
// _isPlay.value = false
|
||||
_currentPos.value = 0f
|
||||
|
|
@ -666,6 +679,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
|||
_editModeState.value = false
|
||||
_harmonyView.value = false
|
||||
setTranspositionInterval(0)
|
||||
setSilentDurBefr(0)
|
||||
_midiMarkersList.value = emptyList()
|
||||
_tuoTimestamps.value = emptyList()
|
||||
_searchTitle.value = ""
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import javax.sound.sampled.FloatControl
|
|||
|
||||
actual class FMediaPlayer actual constructor(
|
||||
private val filename: String,
|
||||
private val sharedScreenModel: SharedScreenModel,
|
||||
private val onFinished: () -> Unit
|
||||
) {
|
||||
private var sequencer: Sequencer? = try {
|
||||
|
|
@ -636,7 +637,64 @@ actual class FMediaPlayer actual constructor(
|
|||
applyVoiceStates()
|
||||
}
|
||||
|
||||
private fun getCountInBeats(measureStr: String): Int {
|
||||
val firstNumeratorChar = measureStr.substringBefore('/').trim()
|
||||
return firstNumeratorChar.toIntOrNull() ?: 4
|
||||
}
|
||||
actual fun play(){
|
||||
if (sequencer?.isOpen == true) {
|
||||
val currentTick = sequencer?.tickPosition ?: 0L
|
||||
if (currentTick == 0L) {
|
||||
playCountIn()
|
||||
} else {
|
||||
startMidiPlayback()
|
||||
}
|
||||
}
|
||||
}
|
||||
private var countInJob: Job? = null
|
||||
|
||||
private fun playCountIn() {
|
||||
val nbMeasure = sharedScreenModel.measureInt.value
|
||||
val nbSilent = sharedScreenModel.silentDurationBefore.value
|
||||
countInJob?.cancel()
|
||||
countInJob = playerScope.launch(Dispatchers.Default) {
|
||||
if (sequencer?.isOpen == true && synthetizer?.isOpen == true) {
|
||||
val bpm = targetBpm
|
||||
val beatDurationMs = (60_000L / bpm).toLong()
|
||||
|
||||
val metronomeChannel = synthetizer?.channels?.getOrNull(9)
|
||||
val noteEvent = 42
|
||||
val velocity = 100
|
||||
|
||||
val originalMeasure = sharedScreenModel.measureInt.value
|
||||
val nbSilent = sharedScreenModel.silentDurationBefore.value
|
||||
|
||||
val adjustedMeasure = when {
|
||||
originalMeasure == 6 -> 3
|
||||
originalMeasure % 2 != 0 && originalMeasure > 3 -> 3
|
||||
originalMeasure % 2 == 0 && originalMeasure > 4 -> 4
|
||||
else -> originalMeasure
|
||||
}
|
||||
|
||||
val anacrouseBeats = nbSilent / 4
|
||||
val totalBeats = if(anacrouseBeats > 5) anacrouseBeats else adjustedMeasure + anacrouseBeats
|
||||
// println("Décompte total : $totalBeats bips ($adjustedMeasure de préparation + $anacrouseBeats d'anacrouse [$nbSilent / $4 ]) à $bpm BPM")
|
||||
|
||||
for (i in 1..totalBeats) {
|
||||
metronomeChannel?.noteOn(noteEvent, velocity)
|
||||
delay(100)
|
||||
metronomeChannel?.noteOff(noteEvent)
|
||||
|
||||
delay((beatDurationMs - 100).coerceAtLeast(0L))
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
startMidiPlayback()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun startMidiPlayback() {
|
||||
if (sequencer?.isOpen == true){
|
||||
applyBpm()
|
||||
sequencer?.start()
|
||||
|
|
@ -645,6 +703,7 @@ actual class FMediaPlayer actual constructor(
|
|||
}
|
||||
}
|
||||
actual fun pause(){
|
||||
countInJob?.cancel()
|
||||
sequencer?.stop()
|
||||
}
|
||||
actual fun requestSync(sharedScreenModel: SharedScreenModel) {
|
||||
|
|
@ -700,6 +759,7 @@ actual class FMediaPlayer actual constructor(
|
|||
|
||||
actual fun stop(){
|
||||
// sequencer?.stop()
|
||||
countInJob?.cancel()
|
||||
sequencer?.microsecondPosition = 0
|
||||
navigationJob?.cancel()
|
||||
resetNavigationFlags()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue