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
+5 -1
View File
@@ -142,10 +142,14 @@ elseif (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
list(APPEND SRC_CDROM cdrom.emscripten.c)
list(APPEND SUMMARY_CDROM "Emscripten")
set(ENABLE_CDROM_EMSCRIPTEN 1)
elseif (CMAKE_SYSTEM_NAME STREQUAL "Android")
list(APPEND SRC_CDROM cdrom.android.c)
list(APPEND SUMMARY_CDROM "Android")
set(ENABLE_CDROM_ANDROID 1)
else()
list(APPEND SRC_CDROM cdrom.empty.c)
endif()
if (SDL2MIXER_FOUND)
if (SDL2MIXER_FOUND AND NOT ANDROID)
list(APPEND SRC_CDROM cdrom.mp3.c)
list(APPEND SUMMARY_CDROM "SDL_mixer (wav|mp3|ogg...)")
set(ENABLE_CDROM_MP3 1)
+2 -2
View File
@@ -34,7 +34,7 @@ cd xsystem35-sdl2/android
```
## How to use
1. Create a ZIP file containing all the game files (`*.ALD`) and [configuration files](https://haniwa.technology/games/preparing-a-game-directory.html), and transfer it to your device.
1. Create a ZIP file containing all the game files (`*.ALD`) and BGM files (for example `Track02.mp3`, `Track03.mp3`, ...), and transfer it to your device.
2. Open the app. A list of installed games is displayed. Since nothing has been installed yet, only the "Install from ZIP" button is displayed. Tap it.
3. Select the zip you created in 1.
4. The game starts. Two-finger touch is treated as a right click.
@@ -43,4 +43,4 @@ To uninstall a game, long-tap the title in the game list.
## TODO
- Improve launcher
- MP3 / MIDI BGM support (currently only ogg is supported)
- MIDI BGM support
-4
View File
@@ -27,10 +27,6 @@ FetchContent_Declare(
URL_HASH SHA1=4e62e29bd5628b262b3ffb6d5d29861c76a1500e
)
# SDL_mixer options
option(SUPPORT_OGG "Enable OGG support in SDL_mixer" ON)
# TODO: Enable MP3 and MIDI too
# Compilation of SDL and companion libraries
FetchContent_GetProperties(SDL)
if(NOT sdl_POPULATED)
@@ -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? {
+1
View File
@@ -8,6 +8,7 @@
#cmakedefine CACHE_TOTALSIZE @CACHE_TOTALSIZE@
#cmakedefine ENABLE_CDROM_ANDROID @ENABLE_CDROM_ANDROID@
#cmakedefine ENABLE_CDROM_BSD @ENABLE_CDROM_BSD@
#cmakedefine ENABLE_CDROM_EMSCRIPTEN @ENABLE_CDROM_EMSCRIPTEN@
#cmakedefine ENABLE_CDROM_LINUX @ENABLE_CDROM_LINUX@
+84
View File
@@ -0,0 +1,84 @@
/*
* Copyright (C) 2020 <KichikuouChrome@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
#include <SDL.h>
#include <jni.h>
#include "portab.h"
#include "cdrom.h"
static int cdrom_init(char *);
static int cdrom_exit();
static int cdrom_start(int, int);
static int cdrom_stop();
static int cdrom_getPlayingInfo(cd_time *);
#define cdrom cdrom_android
cdromdevice_t cdrom = {
cdrom_init,
cdrom_exit,
cdrom_start,
cdrom_stop,
cdrom_getPlayingInfo,
NULL,
NULL
};
int cdrom_init(char *name) {
return OK;
}
int cdrom_exit() {
cdrom_stop();
return OK;
}
#define COMMAND_CDROM_START 0x8000
int cdrom_start(int trk, int loop) {
JNIEnv *env = SDL_AndroidGetJNIEnv();
jobject context = SDL_AndroidGetActivity();
jmethodID mid = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, context),
"cddaStart", "(II)V");
(*env)->CallVoidMethod(env, context, mid, trk, loop);
(*env)->DeleteLocalRef(env, context);
return OK;
}
int cdrom_stop() {
JNIEnv *env = SDL_AndroidGetJNIEnv();
jobject context = SDL_AndroidGetActivity();
jmethodID mid = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, context),
"cddaStop", "()V");
(*env)->CallVoidMethod(env, context, mid);
(*env)->DeleteLocalRef(env, context);
return OK;
}
int cdrom_getPlayingInfo (cd_time *info) {
JNIEnv *env = SDL_AndroidGetJNIEnv();
jobject context = SDL_AndroidGetActivity();
jmethodID mid = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, context),
"cddaCurrentPosition", "()I");
int t = (*env)->CallIntMethod(env, context, mid);
(*env)->DeleteLocalRef(env, context);
info->t = t & 0xff;
FRAMES_TO_MSF(t >> 8, &info->m, &info->s, &info->f);
return OK;
}
+6 -2
View File
@@ -44,6 +44,10 @@ extern cdromdevice_t cdrom_bsd;
extern cdromdevice_t cdrom_emscripten;
#define DEV_PLAY_MODE &cdrom_emscripten
#elif defined(ENABLE_CDROM_ANDROID)
extern cdromdevice_t cdrom_android;
#define DEV_PLAY_MODE &cdrom_android
#else
extern cdromdevice_t cdrom_empty;
@@ -73,7 +77,7 @@ static char *dev = CDROM_DEVICE;
失敗 -1
*/
int cd_init(cdromdevice_t *cd) {
#ifdef ENABLE_CDROM_EMSCRIPTEN
#if defined(ENABLE_CDROM_EMSCRIPTEN) || defined(ENABLE_CDROM_ANDROID)
memcpy(cd, DEV_PLAY_MODE, sizeof(cdromdevice_t));
return cd->init(dev);
#else
@@ -102,7 +106,7 @@ int cd_init(cdromdevice_t *cd) {
ret = NG;
}
return ret;
#endif // ENABLE_CDROM_EMSCRIPTEN
#endif // ENABLE_CDROM_EMSCRIPTEN || ENABLE_CDROM_ANDROID
}
void cd_set_devicename(char *name) {