feat: pass beat times to native player
This commit is contained in:
@@ -37,8 +37,14 @@ public final class LibPasada {
|
|||||||
* @param fd open read FD (Java retains ownership; do not close until track changes)
|
* @param fd open read FD (Java retains ownership; do not close until track changes)
|
||||||
* @param offset start offset within the FD (0 for whole file)
|
* @param offset start offset within the FD (0 for whole file)
|
||||||
* @param length byte length from offset ({@code -1} if unknown / to EOF)
|
* @param length byte length from offset ({@code -1} if unknown / to EOF)
|
||||||
|
* @param beatTimesSec beat times in seconds from track start, or {@code null} when unknown
|
||||||
*/
|
*/
|
||||||
public static native void play(int fd, long offset, long length);
|
public static native void play(int fd, long offset, long length, double[] beatTimesSec);
|
||||||
|
|
||||||
|
/** Same as {@link #play(int, long, long, double[])} with no beat annotation. */
|
||||||
|
public static void play(int fd, long offset, long length) {
|
||||||
|
play(fd, offset, length, null);
|
||||||
|
}
|
||||||
|
|
||||||
/** PLAYING → PAUSED (silent output, graph kept alive). */
|
/** PLAYING → PAUSED (silent output, graph kept alive). */
|
||||||
public static native void pause();
|
public static native void pause();
|
||||||
|
|||||||
@@ -16,9 +16,11 @@ import androidx.media.app.NotificationCompat.MediaStyle
|
|||||||
import at.lockstep.player.LockstepApplication
|
import at.lockstep.player.LockstepApplication
|
||||||
import at.lockstep.player.MainActivity
|
import at.lockstep.player.MainActivity
|
||||||
import at.lockstep.player.R
|
import at.lockstep.player.R
|
||||||
|
import at.lockstep.player.data.db.FileMetadataEntity
|
||||||
import at.lockstep.player.playback.engine.ExoPlayerMusicPlayerEngine
|
import at.lockstep.player.playback.engine.ExoPlayerMusicPlayerEngine
|
||||||
import at.lockstep.player.playback.engine.PasadaMusicPlayerEngine
|
import at.lockstep.player.playback.engine.PasadaMusicPlayerEngine
|
||||||
import at.lockstep.player.playback.engine.MusicPlayerEngine
|
import at.lockstep.player.playback.engine.MusicPlayerEngine
|
||||||
|
import at.lockstep.player.util.BeatAnnotationStorage
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -291,32 +293,59 @@ class PlaybackService : Service() {
|
|||||||
|
|
||||||
private fun publishCurrentTrack() {
|
private fun publishCurrentTrack() {
|
||||||
val item = queue.getOrNull(index) ?: return
|
val item = queue.getOrNull(index) ?: return
|
||||||
applyCurrentMediaItem(item)
|
scope.launch {
|
||||||
val durationSec =
|
val beatTimesSec =
|
||||||
(engine?.getDurationMs()?.takeIf { it > 0 }?.div(1000)?.toInt())
|
withContext(Dispatchers.IO) {
|
||||||
?: (item.durationMsHint / 1000).coerceAtLeast(1)
|
loadBeatTimesSecForTrack(item.id)
|
||||||
_uiState.value =
|
}
|
||||||
PlaybackUiState(
|
withContext(Dispatchers.Main) {
|
||||||
title = item.title,
|
val current = queue.getOrNull(index) ?: return@withContext
|
||||||
artist = item.artist,
|
if (current.id != item.id) {
|
||||||
progress = 0f,
|
return@withContext
|
||||||
durationSeconds = durationSec,
|
}
|
||||||
isPlaying = _uiState.value.isPlaying,
|
applyCurrentMediaItem(current, beatTimesSec)
|
||||||
currentTrackId = item.id,
|
val durationSec =
|
||||||
currentQueueIndex = index,
|
(engine?.getDurationMs()?.takeIf { it > 0 }?.div(1000)?.toInt())
|
||||||
currentPlaylistPosition = item.playlistPosition,
|
?: (current.durationMsHint / 1000).coerceAtLeast(1)
|
||||||
queueSize = queue.size,
|
_uiState.value =
|
||||||
)
|
PlaybackUiState(
|
||||||
updateProgressFromEngine()
|
title = current.title,
|
||||||
updateSessionMetadata(item, durationSec)
|
artist = current.artist,
|
||||||
updatePlaybackStateFromEngine()
|
progress = 0f,
|
||||||
refreshForegroundNotification()
|
durationSeconds = durationSec,
|
||||||
|
isPlaying = _uiState.value.isPlaying,
|
||||||
|
currentTrackId = current.id,
|
||||||
|
currentQueueIndex = index,
|
||||||
|
currentPlaylistPosition = current.playlistPosition,
|
||||||
|
queueSize = queue.size,
|
||||||
|
)
|
||||||
|
updateProgressFromEngine()
|
||||||
|
updateSessionMetadata(current, durationSec)
|
||||||
|
updatePlaybackStateFromEngine()
|
||||||
|
refreshForegroundNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyCurrentMediaItem(item: TrackQueueItem) {
|
private suspend fun loadBeatTimesSecForTrack(trackId: String): DoubleArray? {
|
||||||
|
val metadata =
|
||||||
|
app.database.fileMetadataDao().findByTrackIdAndType(
|
||||||
|
trackId,
|
||||||
|
FileMetadataEntity.TYPE_BEATS,
|
||||||
|
) ?: return null
|
||||||
|
if (metadata.fileUri.isBlank()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return BeatAnnotationStorage.readBeatTimesSec(app, metadata.fileUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyCurrentMediaItem(
|
||||||
|
item: TrackQueueItem,
|
||||||
|
beatTimesSec: DoubleArray?,
|
||||||
|
) {
|
||||||
val uri = item.localUri ?: return
|
val uri = item.localUri ?: return
|
||||||
val e = ensureEngine()
|
val e = ensureEngine()
|
||||||
e.prepareTrack(uri)
|
e.prepareTrack(uri, beatTimesSec)
|
||||||
if (_uiState.value.isPlaying) {
|
if (_uiState.value.isPlaying) {
|
||||||
e.play()
|
e.play()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ class ExoPlayerMusicPlayerEngine(
|
|||||||
.also { it.addListener(playerListener) }
|
.also { it.addListener(playerListener) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun prepareTrack(uri: Uri) {
|
override fun prepareTrack(
|
||||||
|
uri: Uri,
|
||||||
|
beatTimesSec: DoubleArray?,
|
||||||
|
) {
|
||||||
val p = requirePlayer()
|
val p = requirePlayer()
|
||||||
p.setMediaItem(MediaItem.fromUri(uri))
|
p.setMediaItem(MediaItem.fromUri(uri))
|
||||||
p.prepare()
|
p.prepare()
|
||||||
|
|||||||
@@ -14,7 +14,13 @@ interface MusicPlayerEngine {
|
|||||||
* Load a track from a local URI. Asynchronous — playback starts after prepare when [play] is
|
* Load a track from a local URI. Asynchronous — playback starts after prepare when [play] is
|
||||||
* called (or immediately if [playWhenReady] is set via [play]).
|
* called (or immediately if [playWhenReady] is set via [play]).
|
||||||
*/
|
*/
|
||||||
fun prepareTrack(uri: Uri)
|
/**
|
||||||
|
* @param beatTimesSec optional beat times in seconds from [TYPE_BEATS] metadata; passed to libpasada on play.
|
||||||
|
*/
|
||||||
|
fun prepareTrack(
|
||||||
|
uri: Uri,
|
||||||
|
beatTimesSec: DoubleArray? = null,
|
||||||
|
)
|
||||||
|
|
||||||
fun play()
|
fun play()
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class PasadaMusicPlayerEngine(
|
|||||||
private var trackFd: Int? = null
|
private var trackFd: Int? = null
|
||||||
private var trackOffset: Long = 0L
|
private var trackOffset: Long = 0L
|
||||||
private var trackLength: Long = -1L
|
private var trackLength: Long = -1L
|
||||||
|
private var pendingBeatTimesSec: DoubleArray? = null
|
||||||
|
|
||||||
/** FD is open but [LibPasada.play] has not been called yet for this track. */
|
/** FD is open but [LibPasada.play] has not been called yet for this track. */
|
||||||
private var pendingStart = false
|
private var pendingStart = false
|
||||||
@@ -73,10 +74,14 @@ class PasadaMusicPlayerEngine(
|
|||||||
sessionInitialized = true
|
sessionInitialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun prepareTrack(uri: Uri) {
|
override fun prepareTrack(
|
||||||
|
uri: Uri,
|
||||||
|
beatTimesSec: DoubleArray?,
|
||||||
|
) {
|
||||||
closeOpenTrack()
|
closeOpenTrack()
|
||||||
try {
|
try {
|
||||||
openTrack(uri)
|
openTrack(uri)
|
||||||
|
pendingBeatTimesSec = beatTimesSec
|
||||||
pendingStart = true
|
pendingStart = true
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
closeOpenTrack()
|
closeOpenTrack()
|
||||||
@@ -146,9 +151,14 @@ class PasadaMusicPlayerEngine(
|
|||||||
|
|
||||||
private fun startPendingTrack() {
|
private fun startPendingTrack() {
|
||||||
val fd = trackFd ?: return
|
val fd = trackFd ?: return
|
||||||
Log.i(TAG, "LibPasada.play(fd=$fd, offset=$trackOffset, length=$trackLength) ...")
|
val beats = pendingBeatTimesSec
|
||||||
LibPasada.play(fd, trackOffset, trackLength)
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
"LibPasada.play(fd=$fd, offset=$trackOffset, length=$trackLength, beats=${beats?.size ?: 0}) ...",
|
||||||
|
)
|
||||||
|
LibPasada.play(fd, trackOffset, trackLength, beats)
|
||||||
Log.i(TAG, "LibPasada.play() done.")
|
Log.i(TAG, "LibPasada.play() done.")
|
||||||
|
pendingBeatTimesSec = null
|
||||||
pendingStart = false
|
pendingStart = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,6 +192,7 @@ class PasadaMusicPlayerEngine(
|
|||||||
trackFd = null
|
trackFd = null
|
||||||
trackOffset = 0L
|
trackOffset = 0L
|
||||||
trackLength = -1L
|
trackLength = -1L
|
||||||
|
pendingBeatTimesSec = null
|
||||||
pendingStart = false
|
pendingStart = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,25 @@ object BeatAnnotationStorage {
|
|||||||
return RunDataStorage.writeOrReplacePublicJsonFile(context, relativePath, fileName, jsonString)
|
return RunDataStorage.writeOrReplacePublicJsonFile(context, relativePath, fileName, jsonString)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Reads [beatTimesSec] from a beats or annotation JSON file; {@code null} if missing or unreadable. */
|
||||||
|
fun readBeatTimesSec(
|
||||||
|
context: Context,
|
||||||
|
fileUri: String,
|
||||||
|
): DoubleArray? {
|
||||||
|
if (fileUri.isBlank()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
context.contentResolver.openInputStream(Uri.parse(fileUri))?.use { stream ->
|
||||||
|
val json = JSONObject(stream.bufferedReader().readText())
|
||||||
|
val arr = json.optJSONArray("beatTimesSec") ?: return null
|
||||||
|
DoubleArray(arr.length()) { i -> arr.getDouble(i) }
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun buildAnnotationJson(
|
fun buildAnnotationJson(
|
||||||
contentId: String,
|
contentId: String,
|
||||||
title: String,
|
title: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user