mirror of
https://github.com/kichikuou/system3-sdl2.git
synced 2026-09-22 22:58:12 +03:00
Replace SDL_mixer with dr_mp3/stb_vorbis + SDL audio
- MP3/OGG files are now decoded with dr_mp3 and stb_vorbis via the new MakoMusic backend. - Merge src/win/mako.cpp into src/generic/mako.cpp: Windows now uses the same SDL audio path. - A single SDL2 audio device mixes FM, BGM and PCM in its callback with SDL_MixAudioFormat. - PCM plays through SDL_AudioStream. - dr_mp3 and stb_vorbis are vendored under deps/ and included from mako_music.cpp. Warnings for that translation unit are silenced.
This commit is contained in:
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Install Deps
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev librtmidi-dev nlohmann-json3-dev
|
||||
sudo apt install libsdl2-dev libsdl2-ttf-dev librtmidi-dev nlohmann-json3-dev
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
|
||||
+11
-4
@@ -44,7 +44,7 @@ if (ANDROID)
|
||||
src/generic/mako.cpp
|
||||
)
|
||||
find_library(ndk_log log)
|
||||
target_link_libraries(system3 PRIVATE SDL2 SDL2_ttf SDL2_mixer ymfm ${ndk_log})
|
||||
target_link_libraries(system3 PRIVATE SDL2 SDL2_ttf ymfm ${ndk_log})
|
||||
|
||||
elseif (EMSCRIPTEN)
|
||||
add_executable(system3)
|
||||
@@ -163,7 +163,7 @@ else() # NOT (ANDROID OR EMSCRIPTEN)
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
target_sources(system3 PRIVATE
|
||||
src/win/nact_win.cpp
|
||||
src/win/mako.cpp
|
||||
src/generic/mako.cpp
|
||||
src/win/resource.rc
|
||||
)
|
||||
target_compile_definitions(system3 PRIVATE NOMINMAX)
|
||||
@@ -172,12 +172,10 @@ else() # NOT (ANDROID OR EMSCRIPTEN)
|
||||
target_link_options(system3 PRIVATE -static)
|
||||
endif()
|
||||
else()
|
||||
pkg_check_modules(SDL2MIXER REQUIRED IMPORTED_TARGET SDL2_mixer)
|
||||
target_sources(system3 PRIVATE
|
||||
src/generic/nact_generic.cpp
|
||||
src/generic/mako.cpp
|
||||
)
|
||||
target_link_libraries(system3 PRIVATE PkgConfig::SDL2MIXER)
|
||||
set(RESOURCE_PATH ${CMAKE_INSTALL_PREFIX}/share/system3/)
|
||||
install(TARGETS system3 RUNTIME DESTINATION bin)
|
||||
install(DIRECTORY resources/
|
||||
@@ -241,5 +239,14 @@ if(NINTENDO_SWITCH)
|
||||
)
|
||||
endif()
|
||||
|
||||
if (NOT EMSCRIPTEN)
|
||||
target_sources(system3 PRIVATE src/sys/mako_music.cpp)
|
||||
target_include_directories(system3 PRIVATE deps/dr_libs deps/stb)
|
||||
# dr_mp3 and stb_vorbis are amalgamated into this translation unit; silence
|
||||
# their warnings so they are not promoted to errors by WARNING_AS_ERROR.
|
||||
set_source_files_properties(src/sys/mako_music.cpp PROPERTIES
|
||||
COMPILE_OPTIONS "$<IF:$<CXX_COMPILER_ID:MSVC>,/w,-w>")
|
||||
endif()
|
||||
|
||||
target_compile_definitions(system3 PRIVATE RESOURCE_PATH="${RESOURCE_PATH}")
|
||||
target_include_directories(system3 PRIVATE src src/sys)
|
||||
|
||||
@@ -40,7 +40,7 @@ are supported.
|
||||
|
||||
#### `-playlist` _filename_
|
||||
_filename_ is a text file that lists the audio files to play in lieu of CD
|
||||
audio tracks, one per line. For example:
|
||||
audio tracks, one per line. MP3, OGG, and WAV files are supported. For example:
|
||||
|
||||
```plaintext
|
||||
# This line is ignored
|
||||
@@ -125,7 +125,7 @@ the game ID. You need to specify the `game` option in `system3.ini`.
|
||||
|
||||
```bash
|
||||
$ git submodule update --init
|
||||
$ sudo apt install g++ cmake libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev librtmidi-dev nlohmann-json3-dev
|
||||
$ sudo apt install g++ cmake libsdl2-dev libsdl2-ttf-dev librtmidi-dev nlohmann-json3-dev
|
||||
$ mkdir -p out/debug
|
||||
$ cd out/debug
|
||||
$ cmake -DCMAKE_BUILD_TYPE=Debug ../../
|
||||
@@ -137,7 +137,7 @@ $ sudo make install
|
||||
|
||||
```bash
|
||||
$ git submodule update --init
|
||||
$ brew install cmake pkg-config sdl2 sdl2_ttf sdl2_mixer rtmidi nlohmann-json
|
||||
$ brew install cmake pkg-config sdl2 sdl2_ttf rtmidi nlohmann-json
|
||||
$ mkdir -p out/debug
|
||||
$ cd out/debug
|
||||
$ cmake -DCMAKE_BUILD_TYPE=Debug ../../
|
||||
|
||||
@@ -15,27 +15,12 @@ FetchContent_Declare(
|
||||
URL https://github.com/libsdl-org/SDL_ttf/releases/download/release-2.22.0/SDL2_ttf-2.22.0.tar.gz
|
||||
URL_HASH SHA1=da5e86b601ad299a697878fab1af6f3be47b529d
|
||||
)
|
||||
FetchContent_Declare(
|
||||
SDL_mixer
|
||||
URL https://github.com/libsdl-org/SDL_mixer/releases/download/release-2.8.0/SDL2_mixer-2.8.0.tar.gz
|
||||
URL_HASH SHA1=a58c69f9d00e44833b9e00e1adb58d85759ca499
|
||||
)
|
||||
|
||||
set(SDL2TTF_SAMPLES OFF CACHE BOOL "Build the SDL2_ttf sample program(s)" FORCE)
|
||||
set(SDL2TTF_INSTALL OFF CACHE BOOL "Enable SDL2_ttf install target" FORCE)
|
||||
set(SDL2TTF_VENDORED ON CACHE BOOL "Use vendored third-party libraries" FORCE)
|
||||
|
||||
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_WAVPACK OFF CACHE BOOL "Enable WavPack 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)
|
||||
|
||||
FetchContent_MakeAvailable(SDL)
|
||||
FetchContent_MakeAvailable(SDL_ttf)
|
||||
FetchContent_MakeAvailable(SDL_mixer)
|
||||
|
||||
# The main CMakeLists.txt of system3
|
||||
add_subdirectory(${PROJECT_ROOT_DIR} src)
|
||||
@@ -50,4 +35,3 @@ 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)
|
||||
|
||||
@@ -12,7 +12,6 @@ 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"),
|
||||
@@ -35,4 +34,4 @@ class LicensesMenuActivity : Activity() {
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+5412
File diff suppressed because it is too large
Load Diff
Vendored
+5584
File diff suppressed because it is too large
Load Diff
+240
-109
@@ -5,30 +5,81 @@
|
||||
*/
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <limits.h>
|
||||
#include <SDL.h>
|
||||
#include <SDL_mixer.h>
|
||||
|
||||
#include "mako.h"
|
||||
#include "mako_midi.h"
|
||||
#include "mako_music.h"
|
||||
#include "fm/mako_ymfm.h"
|
||||
#include "config.h"
|
||||
#include "dri.h"
|
||||
#include "game_id.h"
|
||||
|
||||
namespace {
|
||||
|
||||
const int SAMPLE_RATE = 44100;
|
||||
|
||||
Mix_Music *mix_music;
|
||||
Mix_Chunk *mix_chunk;
|
||||
SDL_mutex* fm_mutex;
|
||||
#ifdef _WIN32
|
||||
// Per-game mapping from music numbers to CD tracks. This is necessary to
|
||||
// forcibly change the sound device with a menu command.
|
||||
//
|
||||
// When the game uses "Z 100+x,y" command, the xth element of the array should
|
||||
// be y. The array must be terminated with -1.
|
||||
const int8_t RANCE41_tracks[] = {2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,1,-1};
|
||||
const int8_t RANCE42_tracks[] = {2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,1,-1};
|
||||
const int8_t DPSALL_tracks[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,5,1,2,3,-1};
|
||||
#endif
|
||||
|
||||
SDL_AudioDeviceID g_device;
|
||||
SDL_AudioSpec g_device_spec;
|
||||
|
||||
std::unique_ptr<MakoMusic> music;
|
||||
std::unique_ptr<MakoYmfm> fm;
|
||||
std::unique_ptr<MAKOMidi> midi;
|
||||
|
||||
void FMHook(void*, Uint8* stream, int len) {
|
||||
SDL_LockMutex(fm_mutex);
|
||||
fm->Process(reinterpret_cast<int16_t*>(stream), len / 4);
|
||||
SDL_UnlockMutex(fm_mutex);
|
||||
// PCM (sound effect) playback. pcm_loops is the number of remaining plays,
|
||||
// or -1 for an infinite loop.
|
||||
SDL_AudioStream* pcm_stream;
|
||||
std::vector<uint8_t> pcm_src;
|
||||
int pcm_loops;
|
||||
bool pcm_input_finished;
|
||||
|
||||
void mix_pcm(Uint8* out, int len)
|
||||
{
|
||||
while (SDL_AudioStreamAvailable(pcm_stream) < len && !pcm_input_finished) {
|
||||
if (pcm_loops == 0) {
|
||||
SDL_AudioStreamFlush(pcm_stream);
|
||||
pcm_input_finished = true;
|
||||
break;
|
||||
}
|
||||
SDL_AudioStreamPut(pcm_stream, pcm_src.data(), static_cast<int>(pcm_src.size()));
|
||||
if (pcm_loops > 0)
|
||||
pcm_loops--;
|
||||
}
|
||||
Uint8* tmp = SDL_stack_alloc(Uint8, len);
|
||||
int got = SDL_AudioStreamGet(pcm_stream, tmp, len);
|
||||
if (got > 0)
|
||||
SDL_MixAudioFormat(out, tmp, AUDIO_S16SYS, got, SDL_MIX_MAXVOLUME);
|
||||
SDL_stack_free(tmp);
|
||||
}
|
||||
|
||||
void audio_callback(void*, Uint8* stream, int len)
|
||||
{
|
||||
SDL_memset(stream, 0, len);
|
||||
if (fm) {
|
||||
int frames = len / 4;
|
||||
int16_t* tmp = SDL_stack_alloc(int16_t, frames * 2);
|
||||
fm->Process(tmp, frames);
|
||||
SDL_MixAudioFormat(stream, reinterpret_cast<Uint8*>(tmp), AUDIO_S16SYS, len, SDL_MIX_MAXVOLUME);
|
||||
SDL_stack_free(tmp);
|
||||
}
|
||||
if (music)
|
||||
music->mix(stream, len);
|
||||
if (pcm_stream)
|
||||
mix_pcm(stream, len);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -39,10 +90,17 @@ MAKO::MAKO(const Config& config, const GameId& game_id) :
|
||||
next_loop(0),
|
||||
game_id(game_id)
|
||||
{
|
||||
int mix_init_flags = 0;
|
||||
if (!config.playlist.empty())
|
||||
load_playlist(config.playlist.c_str());
|
||||
|
||||
if (!config.playlist.empty() && load_playlist(config.playlist.c_str()))
|
||||
mix_init_flags |= MIX_INIT_MP3 | MIX_INIT_OGG;
|
||||
#ifdef _WIN32
|
||||
if (!is_cd_available()) {
|
||||
SDL_Event event = {};
|
||||
event.user.type = sdl_custom_event_type;
|
||||
event.user.code = DISABLE_CD_MENU;
|
||||
SDL_PushEvent(&event);
|
||||
}
|
||||
#endif
|
||||
|
||||
amus.open("AMUS.DAT");
|
||||
awav.open("AWAV.DAT");
|
||||
@@ -51,10 +109,21 @@ MAKO::MAKO(const Config& config, const GameId& game_id) :
|
||||
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)
|
||||
WARNING("Mix_OpenAudio failed: %s", Mix_GetError());
|
||||
SDL_InitSubSystem(SDL_INIT_AUDIO);
|
||||
SDL_AudioSpec want;
|
||||
SDL_zero(want);
|
||||
want.freq = SAMPLE_RATE;
|
||||
want.format = AUDIO_S16SYS;
|
||||
want.channels = 2;
|
||||
want.samples = 4096;
|
||||
want.callback = &audio_callback;
|
||||
g_device = SDL_OpenAudioDevice(nullptr, 0, &want, &g_device_spec, 0);
|
||||
if (!g_device) {
|
||||
WARNING("SDL_OpenAudioDevice failed: %s", SDL_GetError());
|
||||
use_fm = false;
|
||||
} else {
|
||||
SDL_PauseAudioDevice(g_device, 0);
|
||||
}
|
||||
|
||||
midi = std::make_unique<MAKOMidi>(config.midi_device);
|
||||
if (!midi->is_available())
|
||||
@@ -63,10 +132,13 @@ MAKO::MAKO(const Config& config, const GameId& game_id) :
|
||||
|
||||
MAKO::~MAKO()
|
||||
{
|
||||
midi.reset();
|
||||
stop_music();
|
||||
stop_pcm();
|
||||
Mix_CloseAudio();
|
||||
Mix_Quit();
|
||||
midi.reset();
|
||||
|
||||
if (g_device)
|
||||
SDL_CloseAudioDevice(g_device);
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
}
|
||||
|
||||
bool MAKO::load_playlist(const char* path)
|
||||
@@ -97,152 +169,211 @@ void MAKO::play_music(int page)
|
||||
|
||||
stop_music();
|
||||
|
||||
int track = page < 100 ? cd_track[page] : 0;
|
||||
size_t track = page < 100 ? cd_track[page] : 0;
|
||||
if (track && is_cd_available()) {
|
||||
if (track < playlist.size() && playlist[track]) {
|
||||
if (track >= playlist.size() || !playlist[track])
|
||||
return;
|
||||
const char* file = playlist[track];
|
||||
#ifdef __ANDROID__
|
||||
// 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);
|
||||
#else
|
||||
mix_music = Mix_LoadMUS(playlist[track]);
|
||||
// dr_mp3/stb_vorbis open the file via fopen, which requires an absolute
|
||||
// path on Android.
|
||||
char abspath[PATH_MAX];
|
||||
if (!realpath(file, abspath))
|
||||
return;
|
||||
file = abspath;
|
||||
#endif
|
||||
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;
|
||||
}
|
||||
}
|
||||
auto m = std::make_unique<MakoMusic>(file, next_loop, g_device_spec);
|
||||
if (!m->is_open())
|
||||
return;
|
||||
SDL_LockAudioDevice(g_device);
|
||||
music = std::move(m);
|
||||
SDL_UnlockAudioDevice(g_device);
|
||||
} else if (use_fm) {
|
||||
std::vector<uint8_t> data = amus.load(page);
|
||||
if (data.empty())
|
||||
return;
|
||||
if (!fm_mutex)
|
||||
fm_mutex = SDL_CreateMutex();
|
||||
|
||||
SDL_LockMutex(fm_mutex);
|
||||
fm = std::make_unique<MakoYmfm>(SAMPLE_RATE, std::move(data));
|
||||
SDL_UnlockMutex(fm_mutex);
|
||||
Mix_HookMusic(&FMHook, this);
|
||||
auto f = std::make_unique<MakoYmfm>(SAMPLE_RATE, std::move(data));
|
||||
SDL_LockAudioDevice(g_device);
|
||||
fm = std::move(f);
|
||||
SDL_UnlockAudioDevice(g_device);
|
||||
} else if (midi->is_available()) {
|
||||
if (!midi->play(game_id, amus, mda, page, next_loop))
|
||||
return;
|
||||
}
|
||||
|
||||
current_music = page;
|
||||
next_loop = 0;
|
||||
}
|
||||
|
||||
void MAKO::stop_music()
|
||||
{
|
||||
if (!current_music)
|
||||
return;
|
||||
|
||||
if (mix_music) {
|
||||
Mix_FreeMusic(mix_music);
|
||||
mix_music = NULL;
|
||||
if (music || fm) {
|
||||
SDL_LockAudioDevice(g_device);
|
||||
std::unique_ptr<MakoMusic> old_music = std::move(music);
|
||||
std::unique_ptr<MakoYmfm> old_fm = std::move(fm);
|
||||
SDL_UnlockAudioDevice(g_device);
|
||||
// old_music/old_fm are destroyed here, outside the lock.
|
||||
}
|
||||
if (fm) {
|
||||
Mix_HookMusic(NULL, NULL);
|
||||
SDL_LockMutex(fm_mutex);
|
||||
fm = nullptr;
|
||||
SDL_UnlockMutex(fm_mutex);
|
||||
}
|
||||
if (midi->is_available()) {
|
||||
if (midi->is_available())
|
||||
midi->stop();
|
||||
}
|
||||
current_music = 0;
|
||||
}
|
||||
|
||||
bool MAKO::check_music()
|
||||
{
|
||||
if (mix_music)
|
||||
return Mix_PlayingMusic();
|
||||
SDL_LockAudioDevice(g_device);
|
||||
if (fm) {
|
||||
int mark, loop;
|
||||
SDL_LockMutex(fm_mutex);
|
||||
fm->get_mark(&mark, &loop);
|
||||
SDL_UnlockMutex(fm_mutex);
|
||||
SDL_UnlockAudioDevice(g_device);
|
||||
return !loop;
|
||||
}
|
||||
if (music) {
|
||||
bool playing = music->is_playing();
|
||||
SDL_UnlockAudioDevice(g_device);
|
||||
return playing;
|
||||
}
|
||||
SDL_UnlockAudioDevice(g_device);
|
||||
return midi->is_playing();
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
void MAKO::select_sound(BGMDevice dev)
|
||||
{
|
||||
// 強制的に音源を変更する
|
||||
int page = current_music;
|
||||
int old_dev = (1 <= page && page <= 99 && cd_track[page]) ? BGM_CD :
|
||||
use_fm ? BGM_FM : BGM_MIDI;
|
||||
|
||||
switch (dev) {
|
||||
case BGM_FM:
|
||||
case BGM_MIDI:
|
||||
for (int i = 1; i <= 99; i++)
|
||||
cd_track[i] = 0;
|
||||
if (midi->is_available())
|
||||
use_fm = dev == BGM_FM;
|
||||
else
|
||||
dev = BGM_FM;
|
||||
break;
|
||||
|
||||
case BGM_CD:
|
||||
const int8_t* tracks;
|
||||
|
||||
switch (game_id.game) {
|
||||
case GameId::RANCE41:
|
||||
tracks = RANCE41_tracks;
|
||||
break;
|
||||
case GameId::RANCE42:
|
||||
tracks = RANCE42_tracks;
|
||||
break;
|
||||
case GameId::DPS_ALL:
|
||||
tracks = DPSALL_tracks;
|
||||
break;
|
||||
|
||||
// For the following games, the default mapping (cd_track[i] = i) works.
|
||||
case GameId::AYUMI_CD:
|
||||
case GameId::FUNNYBEE_CD:
|
||||
case GameId::ONLYYOU:
|
||||
default:
|
||||
tracks = nullptr;
|
||||
}
|
||||
|
||||
if (tracks) {
|
||||
for (int i = 0; tracks[i] >= 0; i++)
|
||||
cd_track[i + 1] = tracks[i];
|
||||
} else {
|
||||
for (int i = 1; i <= 99; i++)
|
||||
cd_track[i] = i;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// デバイスが変更された場合は再演奏する
|
||||
if (dev != old_dev && page) {
|
||||
stop_music();
|
||||
play_music(page);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void MAKO::get_mark(int* mark, int* loop)
|
||||
{
|
||||
SDL_LockMutex(fm_mutex);
|
||||
SDL_LockAudioDevice(g_device);
|
||||
if (fm) {
|
||||
fm->get_mark(mark, loop);
|
||||
SDL_UnlockMutex(fm_mutex);
|
||||
SDL_UnlockAudioDevice(g_device);
|
||||
return;
|
||||
}
|
||||
SDL_UnlockMutex(fm_mutex);
|
||||
SDL_UnlockAudioDevice(g_device);
|
||||
midi->get_mark(mark, loop);
|
||||
}
|
||||
|
||||
void MAKO::play_pcm(int page, int loops)
|
||||
{
|
||||
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();
|
||||
|
||||
std::vector<uint8_t> buffer = awav.load(page);
|
||||
if (!buffer.empty()) {
|
||||
// WAV形式 (Only You)
|
||||
mix_chunk = Mix_LoadWAV_RW(SDL_RWFromConstMem(buffer.data(), buffer.size()), 1 /* freesrc */);
|
||||
Mix_PlayChannel(-1, mix_chunk, loops ? loops : -1);
|
||||
SDL_AudioStream* stream = nullptr;
|
||||
std::vector<uint8_t> src;
|
||||
|
||||
// WAV形式 (Only You)
|
||||
std::vector<uint8_t> data = awav.load(page);
|
||||
if (!data.empty()) {
|
||||
SDL_AudioSpec spec;
|
||||
Uint8* wav;
|
||||
Uint32 wavlen;
|
||||
if (!SDL_LoadWAV_RW(SDL_RWFromConstMem(data.data(), static_cast<int>(data.size())), 1, &spec, &wav, &wavlen)) {
|
||||
WARNING("SDL_LoadWAV_RW failed: %s", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
src.assign(wav, wav + wavlen);
|
||||
SDL_FreeWAV(wav);
|
||||
stream = SDL_NewAudioStream(spec.format, spec.channels, spec.freq,
|
||||
g_device_spec.format, g_device_spec.channels, g_device_spec.freq);
|
||||
} else {
|
||||
// AMSE形式 (乙女戦記)
|
||||
data = amse.load(page);
|
||||
if (data.empty())
|
||||
return;
|
||||
uint32_t amse_size = SDL_SwapLE32(*reinterpret_cast<uint32_t*>(&data[8]));
|
||||
// 4-bit PCM -> 8-bit PCM, mono, 8000Hz
|
||||
for (uint32_t i = 12; i < amse_size; i++) {
|
||||
src.push_back(data[i] & 0xf0);
|
||||
src.push_back((data[i] & 0x0f) << 4);
|
||||
}
|
||||
stream = SDL_NewAudioStream(AUDIO_U8, 1, 8000,
|
||||
g_device_spec.format, g_device_spec.channels, g_device_spec.freq);
|
||||
}
|
||||
if (!stream) {
|
||||
WARNING("SDL_NewAudioStream failed: %s", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
buffer = amse.load(page);
|
||||
if (!buffer.empty()) {
|
||||
// AMSE形式 (乙女戦記)
|
||||
uint32_t amse_size = SDL_SwapLE32(*reinterpret_cast<uint32_t*>(&buffer[8]));
|
||||
int samples = (amse_size - 12) * 2;
|
||||
int total = samples + 0x24;
|
||||
|
||||
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 (uint32_t i = 12, p = 44; i < amse_size; i++) {
|
||||
wav[p++] = buffer[i] & 0xf0;
|
||||
wav[p++] = (buffer[i] & 0x0f) << 4;
|
||||
}
|
||||
|
||||
mix_chunk = Mix_LoadWAV_RW(SDL_RWFromConstMem(wav, total + 8), 1 /* freesrc */);
|
||||
free(wav);
|
||||
Mix_PlayChannel(-1, mix_chunk, loops ? loops : -1);
|
||||
}
|
||||
SDL_LockAudioDevice(g_device);
|
||||
pcm_stream = stream;
|
||||
pcm_src = std::move(src);
|
||||
pcm_loops = loops ? loops : -1;
|
||||
pcm_input_finished = false;
|
||||
SDL_UnlockAudioDevice(g_device);
|
||||
}
|
||||
|
||||
void MAKO::stop_pcm()
|
||||
{
|
||||
Mix_HaltChannel(-1);
|
||||
if (mix_chunk) {
|
||||
Mix_FreeChunk(mix_chunk);
|
||||
mix_chunk = NULL;
|
||||
}
|
||||
SDL_LockAudioDevice(g_device);
|
||||
SDL_AudioStream* old = pcm_stream;
|
||||
pcm_stream = nullptr;
|
||||
pcm_src.clear();
|
||||
pcm_input_finished = false;
|
||||
SDL_UnlockAudioDevice(g_device);
|
||||
if (old)
|
||||
SDL_FreeAudioStream(old);
|
||||
}
|
||||
|
||||
bool MAKO::check_pcm()
|
||||
{
|
||||
return Mix_Playing(-1) != 0;
|
||||
// 再生中でtrue
|
||||
SDL_LockAudioDevice(g_device);
|
||||
bool playing = pcm_stream &&
|
||||
(!pcm_input_finished || SDL_AudioStreamAvailable(pcm_stream) > 0);
|
||||
SDL_UnlockAudioDevice(g_device);
|
||||
return playing;
|
||||
}
|
||||
|
||||
bool MAKO::is_cd_available() const
|
||||
|
||||
@@ -44,7 +44,6 @@ public:
|
||||
|
||||
#ifdef _WIN32
|
||||
void select_sound(BGMDevice dev);
|
||||
void on_mci_notify(const SDL_SysWMmsg* msg);
|
||||
#endif
|
||||
|
||||
bool use_fm;
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
#include "mako_music.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <memory>
|
||||
#include "common.h"
|
||||
|
||||
#define DR_MP3_IMPLEMENTATION
|
||||
#include "dr_mp3.h"
|
||||
#include "stb_vorbis.h"
|
||||
|
||||
struct DecodedChunk {
|
||||
const Uint8* data;
|
||||
int frames;
|
||||
};
|
||||
|
||||
class MakoMusicDecoder {
|
||||
public:
|
||||
virtual ~MakoMusicDecoder() = default;
|
||||
virtual bool is_open() const = 0;
|
||||
virtual const SDL_AudioSpec& spec() const = 0;
|
||||
virtual DecodedChunk decode(int max_frames) = 0;
|
||||
virtual void seek_start() = 0;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
class Mp3Decoder final : public MakoMusicDecoder {
|
||||
public:
|
||||
explicit Mp3Decoder(const std::string& path)
|
||||
{
|
||||
if (!drmp3_init_file(&mp3, path.c_str(), nullptr)) {
|
||||
WARNING("drmp3_init_file failed: %s", path.c_str());
|
||||
return;
|
||||
}
|
||||
initialized = true;
|
||||
spec_.freq = static_cast<int>(mp3.sampleRate);
|
||||
spec_.format = AUDIO_S16SYS;
|
||||
spec_.channels = static_cast<Uint8>(mp3.channels);
|
||||
}
|
||||
|
||||
~Mp3Decoder() override
|
||||
{
|
||||
if (initialized)
|
||||
drmp3_uninit(&mp3);
|
||||
}
|
||||
|
||||
bool is_open() const override { return initialized; }
|
||||
const SDL_AudioSpec& spec() const override { return spec_; }
|
||||
|
||||
DecodedChunk decode(int max_frames) override
|
||||
{
|
||||
int frames = static_cast<int>(drmp3_read_pcm_frames_s16(&mp3, max_frames, pcm.data()));
|
||||
return { reinterpret_cast<const Uint8*>(pcm.data()), frames };
|
||||
}
|
||||
|
||||
void seek_start() override { drmp3_seek_to_pcm_frame(&mp3, 0); }
|
||||
|
||||
private:
|
||||
drmp3 mp3{};
|
||||
SDL_AudioSpec spec_{};
|
||||
std::array<int16_t, 1024 * 2> pcm;
|
||||
bool initialized = false;
|
||||
};
|
||||
|
||||
class OggDecoder final : public MakoMusicDecoder {
|
||||
public:
|
||||
explicit OggDecoder(const std::string& path)
|
||||
{
|
||||
int error = 0;
|
||||
vorbis = stb_vorbis_open_filename(path.c_str(), &error, nullptr);
|
||||
if (!vorbis) {
|
||||
WARNING("stb_vorbis_open_filename failed: %s (error %d)", path.c_str(), error);
|
||||
return;
|
||||
}
|
||||
stb_vorbis_info info = stb_vorbis_get_info(vorbis);
|
||||
spec_.freq = static_cast<int>(info.sample_rate);
|
||||
spec_.format = AUDIO_S16SYS;
|
||||
spec_.channels = static_cast<Uint8>(info.channels);
|
||||
}
|
||||
|
||||
~OggDecoder() override
|
||||
{
|
||||
if (vorbis)
|
||||
stb_vorbis_close(vorbis);
|
||||
}
|
||||
|
||||
bool is_open() const override { return vorbis != nullptr; }
|
||||
const SDL_AudioSpec& spec() const override { return spec_; }
|
||||
|
||||
DecodedChunk decode(int max_frames) override
|
||||
{
|
||||
int frames = stb_vorbis_get_samples_short_interleaved(
|
||||
vorbis, spec_.channels, pcm.data(), max_frames * spec_.channels);
|
||||
return { reinterpret_cast<const Uint8*>(pcm.data()), frames };
|
||||
}
|
||||
|
||||
void seek_start() override { stb_vorbis_seek_start(vorbis); }
|
||||
|
||||
private:
|
||||
stb_vorbis* vorbis = nullptr;
|
||||
SDL_AudioSpec spec_{};
|
||||
std::array<int16_t, 1024 * 2> pcm;
|
||||
};
|
||||
|
||||
class WavDecoder final : public MakoMusicDecoder {
|
||||
public:
|
||||
explicit WavDecoder(const std::string& path)
|
||||
{
|
||||
if (!SDL_LoadWAV(path.c_str(), &spec_, &data, &data_size)) {
|
||||
WARNING("SDL_LoadWAV failed: %s: %s", path.c_str(), SDL_GetError());
|
||||
return;
|
||||
}
|
||||
frame_size = SDL_AUDIO_BITSIZE(spec_.format) / 8 * spec_.channels;
|
||||
if (data_size == 0 || frame_size <= 0)
|
||||
WARNING("Invalid or empty WAV file: %s", path.c_str());
|
||||
}
|
||||
|
||||
~WavDecoder() override
|
||||
{
|
||||
if (data)
|
||||
SDL_FreeWAV(data);
|
||||
}
|
||||
|
||||
bool is_open() const override { return data_size > 0 && frame_size > 0; }
|
||||
const SDL_AudioSpec& spec() const override { return spec_; }
|
||||
|
||||
DecodedChunk decode(int max_frames) override
|
||||
{
|
||||
size_t available_frames = (data_size - pos) / frame_size;
|
||||
int frames = static_cast<int>(std::min<size_t>(available_frames, max_frames));
|
||||
const Uint8* chunk_data = data + pos;
|
||||
pos += frames * frame_size;
|
||||
return { chunk_data, frames };
|
||||
}
|
||||
|
||||
void seek_start() override { pos = 0; }
|
||||
|
||||
private:
|
||||
SDL_AudioSpec spec_{};
|
||||
Uint8* data = nullptr;
|
||||
Uint32 data_size = 0;
|
||||
size_t pos = 0;
|
||||
int frame_size = 0;
|
||||
};
|
||||
|
||||
bool has_extension(const std::string& path, const char* ext)
|
||||
{
|
||||
size_t len = strlen(ext);
|
||||
if (path.size() < len)
|
||||
return false;
|
||||
std::string tail = path.substr(path.size() - len);
|
||||
std::transform(tail.begin(), tail.end(), tail.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
return tail == ext;
|
||||
}
|
||||
|
||||
std::unique_ptr<MakoMusicDecoder> create_decoder(const std::string& path)
|
||||
{
|
||||
std::unique_ptr<MakoMusicDecoder> decoder;
|
||||
if (has_extension(path, ".ogg") || has_extension(path, ".oga"))
|
||||
decoder = std::make_unique<OggDecoder>(path);
|
||||
else if (has_extension(path, ".wav"))
|
||||
decoder = std::make_unique<WavDecoder>(path);
|
||||
else
|
||||
decoder = std::make_unique<Mp3Decoder>(path);
|
||||
if (!decoder->is_open())
|
||||
return nullptr;
|
||||
return decoder;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MakoMusic::MakoMusic(const std::string& path, int loops, const SDL_AudioSpec& device_spec)
|
||||
: decoder(create_decoder(path)), loops_(loops)
|
||||
{
|
||||
if (!decoder)
|
||||
return;
|
||||
const SDL_AudioSpec& src_spec = decoder->spec();
|
||||
|
||||
stream = SDL_NewAudioStream(src_spec.format, src_spec.channels, src_spec.freq,
|
||||
device_spec.format, device_spec.channels, device_spec.freq);
|
||||
if (!stream) {
|
||||
WARNING("SDL_NewAudioStream failed: %s", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
playing = true;
|
||||
}
|
||||
|
||||
MakoMusic::~MakoMusic()
|
||||
{
|
||||
if (stream)
|
||||
SDL_FreeAudioStream(stream);
|
||||
}
|
||||
|
||||
void MakoMusic::decode()
|
||||
{
|
||||
constexpr int CHUNK_FRAMES = 1024;
|
||||
|
||||
DecodedChunk chunk = decoder->decode(CHUNK_FRAMES);
|
||||
|
||||
if (chunk.frames == 0) {
|
||||
if (loops_ && --loops_ == 0) {
|
||||
SDL_AudioStreamFlush(stream);
|
||||
input_finished = true;
|
||||
} else {
|
||||
decoder->seek_start();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const SDL_AudioSpec& spec = decoder->spec();
|
||||
int bytes = chunk.frames * SDL_AUDIO_BITSIZE(spec.format) / 8 * spec.channels;
|
||||
if (SDL_AudioStreamPut(stream, chunk.data, bytes) < 0) {
|
||||
WARNING("SDL_AudioStreamPut failed: %s", SDL_GetError());
|
||||
input_finished = true;
|
||||
}
|
||||
}
|
||||
|
||||
void MakoMusic::mix(Uint8* out, int len)
|
||||
{
|
||||
if (!stream || !playing)
|
||||
return;
|
||||
|
||||
while (SDL_AudioStreamAvailable(stream) < len && !input_finished)
|
||||
decode();
|
||||
|
||||
Uint8* tmp = SDL_stack_alloc(Uint8, len);
|
||||
int got = SDL_AudioStreamGet(stream, tmp, len);
|
||||
if (got > 0)
|
||||
SDL_MixAudioFormat(out, tmp, AUDIO_S16SYS, got, SDL_MIX_MAXVOLUME);
|
||||
SDL_stack_free(tmp);
|
||||
if (input_finished && SDL_AudioStreamAvailable(stream) <= 0)
|
||||
playing = false;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef MAKO_MUSIC_H_
|
||||
#define MAKO_MUSIC_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <SDL.h>
|
||||
|
||||
class MakoMusicDecoder;
|
||||
|
||||
// Plays a single BGM file (MP3, OGG, or WAV, chosen by file extension),
|
||||
// decoding on demand and mixing into the audio callback's output buffer.
|
||||
class MakoMusic {
|
||||
public:
|
||||
// loops: number of times to play; 0 means loop forever.
|
||||
MakoMusic(const std::string& path, int loops, const SDL_AudioSpec& device_spec);
|
||||
~MakoMusic();
|
||||
|
||||
bool is_open() const { return stream != nullptr; }
|
||||
bool is_playing() const { return playing; }
|
||||
|
||||
// Called from the audio callback.
|
||||
void mix(Uint8* out, int len);
|
||||
|
||||
private:
|
||||
// Decodes and queues one chunk, handling EOF and decoder errors.
|
||||
void decode();
|
||||
|
||||
std::unique_ptr<MakoMusicDecoder> decoder;
|
||||
SDL_AudioStream* stream = nullptr;
|
||||
int loops_; // number of times to play, 0 for infinite loop
|
||||
bool playing = false;
|
||||
bool input_finished = false;
|
||||
};
|
||||
|
||||
#endif // MAKO_MUSIC_H_
|
||||
@@ -1,485 +0,0 @@
|
||||
/*
|
||||
ALICE SOFT SYSTEM 3 for Win32
|
||||
|
||||
[ MAKO ]
|
||||
*/
|
||||
|
||||
#include <windows.h>
|
||||
#undef ERROR
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <SDL.h>
|
||||
#include <SDL_syswm.h>
|
||||
#include "mako.h"
|
||||
#include "mako_midi.h"
|
||||
#include "fm/mako_ymfm.h"
|
||||
#include "config.h"
|
||||
#include "dri.h"
|
||||
#include "game_id.h"
|
||||
|
||||
extern SDL_Window* g_window;
|
||||
|
||||
namespace {
|
||||
|
||||
const int SAMPLE_RATE = 44100;
|
||||
|
||||
// Per-game mapping from music numbers to CD tracks. This is necessary to
|
||||
// forcibly change the sound device with a menu command.
|
||||
//
|
||||
// When the game uses "Z 100+x,y" command, the xth element of the array should
|
||||
// be y. The array must be terminated with -1.
|
||||
const int8_t RANCE41_tracks[] = {2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,1,-1};
|
||||
const int8_t RANCE42_tracks[] = {2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,1,-1};
|
||||
const int8_t DPSALL_tracks[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,5,1,2,3,-1};
|
||||
|
||||
enum MakoThreadMessage {
|
||||
MAKO_OPEN = WM_APP,
|
||||
MAKO_PLAY,
|
||||
MAKO_STOP,
|
||||
MAKO_EXIT
|
||||
};
|
||||
|
||||
const char* mci_geterror(DWORD err)
|
||||
{
|
||||
static char buf[128];
|
||||
mciGetErrorString(err, buf, sizeof(buf));
|
||||
return buf;
|
||||
}
|
||||
|
||||
class MCIThread {
|
||||
public:
|
||||
MCIThread(HWND hwnd_notify) : hwnd_notify(hwnd_notify) {
|
||||
if (!_beginthreadex(NULL, 0, &MCIThread::run, this, 0, &thread_id))
|
||||
sys_error("Cannot create thread: %s", strerror(errno));
|
||||
}
|
||||
|
||||
bool post_message(UINT msg, WPARAM wparam, LPARAM lparam) {
|
||||
return PostThreadMessage(thread_id, msg, wparam, lparam);
|
||||
}
|
||||
|
||||
MCIDEVICEID device_id() { return devid; }
|
||||
|
||||
private:
|
||||
static unsigned __stdcall run(void* param)
|
||||
{
|
||||
MCIThread* t = reinterpret_cast<MCIThread*>(param);
|
||||
t->message_loop();
|
||||
delete t;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void message_loop()
|
||||
{
|
||||
MSG msg;
|
||||
while (GetMessage(&msg, NULL, 0, 0)) {
|
||||
switch (msg.message) {
|
||||
case MAKO_OPEN:
|
||||
close();
|
||||
open(reinterpret_cast<std::string*>(msg.wParam));
|
||||
break;
|
||||
|
||||
case MAKO_PLAY:
|
||||
play();
|
||||
break;
|
||||
|
||||
case MAKO_STOP:
|
||||
close();
|
||||
break;
|
||||
|
||||
case MAKO_EXIT:
|
||||
close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void open(std::string* file)
|
||||
{
|
||||
file_playing = file;
|
||||
|
||||
MCI_OPEN_PARMS open;
|
||||
open.lpstrElementName = file_playing->c_str();
|
||||
DWORD flags = MCI_OPEN_ELEMENT | MCI_WAIT;
|
||||
MCIERROR err = mciSendCommand(0, MCI_OPEN, flags, (DWORD_PTR)&open);
|
||||
if (err) {
|
||||
WARNING("MCI_OPEN failed: %s: %s",
|
||||
file_playing->c_str(), mci_geterror(err));
|
||||
return;
|
||||
}
|
||||
devid = open.wDeviceID;
|
||||
}
|
||||
|
||||
void play()
|
||||
{
|
||||
MCI_PLAY_PARMS play;
|
||||
play.dwCallback = (DWORD_PTR)hwnd_notify;
|
||||
play.dwFrom = 0;
|
||||
DWORD flags = MCI_FROM | MCI_NOTIFY;
|
||||
MCIERROR err = mciSendCommand(devid, MCI_PLAY, flags, (DWORD_PTR)&play);
|
||||
if (err)
|
||||
WARNING("MCI_PLAY failed: %s", mci_geterror(err));
|
||||
}
|
||||
|
||||
void close() {
|
||||
if (devid) {
|
||||
mciSendCommand(devid, MCI_CLOSE, 0, 0);
|
||||
devid = 0;
|
||||
}
|
||||
if (file_playing) {
|
||||
delete file_playing;
|
||||
file_playing = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
HWND hwnd_notify;
|
||||
unsigned thread_id;
|
||||
std::string* file_playing = nullptr;
|
||||
MCIDEVICEID devid = 0;
|
||||
};
|
||||
|
||||
MCIThread* mci_thread;
|
||||
|
||||
class Music {
|
||||
public:
|
||||
Music(const char* fname, int loop) : loops(loop) {
|
||||
std::string* fname_str = new std::string(fname);
|
||||
mci_thread->post_message(MAKO_OPEN, (WPARAM)fname_str, 0);
|
||||
mci_thread->post_message(MAKO_PLAY, 0, 0);
|
||||
playing = true;
|
||||
}
|
||||
|
||||
~Music() {
|
||||
close();
|
||||
}
|
||||
|
||||
void close() {
|
||||
mci_thread->post_message(MAKO_STOP, 0, 0);
|
||||
playing = false;
|
||||
}
|
||||
|
||||
bool is_playing() {
|
||||
return playing;
|
||||
}
|
||||
|
||||
bool on_mci_notify(const SDL_SysWMmsg* msg) {
|
||||
if (msg->msg.win.lParam != mci_thread->device_id())
|
||||
return true;
|
||||
switch (msg->msg.win.wParam) {
|
||||
case MCI_NOTIFY_FAILURE:
|
||||
close();
|
||||
return false;
|
||||
case MCI_NOTIFY_SUCCESSFUL:
|
||||
if (loops && --loops == 0) {
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
mci_thread->post_message(MAKO_PLAY, 0, 0);
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
int loops; // number of times to play, 0 for infinite loop
|
||||
bool playing;
|
||||
};
|
||||
|
||||
Music* music;
|
||||
uint8* wav_buffer;
|
||||
SDL_mutex* fm_mutex;
|
||||
std::unique_ptr<MakoYmfm> fm;
|
||||
std::unique_ptr<MAKOMidi> midi;
|
||||
|
||||
void audio_callback(void*, Uint8* stream, int len) {
|
||||
SDL_LockMutex(fm_mutex);
|
||||
fm->Process(reinterpret_cast<int16_t*>(stream), len/ 4);
|
||||
SDL_UnlockMutex(fm_mutex);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MAKO::MAKO(const Config& config, const GameId& game_id) :
|
||||
use_fm(config.use_fm),
|
||||
current_music(0),
|
||||
next_loop(0),
|
||||
game_id(game_id)
|
||||
{
|
||||
if (!config.playlist.empty())
|
||||
load_playlist(config.playlist.c_str());
|
||||
if (!is_cd_available()) {
|
||||
SDL_Event event = {};
|
||||
event.user.type = sdl_custom_event_type;
|
||||
event.user.code = DISABLE_CD_MENU;
|
||||
SDL_PushEvent(&event);
|
||||
}
|
||||
|
||||
amus.open("AMUS.DAT");
|
||||
awav.open("AWAV.DAT");
|
||||
amse.open("AMSE.DAT");
|
||||
mda.open("AMUS.MDA");
|
||||
|
||||
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();
|
||||
|
||||
SDL_SysWMinfo info;
|
||||
SDL_VERSION(&info.version);
|
||||
if (!SDL_GetWindowWMInfo(g_window, &info))
|
||||
sys_error("SDL_GetWindowWMInfo failed: %s", SDL_GetError());
|
||||
mci_thread = new MCIThread(info.info.win.window);
|
||||
|
||||
midi = std::make_unique<MAKOMidi>(config.midi_device);
|
||||
if (!midi->is_available())
|
||||
use_fm = true;
|
||||
}
|
||||
|
||||
MAKO::~MAKO()
|
||||
{
|
||||
stop_music();
|
||||
stop_pcm();
|
||||
mci_thread->post_message(MAKO_EXIT, 0, 0);
|
||||
mci_thread = nullptr; // MCIThread self-destructs on the worker thread.
|
||||
midi.reset();
|
||||
|
||||
SDL_LockMutex(fm_mutex);
|
||||
SDL_CloseAudio();
|
||||
SDL_UnlockMutex(fm_mutex);
|
||||
SDL_DestroyMutex(fm_mutex);
|
||||
fm_mutex = nullptr;
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
}
|
||||
|
||||
bool MAKO::load_playlist(const char* path)
|
||||
{
|
||||
FILE* fp = fopen(path, "rt");
|
||||
if (!fp) {
|
||||
WARNING("Cannot open %s", path);
|
||||
return false;
|
||||
}
|
||||
char buf[256];
|
||||
while (fgets(buf, sizeof(buf) - 1, fp)) {
|
||||
char *p = &buf[strlen(buf) - 1];
|
||||
if (*p == '\n')
|
||||
*p = '\0';
|
||||
playlist.push_back(buf[0] ? strdup(buf) : NULL);
|
||||
}
|
||||
fclose(fp);
|
||||
return true;
|
||||
}
|
||||
|
||||
void MAKO::play_music(int page)
|
||||
{
|
||||
if (current_music == page)
|
||||
return;
|
||||
|
||||
stop_music();
|
||||
|
||||
size_t track = page < 100 ? cd_track[page] : 0;
|
||||
if (track && is_cd_available()) {
|
||||
if (track >= playlist.size() || !playlist[track])
|
||||
return;
|
||||
music = new Music(playlist[track], next_loop);
|
||||
} else if (use_fm) {
|
||||
std::vector<uint8_t> data = amus.load(page);
|
||||
if (data.empty())
|
||||
return;
|
||||
|
||||
SDL_LockMutex(fm_mutex);
|
||||
fm = std::make_unique<MakoYmfm>(SAMPLE_RATE, std::move(data));
|
||||
SDL_UnlockMutex(fm_mutex);
|
||||
SDL_PauseAudio(0);
|
||||
} else if (midi->is_available()) {
|
||||
if (!midi->play(game_id, amus, mda, page, next_loop))
|
||||
return;
|
||||
}
|
||||
current_music = page;
|
||||
}
|
||||
|
||||
void MAKO::stop_music()
|
||||
{
|
||||
if (music) {
|
||||
delete music;
|
||||
music = nullptr;
|
||||
}
|
||||
if (fm) {
|
||||
SDL_PauseAudio(1);
|
||||
SDL_LockMutex(fm_mutex);
|
||||
fm = nullptr;
|
||||
SDL_UnlockMutex(fm_mutex);
|
||||
}
|
||||
if (midi->is_available()) {
|
||||
midi->stop();
|
||||
}
|
||||
current_music = 0;
|
||||
}
|
||||
|
||||
bool MAKO::check_music()
|
||||
{
|
||||
if (fm) {
|
||||
int mark, loop;
|
||||
SDL_LockMutex(fm_mutex);
|
||||
fm->get_mark(&mark, &loop);
|
||||
SDL_UnlockMutex(fm_mutex);
|
||||
return !loop;
|
||||
}
|
||||
else if (music) {
|
||||
return music->is_playing();
|
||||
} else {
|
||||
return midi->is_playing();
|
||||
}
|
||||
}
|
||||
|
||||
void MAKO::select_sound(BGMDevice dev)
|
||||
{
|
||||
// 強制的に音源を変更する
|
||||
int page = current_music;
|
||||
int old_dev = (1 <= page && page <= 99 && cd_track[page]) ? BGM_CD :
|
||||
use_fm ? BGM_FM : BGM_MIDI;
|
||||
|
||||
switch (dev) {
|
||||
case BGM_FM:
|
||||
case BGM_MIDI:
|
||||
for (int i = 1; i <= 99; i++)
|
||||
cd_track[i] = 0;
|
||||
if (midi->is_available())
|
||||
use_fm = dev == BGM_FM;
|
||||
else
|
||||
dev = BGM_FM;
|
||||
break;
|
||||
|
||||
case BGM_CD:
|
||||
const int8_t* tracks;
|
||||
|
||||
switch (game_id.game) {
|
||||
case GameId::RANCE41:
|
||||
tracks = RANCE41_tracks;
|
||||
break;
|
||||
case GameId::RANCE42:
|
||||
tracks = RANCE42_tracks;
|
||||
break;
|
||||
case GameId::DPS_ALL:
|
||||
tracks = DPSALL_tracks;
|
||||
break;
|
||||
|
||||
// For the following games, the default mapping (cd_track[i] = i) works.
|
||||
case GameId::AYUMI_CD:
|
||||
case GameId::FUNNYBEE_CD:
|
||||
case GameId::ONLYYOU:
|
||||
default:
|
||||
tracks = nullptr;
|
||||
}
|
||||
|
||||
if (tracks) {
|
||||
for (int i = 0; tracks[i] >= 0; i++)
|
||||
cd_track[i + 1] = tracks[i];
|
||||
} else {
|
||||
for (int i = 1; i <= 99; i++)
|
||||
cd_track[i] = i;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// デバイスが変更された場合は再演奏する
|
||||
if (dev != old_dev && page) {
|
||||
stop_music();
|
||||
play_music(page);
|
||||
}
|
||||
}
|
||||
|
||||
void MAKO::get_mark(int* mark, int* loop)
|
||||
{
|
||||
SDL_LockMutex(fm_mutex);
|
||||
if (fm) {
|
||||
fm->get_mark(mark, loop);
|
||||
SDL_UnlockMutex(fm_mutex);
|
||||
return;
|
||||
}
|
||||
SDL_UnlockMutex(fm_mutex);
|
||||
midi->get_mark(mark, loop);
|
||||
}
|
||||
|
||||
void MAKO::play_pcm(int page, int loops)
|
||||
{
|
||||
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();
|
||||
|
||||
// WAV形式 (Only You)
|
||||
std::vector<uint8_t> wav_buffer = awav.load(page);
|
||||
if (wav_buffer.empty()) {
|
||||
std::vector<uint8_t> buffer = amse.load(page);
|
||||
if (!buffer.empty()) {
|
||||
// AMSE形式 (乙女戦記)
|
||||
uint32_t amse_size = SDL_SwapLE32(*reinterpret_cast<uint32_t*>(&buffer[8]));
|
||||
int samples = (static_cast<int>(amse_size) - 12) * 2;
|
||||
int total = samples + 0x24;
|
||||
|
||||
wav_buffer.resize(total + 8);
|
||||
memcpy(wav_buffer.data(), header, 44);
|
||||
wav_buffer[ 4] = (total >> 0) & 0xff;
|
||||
wav_buffer[ 5] = (total >> 8) & 0xff;
|
||||
wav_buffer[ 6] = (total >> 16) & 0xff;
|
||||
wav_buffer[ 7] = (total >> 24) & 0xff;
|
||||
wav_buffer[40] = (samples >> 0) & 0xff;
|
||||
wav_buffer[41] = (samples >> 8) & 0xff;
|
||||
wav_buffer[42] = (samples >> 16) & 0xff;
|
||||
wav_buffer[43] = (samples >> 24) & 0xff;
|
||||
for (uint32_t i = 12, p = 44; i < amse_size; i++) {
|
||||
wav_buffer[p++] = buffer[i] & 0xf0;
|
||||
wav_buffer[p++] = (buffer[i] & 0x0f) << 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
PlaySound((LPCTSTR)wav_buffer.data(), NULL, SND_ASYNC | SND_MEMORY | (loops ? 0 : SND_LOOP));
|
||||
}
|
||||
|
||||
void MAKO::stop_pcm()
|
||||
{
|
||||
PlaySound(NULL, NULL, SND_PURGE);
|
||||
if (wav_buffer) {
|
||||
free(wav_buffer);
|
||||
wav_buffer = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool MAKO::check_pcm()
|
||||
{
|
||||
// 再生中でtrue
|
||||
static const char null_wav[45] = {
|
||||
'R' , 'I' , 'F' , 'F' , 0x25, 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' , 0x01, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
return !PlaySound(null_wav, NULL, SND_ASYNC | SND_MEMORY | SND_NOSTOP);
|
||||
}
|
||||
|
||||
bool MAKO::is_cd_available() const
|
||||
{
|
||||
return !playlist.empty();
|
||||
}
|
||||
|
||||
void MAKO::on_mci_notify(const SDL_SysWMmsg* msg)
|
||||
{
|
||||
if (!music)
|
||||
return;
|
||||
if (!music->on_mci_notify(msg))
|
||||
current_music = 0;
|
||||
}
|
||||
@@ -244,9 +244,6 @@ bool NACT::handle_platform_event(const SDL_Event& e)
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case MM_MCINOTIFY:
|
||||
mako->on_mci_notify(msg);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
+2
-2
@@ -5,11 +5,11 @@ Prerequisites:
|
||||
- CMake >=3.20
|
||||
- devkitPro environment ([how to install](https://devkitpro.org/wiki/devkitPro_pacman))
|
||||
- Nintendo Switch development tools package (switch-dev)
|
||||
- SDL2, SDL2_Mixer and SDL2_ttf Switch ports (switch-sdl2, switch-sdl2_ttf, switch-sdl2_mixer)
|
||||
- SDL2 and SDL2_ttf Switch ports (switch-sdl2, switch-sdl2_ttf)
|
||||
|
||||
```sh
|
||||
git submodule update --init
|
||||
sudo (dkp-)pacman -S switch-dev switch-sdl2 switch-sdl2_ttf switch-sdl2_mixer
|
||||
sudo (dkp-)pacman -S switch-dev switch-sdl2 switch-sdl2_ttf
|
||||
mkdir -p out/debug
|
||||
cd out/debug
|
||||
/opt/devkitpro/portlibs/switch/bin/aarch64-none-elf-cmake -DCMAKE_BUILD_TYPE=Debug ../../
|
||||
|
||||
Reference in New Issue
Block a user