diff --git a/src/xenia/apu/audio_media_player.cc b/src/xenia/apu/audio_media_player.cc new file mode 100644 index 000000000..6c11f2f8e --- /dev/null +++ b/src/xenia/apu/audio_media_player.cc @@ -0,0 +1,558 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2024 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include "xenia/apu/audio_media_player.h" +#include "xenia/apu/audio_driver.h" +#include "xenia/apu/audio_system.h" +#include "xenia/apu/xma_context.h" +#include "xenia/base/logging.h" + +#include + +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/libavformat/avio.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 apu { + +int32_t InitializeAndOpenAvCodec(std::vector* song_data, + AVFormatContext*& format_context, + AVCodecContext*& av_context) { + AVIOContext* io_ctx = + avio_alloc_context(song_data->data(), (int)song_data->size(), 0, nullptr, + nullptr, nullptr, nullptr); + + format_context = avformat_alloc_context(); + format_context->pb = io_ctx; + + int ret = avformat_open_input(&format_context, nullptr, nullptr, nullptr); + if (ret < 0) { + return ret; + } + // Processing data + ret = avformat_find_stream_info(format_context, nullptr); + if (ret < 0) { + return ret; + } + AVStream* stream = format_context->streams[0]; + + // find & open codec + AVCodecParameters* codec = stream->codecpar; + auto decoder = avcodec_find_decoder(codec->codec_id); + av_context = avcodec_alloc_context3(decoder); + + // Fill codec context with codec parameters + ret = avcodec_parameters_to_context(av_context, codec); + if (ret < 0) { + return ret; + } + + ret = avcodec_open2(av_context, decoder, NULL); + return ret; +} + +void ConvertAudioFrame(AVFrame* frame, int channel_count, + std::vector* framebuffer) { + framebuffer->reserve(frame->nb_samples * channel_count); + + switch (frame->format) { + case AV_SAMPLE_FMT_FLTP: { + for (int sample = 0; sample < frame->nb_samples; sample++) { + for (int ch = 0; ch < channel_count; ch++) { + float sampleValue = reinterpret_cast(frame->data[ch])[sample]; + framebuffer->push_back(sampleValue); + } + } + break; + } + + case AV_SAMPLE_FMT_FLT: { + float* frameData = reinterpret_cast(frame->data[0]); + framebuffer->insert(framebuffer->end(), frameData, + frameData + frame->nb_samples * channel_count); + + break; + } + + default: + break; + } +} + +ProcessAudioResult ProcessAudioLoop(AudioMediaPlayer* player, + AudioDriver* driver, AVFormatContext* s, + AVCodecContext* avctx, int streamIndex) { + AVPacket* packet = av_packet_alloc(); + AVFrame* frame = av_frame_alloc(); + std::vector frameBuffer; + + while (av_read_frame(s, packet) >= 0) { + if (!player->IsSongLoaded()) { + av_frame_free(&frame); + av_packet_free(&packet); + return ProcessAudioResult::ForcedFinish; + } + + if (packet->stream_index == streamIndex) { + int ret = avcodec_send_packet(avctx, packet); + if (ret < 0) { + XELOGE("Error sending packet for decoding: {:X}", ret); + break; + } + + while (ret >= 0) { + if (!player->IsSongLoaded()) { + break; + } + + ret = avcodec_receive_frame(avctx, frame); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break; + if (ret < 0) { + XELOGW("Error during decoding: {:X}", ret); + break; + } + + ConvertAudioFrame(frame, avctx->channels, &frameBuffer); + player->ProcessAudioBuffer(&frameBuffer); + } + } + av_packet_unref(packet); + } + + av_frame_free(&frame); + av_packet_free(&packet); + return ProcessAudioResult::Successful; +} + +AudioMediaPlayer::AudioMediaPlayer(apu::AudioSystem* audio_system, + kernel::KernelState* kernel_state) + : audio_system_(audio_system), + kernel_state_(kernel_state), + active_playlist_(nullptr), + active_song_(nullptr) {}; + +AudioMediaPlayer::~AudioMediaPlayer() { + Stop(); + DeleteDriver(); +}; + +void AudioMediaPlayer::WorkerThreadMain() { + while (worker_running_) { + if (!IsPlaying()) { + resume_fence_.Wait(); + } + + if (!active_playlist_) { + xe::threading::Sleep(std::chrono::milliseconds(500)); + continue; + } + + if (active_song_) { + Play(); + } + } +} + +void AudioMediaPlayer::Setup() { + if (!cvars::enable_xmp) { + return; + } + + // sample_buffer_ptr_ = kernel_state_->memory()->SystemHeapAlloc( + // xe::apu::AudioDriver::kFrameSamplesMax); + + worker_running_ = true; + worker_thread_ = threading::Thread::Create({}, [&] { WorkerThreadMain(); }); + worker_thread_->set_name("Audio Media Player"); +}; + +X_STATUS AudioMediaPlayer::Play(uint32_t playlist_handle, uint32_t song_handle, + bool force) { + auto playlist_itr = playlists_.find(playlist_handle); + if (playlist_itr == playlists_.cend()) { + return X_STATUS_UNSUCCESSFUL; + } + + active_playlist_ = playlist_itr->second.get(); + + // I've set it to false (force) for PGR3 + if (!IsIdle()) { + Stop(false, force); + } + + if (!song_handle) { + active_song_ = active_playlist_->songs.cbegin()->get(); + resume_fence_.Signal(); + return X_STATUS_SUCCESS; + } + + auto song_itr = std::find_if( + active_playlist_->songs.cbegin(), active_playlist_->songs.cend(), + [song_handle](const std::unique_ptr& song) { + return song->handle == song_handle; + }); + + if (song_itr == active_playlist_->songs.cend()) { + return X_STATUS_UNSUCCESSFUL; + } + + active_song_ = song_itr->get(); + resume_fence_.Signal(); + return X_STATUS_SUCCESS; +} + +void AudioMediaPlayer::Play() { + std::vector* song_buffer = new std::vector(); + + if (!LoadSongToMemory(song_buffer)) { + return; + } + + AVFormatContext* formatContext = nullptr; + AVCodecContext* codecContext = nullptr; + InitializeAndOpenAvCodec(song_buffer, formatContext, codecContext); + + if (!SetupDriver(codecContext->sample_rate, codecContext->channels)) { + XELOGE("Driver initialization failed!"); + avcodec_free_context(&codecContext); + av_freep(&formatContext->pb->buffer); + avio_context_free(&formatContext->pb); + avformat_close_input(&formatContext); + return; + } + + state_ = XmpApp::State::kPlaying; + current_song_handle_ = active_song_->handle; + OnStateChanged(); + + if (volume_ == 0.0f) { + volume_ = cvars::xmp_default_volume / 100.0f; + SetVolume(volume_); + } + + auto result = + ProcessAudioLoop(this, driver_.get(), formatContext, codecContext, 0); + + // We need to stop playback only if it wasn't + if (result != ProcessAudioResult::ForcedFinish) { + Stop(); + } + + // We're waiting for dangling samples to finish playing. + if (result == ProcessAudioResult::Successful) { + xe::threading::Wait(driver_semaphore_.get(), true, + std::chrono::milliseconds(500)); + } + + // Cleanup after work + avcodec_free_context(&codecContext); + av_freep(&formatContext->pb->buffer); + avio_context_free(&formatContext->pb); + avformat_close_input(&formatContext); + + DeleteDriver(); + processing_end_fence_.Signal(); + + if (result == ProcessAudioResult::ForcedFinish) { + return; + } + + if (!IsLastSongInPlaylist()) { + Next(); + return; + } + + // We're after last song in playlist + if (IsInRepeatMode()) { + Play(active_playlist_->handle, 0, true); + }; +} + +void AudioMediaPlayer::Pause() { + if (!IsPlaying()) { + return; + } + + if (driver_) { + driver_->Pause(); + } + state_ = XmpApp::State::kPaused; + OnStateChanged(); +}; + +void AudioMediaPlayer::Stop(bool change_state, bool force) { + if (IsIdle()) { + return; + } + + if (driver_ && IsPaused()) { + driver_->Resume(); + } + + state_ = XmpApp::State::kIdle; + active_song_ = nullptr; + + if (!force) { + processing_end_fence_.Wait(); + } + + if (change_state) { + OnStateChanged(); + } +}; + +void AudioMediaPlayer::Continue() { + if (!IsPaused()) { + return; + } + + state_ = XmpApp::State::kPlaying; + resume_fence_.Signal(); + if (driver_) { + driver_->Resume(); + } + OnStateChanged(); +} + +X_STATUS AudioMediaPlayer::Next() { + if (!active_playlist_) { + return X_STATUS_UNSUCCESSFUL; + } + + if (active_song_) { + Stop(false, false); + } + + auto itr = std::find_if(active_playlist_->songs.cbegin(), + active_playlist_->songs.cend(), + [this](const std::unique_ptr& song) { + return song->handle == current_song_handle_; + }); + + // There is no song with such ID? + if (itr == active_playlist_->songs.cend()) { + return X_STATUS_UNSUCCESSFUL; + } + + itr = std::next(itr); + + if (itr != active_playlist_->songs.cend()) { + active_song_ = itr->get(); + resume_fence_.Signal(); + return X_STATUS_SUCCESS; + } + + active_song_ = active_playlist_->songs.cbegin()->get(); + resume_fence_.Signal(); + return X_STATUS_SUCCESS; +} + +X_STATUS AudioMediaPlayer::Previous() { + if (!active_playlist_) { + return X_STATUS_UNSUCCESSFUL; + } + + if (active_song_) { + Stop(false, false); + } + + auto itr = std::find_if(active_playlist_->songs.cbegin(), + active_playlist_->songs.cend(), + [this](const std::unique_ptr& song) { + return song->handle == current_song_handle_; + }); + + // We're at the first entry, we need to go to the end. + if (itr == active_playlist_->songs.cbegin()) { + active_song_ = active_playlist_->songs.crbegin()->get(); + resume_fence_.Signal(); + return X_STATUS_SUCCESS; + } + + itr = std::prev(itr); + active_song_ = itr->get(); + resume_fence_.Signal(); + return X_STATUS_SUCCESS; +} + +bool AudioMediaPlayer::LoadSongToMemory(std::vector* buffer) { + if (!active_song_) { + return false; + } + + // Find file based on provided path? + vfs::File* vfs_file; + vfs::FileAction file_action; + X_STATUS result = kernel_state_->file_system()->OpenFile( + nullptr, xe::to_utf8(active_song_->file_path), + vfs::FileDisposition::kOpen, vfs::FileAccess::kGenericRead, false, true, + &vfs_file, &file_action); + + if (result) { + return false; + } + + buffer->resize(vfs_file->entry()->size()); + size_t bytes_read = 0; + result = vfs_file->ReadSync(buffer->data(), vfs_file->entry()->size(), 0, + &bytes_read); + + return !result; +} + +void AudioMediaPlayer::AddPlaylist(uint32_t handle, + std::unique_ptr playlist) { + if (playlists_.count(handle) != 0) { + return; + } + + if (playback_mode_ == XmpApp::PlaybackMode::kShuffle) { + auto rng = std::default_random_engine{}; + std::shuffle(playlist->songs.begin(), playlist->songs.end(), rng); + } + + playlists_.insert({handle, std::move(playlist)}); +} + +void AudioMediaPlayer::RemovePlaylist(uint32_t handle) { + if (playlists_.count(handle) == 0) { + return; + } + + // TODO: Check if currently played song is from that playlist and stop + // playback. + if (active_playlist_ && active_song_) { + Stop(); + } + + playlists_.erase(handle); +} + +X_STATUS AudioMediaPlayer::SetVolume(float volume) { + volume_ = std::min(volume, 1.0f); + + std::unique_lock guard(driver_mutex_); + if (!driver_) { + return X_STATUS_UNSUCCESSFUL; + } + + driver_->SetVolume(volume_); + return X_STATUS_SUCCESS; +} + +bool AudioMediaPlayer::IsLastSongInPlaylist() const { + if (!active_playlist_) { + return false; + } + + auto itr = std::find_if(active_playlist_->songs.cbegin(), + active_playlist_->songs.cend(), + [this](const std::unique_ptr& song) { + return song->handle == current_song_handle_; + }); + itr = std::next(itr); + return itr == active_playlist_->songs.cend(); +} + +void AudioMediaPlayer::SetCaptureCallback(uint32_t callback, uint32_t context, + bool title_render) { + // TODO: Something is incorrect with callback and causes audio to be muted! + callback_ = 0; + callback_context_ = context; + is_title_rendering_enabled_ = false; // title_render; +} + +void AudioMediaPlayer::OnStateChanged() { + kernel_state_->BroadcastNotification(kNotificationXmpStateChanged, + static_cast(state_)); +} + +void AudioMediaPlayer::ProcessAudioBuffer(std::vector* buffer) { + while (buffer->size() >= xe::apu::AudioDriver::kFrameSamplesMax) { + xe::threading::Wait(driver_semaphore_.get(), true); + + if (!IsSongLoaded()) { + buffer->clear(); + break; + } + + // XMP should be processed on Audio thread. Some games goes insane when they + // have additional unexpected guest thread (NG2 for example). + /* + if (callback_) { + std::memcpy(kernel_state_->memory()->TranslateVirtual(sample_buffer_ptr_), + buffer->data(), xe::apu::AudioDriver::kFrameSamplesMax); + + uint64_t args[] = {sample_buffer_ptr_, callback_context_, true}; + audio_system_->processor()->Execute(worker_thread_->thread_state(), + callback_, args, xe::countof(args)); + }*/ + + driver_->SubmitFrame(buffer->data()); + buffer->erase(buffer->begin(), + buffer->begin() + xe::apu::AudioDriver::kFrameSamplesMax); + } +} + +bool AudioMediaPlayer::SetupDriver(uint32_t sample_rate, uint32_t channels) { + DeleteDriver(); + + std::unique_lock guard(driver_mutex_); + driver_semaphore_ = xe::threading::Semaphore::Create( + AudioSystem::kMaximumQueuedFrames, AudioSystem::kMaximumQueuedFrames); + + if (!driver_semaphore_) { + return false; + } + + driver_ = std::unique_ptr(audio_system_->CreateDriver( + driver_semaphore_.get(), sample_rate, channels, false)); + if (!driver_) { + driver_semaphore_.reset(); + return false; + } + + if (!driver_->Initialize()) { + driver_semaphore_.reset(); + driver_->Shutdown(); + driver_.reset(); + return false; + } + + return true; +} + +void AudioMediaPlayer::DeleteDriver() { + std::unique_lock guard(driver_mutex_); + if (driver_) { + if (driver_semaphore_) { + driver_semaphore_.reset(); + } + + driver_->Shutdown(); + driver_.reset(); + } +} + +} // namespace apu +} // namespace xe \ No newline at end of file diff --git a/src/xenia/apu/audio_media_player.h b/src/xenia/apu/audio_media_player.h new file mode 100644 index 000000000..81ed9a0db --- /dev/null +++ b/src/xenia/apu/audio_media_player.h @@ -0,0 +1,138 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2024 Xenia Canary. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_APU_AUDIO_MEDIA_PLAYER_H_ +#define XENIA_APU_AUDIO_MEDIA_PLAYER_H_ + +#include "xenia/apu/audio_system.h" +#include "xenia/kernel/xam/apps/xmp_app.h" + +namespace xe { +namespace apu { + +using XmpApp = kernel::xam::apps::XmpApp; + +enum ProcessAudioResult { Successful = 0, ForcedFinish = 1, Failure = 2 }; + +class AudioMediaPlayer { + public: + AudioMediaPlayer(apu::AudioSystem* audio_system, + kernel::KernelState* kernel_state); + ~AudioMediaPlayer(); + + void Setup(); + + X_STATUS Play(uint32_t playlist_handle, uint32_t song_handle, bool force); + X_STATUS Next(); + X_STATUS Previous(); + + void Stop(bool change_state = false, bool force = true); + void Pause(); + void Continue(); + + XmpApp::State GetState() const { return state_; } + + bool IsIdle() const { return state_ == XmpApp::State::kIdle; } + bool IsPlaying() const { return state_ == XmpApp::State::kPlaying; } + bool IsPaused() const { return state_ == XmpApp::State::kPaused; } + bool IsSongLoaded() const { return active_song_; } + + bool IsInRepeatMode() const { + return repeat_mode_ == XmpApp::RepeatMode::kPlaylist; + } + + bool IsLastSongInPlaylist() const; + + X_STATUS SetVolume(float volume); + float GetVolume() const { return volume_; } + + void SetPlaybackMode(XmpApp::PlaybackMode playback_mode) { + playback_mode_ = playback_mode; + } + XmpApp::PlaybackMode GetPlaybackMode() const { return playback_mode_; } + + void SetRepeatMode(XmpApp::RepeatMode repeat_mode) { + repeat_mode_ = repeat_mode; + } + XmpApp::RepeatMode GetRepeatMode() { return repeat_mode_; } + + void SetPlaybackFlags(XmpApp::PlaybackFlags playback_flags) { + playback_flags_ = playback_flags; + } + XmpApp::PlaybackFlags GetPlaybackFlags() const { return playback_flags_; } + + void SetPlaybackClient(XmpApp::PlaybackClient playback_client) { + playback_client_ = playback_client; + } + + XmpApp::PlaybackClient GetPlaybackClient() const { return playback_client_; } + + bool IsTitleInPlaybackControl() const { + return playback_client_ == XmpApp::PlaybackClient::kTitle || + is_title_rendering_enabled_; + } + + void SetCaptureCallback(uint32_t callback, uint32_t context, + bool title_render); + + void AddPlaylist(uint32_t handle, std::unique_ptr playlist); + void RemovePlaylist(uint32_t handle); + + XmpApp::Song* GetCurrentSong() const { return active_song_; } + + void ProcessAudioBuffer(std::vector* buffer); + + private: + void OnStateChanged(); + + void Play(); + void WorkerThreadMain(); + bool LoadSongToMemory(std::vector* buffer); + + XmpApp::State state_ = XmpApp::State::kIdle; + XmpApp::PlaybackClient playback_client_ = XmpApp::PlaybackClient::kSystem; + XmpApp::PlaybackMode playback_mode_ = XmpApp::PlaybackMode::kInOrder; + XmpApp::RepeatMode repeat_mode_ = XmpApp::RepeatMode::kPlaylist; + XmpApp::PlaybackFlags playback_flags_ = XmpApp::PlaybackFlags::kDefault; + float volume_ = 1.0f; + + std::unordered_map> playlists_; + XmpApp::Playlist* active_playlist_; + XmpApp::Song* active_song_; + + size_t current_song_handle_ = 0; + + uint32_t callback_ = 0; + uint32_t callback_context_ = 0; + uint32_t sample_buffer_ptr_ = 0; + bool is_title_rendering_enabled_ = false; + + AudioSystem* audio_system_ = nullptr; + kernel::KernelState* kernel_state_ = nullptr; + + xe::global_critical_region global_critical_region_; + std::atomic worker_running_ = {false}; + std::unique_ptr worker_thread_; + xe::threading::Fence resume_fence_; // Signaled when resume requested. + xe::threading::Fence processing_end_fence_; + + // Driver part - This should be integrated into audio_system, but it isn't + // really compatible with it. + std::unique_ptr driver_ = nullptr; + std::unique_ptr driver_semaphore_ = {}; + xe::xe_fast_mutex driver_mutex_ = {}; + + bool SetupDriver(uint32_t sample_rate, uint32_t channels); + void DeleteDriver(); +}; + +} // namespace apu +} // namespace xe + +#endif diff --git a/src/xenia/apu/audio_system.h b/src/xenia/apu/audio_system.h index 482559b6d..e18ce7c1d 100644 --- a/src/xenia/apu/audio_system.h +++ b/src/xenia/apu/audio_system.h @@ -30,6 +30,10 @@ class XmaDecoder; class AudioSystem { public: + // TODO(gibbed): respect XAUDIO2_MAX_QUEUED_BUFFERS somehow (ie min(64, + // XAUDIO2_MAX_QUEUED_BUFFERS)) + static const size_t kMaximumQueuedFrames = 64; + virtual ~AudioSystem(); Memory* memory() const { return memory_; } @@ -68,10 +72,6 @@ class AudioSystem { AudioDriver** out_driver) = 0; virtual void DestroyDriver(AudioDriver* driver) = 0; - // TODO(gibbed): respect XAUDIO2_MAX_QUEUED_BUFFERS somehow (ie min(64, - // XAUDIO2_MAX_QUEUED_BUFFERS)) - static const size_t kMaximumQueuedFrames = 64; - Memory* memory_ = nullptr; cpu::Processor* processor_ = nullptr; std::unique_ptr xma_decoder_; diff --git a/src/xenia/apu/premake5.lua b/src/xenia/apu/premake5.lua index 3f1b45aed..8f04e7593 100644 --- a/src/xenia/apu/premake5.lua +++ b/src/xenia/apu/premake5.lua @@ -9,6 +9,7 @@ project("xenia-apu") links({ "libavcodec", "libavutil", + "libavformat", "xenia-base", }) defines({ diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc index 8e00cb0ae..7fd639525 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -120,6 +120,7 @@ Emulator::Emulator(const std::filesystem::path& command_line, display_window_(nullptr), memory_(), audio_system_(), + audio_media_player_(), graphics_system_(), input_system_(), export_resolver_(), @@ -176,6 +177,7 @@ Emulator::~Emulator() { input_system_.reset(); graphics_system_.reset(); audio_system_.reset(); + audio_media_player_.reset(); kernel_state_.reset(); file_system_.reset(); @@ -311,6 +313,9 @@ X_STATUS Emulator::Setup( if (result) { return result; } + audio_media_player_ = std::make_unique( + audio_system_.get(), kernel_state_.get()); + audio_media_player_->Setup(); } // Initialize emulator fallback exception handling last. diff --git a/src/xenia/emulator.h b/src/xenia/emulator.h index ccf37d7f9..6bc037759 100644 --- a/src/xenia/emulator.h +++ b/src/xenia/emulator.h @@ -17,6 +17,7 @@ #include #include +#include "xenia/apu/audio_media_player.h" #include "xenia/base/delegate.h" #include "xenia/base/exception_handler.h" #include "xenia/kernel/kernel_state.h" @@ -141,6 +142,11 @@ class Emulator { // Audio hardware emulation for decoding and playback. apu::AudioSystem* audio_system() const { return audio_system_.get(); } + // Xbox media player (XMP) emulation for WMA and MP3 playback. + apu::AudioMediaPlayer* audio_media_player() const { + return audio_media_player_.get(); + } + // GPU emulation for command list processing. gpu::GraphicsSystem* graphics_system() const { return graphics_system_.get(); @@ -301,6 +307,7 @@ class Emulator { std::unique_ptr processor_; std::unique_ptr audio_system_; + std::unique_ptr audio_media_player_; std::unique_ptr graphics_system_; std::unique_ptr input_system_; diff --git a/src/xenia/kernel/premake5.lua b/src/xenia/kernel/premake5.lua index 972e9ebd5..bd00330bc 100644 --- a/src/xenia/kernel/premake5.lua +++ b/src/xenia/kernel/premake5.lua @@ -16,15 +16,9 @@ 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 bd9e91e94..cceae3726 100644 --- a/src/xenia/kernel/xam/apps/xmp_app.cc +++ b/src/xenia/kernel/xam/apps/xmp_app.cc @@ -15,279 +15,14 @@ #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"); +#include "xenia/apu/audio_media_player.h" namespace xe { namespace kernel { namespace xam { namespace apps { -XmpApp::XmpApp(KernelState* kernel_state) - : App(kernel_state, 0xFA), - state_(State::kIdle), - playback_client_(PlaybackClient::kTitle), - playback_mode_(PlaybackMode::kUnknown), - repeat_mode_(RepeatMode::kUnknown), - unknown_flags_(0), - volume_(cvars::xmp_default_volume / 100.0f), - active_playlist_(nullptr), - active_song_index_(0), - next_playlist_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)); - } - } -} +XmpApp::XmpApp(KernelState* kernel_state) : App(kernel_state, 0xFA) {} X_HRESULT XmpApp::XMPGetStatus(uint32_t state_ptr) { if (!XThread::GetCurrentThread()->main_thread()) { @@ -296,9 +31,11 @@ X_HRESULT XmpApp::XMPGetStatus(uint32_t state_ptr) { xe::threading::Sleep(std::chrono::milliseconds(1)); } - XELOGD("XMPGetStatus({:08X}) -> {:d}", state_ptr, (uint32_t)state_); - xe::store_and_swap(memory_->TranslateVirtual(state_ptr), - static_cast(state_)); + const uint32_t state = static_cast( + kernel_state_->emulator()->audio_media_player()->GetState()); + + XELOGD("XMPGetStatus({:08X}) -> {:d}", state_ptr, state); + xe::store_and_swap(memory_->TranslateVirtual(state_ptr), state); return X_E_SUCCESS; } @@ -311,40 +48,41 @@ X_HRESULT XmpApp::XMPCreateTitlePlaylist( "{:08X})", songs_ptr, song_count, playlist_name_ptr, xe::to_utf8(playlist_name), flags, out_song_handles, out_playlist_handle); + auto playlist = std::make_unique(); playlist->handle = ++next_playlist_handle_; playlist->name = playlist_name; playlist->flags = flags; if (songs_ptr) { + XMP_SONGDESCRIPTOR* song_descriptor = + memory_->TranslateVirtual(songs_ptr); + for (uint32_t i = 0; i < song_count; ++i) { auto song = std::make_unique(); song->handle = ++next_song_handle_; - uint8_t* song_base = memory_->TranslateVirtual(songs_ptr + (i * 36)); - song->file_path = - xe::load_and_swap(memory_->TranslateVirtual( - xe::load_and_swap(song_base + 0))); - song->name = xe::load_and_swap(memory_->TranslateVirtual( - xe::load_and_swap(song_base + 4))); - song->artist = - xe::load_and_swap(memory_->TranslateVirtual( - xe::load_and_swap(song_base + 8))); - song->album = xe::load_and_swap(memory_->TranslateVirtual( - xe::load_and_swap(song_base + 12))); - song->album_artist = - xe::load_and_swap(memory_->TranslateVirtual( - xe::load_and_swap(song_base + 16))); - song->genre = xe::load_and_swap(memory_->TranslateVirtual( - xe::load_and_swap(song_base + 20))); - song->track_number = xe::load_and_swap(song_base + 24); - song->duration_ms = xe::load_and_swap(song_base + 28); + song->file_path = xe::load_and_swap( + memory_->TranslateVirtual(song_descriptor[i].file_path_ptr)); + song->name = xe::load_and_swap( + memory_->TranslateVirtual(song_descriptor[i].title_ptr)); + song->artist = xe::load_and_swap( + memory_->TranslateVirtual(song_descriptor[i].artist_ptr)); + song->album = xe::load_and_swap( + memory_->TranslateVirtual(song_descriptor[i].album_ptr)); + song->album_artist = xe::load_and_swap( + memory_->TranslateVirtual(song_descriptor[i].album_artist_ptr)); + song->genre = xe::load_and_swap( + memory_->TranslateVirtual(song_descriptor[i].genre_ptr)); + song->track_number = song_descriptor[i].track_number; + song->duration_ms = song_descriptor[i].duration; song->format = static_cast( - xe::load_and_swap(song_base + 32)); + xe::byte_swap(song_descriptor[i].song_format)); + if (out_song_handles) { xe::store_and_swap( memory_->TranslateVirtual(out_song_handles + (i * 4)), song->handle); } - playlist->songs.emplace_back(std::move(song)); + playlist->songs.push_back(std::move(song)); } } if (out_playlist_handle) { @@ -352,48 +90,24 @@ X_HRESULT XmpApp::XMPCreateTitlePlaylist( playlist->handle); } - auto global_lock = global_critical_region_.Acquire(); - playlists_.insert({playlist->handle, playlist.get()}); - playlist.release(); + kernel_state_->emulator()->audio_media_player()->AddPlaylist( + next_playlist_handle_, std::move(playlist)); + return X_E_SUCCESS; } X_HRESULT XmpApp::XMPDeleteTitlePlaylist(uint32_t playlist_handle) { XELOGD("XMPDeleteTitlePlaylist({:08X})", playlist_handle); - auto global_lock = global_critical_region_.Acquire(); - auto it = playlists_.find(playlist_handle); - if (it == playlists_.end()) { - XELOGE("Playlist {:08X} not found", playlist_handle); - return X_E_NOTFOUND; - } - auto playlist = it->second; - if (playlist == active_playlist_) { - XMPStop(0); - } - playlists_.erase(it); - delete playlist; + kernel_state_->emulator()->audio_media_player()->RemovePlaylist( + playlist_handle); return X_E_SUCCESS; } X_HRESULT XmpApp::XMPPlayTitlePlaylist(uint32_t playlist_handle, uint32_t song_handle) { XELOGD("XMPPlayTitlePlaylist({:08X}, {:08X})", playlist_handle, song_handle); - Playlist* playlist = nullptr; - { - auto global_lock = global_critical_region_.Acquire(); - auto it = playlists_.find(playlist_handle); - if (it == playlists_.end()) { - XELOGE("Playlist {:08X} not found", playlist_handle); - return X_E_NOTFOUND; - } - playlist = it->second; - } - - active_playlist_ = playlist; - active_song_index_ = 0; - state_ = State::kPlaying; - resume_fence_.Signal(); - OnStateChanged(); + kernel_state_->emulator()->audio_media_player()->Play(playlist_handle, + song_handle, false); kernel_state_->BroadcastNotification(kNotificationXmpPlaybackBehaviorChanged, 1); return X_E_SUCCESS; @@ -401,75 +115,35 @@ X_HRESULT XmpApp::XMPPlayTitlePlaylist(uint32_t playlist_handle, 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(); + kernel_state_->emulator()->audio_media_player()->Continue(); return X_E_SUCCESS; } X_HRESULT XmpApp::XMPStop(uint32_t unk) { assert_zero(unk); XELOGD("XMPStop({:08X})", unk); - active_playlist_ = nullptr; // ? - active_song_index_ = 0; - state_ = State::kIdle; - OnStateChanged(); + kernel_state_->emulator()->audio_media_player()->Stop(true, false); return X_E_SUCCESS; } 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(); + kernel_state_->emulator()->audio_media_player()->Pause(); return X_E_SUCCESS; } X_HRESULT XmpApp::XMPNext() { XELOGD("XMPNext()"); - if (!active_playlist_) { - return X_E_NOTFOUND; - } - state_ = State::kPlaying; - active_song_index_ = - (active_song_index_ + 1) % active_playlist_->songs.size(); - resume_fence_.Signal(); - OnStateChanged(); + kernel_state_->emulator()->audio_media_player()->Next(); return X_E_SUCCESS; } X_HRESULT XmpApp::XMPPrevious() { XELOGD("XMPPrevious()"); - if (!active_playlist_) { - return X_E_NOTFOUND; - } - state_ = State::kPlaying; - if (!active_song_index_) { - active_song_index_ = static_cast(active_playlist_->songs.size()) - 1; - } else { - --active_song_index_; - } - resume_fence_.Signal(); - OnStateChanged(); + kernel_state_->emulator()->audio_media_player()->Previous(); return X_E_SUCCESS; } -void XmpApp::OnStateChanged() { - kernel_state_->BroadcastNotification(kNotificationXmpStateChanged, - static_cast(state_)); -} - X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, uint32_t buffer_length) { // NOTE: buffer_length may be zero or valid. @@ -531,9 +205,14 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, XELOGD("XMPSetPlaybackBehavior({:08X}, {:08X}, {:08X})", uint32_t(args->playback_mode), uint32_t(args->repeat_mode), uint32_t(args->flags)); - playback_mode_ = static_cast(uint32_t(args->playback_mode)); - repeat_mode_ = static_cast(uint32_t(args->repeat_mode)); - unknown_flags_ = args->flags; + + kernel_state_->emulator()->audio_media_player()->SetPlaybackMode( + static_cast(uint32_t(args->playback_mode))); + kernel_state_->emulator()->audio_media_player()->SetRepeatMode( + static_cast(uint32_t(args->repeat_mode))); + kernel_state_->emulator()->audio_media_player()->SetPlaybackFlags( + static_cast(uint32_t(args->flags))); + kernel_state_->BroadcastNotification( kNotificationXmpPlaybackBehaviorChanged, 0); return X_E_SUCCESS; @@ -556,8 +235,10 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, assert_true(args->xmp_client == 0x00000002); XELOGD("XMPGetVolume({:08X})", uint32_t(args->volume_ptr)); - xe::store_and_swap(memory_->TranslateVirtual(args->volume_ptr), - volume_); + + xe::store_and_swap( + memory_->TranslateVirtual(args->volume_ptr), + kernel_state_->emulator()->audio_media_player()->GetVolume()); return X_E_SUCCESS; } case 0x0007000C: { @@ -569,12 +250,9 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, static_assert_size(decltype(*args), 8); 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_); - } + XELOGD("XMPSetVolume({:d}, {:g})", args->xmp_client, float(args->value)); + kernel_state_->emulator()->audio_media_player()->SetVolume( + float(args->value)); return X_E_SUCCESS; } case 0x0007000D: { @@ -604,9 +282,7 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, playlist_name = xe::load_and_swap( memory_->TranslateVirtual(args->playlist_name_ptr)); } - // dummy_alloc_ptr is the result of a XamAlloc of storage_size. - assert_true(uint32_t(args->storage_size) == - 4 + uint32_t(args->song_count) * 128); + return XMPCreateTitlePlaylist(args->songs_ptr, args->song_count, args->playlist_name_ptr, playlist_name, args->flags, args->song_handles_ptr, @@ -621,26 +297,31 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, }* args = memory_->TranslateVirtual(buffer_ptr); static_assert_size(decltype(*args), 12); - auto info = memory_->TranslateVirtual(args->info_ptr); + auto info = memory_->TranslateVirtual(args->info_ptr); assert_true(args->xmp_client == 0x00000002); assert_zero(args->unk_ptr); - XELOGE("XMPGetInfo?({:08X}, {:08X})", uint32_t(args->unk_ptr), + XELOGE("XMPGetCurrentSong({:08X}, {:08X})", uint32_t(args->unk_ptr), uint32_t(args->info_ptr)); - if (!active_playlist_) { + + Song* current_song = + kernel_state_->emulator()->audio_media_player()->GetCurrentSong(); + + if (!current_song) { return X_E_FAIL; } - auto& song = active_playlist_->songs[active_song_index_]; - xe::store_and_swap(info + 0, song->handle); - xe::store_and_swap(info + 4 + 572 + 0, song->name); - xe::store_and_swap(info + 4 + 572 + 40, song->artist); - xe::store_and_swap(info + 4 + 572 + 80, song->album); - xe::store_and_swap(info + 4 + 572 + 120, - song->album_artist); - xe::store_and_swap(info + 4 + 572 + 160, song->genre); - xe::store_and_swap(info + 4 + 572 + 200, song->track_number); - xe::store_and_swap(info + 4 + 572 + 204, song->duration_ms); - xe::store_and_swap(info + 4 + 572 + 208, - static_cast(song->format)); + + memset(info, 0, sizeof(XMP_SONGINFO)); + + info->handle = current_song->handle; + xe::store_and_swap(info->title, current_song->name); + xe::store_and_swap(info->artist, current_song->artist); + xe::store_and_swap(info->album, current_song->album); + xe::store_and_swap(info->album_artist, + current_song->album_artist); + xe::store_and_swap(info->genre, current_song->genre); + info->track_number = current_song->track_number; + info->duration = current_song->duration_ms; + info->song_format = static_cast(current_song->format); return X_E_SUCCESS; } case 0x00070013: { @@ -673,9 +354,14 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, XELOGD("XMPSetPlaybackController({:08X}, {:08X})", uint32_t(args->controller), uint32_t(args->playback_client)); - playback_client_ = PlaybackClient(uint32_t(args->playback_client)); + kernel_state_->emulator()->audio_media_player()->SetPlaybackClient( + PlaybackClient(uint32_t(args->playback_client))); + kernel_state_->BroadcastNotification( - kNotificationXmpPlaybackControllerChanged, !args->playback_client); + kNotificationXmpPlaybackControllerChanged, + kernel_state_->emulator() + ->audio_media_player() + ->IsTitleInPlaybackControl()); return X_E_SUCCESS; } case 0x0007001B: { @@ -717,7 +403,7 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, xe::be xmp_client; xe::be playback_mode_ptr; xe::be repeat_mode_ptr; - xe::be unk3_ptr; + xe::be playback_flags_ptr; }* args = memory_->TranslateVirtual(buffer_ptr); static_assert_size(decltype(*args), 16); @@ -725,20 +411,27 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, args->xmp_client == 0x00000000); XELOGD("XMPGetPlaybackBehavior({:08X}, {:08X}, {:08X})", uint32_t(args->playback_mode_ptr), uint32_t(args->repeat_mode_ptr), - uint32_t(args->unk3_ptr)); + uint32_t(args->playback_flags_ptr)); if (args->playback_mode_ptr) { xe::store_and_swap( memory_->TranslateVirtual(args->playback_mode_ptr), - static_cast(playback_mode_)); + static_cast(kernel_state_->emulator() + ->audio_media_player() + ->GetPlaybackMode())); } if (args->repeat_mode_ptr) { xe::store_and_swap( memory_->TranslateVirtual(args->repeat_mode_ptr), - static_cast(repeat_mode_)); + static_cast(kernel_state_->emulator() + ->audio_media_player() + ->GetRepeatMode())); } - if (args->unk3_ptr) { - xe::store_and_swap(memory_->TranslateVirtual(args->unk3_ptr), - unknown_flags_); + if (args->playback_flags_ptr) { + xe::store_and_swap( + memory_->TranslateVirtual(args->playback_flags_ptr), + static_cast(kernel_state_->emulator() + ->audio_media_player() + ->GetPlaybackFlags())); } return X_E_SUCCESS; } @@ -772,10 +465,22 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, return X_E_FAIL; } case 0x0007003D: { - // XMPCaptureOutput - not sure how this works :/ - XELOGD("XMPCaptureOutput(...)"); - assert_always("XMP output not unimplemented"); - return X_E_FAIL; + // XMPCaptureOutput + assert_true(!buffer_length || buffer_length == 16); + struct { + xe::be xmp_client; + xe::be callback; + xe::be context; + xe::be title_render; + }* args = memory_->TranslateVirtual(buffer_ptr); + static_assert_size(decltype(*args), 16); + + XELOGD("XMPCaptureOutput({:08X}, {:08X}, {:08X}, {:08X})", + args->xmp_client, args->callback, args->context, + args->title_render); + kernel_state_->emulator()->audio_media_player()->SetCaptureCallback( + args->callback, args->context, static_cast(args->title_render)); + return X_E_SUCCESS; } case 0x00070044: { // Called on the start up of all dashboard versions before kinect diff --git a/src/xenia/kernel/xam/apps/xmp_app.h b/src/xenia/kernel/xam/apps/xmp_app.h index 21628c626..2d0b94136 100644 --- a/src/xenia/kernel/xam/apps/xmp_app.h +++ b/src/xenia/kernel/xam/apps/xmp_app.h @@ -20,8 +20,6 @@ #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 { @@ -30,6 +28,36 @@ namespace apps { // Only source of docs for a lot of these functions: // https://github.com/oukiar/freestyledash/blob/master/Freestyle/Scenes/Media/Music/ScnMusic.cpp +struct XMP_SONGDESCRIPTOR { + xe::be file_path_ptr; + xe::be title_ptr; + xe::be artist_ptr; + xe::be album_ptr; + xe::be album_artist_ptr; + xe::be genre_ptr; + xe::be track_number; + xe::be duration; + xe::be song_format; +}; +static_assert_size(XMP_SONGDESCRIPTOR, 36); + +constexpr uint32_t kMaxXmpMetadataStringLength = 40; + +struct XMP_SONGINFO { + X_HANDLE handle; + + uint8_t unknown[0x23C]; + xe::be title[kMaxXmpMetadataStringLength]; + xe::be artist[kMaxXmpMetadataStringLength]; + xe::be album[kMaxXmpMetadataStringLength]; + xe::be album_artist[kMaxXmpMetadataStringLength]; + xe::be genre[kMaxXmpMetadataStringLength]; + xe::be track_number; + xe::be duration; + xe::be song_format; +}; +static_assert_size(XMP_SONGINFO, 988); + class XmpApp : public App { public: enum class State : uint32_t { @@ -42,12 +70,16 @@ class XmpApp : public App { kTitle = 1, }; enum class PlaybackMode : uint32_t { - // kInOrder = ?, - kUnknown = 0, + kInOrder = 0, + kShuffle = 1, }; enum class RepeatMode : uint32_t { - // kNoRepeat = ?, - kUnknown = 0, + kPlaylist = 0, + kNoRepeat = 1, + }; + enum class PlaybackFlags : uint32_t { + kDefault = 0, + kAutoPause = 1, }; struct Song { enum class Format : uint32_t { @@ -95,31 +127,11 @@ class XmpApp : public App { uint32_t buffer_length) override; private: - void OnStateChanged(); - void WorkerThreadMain(); - bool PlayFile(std::string_view filename); - - State state_; - PlaybackClient playback_client_; - PlaybackMode playback_mode_; - RepeatMode repeat_mode_; - uint32_t unknown_flags_; - float volume_; - Playlist* active_playlist_; - int active_song_index_; - xe::global_critical_region global_critical_region_; - 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; + // TODO: Remove it and replace with guest handles! + uint32_t next_playlist_handle_ = 0; + uint32_t next_song_handle_ = 0; }; } // namespace apps