From 41e69d76103fc8ca83830e3ee8c8b1f95c30d6bd Mon Sep 17 00:00:00 2001 From: kichikuou Date: Wed, 14 Aug 2024 13:45:27 +0900 Subject: [PATCH] Real-time MIDI playback using RtMidi Before this, MIDI playback was done by generating an SMF data stream, which was then played using platform-specific methods (MCI on Windows, MediaPlayer on Android, SDL_Mixer on other platforms). However, this approach had an issue where the Y18 command (which reads loop counts and markers) did not work correctly. With this change, the MAKOMidi class now uses the RtMidi library to control MIDI devices in real-time. This approach was used in the original "System3 for win32", but has now been made cross-platform. The dependency on RtMidi is optional. If RtMidi is not available at build time or a MIDI device is unavailable at runtime, FM synthesis will be used. --- .github/workflows/linux.yml | 2 +- .github/workflows/windows.yml | 1 + CMakeLists.txt | 19 +- README.md | 13 +- src/config.cpp | 4 - src/config.h | 1 - src/linux/mako.cpp | 48 +-- src/sdlmain.cpp | 4 - src/sys/mako_midi.cpp | 715 ++++++++++++++++++++-------------- src/sys/mako_midi.h | 62 +-- src/win/mako.cpp | 84 ++-- switch/README.md | 3 - 12 files changed, 521 insertions(+), 435 deletions(-) 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.