diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f66f7665a..5dc63411cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,6 +40,10 @@ add_library(${PROJECT_NAME} STATIC src/audio.h src/audio_midi.cpp src/audio_midi.h + src/audio_midiout.cpp + src/audio_midiout.h + src/audio_midiout_device.cpp + src/audio_midiout_device.h src/audio_resampler.cpp src/audio_resampler.h src/audio_sdl.cpp @@ -533,13 +537,17 @@ if(WIN32) target_sources(${PROJECT_NAME} PRIVATE src/registry.cpp src/platform/windows/utils.cpp - src/platform/windows/utils.h) + src/platform/windows/utils.h + src/platform/windows/midiout_device_win32.cpp + src/platform/windows/midiout_device_win32.h) endif() if(APPLE) target_sources(${PROJECT_NAME} PRIVATE src/platform/macos/utils.mm - src/platform/macos/utils.h) + src/platform/macos/utils.h + src/platform/macos/midiout_device_coreaudio.cpp + src/platform/macos/midiout_device_coreaudio.h) find_library(MACOSFOUNDATION Foundation) target_link_libraries(${PROJECT_NAME} ${MACOSFOUNDATION}) diff --git a/Makefile.am b/Makefile.am index 345618a783..756ee0789c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -26,6 +26,10 @@ libeasyrpg_player_a_SOURCES = \ src/audio_generic.h \ src/audio_midi.cpp \ src/audio_midi.h \ + src/audio_midiout.cpp \ + src/audio_midiout.h \ + src/audio_midiout_device.cpp \ + src/audio_midiout_device.h \ src/audio_resampler.cpp \ src/audio_resampler.h \ src/audio_sdl.cpp \ diff --git a/src/audio_generic.cpp b/src/audio_generic.cpp index 59f214fedc..c47f7f7649 100644 --- a/src/audio_generic.cpp +++ b/src/audio_generic.cpp @@ -34,6 +34,10 @@ std::vector GenericAudio::mixer_buffer = {}; GenericAudio::GenericAudio() { for (auto& BGM_Channel : BGM_Channels) { + if (BGM_Channel.midiout) { + BGM_Channel.midiout->Reset(); + } + BGM_Channel.midiout.reset(); BGM_Channel.decoder.reset(); } for (auto& SE_Channel : SE_Channels) { @@ -51,9 +55,10 @@ GenericAudio::~GenericAudio() { void GenericAudio::BGM_Play(Filesystem_Stream::InputStream stream, int volume, int pitch, int fadein) { bool bgm_set = false; + LockMidiOutMutex(); for (auto& BGM_Channel : BGM_Channels) { BGM_Channel.stopped = true; //Stop all running background music - if (!BGM_Channel.decoder && !bgm_set) { + if (!BGM_Channel.decoder && !BGM_Channel.midiout && !bgm_set) { //If there is an unused bgm channel bgm_set = true; LockMutex(); @@ -62,31 +67,49 @@ void GenericAudio::BGM_Play(Filesystem_Stream::InputStream stream, int volume, i PlayOnChannel(BGM_Channel, std::move(stream), volume, pitch, fadein); } } + UnlockMidiOutMutex(); } void GenericAudio::BGM_Pause() { + LockMidiOutMutex(); for (auto& BGM_Channel : BGM_Channels) { - if (BGM_Channel.decoder) { + if (BGM_Channel.decoder || BGM_Channel.midiout) { + if (BGM_Channel.midiout) { + BGM_Channel.midiout->Pause(); + } BGM_Channel.paused = true; } } + UnlockMidiOutMutex(); } void GenericAudio::BGM_Resume() { + LockMidiOutMutex(); for (auto& BGM_Channel : BGM_Channels) { - if (BGM_Channel.decoder) { + if (BGM_Channel.decoder || BGM_Channel.midiout) { + if (BGM_Channel.midiout) { + BGM_Channel.midiout->Resume(); + } BGM_Channel.paused = false; } } + UnlockMidiOutMutex(); } void GenericAudio::BGM_Stop() { + LockMidiOutMutex(); for (auto& BGM_Channel : BGM_Channels) { BGM_Channel.stopped = true; //Stop all running background music - LockMutex(); - BGM_Channel.decoder.reset(); - UnlockMutex(); + if (BGM_Channel.midiout) { + BGM_Channel.midiout->Reset(); + BGM_Channel.midiout.reset(); + } else if (BGM_Channel.decoder) { + LockMutex(); + BGM_Channel.decoder.reset(); + UnlockMutex(); + } } + UnlockMidiOutMutex(); } bool GenericAudio::BGM_PlayedOnce() const { @@ -105,43 +128,60 @@ bool GenericAudio::BGM_IsPlaying() const { int GenericAudio::BGM_GetTicks() const { unsigned ticks = 0; LockMutex(); + LockMidiOutMutex(); for (auto& BGM_Channel : BGM_Channels) { - if (BGM_Channel.decoder) { + if (BGM_Channel.midiout) { + ticks = BGM_Channel.midiout->GetTicks(); + } else if (BGM_Channel.decoder) { ticks = BGM_Channel.decoder->GetTicks(); break; } } + UnlockMidiOutMutex(); UnlockMutex(); return ticks; } void GenericAudio::BGM_Fade(int fade) { LockMutex(); + LockMidiOutMutex(); for (auto& BGM_Channel : BGM_Channels) { - if (BGM_Channel.decoder) { + if (BGM_Channel.midiout) { + BGM_Channel.midiout->SetFade(BGM_Channel.midiout->GetVolume(), 0, fade); + } else if (BGM_Channel.decoder) { BGM_Channel.decoder->SetFade(BGM_Channel.decoder->GetVolume(), 0, fade); } + } + UnlockMidiOutMutex(); UnlockMutex(); } void GenericAudio::BGM_Volume(int volume) { LockMutex(); + LockMidiOutMutex(); for (auto& BGM_Channel : BGM_Channels) { - if (BGM_Channel.decoder) { + if (BGM_Channel.midiout) { + BGM_Channel.midiout->SetVolume(volume); + } else if (BGM_Channel.decoder) { BGM_Channel.decoder->SetVolume(volume); } } + UnlockMidiOutMutex(); UnlockMutex(); } void GenericAudio::BGM_Pitch(int pitch) { LockMutex(); + LockMidiOutMutex(); for (auto& BGM_Channel : BGM_Channels) { - if (BGM_Channel.decoder) { + if (BGM_Channel.midiout) { + BGM_Channel.midiout->SetPitch(pitch); + } else if (BGM_Channel.decoder) { BGM_Channel.decoder->SetPitch(pitch); } } + UnlockMidiOutMutex(); UnlockMutex(); } @@ -167,6 +207,16 @@ void GenericAudio::Update() { // no-op, handled by the Decode function called through a thread } +void GenericAudio::UpdateMidiOut(long long delta) { + LockMidiOutMutex(); + for (auto& BGM_Channel : BGM_Channels) { + if (BGM_Channel.midiout && !BGM_Channel.paused) { + BGM_Channel.midiout->Update(delta); + } + } + UnlockMidiOutMutex(); +} + void GenericAudio::SetFormat(int frequency, AudioDecoder::Format format, int channels) { output_format.frequency = frequency; output_format.format = format; @@ -182,6 +232,16 @@ bool GenericAudio::PlayOnChannel(BgmChannel& chan, Filesystem_Stream::InputStrea return false; } + chan.midiout = MidiOut::Create(filestream); + if (chan.midiout && chan.midiout->Open(std::move(filestream))) { + chan.midiout->SetPitch(pitch); + chan.midiout->SetVolume(volume); + chan.midiout->SetFade(0, volume, fadein); + chan.midiout->SetLooping(true); + chan.paused = false; + return true; + } + chan.decoder = AudioDecoder::Create(filestream); if (chan.decoder && chan.decoder->Open(std::move(filestream))) { chan.decoder->SetPitch(pitch); diff --git a/src/audio_generic.h b/src/audio_generic.h index 9e9ab07069..df1cfbf830 100644 --- a/src/audio_generic.h +++ b/src/audio_generic.h @@ -21,6 +21,7 @@ #include "audio.h" #include "audio_decoder.h" #include "audio_secache.h" +#include "audio_midiout.h" /** * A software implementation for handling EasyRPG Audio utilizing the @@ -54,17 +55,21 @@ struct GenericAudio : public AudioInterface { void SE_Play(Filesystem_Stream::InputStream stream, int volume, int pitch) override; void SE_Stop() override; virtual void Update() override; + virtual void UpdateMidiOut(long long delta); void SetFormat(int frequency, AudioDecoder::Format format, int channels); virtual void LockMutex() const = 0; virtual void UnlockMutex() const = 0; + virtual void LockMidiOutMutex() const = 0; + virtual void UnlockMidiOutMutex() const = 0; void Decode(uint8_t* output_buffer, int buffer_length); private: struct BgmChannel { std::unique_ptr decoder; + std::unique_ptr midiout; bool paused; bool stopped; }; diff --git a/src/audio_midiout.cpp b/src/audio_midiout.cpp new file mode 100644 index 0000000000..2fc3a6674c --- /dev/null +++ b/src/audio_midiout.cpp @@ -0,0 +1,387 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#include +#include +#include "audio_midiout.h" +#include "midisequencer.h" +#include "output.h" +#ifdef _WIN32 +#include "platform/windows/midiout_device_win32.h" +#elif __APPLE__ +#include "platform/macos/midiout_device_coreaudio.h" +#endif + +class GenericMidiOut : public MidiOut, public midisequencer::output { +public: + GenericMidiOut(MidiOutDevice* device); + + void SetFade(int begin, int end, int duration) override; + void SetVolume(int volume) override; + virtual int GetVolume() const override; + virtual void Pause() override; + virtual void Resume() override; + virtual void Rewind() override; + virtual int GetLoopCount() const override; + virtual bool IsFinished() const override; + virtual bool SetPitch(int pitch) override; + virtual int GetPitch() const override; + virtual int GetTicks() const override; + virtual void Update(long long delta) override; + virtual bool Open(Filesystem_Stream::InputStream stream) override; + virtual bool Seek(std::streamoff offset, std::ios_base::seekdir origin) override; + virtual void Reset() override; + + virtual std::string GetError() const override { + return error_msg; + }; + + std::vector file_buffer; + size_t file_buffer_pos = 0; + +private: + void SendMessageToAllChannels(uint32_t midi_msg); + std::unique_ptr device; + std::unique_ptr seq; + + // midisequencer::output interface + void midi_message(int, uint_least32_t message) override; + void sysex_message(int, const void* data, std::size_t size) override; + void meta_event(int, const void*, std::size_t) override; + void reset() override; + + float mtime = 0.0f; + float tempo_multiplier = 1.0f; + bool paused = false; + float volume = 0; + float fade_end = 0; + float delta_step = 0; + int fade_steps = 0; + float last_fade_mtime = 0.0f; + + std::string error_msg; + + // What was the mtime when the last set of volume MIDI messages were sent out + float last_fade_msg_sent = 0.0f; + std::array channel_volumes; + + int loop_count = 0; + + static constexpr int midi_default_tempo = 500000; + struct MidiTempoData { + MidiTempoData(const GenericMidiOut* midi, uint32_t cur_tempo, const MidiTempoData* prev = nullptr); + + uint32_t tempo = midi_default_tempo; + float ticks_per_sec = 0.0f; + float mtime = 0.0f; + int ticks = 0; + + int GetTicks(float cur_mtime) const; + }; + + // Contains one entry per tempo change (latest on top) + // When looping all entries after the loop point are dropped + std::vector tempo; + + void reset_tempos_after_loop(); +}; + +static const uint8_t midi_event_control_change = 0b1011; +static const uint8_t midi_control_volume = 7; +static const uint8_t midi_control_all_sound_off = 120; +static const uint8_t midi_control_all_note_off = 123; +static const uint8_t midi_control_reset_all_controller = 121; + +static uint32_t midimsg_make(uint8_t event_type, uint8_t channel, uint8_t value1, uint8_t value2) { + uint32_t msg = 0; + msg |= (((event_type << 4) & 0xF0) | (channel & 0x0F)) & 0x0000FF; + msg |= (value1 << 8) & 0x00FF00; + msg |= (value2 << 16) & 0xFF0000; + return msg; +} + +static uint32_t midimsg_all_note_off(uint8_t channel) { + return midimsg_make(midi_event_control_change, channel, midi_control_all_note_off, 0); +} + +static uint32_t midimsg_all_sound_off(uint8_t channel) { + return midimsg_make(midi_event_control_change, channel, midi_control_all_sound_off, 0); +} + +static uint32_t midimsg_volume(uint8_t channel, uint8_t volume) { + return midimsg_make(midi_event_control_change, channel, midi_control_volume, volume); +} + +static uint32_t midimsg_reset_all_controller(uint8_t channel) { + return midimsg_make(midi_event_control_change, channel, midi_control_reset_all_controller, 0); +} + +GenericMidiOut::GenericMidiOut(MidiOutDevice* device) + : device(device) { + seq = std::make_unique(); + channel_volumes.fill(127); +} + +std::unique_ptr MidiOut::Create(Filesystem_Stream::InputStream& stream, const std::string& filename) { + std::unique_ptr midiout = nullptr; + char magic[4] = { 0 }; + if (!stream.ReadIntoObj(magic)) { + return nullptr; + } + stream.seekg(0, std::ios::beg); + if (strncmp(magic, "MThd", 4) != 0) { + return nullptr; + } +#ifdef _WIN32 + std::unique_ptr device = std::make_unique(); + if (!device->IsOK()) { + return nullptr; + } + midiout = std::make_unique(device.release()); +#endif +#ifdef __APPLE__ + std::unique_ptr device = std::make_unique(); + if (!device->IsOK()) { + return nullptr; + } + midiout = std::make_unique(device.release()); +#endif + return midiout; +} + +static int read_func(void* instance) { + GenericMidiOut* midiout = reinterpret_cast(instance); + + if (midiout->file_buffer_pos >= midiout->file_buffer.size()) { + return EOF; + } + + return midiout->file_buffer[midiout->file_buffer_pos++]; +} + +bool GenericMidiOut::Open(Filesystem_Stream::InputStream stream) { + seq->clear(); + file_buffer = Utils::ReadStream(stream); + + if (!seq->load(this, read_func)) { + error_msg = "Midi: Error reading file"; + return false; + } + seq->rewind(); + mtime = seq->get_start_skipping_silence(); + + tempo.emplace_back(this, midi_default_tempo); + + return true; +} + +void GenericMidiOut::SetFade(int begin, int end, int duration) { + fade_steps = 0; + last_fade_mtime = 0.0f; + + if (duration <= 0.0) { + SetVolume(end); + return; + } + + if (begin == end) { + SetVolume(end); + return; + } + + volume = begin / 100.0f; + fade_end = end / 100.0f; + fade_steps = duration / 100; + delta_step = (fade_end - volume) / fade_steps; +} + +void GenericMidiOut::SetVolume(int new_volume) { + // cancel any pending fades + fade_steps = 0; + + volume = new_volume / 100.0f; + for (int i = 0; i < 16; i++) { + uint32_t msg = midimsg_volume(i, static_cast(channel_volumes[i] * volume)); + device->SendMidiMessage(msg); + } +} + +int GenericMidiOut::GetVolume()const { + if (fade_steps > 0) { + return static_cast(fade_end * 100); + } + return static_cast(volume * 100); +} + +void GenericMidiOut::Pause() { + paused = true; + for (int i = 0; i < 16; i++) { + uint32_t msg = midimsg_volume(i, 0); + device->SendMidiMessage(msg); + } +} + +void GenericMidiOut::Resume() { + paused = false; + for (int i = 0; i < 16; i++) { + uint32_t msg = midimsg_volume(i, static_cast(channel_volumes[i] * volume)); + device->SendMidiMessage(msg); + } +} + +void GenericMidiOut::Rewind() { + seq->rewind(); +} + +int GenericMidiOut::GetLoopCount() const { + return loop_count; +} + +bool GenericMidiOut::IsFinished() const { + return seq->is_at_end(); +} + +bool GenericMidiOut::SetPitch(int pitch) { + tempo_multiplier = pitch / 100.0f; + return true; +} + +int GenericMidiOut::GetPitch() const { + return static_cast(tempo_multiplier * 100); +} + +int GenericMidiOut::GetTicks() const { + assert(!tempo.empty()); + + return tempo.back().GetTicks(mtime); +} + +void GenericMidiOut::Update(long long delta) { + if (paused) { + return; + } + if (fade_steps >= 0 && mtime - last_fade_mtime > 0.1f) { + volume = std::max(0.0f, std::min(1.0f, volume + delta_step)); + for (int i = 0; i < 16; i++) { + uint32_t msg = midimsg_volume(i, static_cast(channel_volumes[i] * volume)); + device->SendMidiMessage(msg); + } + last_fade_mtime = mtime; + fade_steps -= 1; + } + + seq->play(mtime, this); + mtime = mtime + ((delta / 1000000.0f) * tempo_multiplier); + + if (IsFinished() && looping) { + mtime = seq->rewind_to_loop(); + reset_tempos_after_loop(); + } +} + +bool GenericMidiOut::Seek(std::streamoff offset, std::ios_base::seekdir origin) { + if (offset == 0 && origin == std::ios_base::beg) { + seq->rewind(); + mtime = 0.0f; + reset_tempos_after_loop(); + SendMessageToAllChannels(midimsg_all_note_off(0)); + return true; + } + return false; +} + +void GenericMidiOut::Reset() { + // Generate a MIDI reset event so the device doesn't + // leave notes playing or keeps any state + reset(); +} + +void GenericMidiOut::SendMessageToAllChannels(uint32_t midi_msg) { + for (int channel = 0; channel < 16; channel++) { + uint8_t event_type = (midi_msg & 0x0000F0) >> 4; + midi_msg |= (((event_type << 4) & 0xF0) | (channel & 0x0F)) & 0x0000FF; + device->SendMidiMessage(midi_msg); + } +} + +void GenericMidiOut::midi_message(int, uint_least32_t message) { + uint8_t event_type = (message & 0x0000F0) >> 4; + uint8_t channel = (message & 0x00000F); + uint8_t value1 = (message & 0x00FF00) >> 8; + uint8_t value2 = (message & 0xFF0000) >> 16; + + if (event_type == midi_event_control_change && value1 == midi_control_volume) { + // Adjust channel volume + channel_volumes[channel] = value2; + // Send the modified volume to midiout + message = midimsg_volume(channel, static_cast(value2 * volume)); + } + device->SendMidiMessage(message); +} + +void GenericMidiOut::sysex_message(int, const void* data, std::size_t size) { + device->SendSysExMessage(data, size); +} + +void GenericMidiOut::meta_event(int event, const void* data, std::size_t size) { + // Meta events are never sent over MIDI ports. + const auto* d = reinterpret_cast(data); + if (size == 3 && event == 0x51) { + uint32_t new_tempo = (static_cast(static_cast(d[0])) << 16) + | (static_cast(d[1]) << 8) + | static_cast(d[2]); + tempo.emplace_back(this, new_tempo, &tempo.back()); + } +} + +void GenericMidiOut::reset() { + // MIDI reset event + SendMessageToAllChannels(midimsg_all_sound_off(0)); + SendMessageToAllChannels(midimsg_reset_all_controller(0)); + device->SendMidiReset(); +} + +void GenericMidiOut::reset_tempos_after_loop() { + if (mtime > 0.0f) { + // Throw away all tempo data after the loop point + auto rit = std::find_if(tempo.rbegin(), tempo.rend(), [&](auto& t) { return t.mtime <= mtime; }); + auto it = rit.base(); + if (it != tempo.end()) { + tempo.erase(it, tempo.end()); + } + } else { + tempo.clear(); + tempo.emplace_back(this, midi_default_tempo); + } +} + +// TODO: Solve the copy-paste job between this and GenericMidiDecoder +GenericMidiOut::MidiTempoData::MidiTempoData(const GenericMidiOut* midi, uint32_t cur_tempo, const MidiTempoData* prev) + : tempo(cur_tempo) { + ticks_per_sec = (float)midi->seq->get_division() / tempo * 1000000; + mtime = midi->mtime; + if (prev) { + float delta = mtime - prev->mtime; + int ticks_since_last = static_cast(ticks_per_sec * delta); + ticks = prev->ticks + ticks_since_last; + } +} + +int GenericMidiOut::MidiTempoData::GetTicks(float mtime_cur) const { + float delta = mtime_cur - mtime; + return ticks + static_cast(ticks_per_sec * delta); +} diff --git a/src/audio_midiout.h b/src/audio_midiout.h new file mode 100644 index 0000000000..dee06dd626 --- /dev/null +++ b/src/audio_midiout.h @@ -0,0 +1,188 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifndef EP_AUDIO_MIDIOUT_H +#define EP_AUDIO_MIDIOUT_H + +#include +#include "filesystem_stream.h" +#include "audio_midiout_device.h" + +/** + * Manages sequencing MIDI files and emitting MIDI events to a MIDI out + * device. + */ +class MidiOut { +public: + virtual ~MidiOut() = default; + + static std::unique_ptr Create(Filesystem_Stream::InputStream& stream, const std::string& filename); + + /** + * Prepares a volume fade in/out effect. + * To do a fade out begin must be larger then end. + * Call Update to do the fade. + * Volume changes will not really modify the volume but are only helper + * functions for retrieving the volume information for the audio hardware. + * + * @param begin Begin volume (from 0-100) + * @param end End volume (from 0-100) + * @param duration Fade duration in ms + */ + virtual void SetFade(int begin, int end, int duration) { + (void)begin; + (void)end; + (void)duration; + } + + /** + * Sets the volume of the audio decoder. + * Volume changes will not really modify the volume but are only helper + * functions for retrieving the volume information for the audio hardware. + * + * @param volume (from 0-100) + */ + virtual void SetVolume(int volume) { + (void)volume; + } + + /** + * Gets the volume of the audio decoder. + * Volume changes will not really modify the volume but are only helper + * functions for retrieving the volume information for the audio hardware. + */ + virtual int GetVolume() const = 0; + + /** + * Pauses the MIDI sequencer. + */ + virtual void Pause() {}; + + /** + * Resumes the MIDI sequencer. + */ + virtual void Resume() {}; + + /** + * Rewinds the MIDI sequencer to the beginning. + */ + virtual void Rewind() {}; + + /** + * Seeks in the MIDI sequence. The value of offset is implementation + * defined but is guaranteed to match the result of Tell. + * Libraries must support at least seek from the start for Rewind(). + * + * @param offset Offset to seek to + * @param origin Position to seek from + * @return Whether seek was successful + */ + virtual bool Seek(std::streamoff offset, std::ios_base::seekdir origin) { + (void)offset; + (void)origin; + return false; + } + + /** + * Gets if the MIDI will loop when it finishes. + * + * @return if looping + */ + bool GetLooping() const { + return looping; + }; + + /** + * Enables/Disables MIDI looping. + * When looping is enabled IsFinished will never return true and the + * sequencer auto-rewinds + * + * @param enable Enable/Disable looping + */ + void SetLooping(bool enable) { + looping = enable; + }; + + /** + * Gets the number of loops + * + * @return loop count + */ + virtual int GetLoopCount() const = 0; + + /** + * Determines whether the MIDI sequence is finished. + * + * @return true sequence ended + */ + virtual bool IsFinished() const = 0; + + /* Call this every ~1ms. + * @param delta Time in microseconds since last called. + */ + virtual void Update(long long delta) = 0; + + /** + * Provides an error message when Open or a Decode function fail. + * + * @return Human readable error message + */ + virtual std::string GetError() const = 0; + + /** + * Returns a value suitable for the GetMidiTicks command. + * For MIDI this is the amount of MIDI ticks. + * + * @return Amount of MIDI ticks + */ + virtual int GetTicks() const { + return 0; + }; + + /** + * Gets the pitch multiplier. + * + * @return pitch multiplier + */ + virtual int GetPitch() const { + return 0; + }; + + /** + * Sets the pitch multiplier. + * 100 = normal speed + * 200 = double speed and so on + * + * @param pitch Pitch multiplier to use + * @return true if pitch was set, false otherwise + */ + virtual bool SetPitch(int pitch) { + return false; + }; + + /** + * Resets all state and instructs the MIDI device to do the same. + */ + virtual void Reset() = 0; + + virtual bool Open(Filesystem_Stream::InputStream stream) = 0; + +protected: + bool looping = false; +}; + +#endif diff --git a/src/audio_midiout_device.cpp b/src/audio_midiout_device.cpp new file mode 100644 index 0000000000..85e14591ce --- /dev/null +++ b/src/audio_midiout_device.cpp @@ -0,0 +1,18 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#include "audio_midiout_device.h" diff --git a/src/audio_midiout_device.h b/src/audio_midiout_device.h new file mode 100644 index 0000000000..774fa751bf --- /dev/null +++ b/src/audio_midiout_device.h @@ -0,0 +1,54 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifndef EP_AUDIO_MIDIOUT_DEVICE_H +#define EP_AUDIO_MIDIOUT_DEVICE_H + +#include +#include + + /** + * Manages sequencing MIDI files and emitting MIDI events to a MIDI out + * device. + */ +class MidiOutDevice { +public: + virtual ~MidiOutDevice() = default; + + virtual void Pause() {} + + virtual void SetVolume(int volume) { + (void)volume; + } + + virtual void SendMidiMessage(uint32_t message) { + (void)message; + } + + virtual void SendSysExMessage(const void* data, size_t size) { + (void)data; + (void)size; + } + + virtual void SendMidiReset() {} + + virtual bool IsOK() { + return true; + } +}; + +#endif diff --git a/src/audio_sdl.cpp b/src/audio_sdl.cpp index 7783d6e6c0..de98660b6e 100644 --- a/src/audio_sdl.cpp +++ b/src/audio_sdl.cpp @@ -20,19 +20,45 @@ #if defined(USE_SDL) && !defined(HAVE_SDL_MIXER) && defined(SUPPORT_AUDIO) #include +#include #include #include #include +#include +#include +#include "game_clock.h" #include "audio_sdl.h" +#include "audio_midiout.h" #include "output.h" +using namespace std::chrono_literals; + namespace { #if SDL_MAJOR_VERSION >= 2 SDL_AudioDeviceID audio_dev_id = 0; #endif } +static int MidioutThreadMain(void* ptr) { + SdlAudio* data = reinterpret_cast(ptr); + bool should_exit = false; + Game_Clock::time_point start_ticks = Game_Clock::now(); + while (!should_exit) { + auto ticks = Game_Clock::now(); + data->LockMidiOutMutex(); + + should_exit = data->midiout_thread_exit; + auto us = std::chrono::duration_cast(ticks - start_ticks); + data->UpdateMidiOut(us.count()); + + data->UnlockMidiOutMutex(); + Game_Clock::SleepFor(1ms); + start_ticks = ticks; + } + return 0; +} + void sdl_audio_callback(void* userdata, uint8_t* stream, int length) { // no mutex locking required, SDL does this before calling @@ -99,9 +125,29 @@ SdlAudio::SdlAudio() : #else SDL_PauseAudio(0); #endif + +#if SDL_MAJOR_VERSION >= 2 + // Start midiout polling thread, SDL2-only. Wii doesn't support MidiOut. + // TODO: don't make a thread if we're on a platform that won't use MidiOut + midiout_mutex = SDL_CreateMutex(); + midiout_thread = SDL_CreateThread(MidioutThreadMain, "MidioutThread", this); + if (!midiout_thread) { + Output::Warning("Couldn't start midiout thread: {}", SDL_GetError()); + return; + } +#endif } SdlAudio::~SdlAudio() { + if (midiout_thread) { + LockMidiOutMutex(); + midiout_thread_exit = true; + UnlockMidiOutMutex(); + SDL_WaitThread(midiout_thread, NULL); + midiout_thread = nullptr; + SDL_DestroyMutex(midiout_mutex); + midiout_mutex = nullptr; + } #if SDL_MAJOR_VERSION >= 2 SDL_CloseAudioDevice(audio_dev_id); #else @@ -125,4 +171,16 @@ void SdlAudio::UnlockMutex() const { #endif } +void SdlAudio::LockMidiOutMutex() const { + if (SDL_LockMutex(midiout_mutex) != 0) { + Output::Debug("SDL_LockMutex failure: {}", SDL_GetError()); + } +} + +void SdlAudio::UnlockMidiOutMutex() const { + if (SDL_UnlockMutex(midiout_mutex) != 0) { + Output::Debug("SDL_UnlockMutex failure: {}", SDL_GetError()); + } +} + #endif diff --git a/src/audio_sdl.h b/src/audio_sdl.h index 7d1a638541..3fbfe74181 100644 --- a/src/audio_sdl.h +++ b/src/audio_sdl.h @@ -18,6 +18,7 @@ #ifndef EP_AUDIO_SDL_H #define EP_AUDIO_SDL_H +#include #include "audio_generic.h" class SdlAudio : public GenericAudio { @@ -27,6 +28,13 @@ class SdlAudio : public GenericAudio { void LockMutex() const override; void UnlockMutex() const override; + void LockMidiOutMutex() const override; + void UnlockMidiOutMutex() const override; + + bool midiout_thread_exit = false; +private: + SDL_Thread *midiout_thread = nullptr; + SDL_mutex *midiout_mutex; }; // class SdlAudio #endif diff --git a/src/midisequencer.cpp b/src/midisequencer.cpp index 4f1eaff3d8..1aa61926d8 100644 --- a/src/midisequencer.cpp +++ b/src/midisequencer.cpp @@ -73,8 +73,15 @@ namespace midisequencer{ float sequencer::rewind_to_loop() { position = loop_position; + if (position != messages.begin()) { + position--; + } return loop_position->time; } + bool sequencer::is_at_end() + { + return position == messages.end(); + } bool sequencer::load(void* fp, int(*fgetc)(void*)) { bool result = false; @@ -99,6 +106,13 @@ namespace midisequencer{ { return getc(static_cast(fp)); } + static bool is_loop_start(uint_least32_t msg) { + // If the message matches the de facto standard MIDI loop instruction: + // Which is a Control Change on any channel with Value 111 + uint8_t event_type = (msg & 0x0000F0) >> 4; + uint8_t value1 = (msg & 0x00FF00) >> 8; + return event_type == 0b1011 && value1 == 111; + } bool sequencer::load(std::FILE* fp) { return load(fp, fpfgetc); @@ -246,6 +260,33 @@ namespace midisequencer{ } } + float sequencer::get_start_skipping_silence() { + for (auto i = messages.begin(); i != messages.end(); ++i) { + // If we find Loop Start before the first NoteOn, just start there + if (is_loop_start(i->message)) { + float time = i->time; + // RPG_RT always rewinds "a little" + // This amount is based on the tempo, and this 2100000 divisor + // I determined experimentally. + time = std::max(0.0f, time - (i->tempo / 2100000.0f)); + return time; + } else if ((i->message & 0xFF) == 0xF0) { + // SysEx message. RPG_RT doesn't skip silence if there's a SysEx + // message in the beginning, so neither should we... + return 0.0f; + } else if ((i->message & 0xF0) == 0x90) { + // NoteOn -- found the first note! + float time = i->time; + // RPG_RT always rewinds "a little" + // This amount is based on the tempo, and this 2100000 divisor + // I determined experimentally. + time = std::max(0.0f, time - (i->tempo / 2100000.0f)); + return time; + } + } + return 0.0f; + } + void sequencer::load_smf(void* fp, int(*fgetc)(void*)) { if(fgetc(fp) != 0 @@ -422,6 +463,7 @@ namespace midisequencer{ } } std::stable_sort(messages.begin(), messages.end()); + loop_position = messages.begin(); if(!(division & 0x8000)){ uint_least32_t tempo = 500000; double time_offset = 0; @@ -441,9 +483,8 @@ namespace midisequencer{ time_offset = i->time; } } - // If the message matches the de facto standard MIDI loop instruction: - // Which is a Control Change on Channel 1 with Value 111 - if ((i->message & 0xFFFF) == 0x6FB0) { + i->tempo = tempo; + if (is_loop_start(i->message)) { // Loop backwards through the messages to find the first message with the same // timestamp as the loop message for (std::vector::iterator j = i; j != messages.begin() && j->time >= i->time; j--) { diff --git a/src/midisequencer.h b/src/midisequencer.h index e014ae0e35..d7d41cff67 100644 --- a/src/midisequencer.h +++ b/src/midisequencer.h @@ -44,6 +44,7 @@ namespace midisequencer{ uint_least32_t message; int port; int track; + uint_least32_t tempo; }; class uncopyable{ @@ -70,6 +71,7 @@ namespace midisequencer{ void clear(); void rewind(); float rewind_to_loop(); + bool is_at_end(); bool load(void* fp, int(*fgetc)(void*)); bool load(std::FILE* fp); int get_num_ports()const; @@ -80,6 +82,7 @@ namespace midisequencer{ uint32_t get_division()const; void play(float time, output* out); void set_time(float time, output* out); + float get_start_skipping_silence(); private: std::vector messages; std::vector::iterator position; diff --git a/src/platform/libretro/libretro_audio.cpp b/src/platform/libretro/libretro_audio.cpp index a885cb7461..ee276cd824 100644 --- a/src/platform/libretro/libretro_audio.cpp +++ b/src/platform/libretro/libretro_audio.cpp @@ -82,6 +82,14 @@ void LibretroAudio::UnlockMutex() const { slock_unlock(mutex); } +void LibretroAudio::LockMidiOutMutex() const { + // No-op: MidiOut unimplemented in Libretro (for now) +} + +void LibretroAudio::UnlockMidiOutMutex() const { + // No-op: MidiOut unimplemented in Libretro (for now) +} + void LibretroAudio::SetRetroAudioCallback(retro_audio_sample_batch_t cb){ RenderAudioFrames = cb; } diff --git a/src/platform/libretro/libretro_audio.h b/src/platform/libretro/libretro_audio.h index 7ae85f663b..aa19fd2674 100644 --- a/src/platform/libretro/libretro_audio.h +++ b/src/platform/libretro/libretro_audio.h @@ -30,6 +30,8 @@ class LibretroAudio : public GenericAudio { void LockMutex() const override; void UnlockMutex() const override; + void LockMidiOutMutex() const override; + void UnlockMidiOutMutex() const override; static void EnableAudio(bool enabled); static void AudioThreadCallback(); diff --git a/src/platform/macos/midiout_device_coreaudio.cpp b/src/platform/macos/midiout_device_coreaudio.cpp new file mode 100644 index 0000000000..0e348099a8 --- /dev/null +++ b/src/platform/macos/midiout_device_coreaudio.cpp @@ -0,0 +1,98 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifdef __APPLE__ +#include "midiout_device_coreaudio.h" +#include "output.h" + +CoreAudioMidiOutDevice::CoreAudioMidiOutDevice() { + OSStatus status = NewAUGraph(&graph); + if (status != noErr) { + Output::Debug("Open MIDI device failed: error {}", status); + return; + } + AudioComponentDescription synthDesc = { + .componentType = kAudioUnitType_MusicDevice, + .componentSubType = kAudioUnitSubType_DLSSynth, + .componentManufacturer = kAudioUnitManufacturer_Apple, + .componentFlags = 0, + .componentFlagsMask = 0 + }; + AUNode synthNode; + status = AUGraphAddNode(graph, &synthDesc, &synthNode); + + AudioComponentDescription limiterDesc = { + .componentType = kAudioUnitType_Effect, + .componentSubType = kAudioUnitSubType_PeakLimiter, + .componentManufacturer = kAudioUnitManufacturer_Apple, + .componentFlags = 0, + .componentFlagsMask = 0 + }; + + AUNode limiterNode; + status = AUGraphAddNode(graph, &limiterDesc, &limiterNode); + + AudioComponentDescription outputDesc = { + .componentType = kAudioUnitType_Output, + .componentSubType = kAudioUnitSubType_DefaultOutput, + .componentManufacturer = kAudioUnitManufacturer_Apple, + .componentFlags = 0, + .componentFlagsMask = 0 + }; + + AUNode soundOutNode; + status = AUGraphAddNode(graph, &outputDesc, &soundOutNode); + + status = AUGraphConnectNodeInput(graph, synthNode, 0, limiterNode, 0); + + status = AUGraphConnectNodeInput(graph, limiterNode, 0, soundOutNode, 0); + + status = AUGraphOpen(graph); + + status = AUGraphNodeInfo(graph, synthNode, nil, &midi_out); + + status = AUGraphInitialize(graph); + + status = AUGraphStart(graph); + +} + +CoreAudioMidiOutDevice::~CoreAudioMidiOutDevice() { + if (graph) { + DisposeAUGraph(graph); + } +} + +void CoreAudioMidiOutDevice::SendMidiMessage(uint32_t message) +{ + uint8_t status = (message & 0x0000FF); + uint8_t value1 = (message & 0x00FF00) >> 8; + uint8_t value2 = (message & 0xFF0000) >> 16; + MusicDeviceMIDIEvent(midi_out, status, value1, value2, 0); +} + +void CoreAudioMidiOutDevice::SendSysExMessage(const void* data, size_t size) +{ + MusicDeviceSysEx(midi_out, (const UInt8*) data, (UInt32) size); +} + +void CoreAudioMidiOutDevice::SendMidiReset() +{ + // TODO: how do you MIDI reset a MusicDevice? +} + +#endif diff --git a/src/platform/macos/midiout_device_coreaudio.h b/src/platform/macos/midiout_device_coreaudio.h new file mode 100644 index 0000000000..3084a9b6a3 --- /dev/null +++ b/src/platform/macos/midiout_device_coreaudio.h @@ -0,0 +1,48 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifndef EP_MIDIOUT_COREAUDIO_H +#define EP_MIDIOUT_COREAUDIO_H + +#ifdef __APPLE__ +#include "audio_midiout_device.h" +#include + +/** + * Plays MIDI through the Apple's AudioToolbox + */ +class CoreAudioMidiOutDevice : public MidiOutDevice { +public: + CoreAudioMidiOutDevice(); + ~CoreAudioMidiOutDevice(); + + void SendMidiMessage(uint32_t message) override; + void SendSysExMessage(const void* data, size_t size) override; + void SendMidiReset() override; + + bool IsOK() override { + return graph != NULL; + } + +private: + AudioUnit midi_out; + AUGraph graph; +}; + +#endif + +#endif diff --git a/src/platform/psvita/psp2_audio.cpp b/src/platform/psvita/psp2_audio.cpp index 91c877dfa7..efd4046c20 100644 --- a/src/platform/psvita/psp2_audio.cpp +++ b/src/platform/psvita/psp2_audio.cpp @@ -94,4 +94,12 @@ void Psp2Audio::UnlockMutex() const { sceKernelUnlockMutex(audio_mutex, 1); } +void Psp2Audio::LockMidiOutMutex() const { + // No-op: PS Vita doesn't support MidiOut +} + +void Psp2Audio::UnlockMidiOutMutex() const { + // No-op: PS Via doesn't support MidiOut +} + #endif diff --git a/src/platform/psvita/psp2_audio.h b/src/platform/psvita/psp2_audio.h index 00bf9b6b5b..bebef6810d 100644 --- a/src/platform/psvita/psp2_audio.h +++ b/src/platform/psvita/psp2_audio.h @@ -30,6 +30,8 @@ class Psp2Audio : public GenericAudio { void LockMutex() const override; void UnlockMutex() const override; + void LockMidiOutMutex() const override; + void UnlockMidiOutMutex() const override; volatile bool termStream = false; diff --git a/src/platform/switch/switch_audio.cpp b/src/platform/switch/switch_audio.cpp index f30c8ecd18..96d045fb50 100644 --- a/src/platform/switch/switch_audio.cpp +++ b/src/platform/switch/switch_audio.cpp @@ -115,4 +115,12 @@ void NxAudio::UnlockMutex() const { mutexUnlock((Mutex*)&audio_mutex); } +void NxAudio::LockMidiOutMutex() const { + // No-op: Switch doesn't use MidiOut. +} + +void NxAudio::UnlockMidiOutMutex() const { + // No-op: Switch doesn't use MidiOut. +} + #endif diff --git a/src/platform/switch/switch_audio.h b/src/platform/switch/switch_audio.h index 59dd158fcb..502a86786d 100644 --- a/src/platform/switch/switch_audio.h +++ b/src/platform/switch/switch_audio.h @@ -30,6 +30,8 @@ class NxAudio : public GenericAudio { void LockMutex() const override; void UnlockMutex() const override; + void LockMidiOutMutex() const override; + void UnlockMidiOutMutex() const override; volatile bool termStream = false; diff --git a/src/platform/windows/midiout_device_win32.cpp b/src/platform/windows/midiout_device_win32.cpp new file mode 100644 index 0000000000..be5f0a1a01 --- /dev/null +++ b/src/platform/windows/midiout_device_win32.cpp @@ -0,0 +1,63 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifdef _WIN32 +#include "midiout_device_win32.h" +#include "output.h" + +Win32MidiOutDevice::Win32MidiOutDevice() { + // TODO: Windows MIDI Mapper was removed in Windows 8. + // This means it's impossible to change the default ("0") MIDI device + // without third party software. We should allow specifying the MIDI device + // ID in a config file. + unsigned int device_id = 0; + + MMRESULT err = midiOutOpen(&midi_out, device_id, 0, 0, CALLBACK_NULL); + if (err != MMSYSERR_NOERROR) { + Output::Debug("Open MIDI device {} failed: error {}", 0, err); + midi_out = NULL; + } +} + +Win32MidiOutDevice::~Win32MidiOutDevice() { + if (midi_out) { + midiOutClose(midi_out); + midi_out = NULL; + } +} + +void Win32MidiOutDevice::SendMidiMessage(uint32_t message) +{ + midiOutShortMsg(midi_out, message); +} + +void Win32MidiOutDevice::SendSysExMessage(const void* data, size_t size) +{ + MIDIHDR hdr; + hdr.dwBufferLength = size; + hdr.dwBytesRecorded = size; + hdr.lpData = (LPSTR) data; + midiOutPrepareHeader(midi_out, &hdr, sizeof(hdr)); + midiOutLongMsg(midi_out, &hdr, sizeof(hdr)); +} + +void Win32MidiOutDevice::SendMidiReset() +{ + midiOutReset(midi_out); +} + +#endif diff --git a/src/platform/windows/midiout_device_win32.h b/src/platform/windows/midiout_device_win32.h new file mode 100644 index 0000000000..70d1a93cbe --- /dev/null +++ b/src/platform/windows/midiout_device_win32.h @@ -0,0 +1,50 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifndef EP_MIDIOUT_WIN32_H +#define EP_MIDIOUT_WIN32_H + +#ifdef _WIN32 + + // Headers +#include +#include +#include +#include "audio_midiout_device.h" + +/** + * Plays MIDI through the Windows API + */ +class Win32MidiOutDevice : public MidiOutDevice { +public: + Win32MidiOutDevice(); + ~Win32MidiOutDevice(); + + void SendMidiMessage(uint32_t message) override; + void SendSysExMessage(const void* data, size_t size) override; + void SendMidiReset() override; + + bool IsOK() override { + return midi_out != NULL; + } + +private: + HMIDIOUT midi_out; +}; +#endif + +#endif