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(
|
actual class FMediaPlayer actual constructor(
|
||||||
private val filename: String,
|
private val filename: String,
|
||||||
|
private val sharedScreenModel: SharedScreenModel,
|
||||||
private val onFinished: () -> Unit
|
private val onFinished: () -> Unit
|
||||||
): KoinComponent {
|
): KoinComponent {
|
||||||
private data class MidiEvent(
|
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() {
|
actual fun play() {
|
||||||
if (sequence == null) return
|
if (sequence == null) return
|
||||||
|
if(currentTickPos == 0L) {
|
||||||
|
playCountIn()
|
||||||
|
} else {
|
||||||
applyVoiceStates()
|
applyVoiceStates()
|
||||||
startPlaybackLoop()
|
startPlaybackLoop()
|
||||||
println("Android MIDI play — tick=$currentTickPos bpm=$targetBpm")
|
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() {
|
actual fun pause() {
|
||||||
|
countInJob?.cancel()
|
||||||
isRunning = false; isHolding = false
|
isRunning = false; isHolding = false
|
||||||
playJob?.cancel(); allNotesOff()
|
playJob?.cancel(); allNotesOff()
|
||||||
}
|
}
|
||||||
actual fun stop() {
|
actual fun stop() {
|
||||||
|
countInJob?.cancel()
|
||||||
isRunning = false; isHolding = false
|
isRunning = false; isHolding = false
|
||||||
playJob?.cancel(); navigationJob?.cancel()
|
playJob?.cancel(); navigationJob?.cancel()
|
||||||
allNotesOff(); currentTickPos = 0L
|
allNotesOff(); currentTickPos = 0L
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import SharedScreenModel
|
||||||
import mg.dot.feufaro.FileRepository
|
import mg.dot.feufaro.FileRepository
|
||||||
import mg.dot.feufaro.viewmodel.MidiMarkers
|
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 play()
|
||||||
fun pause()
|
fun pause()
|
||||||
fun stop()
|
fun stop()
|
||||||
|
|
|
||||||
|
|
@ -1991,12 +1991,41 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
|
||||||
T.add(newTemplateItem)
|
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) {
|
fun loadU(line: String) {
|
||||||
val measureLong = meta.getOrElse("m") { "4/4" }
|
val measureLong = meta.getOrElse("m") { "4/4" }
|
||||||
val measure = measureLong.split("/")[0].toIntOrNull() ?: 4
|
val measure = measureLong.split("/")[0].toIntOrNull() ?: 4
|
||||||
val uObject = ParseULine(line, measure)
|
val uObject = ParseULine(line, measure)
|
||||||
val parsedULine = uObject.parsed()
|
val parsedULine = uObject.parsed()
|
||||||
loadT(parsedULine)
|
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) {
|
fun loadO(index: Int, line: String) {
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,19 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
||||||
}
|
}
|
||||||
.stateIn(screenModelScope, SharingStarted.Lazily, emptyList())
|
.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 playlistFilename = "playlist.json"
|
||||||
private val _playlistPaths = MutableStateFlow<List<String>>(emptyList())
|
private val _playlistPaths = MutableStateFlow<List<String>>(emptyList())
|
||||||
val playlistItems: StateFlow<List<DrawerItem>> = combine(_playlistPaths, _drawerItems) { paths, allItems ->
|
val playlistItems: StateFlow<List<DrawerItem>> = combine(_playlistPaths, _drawerItems) { paths, allItems ->
|
||||||
|
|
@ -567,7 +580,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
||||||
try {
|
try {
|
||||||
val midiFileName = fileRepository.getFileName(newMidiFile)
|
val midiFileName = fileRepository.getFileName(newMidiFile)
|
||||||
println("Opening xx129 $midiFileName")
|
println("Opening xx129 $midiFileName")
|
||||||
_mediaPlayer = FMediaPlayer(filename = midiFileName, onFinished = {
|
_mediaPlayer = FMediaPlayer(filename = midiFileName, sharedScreenModel = this, onFinished = {
|
||||||
// _isPos.value = true
|
// _isPos.value = true
|
||||||
// _isPlay.value = false
|
// _isPlay.value = false
|
||||||
_currentPos.value = 0f
|
_currentPos.value = 0f
|
||||||
|
|
@ -666,6 +679,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
||||||
_editModeState.value = false
|
_editModeState.value = false
|
||||||
_harmonyView.value = false
|
_harmonyView.value = false
|
||||||
setTranspositionInterval(0)
|
setTranspositionInterval(0)
|
||||||
|
setSilentDurBefr(0)
|
||||||
_midiMarkersList.value = emptyList()
|
_midiMarkersList.value = emptyList()
|
||||||
_tuoTimestamps.value = emptyList()
|
_tuoTimestamps.value = emptyList()
|
||||||
_searchTitle.value = ""
|
_searchTitle.value = ""
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import javax.sound.sampled.FloatControl
|
||||||
|
|
||||||
actual class FMediaPlayer actual constructor(
|
actual class FMediaPlayer actual constructor(
|
||||||
private val filename: String,
|
private val filename: String,
|
||||||
|
private val sharedScreenModel: SharedScreenModel,
|
||||||
private val onFinished: () -> Unit
|
private val onFinished: () -> Unit
|
||||||
) {
|
) {
|
||||||
private var sequencer: Sequencer? = try {
|
private var sequencer: Sequencer? = try {
|
||||||
|
|
@ -636,7 +637,64 @@ actual class FMediaPlayer actual constructor(
|
||||||
applyVoiceStates()
|
applyVoiceStates()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getCountInBeats(measureStr: String): Int {
|
||||||
|
val firstNumeratorChar = measureStr.substringBefore('/').trim()
|
||||||
|
return firstNumeratorChar.toIntOrNull() ?: 4
|
||||||
|
}
|
||||||
actual fun play(){
|
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){
|
if (sequencer?.isOpen == true){
|
||||||
applyBpm()
|
applyBpm()
|
||||||
sequencer?.start()
|
sequencer?.start()
|
||||||
|
|
@ -645,6 +703,7 @@ actual class FMediaPlayer actual constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
actual fun pause(){
|
actual fun pause(){
|
||||||
|
countInJob?.cancel()
|
||||||
sequencer?.stop()
|
sequencer?.stop()
|
||||||
}
|
}
|
||||||
actual fun requestSync(sharedScreenModel: SharedScreenModel) {
|
actual fun requestSync(sharedScreenModel: SharedScreenModel) {
|
||||||
|
|
@ -700,6 +759,7 @@ actual class FMediaPlayer actual constructor(
|
||||||
|
|
||||||
actual fun stop(){
|
actual fun stop(){
|
||||||
// sequencer?.stop()
|
// sequencer?.stop()
|
||||||
|
countInJob?.cancel()
|
||||||
sequencer?.microsecondPosition = 0
|
sequencer?.microsecondPosition = 0
|
||||||
navigationJob?.cancel()
|
navigationJob?.cancel()
|
||||||
resetNavigationFlags()
|
resetNavigationFlags()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue