From 8f10145fc4a736dcc3900bfbe51832efbf5de649 Mon Sep 17 00:00:00 2001 From: kichikuou Date: Sun, 19 Jan 2020 14:50:29 +0900 Subject: [PATCH] Android: Use android.media.MediaPlayer for BGM playback Playlist is written to /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. --- CMakeLists.txt | 6 +- android/README.md | 4 +- android/app/jni/CMakeLists.txt | 4 - .../kichikuou/xsystem35/GameActivity.kt | 90 +++++++++++++++++++ .../kichikuou/xsystem35/LauncherActivity.kt | 25 +++++- config.h.in | 1 + src/cdrom.android.c | 84 +++++++++++++++++ src/cdrom.c | 8 +- 8 files changed, 212 insertions(+), 10 deletions(-) create mode 100644 src/cdrom.android.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 315eb34..6736451 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/android/README.md b/android/README.md index de0ab05..89c2086 100644 --- a/android/README.md +++ b/android/README.md @@ -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 diff --git a/android/app/jni/CMakeLists.txt b/android/app/jni/CMakeLists.txt index a0a12fa..16383f0 100644 --- a/android/app/jni/CMakeLists.txt +++ b/android/app/jni/CMakeLists.txt @@ -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) diff --git a/android/app/src/main/java/io/github/kichikuou/xsystem35/GameActivity.kt b/android/app/src/main/java/io/github/kichikuou/xsystem35/GameActivity.kt index a32d8e7..c73ac8e 100644 --- a/android/app/src/main/java/io/github/kichikuou/xsystem35/GameActivity.kt +++ b/android/app/src/main/java/io/github/kichikuou/xsystem35/GameActivity.kt @@ -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 { @@ -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 + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/io/github/kichikuou/xsystem35/LauncherActivity.kt b/android/app/src/main/java/io/github/kichikuou/xsystem35/LauncherActivity.kt index 3c18951..76b0f91 100644 --- a/android/app/src/main/java/io/github/kichikuou/xsystem35/LauncherActivity.kt +++ b/android/app/src/main/java/io/github/kichikuou/xsystem35/LauncherActivity.kt @@ -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 = 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? { diff --git a/config.h.in b/config.h.in index c20a20e..b7bc127 100644 --- a/config.h.in +++ b/config.h.in @@ -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@ diff --git a/src/cdrom.android.c b/src/cdrom.android.c new file mode 100644 index 0000000..069d73f --- /dev/null +++ b/src/cdrom.android.c @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2020 + * + * 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 +#include +#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; +} diff --git a/src/cdrom.c b/src/cdrom.c index 58c51ea..da1fa8f 100644 --- a/src/cdrom.c +++ b/src/cdrom.c @@ -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) {