Android: Add support for PCM sound

Now android/mako.cpp depends on SDL_mixer, and CD-DA is also played with
SDL_mixer.
This commit is contained in:
kichikuou
2023-03-18 13:49:36 +09:00
parent 236eb0b581
commit 431be235d6
7 changed files with 166 additions and 138 deletions
+17
View File
@@ -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)
@@ -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<String> {
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
@@ -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"),
+1 -1
View File
@@ -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)
+129 -48
View File
@@ -1,6 +1,8 @@
#include <memory>
#include <SDL.h>
#include <SDL_mixer.h>
#include <android/log.h>
#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<MakoYmfm> fm;
void audio_callback(void*, Uint8* stream, int len) {
void FMHook(void*, Uint8* stream, int len) {
SDL_LockMutex(fm_mutex);
fm->Process(reinterpret_cast<int16_t*>(stream), len/ 4);
fm->Process(reinterpret_cast<int16_t*>(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<MakoYmfm>(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<MAKOMidi>(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_) {
+15 -13
View File
@@ -29,7 +29,7 @@ Mix_Chunk *mix_chunk;
SDL_mutex* fm_mutex;
std::unique_ptr<MakoYmfm> fm;
void FMHook(void *udata, Uint8 *stream, int len) {
void FMHook(void*, Uint8* stream, int len) {
SDL_LockMutex(fm_mutex);
fm->Process(reinterpret_cast<int16_t*>(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<MakoYmfm>(SAMPLE_RATE, data, true);
Mix_HookMusic(&FMHook, this);
SDL_UnlockMutex(fm_mutex);
Mix_HookMusic(&FMHook, this);
} else {
auto midi = std::make_unique<MAKOMidi>(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;
}
+1 -1
View File
@@ -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<const char*> playlist;