From 431be235d6eedc805b5a4a692a9663fd705709bc Mon Sep 17 00:00:00 2001 From: kichikuou Date: Sat, 18 Mar 2023 13:27:46 +0900 Subject: [PATCH] Android: Add support for PCM sound Now android/mako.cpp depends on SDL_mixer, and CD-DA is also played with SDL_mixer. --- android/app/jni/CMakeLists.txt | 17 ++ .../github/kichikuou/system3/GameActivity.kt | 77 +------- .../kichikuou/system3/LicensesMenuActivity.kt | 1 + src/CMakeLists.txt | 2 +- src/android/mako.cpp | 177 +++++++++++++----- src/linux/mako.cpp | 28 +-- src/sys/mako.h | 2 +- 7 files changed, 166 insertions(+), 138 deletions(-) diff --git a/android/app/jni/CMakeLists.txt b/android/app/jni/CMakeLists.txt index 0b8948b..2d04794 100644 --- a/android/app/jni/CMakeLists.txt +++ b/android/app/jni/CMakeLists.txt @@ -14,6 +14,11 @@ FetchContent_Declare( URL https://github.com/libsdl-org/SDL_ttf/releases/download/release-2.20.1/SDL2_ttf-2.20.1.tar.gz URL_HASH SHA1=f774ff407915ae4afa3d03e5a9395283d9090f28 ) +FetchContent_Declare( + SDL_mixer + URL https://github.com/libsdl-org/SDL_mixer/releases/download/release-2.6.2/SDL2_mixer-2.6.2.tar.gz + URL_HASH SHA1=036fc7839a6b8dc1af3dbfed4e2531224bbacdd7 +) # Compilation of SDL and companion libraries FetchContent_GetProperties(SDL) @@ -29,6 +34,17 @@ if(NOT sdl_ttf_POPULATED) set(SDL2TTF_VENDORED ON CACHE BOOL "Use vendored third-party libraries" FORCE) add_subdirectory(${sdl_ttf_SOURCE_DIR} ${sdl_ttf_BINARY_DIR}) endif() +FetchContent_GetProperties(SDL_mixer) +if(NOT sdl_mixer_POPULATED) + FetchContent_Populate(SDL_mixer) + set(SDL2MIXER_OPUS OFF CACHE BOOL "Enable Opus music" FORCE) + set(SDL2MIXER_FLAC OFF CACHE BOOL "Enable FLAC music" FORCE) + set(SDL2MIXER_MOD OFF CACHE BOOL "Support loading MOD music" FORCE) + set(SDL2MIXER_MIDI OFF CACHE BOOL "Enable MIDI music" FORCE) + set(SDL2MIXER_SAMPLES OFF CACHE BOOL "Build the SDL2_mixer sample program(s)" FORCE) + set(SDL2MIXER_INSTALL OFF CACHE BOOL "Enable SDL2_mixer install target" FORCE) + add_subdirectory(${sdl_mixer_SOURCE_DIR} ${sdl_mixer_BINARY_DIR}) +endif() # The main CMakeLists.txt of system3 add_subdirectory(${PROJECT_ROOT_DIR}/src src) @@ -43,3 +59,4 @@ file(COPY_FILE ${sdl_SOURCE_DIR}/LICENSE.txt ${ASSETS_DIR}/licenses/SDL) file(COPY_FILE ${sdl_ttf_SOURCE_DIR}/LICENSE.txt ${ASSETS_DIR}/licenses/SDL_ttf) file(COPY_FILE ${sdl_ttf_SOURCE_DIR}/external/freetype/docs/GPLv2.TXT ${ASSETS_DIR}/licenses/freetype) file(COPY_FILE ${sdl_ttf_SOURCE_DIR}/external/harfbuzz/COPYING ${ASSETS_DIR}/licenses/harfbuzz) +file(COPY_FILE ${sdl_mixer_SOURCE_DIR}/LICENSE.txt ${ASSETS_DIR}/licenses/SDL_mixer) diff --git a/android/app/src/main/java/io/github/kichikuou/system3/GameActivity.kt b/android/app/src/main/java/io/github/kichikuou/system3/GameActivity.kt index c90c489..991d024 100644 --- a/android/app/src/main/java/io/github/kichikuou/system3/GameActivity.kt +++ b/android/app/src/main/java/io/github/kichikuou/system3/GameActivity.kt @@ -44,7 +44,6 @@ class GameActivity : SDLActivity() { } private lateinit var gameRoot: File - private lateinit var cdda: CddaPlayer private lateinit var prefs: SharedPreferences private val midi = MidiPlayer() private var useFM = true @@ -52,7 +51,6 @@ class GameActivity : SDLActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) gameRoot = File(intent.getStringExtra(EXTRA_GAME_ROOT)!!) - cdda = CddaPlayer(File(gameRoot, Launcher.PLAYLIST_FILE)) prefs = getSharedPreferences("system3", Context.MODE_PRIVATE) useFM = prefs.getBoolean(PREF_USE_FM, useFM) registerForContextMenu(mLayout) @@ -60,13 +58,11 @@ class GameActivity : SDLActivity() { override fun onStop() { super.onStop() - cdda.onActivityStop() midi.onActivityStop() } override fun onResume() { super.onResume() - cdda.onActivityResume() midi.onActivityResume() } @@ -77,7 +73,8 @@ class GameActivity : SDLActivity() { override fun getArguments(): Array { val args = arrayListOf( "-gamedir", intent.getStringExtra(EXTRA_GAME_ROOT)!!, - "-savedir", intent.getStringExtra(EXTRA_SAVE_DIR)!! + "/@") + "-savedir", intent.getStringExtra(EXTRA_SAVE_DIR)!! + "/@", + "-playlist", Launcher.PLAYLIST_FILE) if (useFM) args.add("-fm") return args.toTypedArray() @@ -157,9 +154,6 @@ class GameActivity : SDLActivity() { private external fun selectSynthesizer(use_fm: Boolean) // The functions below are called in the SDL thread by JNI. - @Suppress("unused") fun cddaStart(track: Int, loop: Boolean) = cdda.start(track, loop) - @Suppress("unused") fun cddaStop() = cdda.stop() - @Suppress("unused") fun cddaCurrentPosition() = cdda.currentPosition() @Suppress("unused") fun midiStart(path: String, loop: Boolean) = midi.start(path, loop) @Suppress("unused") fun midiStop() = midi.stop() @Suppress("unused") fun midiCurrentPosition() = midi.currentPosition() @@ -183,73 +177,6 @@ class GameActivity : SDLActivity() { } } -private class CddaPlayer(private val playlistPath: File) { - private val playlist = - try { - playlistPath.readLines() - } catch (e: IOException) { - Log.e("loadPlaylist", "Cannot load $playlistPath", e) - emptyList() - } - private var currentTrack = 0 - private val player = MediaPlayer() - private var playerPaused = false - - init { - player.setOnCompletionListener { currentTrack = 0 } - } - - fun start(track: Int, loop: Boolean) { - val f = playlist.elementAtOrNull(track - 1) - if (f.isNullOrEmpty()) { - Log.w("cddaStart", "No playlist entry for track $track") - return - } - Log.v("cddaStart", "$f Loop:$loop") - try { - player.apply { - reset() - setDataSource(File(playlistPath.parent, f).path) - isLooping = loop - prepare() - start() - } - currentTrack = track - } catch (e: IOException) { - Log.e("cddaStart", "Cannot play $f", e) - player.reset() - } - } - - fun stop() { - if (currentTrack > 0 && player.isPlaying) { - player.stop() - currentTrack = 0 - } - } - - fun currentPosition(): Int { - if (currentTrack == 0) - return 0 - val frames = player.currentPosition * 75 / 1000 - return currentTrack or (frames shl 8) - } - - fun onActivityStop() { - if (currentTrack > 0 && player.isPlaying) { - player.pause() - playerPaused = true - } - } - - fun onActivityResume() { - if (playerPaused) { - player.start() - playerPaused = false - } - } -} - private class MidiPlayer { private val player = MediaPlayer() private var playing = false diff --git a/android/app/src/main/java/io/github/kichikuou/system3/LicensesMenuActivity.kt b/android/app/src/main/java/io/github/kichikuou/system3/LicensesMenuActivity.kt index 6d16a00..b00e6a5 100644 --- a/android/app/src/main/java/io/github/kichikuou/system3/LicensesMenuActivity.kt +++ b/android/app/src/main/java/io/github/kichikuou/system3/LicensesMenuActivity.kt @@ -12,6 +12,7 @@ class LicensesMenuActivity : Activity() { Entry("system3-sdl2", "system3", "https://github.com/kichikuou/system3-sdl2"), Entry("SDL", "SDL", "https://www.libsdl.org/"), Entry("SDL_ttf", "SDL_ttf", "https://github.com/libsdl-org/SDL_ttf"), + Entry("SDL_mixer", "SDL_mixer", "https://github.com/libsdl-org/SDL_mixer"), Entry("FreeType", "freetype", "https://freetype.org/"), Entry("HarfBuzz", "harfbuzz", "https://harfbuzz.github.io/"), Entry("ymfm", "ymfm", "https://github.com/aaronsgiles/ymfm"), diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5351856..96ac118 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -32,7 +32,7 @@ if (ANDROID) android/mako.cpp sys/mako_midi.cpp ) - target_link_libraries(system3 PRIVATE SDL2 SDL2_ttf ymfm) + target_link_libraries(system3 PRIVATE SDL2 SDL2_ttf SDL2_mixer ymfm) elseif (EMSCRIPTEN) add_executable(system3) diff --git a/src/android/mako.cpp b/src/android/mako.cpp index 1573944..7cc0895 100644 --- a/src/android/mako.cpp +++ b/src/android/mako.cpp @@ -1,6 +1,8 @@ #include #include +#include #include + #include "../config.h" #include "jnihelper.h" #include "dri.h" @@ -13,12 +15,14 @@ namespace { const int SAMPLE_RATE = 44100; MAKO *g_mako; +Mix_Music *mix_music; +Mix_Chunk *mix_chunk; SDL_mutex* fm_mutex; std::unique_ptr fm; -void audio_callback(void*, Uint8* stream, int len) { +void FMHook(void*, Uint8* stream, int len) { SDL_LockMutex(fm_mutex); - fm->Process(reinterpret_cast(stream), len/ 4); + fm->Process(reinterpret_cast(stream), len / 4); SDL_UnlockMutex(fm_mutex); } @@ -31,32 +35,47 @@ MAKO::MAKO(NACT* parent, const Config& config) : nact(parent) { g_mako = this; + + load_playlist(config.playlist.c_str()); + strcpy(amus, "AMUS.DAT"); + strcpy(amse, "AMSE.DAT"); // unused for (int i = 1; i <= 99; i++) cd_track[i] = 0; - SDL_InitSubSystem(SDL_INIT_AUDIO); - SDL_AudioSpec fmt; - SDL_zero(fmt); - fmt.freq = SAMPLE_RATE; - fmt.format = AUDIO_S16; - fmt.channels = 2; - fmt.samples = 4096; - fmt.callback = &audio_callback; - if (SDL_OpenAudio(&fmt, NULL) < 0) { - WARNING("SDL_OpenAudio: %s", SDL_GetError()); - use_fm = false; - } - fm_mutex = SDL_CreateMutex(); + const int mix_init_flags = MIX_INIT_MP3 | MIX_INIT_OGG; + if (Mix_Init(mix_init_flags) != mix_init_flags) + WARNING("Mix_Init(0x%x) failed", mix_init_flags); + if (Mix_OpenAudio(SAMPLE_RATE, AUDIO_S16LSB, 2, 4096) < 0) + WARNING("Mix_OpenAudio failed: %s", Mix_GetError()); } -MAKO::~MAKO() { - SDL_LockMutex(fm_mutex); - SDL_CloseAudio(); - SDL_UnlockMutex(fm_mutex); - SDL_DestroyMutex(fm_mutex); - fm_mutex = nullptr; - SDL_QuitSubSystem(SDL_INIT_AUDIO); +MAKO::~MAKO() +{ + stop_pcm(); + Mix_CloseAudio(); + Mix_Quit(); +} + +bool MAKO::load_playlist(const char* path) +{ + FILE* fp = fopen(path, "r"); + if (!fp) { + WARNING("Cannot open %s", path); + return false; + } + char buf[256]; + while (fgets(buf, sizeof(buf) - 1, fp)) { + for (char *p = buf; *p; p++) { + if (*p == '\\') + *p = '/'; + else if (*p == '\r' || *p == '\n') + *p = '\0'; + } + playlist.push_back(buf[0] ? strdup(buf) : NULL); + } + fclose(fp); + return true; } void MAKO::play_music(int page) @@ -66,26 +85,42 @@ void MAKO::play_music(int page) stop_music(); - JNILocalFrame jni(16); - if (!jni.env()) - return; - int track = page < 100 ? cd_track[page] : 0; if (track) { - jmethodID mid = jni.GetMethodID("cddaStart", "(IZ)V"); - jni.env()->CallVoidMethod(jni.context(), mid, track + 1, next_loop ? 0 : 1); + if (track < playlist.size() && playlist[track]) { + // Mix_LoadMUS uses SDL_RWFromFile which requires absolute path on Android + char path[PATH_MAX]; + if (!realpath(playlist[track], path)) + return; + mix_music = Mix_LoadMUS(path); + if (!mix_music) { + WARNING("Mix_LoadMUS failed: %s: %s", playlist[track], Mix_GetError()); + return; + } + if (Mix_PlayMusic(mix_music, next_loop ? next_loop : -1) != 0) { + WARNING("Mix_PlayMusic failed: %s", Mix_GetError()); + Mix_FreeMusic(mix_music); + return; + } + } } else if (use_fm) { DRI dri; int size; uint8* data = dri.load(amus, page, &size); if (!data) return; + if (!fm_mutex) + fm_mutex = SDL_CreateMutex(); SDL_LockMutex(fm_mutex); fm = std::make_unique(SAMPLE_RATE, data, true); SDL_UnlockMutex(fm_mutex); - SDL_PauseAudio(0); + Mix_HookMusic(&FMHook, this); } else { + JNILocalFrame jni(16); + if (!jni.env()) + return; + auto midi = std::make_unique(nact, amus); if (!midi->load_mml(page)) { WARNING("load_mml(%d) failed", page); @@ -122,19 +157,19 @@ void MAKO::stop_music() if (!current_music) return; + if (mix_music) { + Mix_FreeMusic(mix_music); + mix_music = NULL; + } + JNILocalFrame jni(16); if (jni.env()) { - if (current_music < 100 && cd_track[current_music]) { - jmethodID cdda = jni.GetMethodID("cddaStop", "()V"); - jni.env()->CallVoidMethod(jni.context(), cdda); - } else { - jmethodID mid = jni.GetMethodID("midiStop", "()V"); - jni.env()->CallVoidMethod(jni.context(), mid); - } + jmethodID mid = jni.GetMethodID("midiStop", "()V"); + jni.env()->CallVoidMethod(jni.context(), mid); } if (fm) { - SDL_PauseAudio(1); + Mix_HookMusic(NULL, NULL); SDL_LockMutex(fm_mutex); fm = nullptr; SDL_UnlockMutex(fm_mutex); @@ -146,6 +181,8 @@ bool MAKO::check_music() { if (!current_music) return false; + if (mix_music) + return Mix_PlayingMusic(); if (fm) { int mark, loop; SDL_LockMutex(fm_mutex); @@ -155,39 +192,83 @@ bool MAKO::check_music() } JNILocalFrame jni(16); - if (!jni.env()) - return false; - if (current_music < 100 && cd_track[current_music]) { - jmethodID cdda = jni.GetMethodID("cddaCurrentPosition", "()I"); - return jni.env()->CallIntMethod(jni.context(), cdda) != 0; - } else { + if (jni.env()) { jmethodID mid = jni.GetMethodID("midiCurrentPosition", "()I"); return jni.env()->CallIntMethod(jni.context(), mid) != 0; } + return false; } void MAKO::get_mark(int* mark, int* loop) { - SDL_LockMutex(fm_mutex); if (fm) { + SDL_LockMutex(fm_mutex); fm->get_mark(mark, loop); + SDL_UnlockMutex(fm_mutex); } else { *mark = 0; *loop = 0; } - SDL_UnlockMutex(fm_mutex); } void MAKO::play_pcm(int page, bool loop) { - WARNING("not implemented"); + static char header[44] = { + 'R' , 'I' , 'F' , 'F' , 0x00, 0x00, 0x00, 0x00, 'W' , 'A' , 'V' , 'E' , 'f' , 'm' , 't' , ' ' , + 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x40, 0x1f, 0x00, 0x00, 0x40, 0x1f, 0x00, 0x00, + 0x01, 0x00, 0x08, 0x00, 'd' , 'a' , 't' , 'a' , 0x00, 0x00, 0x00, 0x00 + }; + + stop_pcm(); + + uint8* buffer = NULL; + int size; + DRI* dri = new DRI(); + + if((buffer = dri->load("AWAV.DAT", page, &size)) != NULL) { + // WAV形式 (Only You) + mix_chunk = Mix_LoadWAV_RW(SDL_RWFromConstMem(buffer, size), 1 /* freesrc */); + free(buffer); + Mix_PlayChannel(-1, mix_chunk, loop ? -1 : 0); + } else if((buffer = dri->load("AMSE.DAT", page, &size)) != NULL) { + // AMSE形式 (乙女戦記) + int total = (size - 12) * 2 + 0x24; + int samples = (size - 12) * 2; + + uint8* wav = (uint8*)malloc(total + 8); + memcpy(wav, header, 44); + wav[ 4] = (total >> 0) & 0xff; + wav[ 5] = (total >> 8) & 0xff; + wav[ 6] = (total >> 16) & 0xff; + wav[ 7] = (total >> 24) & 0xff; + wav[40] = (samples >> 0) & 0xff; + wav[41] = (samples >> 8) & 0xff; + wav[42] = (samples >> 16) & 0xff; + wav[43] = (samples >> 24) & 0xff; + for(int i = 12, p = 44; i < size; i++) { + wav[p++] = buffer[i] & 0xf0; + wav[p++] = (buffer[i] & 0x0f) << 4; + } + free(buffer); + + mix_chunk = Mix_LoadWAV_RW(SDL_RWFromConstMem(wav, total + 8), 1 /* freesrc */); + free(wav); + Mix_PlayChannel(-1, mix_chunk, loop ? -1 : 0); + } } -void MAKO::stop_pcm() {} +void MAKO::stop_pcm() +{ + Mix_HaltChannel(-1); + if (mix_chunk) { + Mix_FreeChunk(mix_chunk); + mix_chunk = NULL; + } +} bool MAKO::check_pcm() { - return false; + return Mix_Playing(-1) != 0; } void MAKO::select_synthesizer(bool use_fm_) { diff --git a/src/linux/mako.cpp b/src/linux/mako.cpp index 82d353a..a20b57e 100644 --- a/src/linux/mako.cpp +++ b/src/linux/mako.cpp @@ -29,7 +29,7 @@ Mix_Chunk *mix_chunk; SDL_mutex* fm_mutex; std::unique_ptr fm; -void FMHook(void *udata, Uint8 *stream, int len) { +void FMHook(void*, Uint8* stream, int len) { SDL_LockMutex(fm_mutex); fm->Process(reinterpret_cast(stream), len / 4); SDL_UnlockMutex(fm_mutex); @@ -48,12 +48,11 @@ MAKO::MAKO(NACT* parent, const Config& config) : if (!config.playlist.empty() && load_playlist(config.playlist.c_str())) mix_init_flags |= MIX_INIT_MP3 | MIX_INIT_OGG; - strcpy_s(amus, 16, "AMUS.DAT"); - strcpy_s(amse, 16, "AMSE.DAT"); // 実際には使わない - - for(int i = 1; i <= 99; i++) { + strcpy(amus, "AMUS.DAT"); + strcpy(amse, "AMSE.DAT"); // unused + for (int i = 1; i <= 99; i++) cd_track[i] = 0; - } + if (Mix_Init(mix_init_flags) != mix_init_flags) WARNING("Mix_Init(0x%x) failed", mix_init_flags); if (Mix_OpenAudio(SAMPLE_RATE, AUDIO_S16LSB, 2, 4096) < 0) @@ -90,13 +89,13 @@ bool MAKO::load_playlist(const char* path) void MAKO::play_music(int page) { - if(current_music == page) + if (current_music == page) return; stop_music(); int track = page < 100 ? cd_track[page] : 0; - if(track) { + if (track) { if (track < playlist.size() && playlist[track]) { mix_music = Mix_LoadMUS(playlist[track]); if (!mix_music) { @@ -120,8 +119,8 @@ void MAKO::play_music(int page) SDL_LockMutex(fm_mutex); fm = std::make_unique(SAMPLE_RATE, data, true); - Mix_HookMusic(&FMHook, this); SDL_UnlockMutex(fm_mutex); + Mix_HookMusic(&FMHook, this); } else { auto midi = std::make_unique(nact, amus); if (midi->load_mml(page)) { @@ -140,7 +139,9 @@ void MAKO::play_music(int page) } } } + current_music = page; + next_loop = 0; } void MAKO::stop_music() @@ -161,6 +162,8 @@ void MAKO::stop_music() bool MAKO::check_music() { + if (mix_music) + return Mix_PlayingMusic(); if (fm) { int mark, loop; SDL_LockMutex(fm_mutex); @@ -168,19 +171,19 @@ bool MAKO::check_music() SDL_UnlockMutex(fm_mutex); return !loop; } - return Mix_PlayingMusic(); + return false; } void MAKO::get_mark(int* mark, int* loop) { - SDL_LockMutex(fm_mutex); if (fm) { + SDL_LockMutex(fm_mutex); fm->get_mark(mark, loop); + SDL_UnlockMutex(fm_mutex); } else { *mark = 0; *loop = 0; } - SDL_UnlockMutex(fm_mutex); } void MAKO::play_pcm(int page, bool loop) @@ -240,7 +243,6 @@ void MAKO::stop_pcm() bool MAKO::check_pcm() { - // 再生中でtrue return Mix_Playing(-1) != 0; } diff --git a/src/sys/mako.h b/src/sys/mako.h index 9da8ef3..b63edac 100644 --- a/src/sys/mako.h +++ b/src/sys/mako.h @@ -60,7 +60,7 @@ private: NACT* nact; int cd_track[100]; // Z -#if !defined(__EMSCRIPTEN__) && !defined(__ANDROID__) +#if !defined(__EMSCRIPTEN__) bool load_playlist(const char* path); std::vector playlist;