mirror of
https://github.com/kichikuou/system3-sdl2.git
synced 2026-09-28 09:58:07 +03:00
Centralize the differences between SDL2 and SDL3 behind compatibility interfaces and select the appropriate implementations with the SYSTEM3_SDL_VERSION CMake option. Android remains on SDL2 for now.
463 lines
11 KiB
C++
463 lines
11 KiB
C++
/*
|
|
ALICE SOFT SYSTEM 3 for Win32
|
|
|
|
[ MAKO ]
|
|
*/
|
|
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <limits.h>
|
|
#include "sdl_compat.h"
|
|
|
|
#include "mako.h"
|
|
#include "mako_midi.h"
|
|
#include "music_decoder.h"
|
|
#include "fm/mako_ymfm.h"
|
|
#include "config.h"
|
|
#include "dri.h"
|
|
#include "game_id.h"
|
|
|
|
namespace {
|
|
|
|
// Plays a single BGM file (MP3, OGG, or WAV, chosen by file extension),
|
|
// decoding on demand into an audio stream bound to the playback device.
|
|
class Music {
|
|
public:
|
|
// loops: number of times to play; 0 means loop forever.
|
|
Music(SDL_AudioDeviceID device, const std::string& path, int loops)
|
|
: decoder(create_music_decoder(path)), loops_(loops)
|
|
{
|
|
if (!decoder)
|
|
return;
|
|
const SDL_AudioSpec& src_spec = decoder->spec();
|
|
SDL_AudioSpec device_spec;
|
|
SDL_GetAudioDeviceFormat(device, &device_spec, nullptr);
|
|
stream = SDL_CreateAudioStream(&src_spec, &device_spec);
|
|
if (!stream) {
|
|
WARNING("SDL_CreateAudioStream failed: %s", SDL_GetError());
|
|
return;
|
|
}
|
|
playing = true;
|
|
SDL_SetAudioStreamGetCallback(stream, [](void* self, SDL_AudioStream*, int additional_amount, int) {
|
|
static_cast<Music*>(self)->audio_callback(additional_amount);
|
|
}, this);
|
|
SDL_BindAudioStream(device, stream);
|
|
}
|
|
|
|
~Music()
|
|
{
|
|
if (stream)
|
|
SDL_DestroyAudioStream(stream);
|
|
}
|
|
|
|
bool is_open() const { return stream != nullptr; }
|
|
|
|
bool is_playing() const
|
|
{
|
|
SDL_LockAudioStream(stream);
|
|
bool result = playing;
|
|
SDL_UnlockAudioStream(stream);
|
|
return result;
|
|
}
|
|
|
|
private:
|
|
void audio_callback(int additional_amount)
|
|
{
|
|
if (!playing)
|
|
return;
|
|
|
|
while (SDL_GetAudioStreamAvailable(stream) < additional_amount && !input_finished)
|
|
decode();
|
|
if (input_finished && SDL_GetAudioStreamAvailable(stream) <= 0)
|
|
playing = false;
|
|
}
|
|
|
|
// Decodes and queues one chunk, handling EOF and decoder errors.
|
|
void decode()
|
|
{
|
|
constexpr int CHUNK_FRAMES = 1024;
|
|
|
|
DecodedChunk chunk = decoder->decode(CHUNK_FRAMES);
|
|
|
|
if (chunk.frames == 0) {
|
|
if (loops_ && --loops_ == 0) {
|
|
SDL_FlushAudioStream(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_PutAudioStreamData(stream, chunk.data, bytes)) {
|
|
WARNING("SDL_PutAudioStreamData failed: %s", SDL_GetError());
|
|
input_finished = true;
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<MusicDecoder> decoder;
|
|
SDL_AudioStream* stream = nullptr;
|
|
int loops_; // number of times to play, 0 for infinite loop
|
|
bool playing = false;
|
|
bool input_finished = false;
|
|
};
|
|
|
|
#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
|
|
|
|
class FmStream {
|
|
public:
|
|
FmStream(SDL_AudioDeviceID device, std::vector<uint8_t> data)
|
|
: FmStream(device, std::move(data), device_format(device)) {}
|
|
~FmStream() { SDL_DestroyAudioStream(stream); } // unbinds and stops fill()
|
|
|
|
// FM music keeps playing until every channel has looped.
|
|
bool is_playing() { int mark, loop; get_mark(&mark, &loop); return !loop; }
|
|
|
|
void get_mark(int* mark, int* loop) {
|
|
SDL_LockAudioStream(stream);
|
|
ymfm.get_mark(mark, loop);
|
|
SDL_UnlockAudioStream(stream);
|
|
}
|
|
|
|
private:
|
|
FmStream(SDL_AudioDeviceID device, std::vector<uint8_t> data, const SDL_AudioSpec& device_spec)
|
|
: ymfm(device_spec.freq, std::move(data))
|
|
{
|
|
SDL_AudioSpec src_spec = { SDL_AUDIO_S16, 2, device_spec.freq };
|
|
stream = SDL_CreateAudioStream(&src_spec, &device_spec);
|
|
SDL_SetAudioStreamGetCallback(stream, [](void* self, SDL_AudioStream*, int additional_amount, int) {
|
|
static_cast<FmStream*>(self)->fill(additional_amount);
|
|
}, this);
|
|
SDL_BindAudioStream(device, stream);
|
|
}
|
|
|
|
static SDL_AudioSpec device_format(SDL_AudioDeviceID device) {
|
|
SDL_AudioSpec spec;
|
|
SDL_GetAudioDeviceFormat(device, &spec, nullptr);
|
|
return spec;
|
|
}
|
|
|
|
void fill(int additional_amount) {
|
|
const int CHUNK = 4096; // bytes; 1024 stereo S16 frames
|
|
int16_t buffer[CHUNK / 2];
|
|
while (additional_amount > 0) {
|
|
int len = additional_amount < CHUNK ? additional_amount : CHUNK;
|
|
ymfm.Process(buffer, len / 4);
|
|
SDL_PutAudioStreamData(stream, buffer, len);
|
|
additional_amount -= len;
|
|
}
|
|
}
|
|
|
|
MakoYmfm ymfm;
|
|
SDL_AudioStream* stream;
|
|
};
|
|
|
|
// The audio device. Each sound source (music, fm, pcm) creates its own
|
|
// SDL_AudioStream and binds it to this device.
|
|
SDL_AudioDeviceID g_device;
|
|
|
|
std::unique_ptr<Music> music;
|
|
std::unique_ptr<FmStream> fm;
|
|
std::unique_ptr<MAKOMidi> midi;
|
|
|
|
// PCM playback. pcm_loops is the number of remaining plays, or -1 for an
|
|
// infinite loop. pcm_stream's get-callback re-feeds pcm_src.
|
|
SDL_AudioStream* pcm_stream;
|
|
std::vector<uint8_t> pcm_src;
|
|
int pcm_loops;
|
|
bool pcm_input_finished;
|
|
|
|
void SDLCALL pcm_audio_callback(void*, SDL_AudioStream* stream, int additional_amount, int /*total_amount*/)
|
|
{
|
|
while (SDL_GetAudioStreamAvailable(stream) < additional_amount && !pcm_input_finished) {
|
|
if (pcm_loops == 0) {
|
|
SDL_FlushAudioStream(stream);
|
|
pcm_input_finished = true;
|
|
break;
|
|
}
|
|
SDL_PutAudioStreamData(stream, pcm_src.data(), static_cast<int>(pcm_src.size()));
|
|
if (pcm_loops > 0)
|
|
pcm_loops--;
|
|
}
|
|
}
|
|
|
|
} // 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());
|
|
|
|
#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");
|
|
amse.open("AMSE.DAT");
|
|
mda.open("AMUS.MDA");
|
|
for (int i = 1; i <= 99; i++)
|
|
cd_track[i] = 0;
|
|
|
|
SDL_InitSubSystem(SDL_INIT_AUDIO);
|
|
g_device = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, nullptr);
|
|
if (!g_device)
|
|
WARNING("Cannot open audio device: %s", SDL_GetError());
|
|
|
|
midi = std::make_unique<MAKOMidi>(config.midi_device);
|
|
if (!midi->is_available())
|
|
use_fm = true;
|
|
}
|
|
|
|
MAKO::~MAKO()
|
|
{
|
|
stop_music();
|
|
stop_pcm();
|
|
midi.reset();
|
|
|
|
if (g_device)
|
|
SDL_CloseAudioDevice(g_device);
|
|
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
|
}
|
|
|
|
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)
|
|
{
|
|
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;
|
|
const char* file = playlist[track];
|
|
#ifdef __ANDROID__
|
|
// 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
|
|
music = std::make_unique<Music>(g_device, file, next_loop);
|
|
if (!music->is_open()) {
|
|
music.reset();
|
|
return;
|
|
}
|
|
} else if (use_fm) {
|
|
std::vector<uint8_t> data = amus.load(page);
|
|
if (data.empty())
|
|
return;
|
|
fm = std::make_unique<FmStream>(g_device, std::move(data));
|
|
} 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()
|
|
{
|
|
music.reset();
|
|
fm.reset();
|
|
if (midi->is_available())
|
|
midi->stop();
|
|
current_music = 0;
|
|
}
|
|
|
|
bool MAKO::check_music()
|
|
{
|
|
if (fm)
|
|
return fm->is_playing();
|
|
if (music)
|
|
return music->is_playing();
|
|
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)
|
|
{
|
|
if (fm) {
|
|
fm->get_mark(mark, loop);
|
|
return;
|
|
}
|
|
midi->get_mark(mark, loop);
|
|
}
|
|
|
|
void MAKO::play_pcm(int page, int loops)
|
|
{
|
|
stop_pcm();
|
|
|
|
SDL_AudioSpec device_spec;
|
|
SDL_GetAudioDeviceFormat(g_device, &device_spec, nullptr);
|
|
|
|
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_IO(SDL_IOFromConstMem(data.data(), static_cast<int>(data.size())), 1, &spec, &wav, &wavlen)) {
|
|
WARNING("SDL_LoadWAV_IO failed: %s", SDL_GetError());
|
|
return;
|
|
}
|
|
src.assign(wav, wav + wavlen);
|
|
SDL_free(wav);
|
|
stream = SDL_CreateAudioStream(&spec, &device_spec);
|
|
} else {
|
|
// AMSE形式 (乙女戦記)
|
|
data = amse.load(page);
|
|
if (data.empty())
|
|
return;
|
|
uint32_t amse_size = SDL_Swap32LE(*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);
|
|
}
|
|
SDL_AudioSpec src_spec = { SDL_AUDIO_U8, 1, 8000 };
|
|
stream = SDL_CreateAudioStream(&src_spec, &device_spec);
|
|
}
|
|
if (!stream) {
|
|
WARNING("SDL_CreateAudioStream failed: %s", SDL_GetError());
|
|
return;
|
|
}
|
|
|
|
pcm_src = std::move(src);
|
|
pcm_loops = loops ? loops : -1;
|
|
pcm_input_finished = false;
|
|
pcm_stream = stream;
|
|
SDL_SetAudioStreamGetCallback(pcm_stream, pcm_audio_callback, nullptr);
|
|
SDL_BindAudioStream(g_device, pcm_stream);
|
|
}
|
|
|
|
void MAKO::stop_pcm()
|
|
{
|
|
// Destroy the stream first (unbinds and stops its get-callback), then it is
|
|
// safe to drop the source buffer the callback was reading.
|
|
if (pcm_stream) {
|
|
SDL_DestroyAudioStream(pcm_stream);
|
|
pcm_stream = nullptr;
|
|
}
|
|
pcm_src.clear();
|
|
pcm_input_finished = false;
|
|
}
|
|
|
|
bool MAKO::check_pcm()
|
|
{
|
|
if (!pcm_stream)
|
|
return false;
|
|
SDL_LockAudioStream(pcm_stream);
|
|
bool playing = pcm_stream &&
|
|
(!pcm_input_finished || SDL_GetAudioStreamAvailable(pcm_stream) > 0);
|
|
SDL_UnlockAudioStream(pcm_stream);
|
|
return playing;
|
|
}
|
|
|
|
bool MAKO::is_cd_available() const
|
|
{
|
|
return !playlist.empty();
|
|
}
|