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.
This commit is contained in:
kichikuou
2024-08-16 10:11:41 +09:00
parent 3a763412c9
commit 41e69d7610
12 changed files with 521 additions and 435 deletions
+1 -1
View File
@@ -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: |
+1
View File
@@ -23,6 +23,7 @@ jobs:
ninja:p
SDL2:p
SDL2_ttf:p
rtmidi:p
- name: Checkout
uses: actions/checkout@v4
+17 -2
View File
@@ -17,6 +17,7 @@ endif ()
if (MSVC)
# Use statically-linked runtime library.
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>: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/)
+4 -9
View File
@@ -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
-4
View File
@@ -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
-1
View File
@@ -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;
+18 -30
View File
@@ -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<uint8> smf;
Mix_Music *mix_music;
Mix_Chunk *mix_chunk;
SDL_mutex* fm_mutex;
std::unique_ptr<MakoYmfm> fm;
std::unique_ptr<MAKOMidi> 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<MAKOMidi>();
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<MakoYmfm>(SAMPLE_RATE, data, true);
SDL_UnlockMutex(fm_mutex);
Mix_HookMusic(&FMHook, this);
} else {
auto midi = std::make_unique<MAKOMidi>(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)
-4
View File
@@ -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");
+419 -296
View File
@@ -4,159 +4,152 @@
[ MAKO - midi ]
*/
#include <memory>
#include <SDL.h>
#include <RtMidi.h>
#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<RtMidiOut> 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<uint8> 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<RtMidiOut>();
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<uint8> 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<uint8> 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<uint8> 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> playback;
Command(Type t, std::unique_ptr<Playback> 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(&current_seq, 0);
SDL_AtomicSet(&current_loop, 0);
SDL_AtomicSet(&current_mark, 0);
}
MAKOMidi::~MAKOMidi()
{
if (thread) {
SDL_LockMutex(queue_mutex);
queue.push(std::make_unique<MAKOMidi::Command>(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<Playback>(nact, amus, loop, seq);
if (!playback->load_mml(page))
return false;
playback->load_mda(page);
SDL_AtomicSet(&current_seq, seq);
SDL_LockMutex(queue_mutex);
queue.push(std::make_unique<MAKOMidi::Command>(Command::PLAY, std::move(playback)));
SDL_UnlockMutex(queue_mutex);
return true;
}
void MAKOMidi::stop()
{
SDL_AtomicSet(&current_seq, 0);
SDL_LockMutex(queue_mutex);
queue.push(std::make_unique<MAKOMidi::Command>(Command::STOP));
SDL_UnlockMutex(queue_mutex);
}
bool MAKOMidi::is_playing()
{
return SDL_AtomicGet(&current_seq) != 0;
}
void MAKOMidi::get_mark(int* mark, int* loop)
{
*mark = SDL_AtomicGet(&current_mark);
*loop = SDL_AtomicGet(&current_loop);
}
void MAKOMidi::thread_loop()
{
std::unique_ptr<Playback> 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(&current_loop, 0);
SDL_AtomicSet(&current_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(&current_loop, &current_mark)) {
SDL_AtomicCAS(&current_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<MAKOMidi*>(data);
mm->thread_loop();
stop_midi();
return 0;
}
+35 -27
View File
@@ -1,42 +1,50 @@
#ifndef _MAKO_MIDI_H_
#define _MAKO_MIDI_H_
#include <vector>
#include <memory>
#include <queue>
#include <SDL.h>
#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<uint8> 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<std::unique_ptr<Command>> 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_
+26 -58
View File
@@ -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<std::string*>(msg.wParam),
static_cast<MakoOpenFlags>(msg.lParam));
open(reinterpret_cast<std::string*>(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<MAKOMidi> midi, int loop) : loops(loop ? 1 : 0) {
std::vector<uint8> 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<MakoYmfm> fm;
std::unique_ptr<MAKOMidi> 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<MAKOMidi>();
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<MakoYmfm>(SAMPLE_RATE, data, true);
SDL_UnlockMutex(fm_mutex);
SDL_PauseAudio(0);
} else {
auto midi = std::make_unique<MAKOMidi>(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)
-3
View File
@@ -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.