diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 50b38e2..7b7cbee 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -17,7 +17,7 @@ jobs: - name: Install Deps run: | sudo apt update - sudo apt install libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev + sudo apt install libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev librtmidi-dev - name: Build run: | diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index eea8927..ac63a4a 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -23,6 +23,7 @@ jobs: ninja:p SDL2:p SDL2_ttf:p + rtmidi:p - name: Checkout uses: actions/checkout@v4 diff --git a/CMakeLists.txt b/CMakeLists.txt index 61fcc48..55eecb8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,7 @@ endif () if (MSVC) # Use statically-linked runtime library. set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + option(BUILD_SHARED_LIBS "Build as shared library" OFF) endif () add_library(ymfm @@ -86,6 +87,16 @@ else() # NOT (ANDROID OR EMSCRIPTEN) get_target_property(SDL2TTF_DLL SDL2_ttf::SDL2_ttf IMPORTED_LOCATION) get_filename_component(SDL2TTF_LIBDIR ${SDL2TTF_DLL} DIRECTORY) + FetchContent_Declare(rtmidi + URL https://github.com/thestk/rtmidi/archive/refs/tags/6.0.0.zip + URL_HASH SHA1=43287aff8c2c03d3b38cff4d44611763f5e149fa) + set(RTMIDI_BUILD_TESTING OFF CACHE BOOL "Build test programs" FORCE) + FetchContent_MakeAvailable(rtmidi) + set_property(TARGET rtmidi PROPERTY COMPILE_WARNING_AS_ERROR OFF) + target_compile_definitions(system3 PRIVATE USE_MIDI) + target_link_libraries(system3 PRIVATE rtmidi) + target_sources(system3 PRIVATE src/sys/mako_midi.cpp) + target_compile_definitions(system3 PRIVATE _CRT_NONSTDC_NO_DEPRECATE _CRT_SECURE_NO_WARNINGS) target_compile_options(system3 PRIVATE /utf-8 /wd4244) target_link_libraries(system3 PRIVATE SDL2::SDL2main SDL2::SDL2 SDL2_ttf::SDL2_ttf) @@ -101,6 +112,12 @@ else() # NOT (ANDROID OR EMSCRIPTEN) pkg_check_modules(SDL2 REQUIRED IMPORTED_TARGET sdl2) pkg_check_modules(SDL2TTF REQUIRED IMPORTED_TARGET SDL2_ttf) target_link_libraries(system3 PRIVATE PkgConfig::SDL2 PkgConfig::SDL2TTF) + pkg_check_modules(RTMIDI IMPORTED_TARGET rtmidi) + if (RTMIDI_FOUND) + target_compile_definitions(system3 PRIVATE USE_MIDI) + target_link_libraries(system3 PRIVATE PkgConfig::RTMIDI) + target_sources(system3 PRIVATE src/sys/mako_midi.cpp) + endif() endif() if (CMAKE_SYSTEM_NAME STREQUAL "Windows") @@ -108,7 +125,6 @@ else() # NOT (ANDROID OR EMSCRIPTEN) src/win/nact_win.cpp src/win/mako.cpp src/win/resource.rc - src/sys/mako_midi.cpp ) target_link_libraries(system3 PRIVATE winmm) if (MSYS) @@ -119,7 +135,6 @@ else() # NOT (ANDROID OR EMSCRIPTEN) target_sources(system3 PRIVATE src/linux/nact_linux.cpp src/linux/mako.cpp - src/sys/mako_midi.cpp ) target_link_libraries(system3 PRIVATE PkgConfig::SDL2MIXER) set(RESOURCE_PATH ${CMAKE_INSTALL_PREFIX}/share/system3/) diff --git a/README.md b/README.md index 7cdba6a..bd44cad 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Toshiya. It supports multiple platforms, including Android and Emscripten. ```bash $ git submodule update --init -$ sudo apt install g++ cmake libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev +$ sudo apt install g++ cmake libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev librtmidi-dev $ mkdir -p out/debug $ cd out/debug $ cmake -DCMAKE_BUILD_TYPE=Debug ../../ @@ -22,7 +22,7 @@ $ sudo make install ```bash $ git submodule update --init -$ brew install cmake pkg-config sdl2 sdl2_ttf sdl2_mixer +$ brew install cmake pkg-config sdl2 sdl2_ttf sdl2_mixer rtmidi $ mkdir -p out/debug $ cd out/debug $ cmake -DCMAKE_BUILD_TYPE=Debug ../../ @@ -34,7 +34,7 @@ $ sudo make install ```bash $ git submodule update --init -$ pacman -S make mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-SDL2 mingw-w64-ucrt-x86_64-SDL2_ttf +$ pacman -S make mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-SDL2 mingw-w64-ucrt-x86_64-SDL2_ttf mingw-w64-ucrt-x86_64-rtmidi $ mkdir -p out/debug $ cd out/debug $ cmake -G"MSYS Makefiles" -DCMAKE_BUILD_TYPE=Debug ../../ @@ -111,12 +111,7 @@ The first line is not used because track 1 on a game CD is usually a data track. #### `-fm` -Uses FM tone generator emulation. If not specified, MIDI sound is used. - -#### `-timiditycfg` _filename_ -Specifies a particular configuration file in a -[format compatible with TiMidity](https://manpages.ubuntu.com/manpages/bionic/en/man5/timidity.cfg.5.html) -to use (this is utilized by SDL_Mixer on some platforms). +By default, system3-sdl2 uses MIDI sound if available. This option forces FM tone generator emulation. #### `-game` _game_id_ As System1-3 have slight variations depending on the game, `system3` uses the diff --git a/src/config.cpp b/src/config.cpp index ff7b16d..6418963 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -102,8 +102,6 @@ Config::Config(int argc, char *argv[]) encoding = argv[++i]; else if (strcmp(argv[i], "-title") == 0) title = argv[++i]; - else if (strcmp(argv[i], "-timiditycfg") == 0) - timidity_cfg = argv[++i]; else if (strcmp(argv[i], "-scanline") == 0) scanline = true; } @@ -160,8 +158,6 @@ void Config::load_ini() encoding = val; else if (!strcasecmp(key, "title")) title = val; - else if (!strcasecmp(key, "timiditycfg")) - timidity_cfg = normalize_path(val); else if (!strcasecmp(key, "scanline")) scanline = to_bool(val, lineno); else diff --git a/src/config.h b/src/config.h index 0603d45..d571dd5 100644 --- a/src/config.h +++ b/src/config.h @@ -28,7 +28,6 @@ struct Config { std::string save_dir; std::string playlist; std::string title; - std::string timidity_cfg; bool use_fm = false; bool no_antialias = false; bool scanline = false; diff --git a/src/linux/mako.cpp b/src/linux/mako.cpp index a20b57e..572bcf3 100644 --- a/src/linux/mako.cpp +++ b/src/linux/mako.cpp @@ -14,20 +14,15 @@ #include "../config.h" #include "dri.h" -// MIX_INIT_FLUIDSYNTH was renamed to MIX_INIT_MID in SDL_mixer 2.0.2 -#if (SDL_MIXER_MAJOR_VERSION == 2) && (SDL_MIXER_MINOR_VERSION == 0) && (SDL_MIXER_PATCHLEVEL < 2) -#define MIX_INIT_MID MIX_INIT_FLUIDSYNTH -#endif - namespace { const int SAMPLE_RATE = 44100; -std::vector smf; Mix_Music *mix_music; Mix_Chunk *mix_chunk; SDL_mutex* fm_mutex; std::unique_ptr fm; +std::unique_ptr midi; void FMHook(void*, Uint8* stream, int len) { SDL_LockMutex(fm_mutex); @@ -43,7 +38,7 @@ MAKO::MAKO(NACT* parent, const Config& config) : next_loop(0), nact(parent) { - int mix_init_flags = MIX_INIT_MID; + int mix_init_flags = 0; if (!config.playlist.empty() && load_playlist(config.playlist.c_str())) mix_init_flags |= MIX_INIT_MP3 | MIX_INIT_OGG; @@ -57,10 +52,15 @@ MAKO::MAKO(NACT* parent, const Config& config) : 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()); + + midi = std::make_unique(); + if (!midi->is_available()) + use_fm = true; } MAKO::~MAKO() { + midi.reset(); stop_pcm(); Mix_CloseAudio(); Mix_Quit(); @@ -121,23 +121,9 @@ void MAKO::play_music(int page) fm = std::make_unique(SAMPLE_RATE, data, true); SDL_UnlockMutex(fm_mutex); Mix_HookMusic(&FMHook, this); - } else { - auto midi = std::make_unique(nact, amus); - if (midi->load_mml(page)) { - midi->load_mda(page); - smf = midi->generate_smf(next_loop); - SDL_RWops *rwops = SDL_RWFromConstMem(smf.data(), smf.size()); - mix_music = Mix_LoadMUSType_RW(rwops, MUS_MID, SDL_TRUE /* freesrc */); - if (!mix_music) { - WARNING("Mix_LoadMUS failed: %s", Mix_GetError()); - return; - } - if (Mix_PlayMusic(mix_music, -1) != 0) { - WARNING("Mix_PlayMusic failed: %s", Mix_GetError()); - Mix_FreeMusic(mix_music); - return; - } - } + } else if (midi->is_available()) { + if (!midi->play(nact, amus, page, next_loop)) + return; } current_music = page; @@ -149,7 +135,6 @@ void MAKO::stop_music() if (mix_music) { Mix_FreeMusic(mix_music); mix_music = NULL; - smf.clear(); } if (fm) { Mix_HookMusic(NULL, NULL); @@ -157,6 +142,9 @@ void MAKO::stop_music() fm = nullptr; SDL_UnlockMutex(fm_mutex); } + if (midi->is_available()) { + midi->stop(); + } current_music = 0; } @@ -171,19 +159,19 @@ bool MAKO::check_music() SDL_UnlockMutex(fm_mutex); return !loop; } - return false; + return midi->is_playing(); } 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; + return; } + SDL_UnlockMutex(fm_mutex); + midi->get_mark(mark, loop); } void MAKO::play_pcm(int page, bool loop) diff --git a/src/sdlmain.cpp b/src/sdlmain.cpp index 511c7d8..fe31ba4 100644 --- a/src/sdlmain.cpp +++ b/src/sdlmain.cpp @@ -28,10 +28,6 @@ int main(int argc, char *argv[]) #endif Config config(argc, argv); - if (!config.timidity_cfg.empty()) { - SDL_setenv("TIMIDITY_CFG", config.timidity_cfg.c_str(), 1); - } - SDL_Init(SDL_INIT_VIDEO); SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"); SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear"); diff --git a/src/sys/mako_midi.cpp b/src/sys/mako_midi.cpp index 06de9b8..5420ae9 100644 --- a/src/sys/mako_midi.cpp +++ b/src/sys/mako_midi.cpp @@ -4,159 +4,152 @@ [ MAKO - midi ] */ +#include +#include +#include + #include "mako_midi.h" #include "nact.h" #include "dri.h" #include "crc32.h" +#define MAX_MMLS (128 * 1024) + #define next_mml(p) mml[p].data[mml[p].addr++] namespace { -// MIDI playing status -struct Play { - int timbre; - int timbre_type; - int level; - int reverb; - int chorus; - int pan; - int key_shift; - int velocity; - int channel; - int note; - int loop_cnt; - int wait_time; - bool loop_flag; - bool note_flag; -}; +std::unique_ptr midiout; -// Standard MIDI File header, where 1 time unit is 10 milliseconds. -const uint8 smf_header[] = { - 0x4d, 0x54, 0x68, 0x64, // "MThd" - 0x00, 0x00, 0x00, 0x06, // Header length - 0x00, 0x00, // Format - 0x00, 0x01, // Number of tracks - 0x00, 0x32, // Division (50 ticks per beat) - 0x4d, 0x54, 0x72, 0x6b, // "MTrk" - 0x00, 0x00, 0x00, 0x00, // Track length (to be filled later) - 0x00, 0xff, 0x51, 0x03, 0x07, 0xa1, 0x20 // Tempo (120 BPM) -}; -const size_t TRACK_LENGTH_OFFSET = 18; - -const int FADEOUT_DURATION = 100; // 1 second -const int MINIMUM_MUSIC_DURATION = 6000; // 60 seconds - -class SmfWriter { -public: - SmfWriter() : - dt(0), total_ticks(0), fadeout_start(-1), - buf(smf_header, smf_header + sizeof(smf_header)) { - for (int i = 0; i < 16; i++) - volume[i] = -1; - } - - std::vector get_smf() { - // "End of track" event - buf.push_back(0x00); - buf.push_back(0xFF); - buf.push_back(0x2F); - buf.push_back(0x00); - - size_t length = buf.size() - (TRACK_LENGTH_OFFSET + 4); - buf[TRACK_LENGTH_OFFSET] = length >> 24 & 0xff; - buf[TRACK_LENGTH_OFFSET + 1] = length >> 16 & 0xff; - buf[TRACK_LENGTH_OFFSET + 2] = length >> 8 & 0xff; - buf[TRACK_LENGTH_OFFSET + 3] = length & 0xff; - - NOTICE("Generated %d seconds of MIDI data", total_ticks / 100); - - return std::move(buf); - } - - bool tick() { - dt++; - total_ticks++; - return fadeout_start < 0 || total_ticks < fadeout_start + FADEOUT_DURATION; - } - - void start_fadeout() { - if (fadeout_start < 0 && total_ticks >= MINIMUM_MUSIC_DURATION) - fadeout_start = total_ticks; - } - - void send_2bytes(uint8 d1, uint8 d2) { - write_delta_time(); - buf.push_back(d1); - buf.push_back(d2); - } - - void send_3bytes(uint8 d1, uint8 d2, uint8 d3) { - write_delta_time(); - buf.push_back(d1); - buf.push_back(d2); - buf.push_back(d3); - } - - void set_volume(uint8 ch, uint8 level) { - volume[ch] = level; - send_3bytes(0xb0 + ch, 0x07, fade(level)); - } - - void update_fader() { - if (fadeout_start < 0) +void initialize_midi() +{ + try { + midiout = std::make_unique(); + int n = midiout->getPortCount(); + if (n == 0) { + NOTICE("No MIDI output device available."); + midiout.reset(); return; - for (int ch = 0; ch < 16; ch++) { - if (volume[ch] < 0) - continue; - send_3bytes(0xb0 + ch, 0x07, fade(volume[ch])); } + for (int i = 0; i < n; i++) { + NOTICE("MIDI #%d: %s", i, midiout->getPortName(i).c_str()); + } + // Open first available port. + midiout->openPort(0); + } catch (RtMidiError &error) { + WARNING("Cannot initialize MIDI: %s", error.getMessage().c_str()); + midiout.reset(); } +} + +void release_midi() +{ + midiout.reset(); +} + +void reset_midi() +{ + uint8_t gs_reset[11] = {0xf0, 0x41, 0x20, 0x42, 0x12, 0x40, 0x00, 0x7f, 0x00, 0x41, 0xf7}; + try { + midiout->sendMessage(gs_reset, sizeof(gs_reset)); + } catch (RtMidiError &error) { + WARNING("MIDI error: %s", error.getMessage().c_str()); + } +} + +void send_2bytes(uint8 d1, uint8 d2) +{ + try { + uint8_t msg[2] = {d1, d2}; + midiout->sendMessage(msg, sizeof(msg)); + } catch (RtMidiError &error) { + WARNING("MIDI error: %s", error.getMessage().c_str()); + } +} + +void send_3bytes(uint8 d1, uint8 d2, uint8 d3) +{ + try { + uint8_t msg[3] = {d1, d2, d3}; + midiout->sendMessage(msg, sizeof(msg)); + } catch (RtMidiError &error) { + WARNING("MIDI error: %s", error.getMessage().c_str()); + } +} + +void stop_midi() +{ + for(int i = 0; i < 10; i++) { + send_3bytes(0xb0 + i, 0x78, 0x00); + } + for(int i = 0; i < 10; i++) { + send_3bytes(0xb0 + i, 0x79, 0x00); + } +} + +class Playback { +public: + Playback(NACT* nact, char* amus, int loop, int seq) : nact(nact), amus(amus), loop_(loop), seq_(seq) {} + bool load_mml(int page); + void load_mda(int page); + void start_midi(); + bool play_midi(SDL_atomic_t* current_loop, SDL_atomic_t* current_mark); + int seq() const { return seq_; } private: - void write_delta_time() { - if (dt < 0x80) { - buf.push_back(dt); - } else if (dt < 0x4000) { - buf.push_back((dt >> 7) | 0x80); - buf.push_back(dt & 0x7f); - } else if (dt < 0x200000) { - buf.push_back((dt >> 14) | 0x80); - buf.push_back((dt >> 7 & 0x7f) | 0x80); - buf.push_back(dt & 0x7f); - } else { - buf.push_back((dt >> 21) | 0x80); - buf.push_back((dt >> 14 & 0x7f) | 0x80); - buf.push_back((dt >> 7 & 0x7f) | 0x80); - buf.push_back(dt & 0x7f); - } - dt = 0; - } + NACT* nact; + char* amus; + int loop_; + int seq_; - int fade(int level) { - if (fadeout_start < 0) - return level; - return level * (fadeout_start + FADEOUT_DURATION - total_ticks) / FADEOUT_DURATION; - } + struct MML { + uint8 data[MAX_MMLS]; + int addr; + }; + MML mml[9]; - std::vector buf; - int dt; - int total_ticks; - int fadeout_start; - int16 volume[16]; + struct MDA { + int bank_select; + int program_change; + int level; + int reverb; + int chorus; + int key_shift; + int pan; + }; + MDA mda[259]; + int drum_map[8][128]; + int tempo, tempo_dif; + + // MIDI playing status + struct Play { + int timbre; + int timbre_type; + int level; + int reverb; + int chorus; + int pan; + int key_shift; + int velocity; + int channel; + int note; + int loop_cnt; + int wait_time; + bool loop_flag; + bool note_flag; + }; + Play play[9]; + bool mute_flag; + float play_time; + Uint32 prev_time; }; -} // namespace - -std::vector MAKOMidi::generate_smf(int num_loops) +void Playback::start_midi() { - SmfWriter w; - Play play[9]; - for(int i = 0; i < 9; i++) { play[i].loop_flag = true; } - bool mute_flag = false; + mute_flag = false; // 再生情報の初期化 for(int i = 0; i < 9; i++) { @@ -194,186 +187,187 @@ std::vector MAKOMidi::generate_smf(int num_loops) play[i + 3].pan = mda[i + 256].pan; } + reset_midi(); + // システムリセット、ボリュームリセット for(int i = 0; i < 10; i++) { - w.send_3bytes(0xb0 + i, 0x79, 0x00); + send_3bytes(0xb0 + i, 0x79, 0x00); } for(int i = 0; i < 10; i++) { - w.set_volume(i, 0x64); + send_3bytes(0xb0 + i, 0x07, 0x64); } // SSG 1-3 の音色 for(int i = 0; i < 3; i++) { if(mda[i + 256].bank_select < 128) { - w.send_3bytes(0xb3 + i, 0x00, mda[i + 256].bank_select); - w.send_3bytes(0xb3 + i, 0x20, 0x00); - w.send_2bytes(0xc3 + i, mda[i + 256].program_change); - w.set_volume(3 + i, mda[i + 256].level); - w.send_3bytes(0xb3 + i, 0x5b, mda[i + 256].reverb); - w.send_3bytes(0xb3 + i, 0x5d, mda[i + 256].chorus); - w.send_3bytes(0xb3 + i, 0x0a, mda[i + 256].pan); + send_3bytes(0xb3 + i, 0x00, mda[i + 256].bank_select); + send_3bytes(0xb3 + i, 0x20, 0x00); + send_2bytes(0xc3 + i, mda[i + 256].program_change); + send_3bytes(0xb3 + i, 0x07, mda[i + 256].level); + send_3bytes(0xb3 + i, 0x5b, mda[i + 256].reverb); + send_3bytes(0xb3 + i, 0x5d, mda[i + 256].chorus); + send_3bytes(0xb3 + i, 0x0a, mda[i + 256].pan); } else { - w.set_volume(9, mda[i + 256].level); - w.send_3bytes(0xb9, 0x5b, mda[i + 256].reverb); - w.send_3bytes(0xb9, 0x5d, mda[i + 256].chorus); - w.send_3bytes(0xb9, 0x0a, mda[i + 256].pan); + send_3bytes(0xb9, 0x07, mda[i + 256].level); + send_3bytes(0xb9, 0x5b, mda[i + 256].reverb); + send_3bytes(0xb9, 0x5d, mda[i + 256].chorus); + send_3bytes(0xb9, 0x0a, mda[i + 256].pan); } } - float play_time = 0; - int current_loop = 0; - int current_mark = 0; - while (w.tick()) { - play_time += 10; // elapse 10ms - // 各パートの更新 - while((play[0].loop_flag || play[1].loop_flag || play[2].loop_flag || - play[3].loop_flag || play[4].loop_flag || play[5].loop_flag || - play[6].loop_flag || play[7].loop_flag || play[8].loop_flag) && play_time > 0.0) { - // 1単位時間だけ経過時間を更新する - for(int i = 0; i < 9; i++) { - if(play[i].loop_flag && play[i].wait_time) { - play[i].wait_time--; - } - } - play_time -= (float)(545455.0 / 480.0 / (float)(tempo + 0x40 - tempo_dif)); - - // 1単位時間だけ各パートを更新 - for(int i = 0; i < 9; i++) { - if(!play[i].loop_flag) { - play[i].wait_time = 1; - } - while(play[i].wait_time == 0) { - int d0 = next_mml(i), d1, d2, d3; - if(d0 == 0xff) { - d1 = next_mml(i); - d2 = next_mml(i); - d3 = next_mml(i); - mml[i].addr = d1 | (d2 << 8) | (d3 << 16); - if(!play[i].note_flag || mute_flag) { - // 再生停止または未使用 - play[i].loop_flag = false; - if(!play[0].loop_flag && !play[1].loop_flag && !play[2].loop_flag && - !play[3].loop_flag && !play[4].loop_flag && !play[5].loop_flag && - !play[6].loop_flag && !play[7].loop_flag && !play[8].loop_flag) { - // 全チャンネルが再生停止 (ループしない曲) - goto finish; - } - play[i].wait_time = 1; - } else { - // 全体としてのループ数を取得 - int loop = ++play[i].loop_cnt; - for(int j = 0; j < 9; j++) { - if(loop > play[j].loop_cnt && play[j].loop_flag) { - loop = play[j].loop_cnt; - } - } - current_loop = loop; - if (num_loops) { - if (current_loop >= num_loops) - goto finish; - } else { - // Infinite loop mode: fade out after one playback. - if (current_loop > 0) - w.start_fadeout(); - } - } - } else if(d0 < 0x80) { - int note = d0; - play[i].wait_time = next_mml(i); - play[i].wait_time |= next_mml(i) << 8; - if(play[i].timbre_type != 128) { - note = drum_map[play[i].timbre_type][note]; - } else { - note = (note + play[i].key_shift) - 64; - } - play[i].note_flag = true; - play[i].note = note; - w.send_3bytes(0x90 + play[i].channel, note, play[i].velocity); - } else if(d0 == 0x80) { - play[i].wait_time = next_mml(i); - play[i].wait_time |= next_mml(i) << 8; - if(play[i].note != 128) { - w.send_3bytes(0x90 + play[i].channel, play[i].note, 0x00); - } - play[i].note = 128; - } else if(d0 == 0xe0) { - current_mark = next_mml(i); - } else if(d0 == 0xe1) { - d1 = next_mml(i); - play[i].velocity += (d1 > 127) ? (d1 - 256) : d1; - } else if(d0 == 0xeb) { - play[i].pan = next_mml(i); - w.send_3bytes(0xb0 + play[i].channel, 0x0a, play[i].pan); - } - else if(d0 == 0xec) { - if(!(nact->crc32_a == CRC32_DPS || nact->crc32_a == CRC32_DPS_SG || nact->crc32_a == CRC32_DPS_SG2 || nact->crc32_a == CRC32_DPS_SG3)) - mute_flag = true; - } - else if(d0 == 0xf5) { - int n = next_mml(i); - if(n != play[i].timbre) { - if(mda[n].bank_select < 128) { - // 通常のパート - play[i].timbre_type = 128; - play[i].channel = i; - w.send_3bytes(0xb0 + play[i].channel, 0x00, mda[n].bank_select); - w.send_3bytes(0xb0 + play[i].channel, 0x20, 0x00); - w.send_2bytes(0xc0 + play[i].channel, mda[n].program_change); - } else { - // ドラムパート (ch.10) - play[i].timbre_type = 255 - mda[n].bank_select; - play[i].channel = 9; - } - if(play[i].level != mda[n].level) { - w.set_volume(play[i].channel, mda[n].level); - } - if(play[i].reverb != mda[n].reverb) { - w.send_3bytes(0xb0 + play[i].channel, 0x5b, mda[n].reverb); - } - if(play[i].chorus != mda[n].chorus) { - w.send_3bytes(0xb0 + play[i].channel, 0x5d, mda[n].chorus); - } - if(play[i].pan != mda[n].pan) { - w.send_3bytes(0xb0 + play[i].channel, 0x0a, mda[n].pan); - } - } - play[i].timbre = n; - play[i].level = mda[n].level; - play[i].reverb = mda[n].reverb; - play[i].chorus = mda[n].chorus; - play[i].key_shift = mda[n].key_shift; - play[i].pan = mda[n].pan; - play[i].velocity = 120; - } else if(d0 == 0xf9) { - play[i].velocity = next_mml(i); - } else if(d0 == 0xfc) { - d1 = next_mml(i); - if(d1 > 127) { - d1 = 256 - d1; - d1 = 0x40 - (d1 > 63 ? 63 : d1); - } else { - d1 = 0x40 + (d1 > 63 ? 63 : d1); - } - w.send_3bytes(0xb0 + play[i].channel, 0x65, 0x00); - w.send_3bytes(0xb0 + play[i].channel, 0x64, 0x01); - w.send_3bytes(0xb0 + play[i].channel, 0x06, d1); - } - } - } - } - w.update_fader(); - } - finish: - for(int j = 0; j < 10; j++) { - w.send_3bytes(0xb0 + j, 0x78, 0x00); - } - for(int j = 0; j < 10; j++) { - w.send_3bytes(0xb0 + j, 0x79, 0x00); - } - return w.get_smf(); + // タイマーリセット + prev_time = SDL_GetTicks(); + play_time = 0; } -bool MAKOMidi::load_mml(int page) +bool Playback::play_midi(SDL_atomic_t* current_loop, SDL_atomic_t* current_mark) +{ + // 経過時間の取得 + Uint32 current_time = SDL_GetTicks(); + if(current_time > prev_time) { + play_time += (float)(current_time - prev_time); + prev_time = current_time; + } + + // 各パートの更新 + while((play[0].loop_flag || play[1].loop_flag || play[2].loop_flag || + play[3].loop_flag || play[4].loop_flag || play[5].loop_flag || + play[6].loop_flag || play[7].loop_flag || play[8].loop_flag) && play_time > 0.0) { + // 1単位時間だけ経過時間を更新する + for(int i = 0; i < 9; i++) { + if(play[i].loop_flag && play[i].wait_time) { + play[i].wait_time--; + } + } + play_time -= (float)(545455.0 / 480.0 / (float)(tempo + 0x40 - tempo_dif)); + + // 1単位時間だけ各パートを更新 + for(int i = 0; i < 9; i++) { + if(!play[i].loop_flag) { + play[i].wait_time = 1; + } + while(play[i].wait_time == 0) { + int d0 = next_mml(i), d1, d2, d3; + if(d0 == 0xff) { + d1 = next_mml(i); + d2 = next_mml(i); + d3 = next_mml(i); + mml[i].addr = d1 | (d2 << 8) | (d3 << 16); + if(!play[i].note_flag || mute_flag) { + // 再生停止または未使用 + play[i].loop_flag = false; + if(!play[0].loop_flag && !play[1].loop_flag && !play[2].loop_flag && + !play[3].loop_flag && !play[4].loop_flag && !play[5].loop_flag && + !play[6].loop_flag && !play[7].loop_flag && !play[8].loop_flag) { + // 全チャンネルが再生停止 (ループしない曲) + stop_midi(); + SDL_AtomicSet(current_loop, 1); + return false; + } + play[i].wait_time = 1; + } else { + // 全体としてのループ数を取得 + int loop = ++play[i].loop_cnt; + for(int j = 0; j < 9; j++) { + if(loop > play[j].loop_cnt && play[j].loop_flag) { + loop = play[j].loop_cnt; + } + } + SDL_AtomicSet(current_loop, loop); + if(loop_ && loop >= loop_) { + // 指定回数だけ再生完了 + stop_midi(); + return false; + } + } + } else if(d0 < 0x80) { + int note = d0; + play[i].wait_time = next_mml(i); + play[i].wait_time |= next_mml(i) << 8; + if(play[i].timbre_type != 128) { + note = drum_map[play[i].timbre_type][note]; + } else { + note = (note + play[i].key_shift) - 64; + } + play[i].note_flag = true; + play[i].note = note; + send_3bytes(0x90 + play[i].channel, note, play[i].velocity); + } else if(d0 == 0x80) { + play[i].wait_time = next_mml(i); + play[i].wait_time |= next_mml(i) << 8; + if(play[i].note != 128) { + send_3bytes(0x90 + play[i].channel, play[i].note, 0x00); + } + play[i].note = 128; + } else if(d0 == 0xe0) { + SDL_AtomicSet(current_mark, next_mml(i)); + } else if(d0 == 0xe1) { + d1 = next_mml(i); + play[i].velocity += (d1 > 127) ? (d1 - 256) : d1; + } else if(d0 == 0xeb) { + play[i].pan = next_mml(i); + send_3bytes(0xb0 + play[i].channel, 0x0a, play[i].pan); + } + else if(d0 == 0xec) { + if(!(nact->crc32_a == CRC32_DPS || nact->crc32_a == CRC32_DPS_SG || nact->crc32_a == CRC32_DPS_SG2 || nact->crc32_a == CRC32_DPS_SG3)) + mute_flag = true; + } + else if(d0 == 0xf5) { + int n = next_mml(i); + if(n != play[i].timbre) { + if(mda[n].bank_select < 128) { + // 通常のパート + play[i].timbre_type = 128; + play[i].channel = i; + send_3bytes(0xb0 + play[i].channel, 0x00, mda[n].bank_select); + send_3bytes(0xb0 + play[i].channel, 0x20, 0x00); + send_2bytes(0xc0 + play[i].channel, mda[n].program_change); + } else { + // ドラムパート (ch.10) + play[i].timbre_type = 255 - mda[n].bank_select; + play[i].channel = 9; + } + if(play[i].level != mda[n].level) { + send_3bytes(0xb0 + play[i].channel, 0x07, mda[n].level); + } + if(play[i].reverb != mda[n].reverb) { + send_3bytes(0xb0 + play[i].channel, 0x5b, mda[n].reverb); + } + if(play[i].chorus != mda[n].chorus) { + send_3bytes(0xb0 + play[i].channel, 0x5d, mda[n].chorus); + } + if(play[i].pan != mda[n].pan) { + send_3bytes(0xb0 + play[i].channel, 0x0a, mda[n].pan); + } + } + play[i].timbre = n; + play[i].level = mda[n].level; + play[i].reverb = mda[n].reverb; + play[i].chorus = mda[n].chorus; + play[i].key_shift = mda[n].key_shift; + play[i].pan = mda[n].pan; + play[i].velocity = 120; + } else if(d0 == 0xf9) { + play[i].velocity = next_mml(i); + } else if(d0 == 0xfc) { + d1 = next_mml(i); + if(d1 > 127) { + d1 = 256 - d1; + d1 = 0x40 - (d1 > 63 ? 63 : d1); + } else { + d1 = 0x40 + (d1 > 63 ? 63 : d1); + } + send_3bytes(0xb0 + play[i].channel, 0x65, 0x00); + send_3bytes(0xb0 + play[i].channel, 0x64, 0x01); + send_3bytes(0xb0 + play[i].channel, 0x06, d1); + } + } + } + } + return true; +} + +bool Playback::load_mml(int page) { // データ読み込み DRI* dri = new DRI(); @@ -543,7 +537,7 @@ bool MAKOMidi::load_mml(int page) return true; } -void MAKOMidi::load_mda(int page) +void Playback::load_mda(int page) { // 未設定時の音色設定 (Piano, Acoustic Bass Drum) for(int i = 0; i < 256 + 3; i++) { @@ -620,3 +614,132 @@ void MAKOMidi::load_mda(int page) delete dri; } +} // namespace + +struct MAKOMidi::Command { + enum Type { + PLAY, + STOP, + TERMINATE + } type; + std::unique_ptr playback; + + Command(Type t, std::unique_ptr p = {}) : type(t), playback(std::move(p)) {} +}; + +MAKOMidi::MAKOMidi() +{ + initialize_midi(); + if (midiout) { + thread = SDL_CreateThread(thread_main, "MAKOMidi", this); + queue_mutex = SDL_CreateMutex(); + } + SDL_AtomicSet(¤t_seq, 0); + SDL_AtomicSet(¤t_loop, 0); + SDL_AtomicSet(¤t_mark, 0); +} + +MAKOMidi::~MAKOMidi() +{ + if (thread) { + SDL_LockMutex(queue_mutex); + queue.push(std::make_unique(Command::TERMINATE)); + SDL_UnlockMutex(queue_mutex); + SDL_WaitThread(thread, NULL); + SDL_DestroyMutex(queue_mutex); + } + release_midi(); +} + +bool MAKOMidi::is_available() +{ + return !!midiout; +} + +bool MAKOMidi::play(NACT* nact, char* amus, int page, int loop) +{ + int seq = ++next_seq; + auto playback = std::make_unique(nact, amus, loop, seq); + if (!playback->load_mml(page)) + return false; + playback->load_mda(page); + SDL_AtomicSet(¤t_seq, seq); + SDL_LockMutex(queue_mutex); + queue.push(std::make_unique(Command::PLAY, std::move(playback))); + SDL_UnlockMutex(queue_mutex); + return true; +} + +void MAKOMidi::stop() +{ + SDL_AtomicSet(¤t_seq, 0); + SDL_LockMutex(queue_mutex); + queue.push(std::make_unique(Command::STOP)); + SDL_UnlockMutex(queue_mutex); +} + +bool MAKOMidi::is_playing() +{ + return SDL_AtomicGet(¤t_seq) != 0; +} + +void MAKOMidi::get_mark(int* mark, int* loop) +{ + *mark = SDL_AtomicGet(¤t_mark); + *loop = SDL_AtomicGet(¤t_loop); +} + +void MAKOMidi::thread_loop() +{ + std::unique_ptr current; + + for (;;) { + SDL_LockMutex(queue_mutex); + while (!queue.empty()) { + auto cmd = std::move(queue.front()); + queue.pop(); + SDL_UnlockMutex(queue_mutex); + switch (cmd->type) { + case Command::PLAY: + if (current) + stop_midi(); + current = std::move(cmd->playback); + current->start_midi(); + SDL_AtomicSet(¤t_loop, 0); + SDL_AtomicSet(¤t_mark, 0); + SDL_Delay(100); // ? + break; + case Command::STOP: + if (current) { + stop_midi(); + current.reset(); + } + break; + case Command::TERMINATE: + return; + } + SDL_LockMutex(queue_mutex); + } + SDL_UnlockMutex(queue_mutex); + if (current) { + if (!current->play_midi(¤t_loop, ¤t_mark)) { + SDL_AtomicCAS(¤t_seq, current->seq(), 0); + current.reset(); + } + } + SDL_Delay(10); + } +} + +// static +int MAKOMidi::thread_main(void* data) +{ + SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH); + + MAKOMidi* mm = reinterpret_cast(data); + mm->thread_loop(); + + stop_midi(); + + return 0; +} diff --git a/src/sys/mako_midi.h b/src/sys/mako_midi.h index f313ff7..79cc10e 100644 --- a/src/sys/mako_midi.h +++ b/src/sys/mako_midi.h @@ -1,42 +1,50 @@ #ifndef _MAKO_MIDI_H_ #define _MAKO_MIDI_H_ -#include +#include +#include +#include #include "../common.h" -#define MAX_MMLS (128 * 1024) - class NACT; +#ifdef USE_MIDI + class MAKOMidi { public: - MAKOMidi(NACT* nact, char* amus) : nact(nact), amus(amus) {} - bool load_mml(int page); - void load_mda(int page); - std::vector generate_smf(int current_max); + MAKOMidi(); + ~MAKOMidi(); + bool is_available(); + bool play(NACT* nact, char* amus, int page, int loop); + void stop(); + bool is_playing(); + void get_mark(int* mark, int* loop); private: - NACT* nact; - char* amus; + struct Command; + std::queue> queue; + SDL_mutex* queue_mutex = nullptr; + SDL_Thread* thread = nullptr; + SDL_atomic_t current_seq; + SDL_atomic_t current_loop; + SDL_atomic_t current_mark; + int next_seq = 0; - struct MML { - uint8 data[MAX_MMLS]; - int addr; - }; - MML mml[9]; - - struct MDA { - int bank_select; - int program_change; - int level; - int reverb; - int chorus; - int key_shift; - int pan; - }; - MDA mda[259]; - int drum_map[8][128]; - int tempo, tempo_dif; + void thread_loop(); + static int thread_main(void* data); }; +#else // USE_MIDI + +class MAKOMidi { +public: + bool is_available() { return false; } + bool play(NACT* nact, char* amus, int page, int loop) { return false; } + void stop() {} + bool is_playing() { return false; } + void get_mark(int* mark, int* loop) { *mark = *loop = 0; } +}; + +#endif // USE_MIDI + #endif // _MAKO_MIDI_H_ diff --git a/src/win/mako.cpp b/src/win/mako.cpp index b51da8c..188486c 100644 --- a/src/win/mako.cpp +++ b/src/win/mako.cpp @@ -41,11 +41,6 @@ enum MakoThreadMessage { MAKO_EXIT }; -enum MakoOpenFlags { - OPEN_CD = 0, - OPEN_MIDI = 1, -}; - const char* mci_geterror(DWORD err) { static char buf[128]; @@ -82,8 +77,7 @@ private: switch (msg.message) { case MAKO_OPEN: close(); - open(reinterpret_cast(msg.wParam), - static_cast(msg.lParam)); + open(reinterpret_cast(msg.wParam)); break; case MAKO_PLAY: @@ -101,18 +95,13 @@ private: } } - void open(std::string* file, MakoOpenFlags open_flags) + void open(std::string* file) { file_playing = file; - is_midi = open_flags & OPEN_MIDI; MCI_OPEN_PARMS open; open.lpstrElementName = file_playing->c_str(); DWORD flags = MCI_OPEN_ELEMENT | MCI_WAIT; - if (is_midi) { - flags |= MCI_OPEN_TYPE | MCI_OPEN_TYPE_ID; - open.lpstrDeviceType = (LPCSTR)MCI_DEVTYPE_SEQUENCER; - } MCIERROR err = mciSendCommand(0, MCI_OPEN, flags, (DWORD_PTR)&open); if (err) { WARNING("MCI_OPEN failed: %s: %s", @@ -139,8 +128,6 @@ private: devid = 0; } if (file_playing) { - if (is_midi) - DeleteFile(file_playing->c_str()); // remove temporary file delete file_playing; file_playing = nullptr; } @@ -149,7 +136,6 @@ private: HWND hwnd_notify; unsigned thread_id; std::string* file_playing = nullptr; - bool is_midi; MCIDEVICEID devid = 0; }; @@ -159,38 +145,7 @@ 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, OPEN_CD); - mci_thread->post_message(MAKO_PLAY, 0, 0); - playing = true; - } - - Music(std::unique_ptr midi, int loop) : loops(loop ? 1 : 0) { - std::vector smf = midi->generate_smf(loop); - char path[MAX_PATH + 1]; - if (!GetTempPath(sizeof(path), path)) { - WARNING("GetTempPath failed: 0x%x", GetLastError()); - return; - } - char fname[MAX_PATH + 1]; - if (!GetTempFileName(path, "mid", 0, fname)) { - WARNING("GetTempFileName failed: 0x%x", GetLastError()); - return; - } - FILE* fp = fopen(fname, "wb"); - if (!fp) { - WARNING("Cannot open %s: %s", fname, strerror(errno)); - return; - } - if (fwrite(smf.data(), smf.size(), 1, fp) != 1) { - WARNING("Cannot write to %s: %s", fname, strerror(errno)); - fclose(fp); - DeleteFile(fname); - return; - } - fclose(fp); - - std::string* fname_str = new std::string(fname); - mci_thread->post_message(MAKO_OPEN, (WPARAM)fname_str, OPEN_MIDI); + mci_thread->post_message(MAKO_OPEN, (WPARAM)fname_str, 0); mci_thread->post_message(MAKO_PLAY, 0, 0); playing = true; } @@ -235,6 +190,7 @@ Music* music; uint8* wav_buffer; SDL_mutex* fm_mutex; std::unique_ptr fm; +std::unique_ptr midi; void audio_callback(void*, Uint8* stream, int len) { SDL_LockMutex(fm_mutex); @@ -279,6 +235,10 @@ MAKO::MAKO(NACT* parent, const Config& config) : if (!SDL_GetWindowWMInfo(g_window, &info)) parent->fatal("SDL_GetWindowWMInfo failed: %s", SDL_GetError()); mci_thread = new MCIThread(parent, info.info.win.window); + + midi = std::make_unique(); + if (!midi->is_available()) + use_fm = true; } MAKO::~MAKO() @@ -287,6 +247,7 @@ MAKO::~MAKO() 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(); @@ -337,12 +298,9 @@ void MAKO::play_music(int page) fm = std::make_unique(SAMPLE_RATE, data, true); SDL_UnlockMutex(fm_mutex); SDL_PauseAudio(0); - } else { - auto midi = std::make_unique(nact, amus); - if (!midi->load_mml(page)) + } else if (midi->is_available()) { + if (!midi->play(nact, amus, page, next_loop)) return; - midi->load_mda(page); - music = new Music(std::move(midi), next_loop); } current_music = page; } @@ -359,6 +317,9 @@ void MAKO::stop_music() fm = nullptr; SDL_UnlockMutex(fm_mutex); } + if (midi->is_available()) { + midi->stop(); + } current_music = 0; } @@ -371,7 +332,11 @@ bool MAKO::check_music() SDL_UnlockMutex(fm_mutex); return !loop; } - return music && music->is_playing(); + else if (music) { + return music->is_playing(); + } else { + return midi->is_playing(); + } } void MAKO::select_sound(BGMDevice dev) @@ -386,7 +351,10 @@ void MAKO::select_sound(BGMDevice dev) case BGM_MIDI: for (int i = 1; i <= 99; i++) cd_track[i] = 0; - use_fm = dev == BGM_FM; + if (midi->is_available()) + use_fm = dev == BGM_FM; + else + dev = BGM_FM; break; case BGM_CD: @@ -435,11 +403,11 @@ void MAKO::get_mark(int* mark, int* loop) SDL_LockMutex(fm_mutex); if (fm) { fm->get_mark(mark, loop); - } else { - *mark = 0; - *loop = 0; + SDL_UnlockMutex(fm_mutex); + return; } SDL_UnlockMutex(fm_mutex); + midi->get_mark(mark, loop); } void MAKO::play_pcm(int page, bool loop) diff --git a/switch/README.md b/switch/README.md index f0196a4..108b117 100644 --- a/switch/README.md +++ b/switch/README.md @@ -20,6 +20,3 @@ make 1. Copy the resulting `system3.nro` file to a folder inside your Nintendo Switch's SD card (eg. `/switch/[game name]/`) 2. Copy all the game files, BGM files and configuration. 3. Open system3 through the Homebrew Launcher - -## Notes -- The Switch needs its own MIDI Sound Library (eg. freepats, configurable through `timiditycfg` on the `system3.ini` config) due to the lack of built-in MIDI files.