mirror of
https://github.com/kichikuou/xsystem35-sdl2.git
synced 2026-09-22 22:48:08 +03:00
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:
+5
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -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? {
|
||||
|
||||
@@ -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@
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user