#include "pch.h" #include "midi.h" #include "pb.h" #include "pinball.h" #ifndef TSF_RENDER_EFFECTSAMPLEBLOCK #define TSF_RENDER_EFFECTSAMPLEBLOCK 64 #endif midi_song midi::currentMidi = {false}; #ifdef MUSIC_TSF tml_message* midi::currentMessage = nullptr; static float midiTime = 0.0f; static float sampPerSec = 1000.0 / 22050.0; static tsf* tsfSynth = nullptr; void midi::sdl_audio_callback(void* data, Uint8 *stream, int len) { memset(stream, 0, len); if (tsfSynth == nullptr) { return; } int SampleBlock, SampleCount = (len / (2 * sizeof(short))); for (SampleBlock = TSF_RENDER_EFFECTSAMPLEBLOCK; SampleCount; SampleCount -= SampleBlock, stream += (SampleBlock * (2 * sizeof(short)))) { if (SampleBlock > SampleCount) SampleBlock = SampleCount; for (midiTime += SampleBlock * sampPerSec; midi::currentMessage && midiTime >= midi::currentMessage->time; ) { switch (midi::currentMessage->type) { case TML_PROGRAM_CHANGE: tsf_channel_set_presetnumber(tsfSynth, midi::currentMessage->channel, midi::currentMessage->program, (midi::currentMessage->channel == 9)); tsf_channel_midi_control(tsfSynth, midi::currentMessage->channel, TML_ALL_NOTES_OFF, 0); break; case TML_NOTE_ON: tsf_channel_note_on(tsfSynth, midi::currentMessage->channel, midi::currentMessage->key, midi::currentMessage->velocity / 127.0f); break; case TML_NOTE_OFF: tsf_channel_note_off(tsfSynth, midi::currentMessage->channel, midi::currentMessage->key); break; case TML_PITCH_BEND: tsf_channel_set_pitchwheel(tsfSynth, midi::currentMessage->channel, midi::currentMessage->pitch_bend); break; case TML_CONTROL_CHANGE: tsf_channel_midi_control(tsfSynth, midi::currentMessage->channel, midi::currentMessage->control, midi::currentMessage->control_value); break; } if (midi::currentMessage->next == nullptr) { midiTime = 0.0f; midi::currentMessage = midi::currentMidi.start; } else { midi::currentMessage = midi::currentMessage->next; } } // Render the block of audio samples in float format tsf_render_short(tsfSynth, (short*)stream, SampleBlock, 0); } } #endif constexpr uint32_t FOURCC(uint8_t a, uint8_t b, uint8_t c, uint8_t d) { return static_cast((d << 24) | (c << 16) | (b << 8) | a); } int ToVariableLen(uint32_t value, uint32_t& dst) { auto count = 1; dst = value & 0x7F; while ((value >>= 7)) { dst <<= 8; dst |= ((value & 0x7F) | 0x80); count++; } return count; } int midi::play_pb_theme(int flag) { if (pb::FullTiltMode) { return play_ft(&track1); } #ifdef MUSIC_SDL int result = 0; music_stop(); if (currentMidi.valid) result = Mix_PlayMusic(currentMidi.handle, -1); return result; #elif defined(MUSIC_TSF) int result = 0; if (currentMidi.valid) { currentMessage = currentMidi.start; midiTime = 0.0f; // Mix_HookMusic(midi::sdl_audio_callback, nullptr); result = 1; } return result; #else return 0; #endif } int midi::music_stop() { if (pb::FullTiltMode) { return stop_ft(); } #ifdef MUSIC_SDL return Mix_HaltMusic(); #else return 0; #endif } #ifdef MUSIC_TSF extern unsigned char gm_sf2[]; extern unsigned int gm_sf2_len; #endif int midi::music_init() { if (pb::FullTiltMode) { return music_init_ft(); } #if defined(MUSIC_SDL) // File name is in lower case, while game data is in upper case. std::string fileName = pinball::get_rc_string(156, 0); std::transform(fileName.begin(), fileName.end(), fileName.begin(), [](unsigned char c) { return std::toupper(c); }); auto midiPath = pinball::make_path_name(fileName); auto song = Mix_LoadMUS(midiPath.c_str()); if (song) { currentMidi = {true, song}; } else { currentMidi = {false}; } return currentMidi.valid; #elif defined(MUSIC_TSF) currentMessage = nullptr; currentMidi = {false}; tsfSynth = tsf_load_memory(gm_sf2, (int)gm_sf2_len); int sampleRate; if (Mix_QuerySpec(&sampleRate, nullptr, nullptr)) { tsf_set_output(tsfSynth, TSF_STEREO_INTERLEAVED, sampleRate, 0.0f); sampPerSec = 1000.0f / float(sampleRate); } auto fileName = std::string(pinball::get_rc_string(156, 0)); std::transform(fileName.begin(), fileName.end(), fileName.begin(), [](unsigned char c) { return std::toupper(c); }); auto filePath = pinball::make_path_name(fileName); auto midi = tml_load_filename(filePath.c_str()); if (midi != nullptr) { currentMidi = {true, midi}; } Mix_HookMusic(midi::sdl_audio_callback, nullptr); return currentMidi.valid; #else return 1; #endif } void midi::music_shutdown() { if (pb::FullTiltMode) { music_shutdown_ft(); return; } #ifdef MUSIC_SDL Mix_FreeMusic(currentMidi.handle); #endif } std::vector midi::TrackList; midi_song midi::track1, midi::track2, midi::track3, midi::active_track, midi::active_track2; int midi::some_flag1; int midi::music_init_ft() { active_track = {false}; //TrackList = new objlist_class(0, 1); TrackList.clear(); track1 = load_track("taba1"); track2 = load_track("taba2"); track3 = load_track("taba3"); if (!track2.valid) track2 = track1; if (!track3.valid) track3 = track1; return 1; } void midi::music_shutdown_ft() { #ifdef MUSIC_SDL if (active_track.valid) Mix_HaltMusic(); for (auto& track : TrackList) { if (track.valid) Mix_FreeMusic(track.handle); } TrackList.clear(); active_track = {false}; #elif defined(MUSIC_TSF) if (active_track.valid) { tsf_note_off_all(tsfSynth); active_track = {false, nullptr}; currentMessage = nullptr; midiTime = 0.0f; } for (auto& track : TrackList) { //if (track.valid) tml_free(track.handle); } TrackList.clear(); #endif } midi_song midi::load_track(std::string fileName) { auto origFile = fileName; // File name is in lower case, while game data is in upper case. std::transform(fileName.begin(), fileName.end(), fileName.begin(), [](unsigned char c) { return std::toupper(c); }); if (pb::FullTiltMode) { // FT sounds are in SOUND subfolder fileName.insert(0, 1, PathSeparator); fileName.insert(0, "SOUND"); } fileName += ".MDS"; auto filePath = pinball::make_path_name(fileName); auto midi = MdsToMidi(filePath); if (!midi) return {false}; // Dump converted MIDI file /*origFile += ".midi"; FILE* fileHandle = fopen(origFile.c_str(), "wb"); fwrite(midi->data(), 1, midi->size(), fileHandle); fclose(fileHandle);*/ #ifdef MUSIC_SDL auto rw = SDL_RWFromMem(midi->data(), static_cast(midi->size())); auto audio = Mix_LoadMUS_RW(rw, 1); // This call seems to leak memory no matter what. #elif defined(MUSIC_TSF) auto audio = tml_load_memory(midi->data(), static_cast(midi->size())); #else void* audio = nullptr; #endif delete midi; if (!audio) return {false}; midi_song song = {true, audio}; TrackList.push_back(song); return song; } int midi::play_ft(midi_song* midi) { int result = 0; stop_ft(); if (!midi || !midi->valid) return 0; if (some_flag1) { active_track2 = *midi; return 0; } #ifdef MUSIC_SDL if (Mix_PlayMusic(midi->handle, -1)) { active_track = {false, nullptr}; result = 0; } else { active_track = *midi; result = 1; } #elif defined(MUSIC_TSF) active_track = *midi; result = 1; #endif return result; } int midi::stop_ft() { int returnCode = 0; #ifdef MUSIC_SDL if (active_track.valid) returnCode = Mix_HaltMusic(); active_track.valid = false; active_track.handle = nullptr; #elif defined(MUSIC_TSF) // Mix_HookMusic(nullptr, nullptr); tsf_note_off_all(tsfSynth); active_track = {false, nullptr}; currentMessage = nullptr; midiTime = 0.0f; #endif return returnCode; } /// /// SDL_mixed does not support MIDS. To support FT music, a conversion to MIDI is required. /// /// Path to .MDS file /// Vector that contains MIDI file std::vector* midi::MdsToMidi(std::string file) { auto fileHandle = fopen(file.c_str(), "rb"); if (!fileHandle) return nullptr; fseek(fileHandle, 0, SEEK_END); auto fileSize = static_cast(ftell(fileHandle)); auto filePtr = reinterpret_cast(memory::allocate(fileSize)); fseek(fileHandle, 0, SEEK_SET); fread(filePtr, 1, fileSize, fileHandle); fclose(fileHandle); int returnCode = 0; std::vector* midiOut = nullptr; do { if (fileSize < 12) { returnCode = 3; break; } if (filePtr->Riff != FOURCC('R', 'I', 'F', 'F') || filePtr->Mids != FOURCC('M', 'I', 'D', 'S') || filePtr->Fmt != FOURCC('f', 'm', 't', ' ')) { returnCode = 3; break; } if (filePtr->FileSize > fileSize - 8) { returnCode = 3; break; } if (fileSize - 12 < 8) { returnCode = 3; break; } if (filePtr->FmtSize < 12 || filePtr->FmtSize > fileSize - 12) { returnCode = 3; break; } auto streamIdUsed = filePtr->dwFlags == 0; auto dataChunk = reinterpret_cast(reinterpret_cast(&filePtr->dwTimeFormat) + filePtr-> FmtSize); if (dataChunk->Data != FOURCC('d', 'a', 't', 'a')) { returnCode = 3; break; } if (dataChunk->DataSize < 4) { returnCode = 3; break; } auto srcPtr = dataChunk->Blocks; std::vector midiEvents{}; for (auto blockIndex = dataChunk->BlocksPerChunk; blockIndex; blockIndex--) { auto eventSizeInt = streamIdUsed ? 3 : 2; auto eventCount = srcPtr->CbBuffer / (4 * eventSizeInt); auto currentTicks = srcPtr->TkStart; auto srcPtr2 = reinterpret_cast(srcPtr->AData); for (auto i = 0u; i < eventCount; i++) { currentTicks += srcPtr2[0]; auto event = streamIdUsed ? srcPtr2[2] : srcPtr2[1]; midiEvents.push_back({currentTicks, event}); srcPtr2 += eventSizeInt; } srcPtr = reinterpret_cast(&srcPtr->AData[srcPtr->CbBuffer]); } // MIDS events can be out of order in the file std::sort(midiEvents.begin(), midiEvents.end(), [](const midi_event& lhs, const midi_event& rhs) { return lhs.iTicks < rhs.iTicks; }); // MThd chunk std::vector& midiBytes = *new std::vector(); midiOut = &midiBytes; midi_header header(SwapByteOrderShort(static_cast(filePtr->dwTimeFormat))); auto headerData = reinterpret_cast(&header); midiBytes.insert(midiBytes.end(), headerData, headerData + sizeof header); // MTrk chunk midi_track track(7); auto trackData = reinterpret_cast(&track); midiBytes.insert(midiBytes.end(), trackData, trackData + sizeof track); auto lengthPos = midiBytes.size() - 4; auto prevTime = 0u; for (const auto& event : midiEvents) { assertm(event.iTicks >= prevTime, "MIDS events: negative delta-time"); uint32_t delta = event.iTicks - prevTime; prevTime = event.iTicks; // Delta time is in variable quantity, Big Endian uint32_t deltaVarLen; auto count = ToVariableLen(delta, deltaVarLen); deltaVarLen = SwapByteOrderInt(deltaVarLen); auto deltaData = reinterpret_cast(&deltaVarLen) + 4 - count; midiBytes.insert(midiBytes.end(), deltaData, deltaData + count); switch (event.iEvent >> 24) { case 0: { // Type 0 - MIDI short message. 3 bytes: xx p1 p2 00, where xx - message, p* - parameters // Some of the messages have only one parameter auto msgMask = (event.iEvent) & 0xF0; auto shortMsg = (msgMask == 0xC0 || msgMask == 0xD0); auto eventData = reinterpret_cast(&event.iEvent); midiBytes.insert(midiBytes.end(), eventData, eventData + (shortMsg ? 2 : 3)); break; } case 1: { // Type 1 - tempo change, 3 bytes: xx xx xx 01 // Meta message, set tempo, 3 bytes payload const uint8_t metaSetTempo[] = {0xFF, 0x51, 0x03}; midiBytes.insert(midiBytes.end(), metaSetTempo, metaSetTempo + 3); auto eventBE = SwapByteOrderInt(event.iEvent); auto eventData = reinterpret_cast(&eventBE) + 1; midiBytes.insert(midiBytes.end(), eventData, eventData + 3); break; } default: assertm(0, "MIDS events: uknown event"); break; } } // Meta message, end of track, 0 bytes payload const uint8_t metaEndTrack[] = {0x00, 0xFF, 0x2f, 0x00}; midiBytes.insert(midiBytes.end(), metaEndTrack, metaEndTrack + 4); // Set final MTrk size auto lengthBE = SwapByteOrderInt(static_cast(midiBytes.size()) - sizeof header - sizeof track); auto lengthData = reinterpret_cast(&lengthBE); std::copy_n(lengthData, 4, midiBytes.begin() + lengthPos); } while (false); if (filePtr) memory::free(filePtr); if (returnCode && midiOut) delete midiOut; return midiOut; }