Android: Use android.media.MediaPlayer for BGM playback

Playlist is written to <gamedir>/playlist.txt during installation
("cdrom_device" in .xsys35rc is not used).
Files that match /(\d+)\.(wav|mp3|ogg)$/ are recognized as BGM files
(digits represent a track number). Note that this convention is the same
as Kichikuou on Web.
This commit is contained in:
kichikuou
2020-01-19 22:18:04 +09:00
parent 0fab2405f8
commit 8f10145fc4
8 changed files with 212 additions and 10 deletions
@@ -17,16 +17,39 @@
*/
package io.github.kichikuou.xsystem35
import android.media.MediaPlayer
import android.os.Bundle
import android.util.Log
import org.libsdl.app.SDLActivity
import java.io.File
import java.io.IOException
// Intent for this activity must have two extras:
// - EXTRA_GAME_ROOT (string): A path to the game installation.
// - EXTRA_TITLE_FILE (string): A file to which the game title will be written.
// - EXTRA_PLAYLIST_FILE (string): A path to the BGM playlist file.
class GameActivity : SDLActivity() {
companion object {
const val EXTRA_GAME_ROOT = "GAME_ROOT"
const val EXTRA_TITLE_FILE = "TITLE_FILE"
const val EXTRA_PLAYLIST_FILE = "PLAYLIST_FILE"
}
private lateinit var player: BGMPlayer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
player = BGMPlayer(intent.getStringExtra(EXTRA_PLAYLIST_FILE))
}
override fun onStop() {
super.onStop()
player.onStop()
}
override fun onResume() {
super.onResume()
player.onResume()
}
override fun getLibraries(): Array<String> {
@@ -44,4 +67,71 @@ class GameActivity : SDLActivity() {
return
intent.getStringExtra(EXTRA_TITLE_FILE)?.let { File(it).writeText(str) }
}
// These functions are called in the SDL thread by JNI.
@Suppress("unused") fun cddaStart(track: Int, loop: Int) = player.cddaStart(track, loop)
@Suppress("unused") fun cddaStop() = player.cddaStop()
@Suppress("unused") fun cddaCurrentPosition(): Int = player.cddaCurrentPosition()
}
private class BGMPlayer(playlistPath: String?) {
private val playlist = playlistPath?.let {
try {
File(it).readLines()
} catch (e: IOException) {
Log.e("loadPlaylist", "Cannot load $playlistPath", e)
null
}
} ?: emptyList()
private var currentTrack = 0
private val player = MediaPlayer()
private var playerPaused = false
fun cddaStart(track: Int, loop: Int) {
val f = playlist.elementAtOrNull(track)
if (f.isNullOrEmpty()) {
Log.w("cddaStart", "No playlist entry for track $track")
return
}
Log.v("cddaStart", f)
try {
player.apply {
reset()
setDataSource(f)
isLooping = loop == 0
prepare()
start()
}
currentTrack = track
} catch (e: IOException) {
Log.e("cddaStart", "Cannot play $f", e)
player.reset()
}
}
fun cddaStop() {
if (currentTrack > 0 && player.isPlaying)
player.stop()
}
fun cddaCurrentPosition(): Int {
if (currentTrack == 0)
return 0
val frames = player.currentPosition * 75 / 1000
return currentTrack or (frames shl 8)
}
fun onStop() {
if (currentTrack > 0 && player.isPlaying) {
player.pause()
playerPaused = true
}
}
fun onResume() {
if (playerPaused) {
player.start()
playerPaused = false
}
}
}
@@ -117,6 +117,7 @@ class LauncherActivity : ListActivity(), AdapterView.OnItemLongClickListener {
i.setClass(applicationContext, GameActivity::class.java)
i.putExtra(GameActivity.EXTRA_GAME_ROOT, gameRoot.path)
i.putExtra(GameActivity.EXTRA_TITLE_FILE, File(path, GameManager.TITLE_FILE).path)
i.putExtra(GameActivity.EXTRA_PLAYLIST_FILE, File(path, GameManager.PLAYLIST_FILE).path)
startActivity(i)
}
@@ -151,6 +152,7 @@ class ProgressDialogFragment : DialogFragment() {
private class GameManager(private val rootDir: File) {
companion object {
const val TITLE_FILE = "title.txt"
const val PLAYLIST_FILE = "playlist.txt"
}
data class Entry(val path: File, val title: String)
@@ -193,6 +195,7 @@ private class GameManager(private val rootDir: File) {
private fun extractFiles(input: InputStream, outDir: File, handler: Handler) {
try {
val playlistWriter = PlaylistWriter()
val zip = if (Build.VERSION.SDK_INT >= 24) {
ZipInputStream(input.buffered(), Charset.forName("Shift_JIS"))
} else {
@@ -200,7 +203,7 @@ private class GameManager(private val rootDir: File) {
}
while (true) {
val zipEntry = zip.nextEntry ?: break
Log.d("extractFiles", zipEntry.name)
Log.v("extractFiles", zipEntry.name)
val path = File(outDir, zipEntry.name)
if (zipEntry.isDirectory)
continue
@@ -209,8 +212,10 @@ private class GameManager(private val rootDir: File) {
val output = FileOutputStream(path).buffered()
zip.copyTo(output)
output.close()
playlistWriter.maybeAdd(path.path)
}
zip.close()
playlistWriter.write(outDir)
handler.sendMessage(handler.obtainMessage(SUCCESS, outDir))
} catch (e: UTFDataFormatException) {
// Attempted to read Shift_JIS zip in Android < 7
@@ -220,6 +225,24 @@ private class GameManager(private val rootDir: File) {
handler.sendMessage(handler.obtainMessage(FAILURE, R.string.zip_extraction_error))
}
}
private class PlaylistWriter {
private val audioRegex = """.*?(\d+)\.(wav|mp3|ogg)""".toRegex(RegexOption.IGNORE_CASE)
private val audioFiles: Array<String?> = arrayOfNulls(100)
fun maybeAdd(path: String) {
audioRegex.matchEntire(path)?.let {
val track = it.groupValues[1].toInt()
if (track < audioFiles.size)
audioFiles[track] = path
}
}
fun write(outDir: File) {
val text = audioFiles.joinToString("\n") { it ?: "" }.trimEnd('\n')
File(outDir, PLAYLIST_FILE).writeText(text)
}
}
}
private fun findGameRoot(path: File): File? {