diff --git a/src/xenia/kernel/premake5.lua b/src/xenia/kernel/premake5.lua index bd00330bc..972e9ebd5 100644 --- a/src/xenia/kernel/premake5.lua +++ b/src/xenia/kernel/premake5.lua @@ -16,9 +16,15 @@ project("xenia-kernel") "xenia-cpu", "xenia-hid", "xenia-vfs", + "libavcodec", + "libavformat", + "libavutil", }) defines({ }) + includedirs({ + project_root.."/third_party/FFmpeg/", + }) recursive_platform_files() files({ "debug_visualizers.natvis", diff --git a/src/xenia/kernel/xam/apps/xmp_app.cc b/src/xenia/kernel/xam/apps/xmp_app.cc index 8a4913c1f..bd9e91e94 100644 --- a/src/xenia/kernel/xam/apps/xmp_app.cc +++ b/src/xenia/kernel/xam/apps/xmp_app.cc @@ -12,8 +12,29 @@ #include "xenia/base/logging.h" #include "xenia/base/threading.h" +#include "xenia/emulator.h" #include "xenia/xbox.h" +#include "xenia/apu/audio_driver.h" +#include "xenia/apu/audio_system.h" + +extern "C" { +#if XE_COMPILER_MSVC +#pragma warning(push) +#pragma warning(disable : 4101 4244 5033) +#endif +#include "third_party/FFmpeg/libavcodec/avcodec.h" +#include "third_party/FFmpeg/libavformat/avformat.h" +#include "third_party/FFmpeg/libavutil/opt.h" +#if XE_COMPILER_MSVC +#pragma warning(pop) +#endif +} // extern "C" + +DEFINE_bool(enable_xmp, true, "Enables Music Player playback.", "APU"); +DEFINE_int32(xmp_default_volume, 70, + "Default music volume if game doesn't set it [0-100].", "APU"); + namespace xe { namespace kernel { namespace xam { @@ -26,11 +47,247 @@ XmpApp::XmpApp(KernelState* kernel_state) playback_mode_(PlaybackMode::kUnknown), repeat_mode_(RepeatMode::kUnknown), unknown_flags_(0), - volume_(1.0f), + volume_(cvars::xmp_default_volume / 100.0f), active_playlist_(nullptr), active_song_index_(0), next_playlist_handle_(1), - next_song_handle_(1) {} + next_song_handle_(1) { + if (cvars::enable_xmp) { + worker_running_ = true; + worker_thread_ = threading::Thread::Create({}, [&] { WorkerThreadMain(); }); + worker_thread_->set_name("Music Player"); + } +} + +struct VFSContext { + xe::vfs::File* file; + size_t byte_offset; +}; + +static int xenia_vfs_read(void* opaque, uint8_t* buf, int buf_size) { + auto ctx = static_cast(opaque); + + size_t bytes_read; + X_STATUS status = + ctx->file->ReadSync(buf, buf_size, ctx->byte_offset, &bytes_read); + + if (XFAILED(status)) { + return status == X_STATUS_END_OF_FILE ? AVERROR_EOF : status; + } + + ctx->byte_offset += bytes_read; + return static_cast(bytes_read); +} + +bool XmpApp::PlayFile(std::string_view filename) { + auto playlist = active_playlist_; + + const int buffer_size = 8192; + uint8_t* buffer = reinterpret_cast(av_malloc(buffer_size)); + VFSContext vfs_ctx = {}; + + xe::vfs::FileAction file_action; + X_STATUS status = kernel_state_->file_system()->OpenFile( + nullptr, filename, xe::vfs::FileDisposition::kOpen, + xe::vfs::FileAccess::kGenericRead, false, true, &vfs_ctx.file, + &file_action); + if (XFAILED(status)) { + XELOGE("Opening {} failed with status {:X}", filename, status); + return false; + } + + AVIOContext* avio_ctx = avio_alloc_context(buffer, buffer_size, 0, &vfs_ctx, + xenia_vfs_read, nullptr, nullptr); + + AVFormatContext* formatContext = avformat_alloc_context(); + formatContext->pb = avio_ctx; + int ret; + if ((ret = avformat_open_input(&formatContext, nullptr, nullptr, nullptr)) != + 0) { + XELOGE("ffmpeg: Could not open WMA file: {:x}", ret); + av_freep(&avio_ctx->buffer); + avio_context_free(&avio_ctx); + return false; + } + + if (avformat_find_stream_info(formatContext, nullptr) < 0) { + XELOGE("ffmpeg: Could not find stream info"); + avformat_close_input(&formatContext); + av_freep(&avio_ctx->buffer); + avio_context_free(&avio_ctx); + return false; + } + + AVCodec* codec = nullptr; + int streamIndex = + av_find_best_stream(formatContext, AVMEDIA_TYPE_AUDIO, -1, -1, &codec, 0); + if (streamIndex < 0) { + XELOGE("ffmpeg: Could not find audio stream"); + avformat_close_input(&formatContext); + av_freep(&avio_ctx->buffer); + avio_context_free(&avio_ctx); + return false; + } + + AVStream* audioStream = formatContext->streams[streamIndex]; + AVCodecContext* codecContext = avcodec_alloc_context3(codec); + avcodec_parameters_to_context(codecContext, audioStream->codecpar); + + if (audioStream->codecpar->format != AV_SAMPLE_FMT_FLTP && + audioStream->codecpar->format != AV_SAMPLE_FMT_FLT) { + XELOGE("Audio stream has unexpected sample format {:d}", + audioStream->codecpar->format); + avcodec_free_context(&codecContext); + avformat_close_input(&formatContext); + av_freep(&avio_ctx->buffer); + avio_context_free(&avio_ctx); + return false; + } + + if (avcodec_open2(codecContext, codec, nullptr) < 0) { + XELOGE("ffmpeg: Could not open codec"); + avcodec_free_context(&codecContext); + avformat_close_input(&formatContext); + av_freep(&avio_ctx->buffer); + avio_context_free(&avio_ctx); + return false; + } + + auto driverReady = xe::threading::Semaphore::Create(64, 64); + { + std::unique_lock guard(driver_mutex_); + driver_ = kernel_state_->emulator()->audio_system()->CreateDriver( + driverReady.get(), codecContext->sample_rate, codecContext->channels, + false); + if (!driver_->Initialize()) { + XELOGE("Driver initialization failed!"); + driver_->Shutdown(); + driver_ = nullptr; + avcodec_free_context(&codecContext); + avformat_close_input(&formatContext); + av_freep(&avio_ctx->buffer); + avio_context_free(&avio_ctx); + return false; + } + } + if (volume_ == 0.0f) { + // Some games set volume to 0 on startup and then never call SetVolume + // again... + volume_ = cvars::xmp_default_volume / 100.0f; + } + driver_->SetVolume(volume_); + + AVPacket* packet = av_packet_alloc(); + AVFrame* frame = av_frame_alloc(); + std::vector frameBuffer; + + // Read frames, decode & send to audio driver + while (av_read_frame(formatContext, packet) >= 0) { + if (active_playlist_ != playlist) { + frameBuffer.clear(); + break; + } + + if (packet->stream_index == streamIndex) { + int ret = avcodec_send_packet(codecContext, packet); + if (ret < 0) { + XELOGE("Error sending packet for decoding: {:X}", ret); + break; + } + + while (ret >= 0) { + ret = avcodec_receive_frame(codecContext, frame); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break; + if (ret < 0) { + XELOGW("Error during decoding: {:X}", ret); + break; + } + + // If the frame is planar, convert it to interleaved + if (frame->format == AV_SAMPLE_FMT_FLTP) { + for (int sample = 0; sample < frame->nb_samples; sample++) { + for (int ch = 0; ch < codecContext->channels; ch++) { + float sampleValue = + reinterpret_cast(frame->data[ch])[sample]; + frameBuffer.push_back(sampleValue); + } + } + } else if (frame->format == AV_SAMPLE_FMT_FLT) { + int frameSizeFloats = frame->nb_samples * codecContext->channels; + float* frameData = reinterpret_cast(frame->data[0]); + frameBuffer.insert(frameBuffer.end(), frameData, + frameData + frameSizeFloats); + } + + while (frameBuffer.size() >= xe::apu::AudioDriver::kFrameSamplesMax) { + xe::threading::Wait(driverReady.get(), true); + + if (active_playlist_ != playlist) { + frameBuffer.clear(); + break; + } + + driver_->SubmitFrame(frameBuffer.data()); + frameBuffer.erase( + frameBuffer.begin(), + frameBuffer.begin() + xe::apu::AudioDriver::kFrameSamplesMax); + } + } + } + av_packet_unref(packet); + } + + if (!frameBuffer.empty()) { + while (frameBuffer.size() < xe::apu::AudioDriver::kFrameSamplesMax) { + frameBuffer.push_back(0.0f); + } + + xe::threading::Wait(driverReady.get(), true); + driver_->SubmitFrame(frameBuffer.data()); + } + + av_frame_free(&frame); + av_packet_free(&packet); + avcodec_free_context(&codecContext); + avformat_close_input(&formatContext); + av_freep(&avio_ctx->buffer); + avio_context_free(&avio_ctx); + + { + std::unique_lock guard(driver_mutex_); + driver_->Shutdown(); + driver_ = nullptr; + } + + if (state_ == State::kPlaying && active_playlist_ == playlist) { + active_playlist_ = nullptr; + state_ = State::kIdle; + OnStateChanged(); + } + + return true; +} + +void XmpApp::WorkerThreadMain() { + while (worker_running_) { + if (state_ != State::kPlaying) { + resume_fence_.Wait(); + } + + auto playlist = active_playlist_; + if (!playlist) { + continue; + } + + auto utf8_path = xe::path_to_utf8(playlist->songs[0].get()->file_path); + XELOGI("Playing file {}", utf8_path); + + if (!PlayFile(utf8_path)) { + XELOGE("Playback failed"); + xe::threading::Sleep(std::chrono::minutes(1)); + } + } +} X_HRESULT XmpApp::XMPGetStatus(uint32_t state_ptr) { if (!XThread::GetCurrentThread()->main_thread()) { @@ -39,7 +296,7 @@ X_HRESULT XmpApp::XMPGetStatus(uint32_t state_ptr) { xe::threading::Sleep(std::chrono::milliseconds(1)); } - XELOGD("XMPGetStatus({:08X})", state_ptr); + XELOGD("XMPGetStatus({:08X}) -> {:d}", state_ptr, (uint32_t)state_); xe::store_and_swap(memory_->TranslateVirtual(state_ptr), static_cast(state_)); return X_E_SUCCESS; @@ -132,16 +389,10 @@ X_HRESULT XmpApp::XMPPlayTitlePlaylist(uint32_t playlist_handle, playlist = it->second; } - if (playback_client_ == PlaybackClient::kSystem) { - XELOGW("XMPPlayTitlePlaylist: System playback is enabled!"); - return X_E_SUCCESS; - } - - // Start playlist? - XELOGW("Playlist playback not supported"); active_playlist_ = playlist; active_song_index_ = 0; state_ = State::kPlaying; + resume_fence_.Signal(); OnStateChanged(); kernel_state_->BroadcastNotification(kNotificationXmpPlaybackBehaviorChanged, 1); @@ -152,6 +403,11 @@ X_HRESULT XmpApp::XMPContinue() { XELOGD("XMPContinue()"); if (state_ == State::kPaused) { state_ = State::kPlaying; + resume_fence_.Signal(); + { + std::unique_lock guard(driver_mutex_); + if (driver_ != nullptr) driver_->Resume(); + } } OnStateChanged(); return X_E_SUCCESS; @@ -170,6 +426,10 @@ X_HRESULT XmpApp::XMPStop(uint32_t unk) { X_HRESULT XmpApp::XMPPause() { XELOGD("XMPPause()"); if (state_ == State::kPlaying) { + { + std::unique_lock guard(driver_mutex_); + if (driver_ != nullptr) driver_->Pause(); + } state_ = State::kPaused; } OnStateChanged(); @@ -184,6 +444,7 @@ X_HRESULT XmpApp::XMPNext() { state_ = State::kPlaying; active_song_index_ = (active_song_index_ + 1) % active_playlist_->songs.size(); + resume_fence_.Signal(); OnStateChanged(); return X_E_SUCCESS; } @@ -199,6 +460,7 @@ X_HRESULT XmpApp::XMPPrevious() { } else { --active_song_index_; } + resume_fence_.Signal(); OnStateChanged(); return X_E_SUCCESS; } @@ -309,6 +571,10 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, assert_true(args->xmp_client == 0x00000002); XELOGD("XMPSetVolume({:g})", float(args->value)); volume_ = args->value; + { + std::unique_lock guard(driver_mutex_); + if (driver_ != nullptr) driver_->SetVolume(volume_); + } return X_E_SUCCESS; } case 0x0007000D: { diff --git a/src/xenia/kernel/xam/apps/xmp_app.h b/src/xenia/kernel/xam/apps/xmp_app.h index bc4a53030..21628c626 100644 --- a/src/xenia/kernel/xam/apps/xmp_app.h +++ b/src/xenia/kernel/xam/apps/xmp_app.h @@ -11,6 +11,7 @@ #define XENIA_KERNEL_XAM_APPS_XMP_APP_H_ #include +#include #include #include #include @@ -19,6 +20,8 @@ #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/xam/app_manager.h" +#include "xenia/apu/audio_driver.h" + namespace xe { namespace kernel { namespace xam { @@ -93,6 +96,8 @@ class XmpApp : public App { private: void OnStateChanged(); + void WorkerThreadMain(); + bool PlayFile(std::string_view filename); State state_; PlaybackClient playback_client_; @@ -107,6 +112,14 @@ class XmpApp : public App { std::unordered_map playlists_; uint32_t next_playlist_handle_; uint32_t next_song_handle_; + + std::atomic worker_running_ = {false}; + std::unique_ptr worker_thread_; + + bool paused_ = true; + xe::threading::Fence resume_fence_; // Signaled when resume requested. + std::mutex driver_mutex_ = {}; + xe::apu::AudioDriver* driver_ = nullptr; }; } // namespace apps