diff --git a/CMakeLists.txt b/CMakeLists.txt index 6736451..51a4a69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -161,8 +161,11 @@ set(CDROM_DEVICE "/dev/cdrom" CACHE STRING "CDROM Device Name") if (EMSCRIPTEN) list(APPEND SRC_MIDI midi.emscripten.c) list(APPEND SUMMARY_MIDI "Emscripten") +elseif (ANDROID) + list(APPEND SRC_MIDI midi.android.c) + list(APPEND SUMMARY_MIDI "Android") else() - if (HAVE_SIGACTION AND NOT ANDROID) + if (HAVE_SIGACTION) list(APPEND SRC_MIDI midi.rawmidi.c midifile.c) list(APPEND SUMMARY_MIDI "raw") set(MIDI_DEVICE "/dev/midi" CACHE STRING "midi device") diff --git a/android/README.md b/android/README.md index 89c2086..75313a3 100644 --- a/android/README.md +++ b/android/README.md @@ -40,7 +40,3 @@ cd xsystem35-sdl2/android 4. The game starts. Two-finger touch is treated as a right click. To uninstall a game, long-tap the title in the game list. - -## TODO -- Improve launcher -- MIDI BGM support 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 c73ac8e..03032fa 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 @@ -19,6 +19,7 @@ package io.github.kichikuou.xsystem35 import android.media.MediaPlayer import android.os.Bundle +import android.util.Base64 import android.util.Log import org.libsdl.app.SDLActivity import java.io.File @@ -35,21 +36,24 @@ class GameActivity : SDLActivity() { const val EXTRA_PLAYLIST_FILE = "PLAYLIST_FILE" } - private lateinit var player: BGMPlayer + private lateinit var cdda: CddaPlayer + private val midi = MidiPlayer() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - player = BGMPlayer(intent.getStringExtra(EXTRA_PLAYLIST_FILE)) + cdda = CddaPlayer(intent.getStringExtra(EXTRA_PLAYLIST_FILE)) } override fun onStop() { super.onStop() - player.onStop() + cdda.onActivityStop() + midi.onActivityStop() } override fun onResume() { super.onResume() - player.onResume() + cdda.onActivityResume() + midi.onActivityResume() } override fun getLibraries(): Array { @@ -69,12 +73,15 @@ class GameActivity : SDLActivity() { } // 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() + @Suppress("unused") fun cddaStart(track: Int, loop: Int) = cdda.start(track, loop) + @Suppress("unused") fun cddaStop() = cdda.stop() + @Suppress("unused") fun cddaCurrentPosition() = cdda.currentPosition() + @Suppress("unused") fun midiStart(buf: ByteArray, loop: Int) = midi.start(buf, loop) + @Suppress("unused") fun midiStop() = midi.stop() + @Suppress("unused") fun midiCurrentPosition() = midi.currentPosition() } -private class BGMPlayer(playlistPath: String?) { +private class CddaPlayer(playlistPath: String?) { private val playlist = playlistPath?.let { try { File(it).readLines() @@ -87,7 +94,7 @@ private class BGMPlayer(playlistPath: String?) { private val player = MediaPlayer() private var playerPaused = false - fun cddaStart(track: Int, loop: Int) { + fun start(track: Int, loop: Int) { val f = playlist.elementAtOrNull(track) if (f.isNullOrEmpty()) { Log.w("cddaStart", "No playlist entry for track $track") @@ -109,29 +116,79 @@ private class BGMPlayer(playlistPath: String?) { } } - fun cddaStop() { - if (currentTrack > 0 && player.isPlaying) + fun stop() { + if (currentTrack > 0 && player.isPlaying) { player.stop() + currentTrack = 0 + } } - fun cddaCurrentPosition(): Int { + fun currentPosition(): Int { if (currentTrack == 0) return 0 val frames = player.currentPosition * 75 / 1000 return currentTrack or (frames shl 8) } - fun onStop() { + fun onActivityStop() { if (currentTrack > 0 && player.isPlaying) { player.pause() playerPaused = true } } - fun onResume() { + fun onActivityResume() { if (playerPaused) { player.start() playerPaused = false } } -} \ No newline at end of file +} + +private class MidiPlayer() { + private val player = MediaPlayer() + private var playing = false + private var playerPaused = false + + fun start(buf: ByteArray, loop: Int) { + val url = "data:audio/midi;base64," + Base64.encodeToString(buf, Base64.DEFAULT) + try { + player.apply { + reset() + setDataSource(url) + isLooping = loop == 0 + prepare() + start() + } + playing = true + } catch (e: IOException) { + Log.e("midiStart", "Cannot play midi", e) + player.reset() + } + } + + fun stop() { + if (playing && player.isPlaying) { + player.stop() + playing = false + } + } + + fun currentPosition(): Int { + return if (playing) player.currentPosition else 0 + } + + fun onActivityStop() { + if (playing && player.isPlaying) { + player.pause() + playerPaused = true + } + } + + fun onActivityResume() { + if (playerPaused) { + player.start() + playerPaused = false + } + } +} diff --git a/src/midi.android.c b/src/midi.android.c new file mode 100644 index 0000000..c91e639 --- /dev/null +++ b/src/midi.android.c @@ -0,0 +1,155 @@ +/* + * 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 "midi.h" + +static int midi_initilize(char *pname, int subdev); +static int midi_exit(); +static int midi_start(int no, int loop, char *data, int datalen); +static int midi_stop(); +static int midi_pause(void); +static int midi_unpause(void); +static int midi_get_playing_info(midiplaystate *st); +static int midi_getflag(int mode, int index); +static int midi_setflag(int mode, int index, int val); +static int midi_setvol(int vol); +static int midi_getvol(); +static int midi_fadestart(int time, int volume, int stop); +static boolean midi_fading(); + +#define midi midi_android +mididevice_t midi = { + midi_initilize, + midi_exit, + midi_start, + midi_stop, + midi_pause, + midi_unpause, + midi_get_playing_info, + midi_getflag, + midi_setflag, + midi_setvol, + midi_getvol, + midi_fadestart, + midi_fading +}; + +static int midino; + +static int midi_initilize(char *pname, int subdev) { + return OK; +} + +static int midi_exit() { + midi_stop(); + return OK; +} + +static int midi_start(int no, int loop, char *data, int datalen) { + if (midino == no) + return OK; + + JNIEnv *env = SDL_AndroidGetJNIEnv(); + + jbyteArray array = (*env)->NewByteArray(env, datalen); + if (!array) + return NG; + jbyte *buf = (*env)->GetByteArrayElements(env, array, NULL); + memcpy(buf, data, datalen); + (*env)->ReleaseByteArrayElements(env, array, buf, 0); + + jobject context = SDL_AndroidGetActivity(); + jmethodID mid = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, context), + "midiStart", "([BI)V"); + (*env)->CallVoidMethod(env, context, mid, array, loop); + (*env)->DeleteLocalRef(env, array); + (*env)->DeleteLocalRef(env, context); + + midino = no; + return OK; +} + +static int midi_stop() { + JNIEnv *env = SDL_AndroidGetJNIEnv(); + jobject context = SDL_AndroidGetActivity(); + jmethodID mid = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, context), + "midiStop", "()V"); + (*env)->CallVoidMethod(env, context, mid); + (*env)->DeleteLocalRef(env, context); + + midino = 0; + return OK; +} + +static int midi_pause(void) { + return NG; // FIXME +} + +static int midi_unpause(void) { + return NG; // FIXME +} + +static int midi_get_playing_info(midiplaystate *st) { + st->in_play = FALSE; + st->play_no = 0; + st->loc_ms = 0; + if (midino == 0) + return OK; + + JNIEnv *env = SDL_AndroidGetJNIEnv(); + jobject context = SDL_AndroidGetActivity(); + jmethodID mid = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, context), + "midiCurrentPosition", "()I"); + int pos = (*env)->CallIntMethod(env, context, mid); + (*env)->DeleteLocalRef(env, context); + + if (pos >= 0) { + st->in_play = TRUE; + st->play_no = midino; + st->loc_ms = pos; + } + return OK; +} + +static int midi_getflag(int mode, int index) { + return 0; +} + +static int midi_setflag(int mode, int index, int val) { + return NG; +} + +static int midi_setvol(int vol) { + return NG; // FIXME +} + +static int midi_getvol() { + return 100; // FIXME +} + +static int midi_fadestart(int time, int volume, int stop) { + return NG; // FIXME +} + +static boolean midi_fading() { + return FALSE; // FIXME +} diff --git a/src/midi.c b/src/midi.c index 34e894f..43847c1 100644 --- a/src/midi.c +++ b/src/midi.c @@ -37,6 +37,10 @@ static int subdev = -1; extern mididevice_t midi_emscripten; #endif +#ifdef __ANDROID__ +extern mididevice_t midi_android; +#endif + #ifdef ENABLE_MIDI_SDLMIXER extern mididevice_t midi_sdlmixer; #endif @@ -61,6 +65,10 @@ int midi_init(mididevice_t *midi) { ret = midi_emscripten.init(NULL, 0); memcpy(midi, &midi_emscripten, sizeof(mididevice_t)); #endif +#ifdef __ANDROID__ + ret = midi_android.init(NULL, 0); + memcpy(midi, &midi_android, sizeof(mididevice_t)); +#endif #ifdef ENABLE_MIDI_SDLMIXER ret = midi_sdlmixer.init(NULL, 0); memcpy(midi, &midi_sdlmixer, sizeof(mididevice_t));