[XMP] Added dedicated AudioMediaPlayer class
This commit is contained in:
parent
996b874d74
commit
b471423c1a
|
@ -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 <XAudio2.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/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<uint8_t>* 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<float>* 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<float*>(frame->data[ch])[sample];
|
||||||
|
framebuffer->push_back(sampleValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case AV_SAMPLE_FMT_FLT: {
|
||||||
|
float* frameData = reinterpret_cast<float*>(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<float> 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<XmpApp::Song>& 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<uint8_t>* song_buffer = new std::vector<uint8_t>();
|
||||||
|
|
||||||
|
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<XmpApp::Song>& 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<XmpApp::Song>& 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<uint8_t>* 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<XmpApp::Playlist> 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<xe::xe_fast_mutex> 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<XmpApp::Song>& 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<uint32_t>(state_));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioMediaPlayer::ProcessAudioBuffer(std::vector<float>* 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<xe::xe_fast_mutex> guard(driver_mutex_);
|
||||||
|
driver_semaphore_ = xe::threading::Semaphore::Create(
|
||||||
|
AudioSystem::kMaximumQueuedFrames, AudioSystem::kMaximumQueuedFrames);
|
||||||
|
|
||||||
|
if (!driver_semaphore_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
driver_ = std::unique_ptr<AudioDriver>(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<xe::xe_fast_mutex> guard(driver_mutex_);
|
||||||
|
if (driver_) {
|
||||||
|
if (driver_semaphore_) {
|
||||||
|
driver_semaphore_.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
driver_->Shutdown();
|
||||||
|
driver_.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace apu
|
||||||
|
} // namespace xe
|
|
@ -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<XmpApp::Playlist> playlist);
|
||||||
|
void RemovePlaylist(uint32_t handle);
|
||||||
|
|
||||||
|
XmpApp::Song* GetCurrentSong() const { return active_song_; }
|
||||||
|
|
||||||
|
void ProcessAudioBuffer(std::vector<float>* buffer);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void OnStateChanged();
|
||||||
|
|
||||||
|
void Play();
|
||||||
|
void WorkerThreadMain();
|
||||||
|
bool LoadSongToMemory(std::vector<uint8_t>* 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<uint32_t, std::unique_ptr<XmpApp::Playlist>> 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<bool> worker_running_ = {false};
|
||||||
|
std::unique_ptr<threading::Thread> 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<AudioDriver> driver_ = nullptr;
|
||||||
|
std::unique_ptr<xe::threading::Semaphore> driver_semaphore_ = {};
|
||||||
|
xe::xe_fast_mutex driver_mutex_ = {};
|
||||||
|
|
||||||
|
bool SetupDriver(uint32_t sample_rate, uint32_t channels);
|
||||||
|
void DeleteDriver();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace apu
|
||||||
|
} // namespace xe
|
||||||
|
|
||||||
|
#endif
|
|
@ -30,6 +30,10 @@ class XmaDecoder;
|
||||||
|
|
||||||
class AudioSystem {
|
class AudioSystem {
|
||||||
public:
|
public:
|
||||||
|
// TODO(gibbed): respect XAUDIO2_MAX_QUEUED_BUFFERS somehow (ie min(64,
|
||||||
|
// XAUDIO2_MAX_QUEUED_BUFFERS))
|
||||||
|
static const size_t kMaximumQueuedFrames = 64;
|
||||||
|
|
||||||
virtual ~AudioSystem();
|
virtual ~AudioSystem();
|
||||||
|
|
||||||
Memory* memory() const { return memory_; }
|
Memory* memory() const { return memory_; }
|
||||||
|
@ -68,10 +72,6 @@ class AudioSystem {
|
||||||
AudioDriver** out_driver) = 0;
|
AudioDriver** out_driver) = 0;
|
||||||
virtual void DestroyDriver(AudioDriver* 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;
|
Memory* memory_ = nullptr;
|
||||||
cpu::Processor* processor_ = nullptr;
|
cpu::Processor* processor_ = nullptr;
|
||||||
std::unique_ptr<XmaDecoder> xma_decoder_;
|
std::unique_ptr<XmaDecoder> xma_decoder_;
|
||||||
|
|
|
@ -9,6 +9,7 @@ project("xenia-apu")
|
||||||
links({
|
links({
|
||||||
"libavcodec",
|
"libavcodec",
|
||||||
"libavutil",
|
"libavutil",
|
||||||
|
"libavformat",
|
||||||
"xenia-base",
|
"xenia-base",
|
||||||
})
|
})
|
||||||
defines({
|
defines({
|
||||||
|
|
|
@ -120,6 +120,7 @@ Emulator::Emulator(const std::filesystem::path& command_line,
|
||||||
display_window_(nullptr),
|
display_window_(nullptr),
|
||||||
memory_(),
|
memory_(),
|
||||||
audio_system_(),
|
audio_system_(),
|
||||||
|
audio_media_player_(),
|
||||||
graphics_system_(),
|
graphics_system_(),
|
||||||
input_system_(),
|
input_system_(),
|
||||||
export_resolver_(),
|
export_resolver_(),
|
||||||
|
@ -176,6 +177,7 @@ Emulator::~Emulator() {
|
||||||
input_system_.reset();
|
input_system_.reset();
|
||||||
graphics_system_.reset();
|
graphics_system_.reset();
|
||||||
audio_system_.reset();
|
audio_system_.reset();
|
||||||
|
audio_media_player_.reset();
|
||||||
|
|
||||||
kernel_state_.reset();
|
kernel_state_.reset();
|
||||||
file_system_.reset();
|
file_system_.reset();
|
||||||
|
@ -311,6 +313,9 @@ X_STATUS Emulator::Setup(
|
||||||
if (result) {
|
if (result) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
audio_media_player_ = std::make_unique<apu::AudioMediaPlayer>(
|
||||||
|
audio_system_.get(), kernel_state_.get());
|
||||||
|
audio_media_player_->Setup();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize emulator fallback exception handling last.
|
// Initialize emulator fallback exception handling last.
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "xenia/apu/audio_media_player.h"
|
||||||
#include "xenia/base/delegate.h"
|
#include "xenia/base/delegate.h"
|
||||||
#include "xenia/base/exception_handler.h"
|
#include "xenia/base/exception_handler.h"
|
||||||
#include "xenia/kernel/kernel_state.h"
|
#include "xenia/kernel/kernel_state.h"
|
||||||
|
@ -141,6 +142,11 @@ class Emulator {
|
||||||
// Audio hardware emulation for decoding and playback.
|
// Audio hardware emulation for decoding and playback.
|
||||||
apu::AudioSystem* audio_system() const { return audio_system_.get(); }
|
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 emulation for command list processing.
|
||||||
gpu::GraphicsSystem* graphics_system() const {
|
gpu::GraphicsSystem* graphics_system() const {
|
||||||
return graphics_system_.get();
|
return graphics_system_.get();
|
||||||
|
@ -301,6 +307,7 @@ class Emulator {
|
||||||
|
|
||||||
std::unique_ptr<cpu::Processor> processor_;
|
std::unique_ptr<cpu::Processor> processor_;
|
||||||
std::unique_ptr<apu::AudioSystem> audio_system_;
|
std::unique_ptr<apu::AudioSystem> audio_system_;
|
||||||
|
std::unique_ptr<apu::AudioMediaPlayer> audio_media_player_;
|
||||||
std::unique_ptr<gpu::GraphicsSystem> graphics_system_;
|
std::unique_ptr<gpu::GraphicsSystem> graphics_system_;
|
||||||
std::unique_ptr<hid::InputSystem> input_system_;
|
std::unique_ptr<hid::InputSystem> input_system_;
|
||||||
|
|
||||||
|
|
|
@ -16,15 +16,9 @@ project("xenia-kernel")
|
||||||
"xenia-cpu",
|
"xenia-cpu",
|
||||||
"xenia-hid",
|
"xenia-hid",
|
||||||
"xenia-vfs",
|
"xenia-vfs",
|
||||||
"libavcodec",
|
|
||||||
"libavformat",
|
|
||||||
"libavutil",
|
|
||||||
})
|
})
|
||||||
defines({
|
defines({
|
||||||
})
|
})
|
||||||
includedirs({
|
|
||||||
project_root.."/third_party/FFmpeg/",
|
|
||||||
})
|
|
||||||
recursive_platform_files()
|
recursive_platform_files()
|
||||||
files({
|
files({
|
||||||
"debug_visualizers.natvis",
|
"debug_visualizers.natvis",
|
||||||
|
|
|
@ -15,279 +15,14 @@
|
||||||
#include "xenia/emulator.h"
|
#include "xenia/emulator.h"
|
||||||
#include "xenia/xbox.h"
|
#include "xenia/xbox.h"
|
||||||
|
|
||||||
#include "xenia/apu/audio_driver.h"
|
#include "xenia/apu/audio_media_player.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 xe {
|
||||||
namespace kernel {
|
namespace kernel {
|
||||||
namespace xam {
|
namespace xam {
|
||||||
namespace apps {
|
namespace apps {
|
||||||
|
|
||||||
XmpApp::XmpApp(KernelState* kernel_state)
|
XmpApp::XmpApp(KernelState* kernel_state) : App(kernel_state, 0xFA) {}
|
||||||
: 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<VFSContext*>(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<int>(bytes_read);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool XmpApp::PlayFile(std::string_view filename) {
|
|
||||||
auto playlist = active_playlist_;
|
|
||||||
|
|
||||||
const int buffer_size = 8192;
|
|
||||||
uint8_t* buffer = reinterpret_cast<uint8_t*>(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<std::mutex> 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<float> 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<float*>(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<float*>(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<std::mutex> 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) {
|
X_HRESULT XmpApp::XMPGetStatus(uint32_t state_ptr) {
|
||||||
if (!XThread::GetCurrentThread()->main_thread()) {
|
if (!XThread::GetCurrentThread()->main_thread()) {
|
||||||
|
@ -296,9 +31,11 @@ X_HRESULT XmpApp::XMPGetStatus(uint32_t state_ptr) {
|
||||||
xe::threading::Sleep(std::chrono::milliseconds(1));
|
xe::threading::Sleep(std::chrono::milliseconds(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
XELOGD("XMPGetStatus({:08X}) -> {:d}", state_ptr, (uint32_t)state_);
|
const uint32_t state = static_cast<uint32_t>(
|
||||||
xe::store_and_swap<uint32_t>(memory_->TranslateVirtual(state_ptr),
|
kernel_state_->emulator()->audio_media_player()->GetState());
|
||||||
static_cast<uint32_t>(state_));
|
|
||||||
|
XELOGD("XMPGetStatus({:08X}) -> {:d}", state_ptr, state);
|
||||||
|
xe::store_and_swap<uint32_t>(memory_->TranslateVirtual(state_ptr), state);
|
||||||
return X_E_SUCCESS;
|
return X_E_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,40 +48,41 @@ X_HRESULT XmpApp::XMPCreateTitlePlaylist(
|
||||||
"{:08X})",
|
"{:08X})",
|
||||||
songs_ptr, song_count, playlist_name_ptr, xe::to_utf8(playlist_name),
|
songs_ptr, song_count, playlist_name_ptr, xe::to_utf8(playlist_name),
|
||||||
flags, out_song_handles, out_playlist_handle);
|
flags, out_song_handles, out_playlist_handle);
|
||||||
|
|
||||||
auto playlist = std::make_unique<Playlist>();
|
auto playlist = std::make_unique<Playlist>();
|
||||||
playlist->handle = ++next_playlist_handle_;
|
playlist->handle = ++next_playlist_handle_;
|
||||||
playlist->name = playlist_name;
|
playlist->name = playlist_name;
|
||||||
playlist->flags = flags;
|
playlist->flags = flags;
|
||||||
if (songs_ptr) {
|
if (songs_ptr) {
|
||||||
|
XMP_SONGDESCRIPTOR* song_descriptor =
|
||||||
|
memory_->TranslateVirtual<XMP_SONGDESCRIPTOR*>(songs_ptr);
|
||||||
|
|
||||||
for (uint32_t i = 0; i < song_count; ++i) {
|
for (uint32_t i = 0; i < song_count; ++i) {
|
||||||
auto song = std::make_unique<Song>();
|
auto song = std::make_unique<Song>();
|
||||||
song->handle = ++next_song_handle_;
|
song->handle = ++next_song_handle_;
|
||||||
uint8_t* song_base = memory_->TranslateVirtual(songs_ptr + (i * 36));
|
song->file_path = xe::load_and_swap<std::u16string>(
|
||||||
song->file_path =
|
memory_->TranslateVirtual(song_descriptor[i].file_path_ptr));
|
||||||
xe::load_and_swap<std::u16string>(memory_->TranslateVirtual(
|
song->name = xe::load_and_swap<std::u16string>(
|
||||||
xe::load_and_swap<uint32_t>(song_base + 0)));
|
memory_->TranslateVirtual(song_descriptor[i].title_ptr));
|
||||||
song->name = xe::load_and_swap<std::u16string>(memory_->TranslateVirtual(
|
song->artist = xe::load_and_swap<std::u16string>(
|
||||||
xe::load_and_swap<uint32_t>(song_base + 4)));
|
memory_->TranslateVirtual(song_descriptor[i].artist_ptr));
|
||||||
song->artist =
|
song->album = xe::load_and_swap<std::u16string>(
|
||||||
xe::load_and_swap<std::u16string>(memory_->TranslateVirtual(
|
memory_->TranslateVirtual(song_descriptor[i].album_ptr));
|
||||||
xe::load_and_swap<uint32_t>(song_base + 8)));
|
song->album_artist = xe::load_and_swap<std::u16string>(
|
||||||
song->album = xe::load_and_swap<std::u16string>(memory_->TranslateVirtual(
|
memory_->TranslateVirtual(song_descriptor[i].album_artist_ptr));
|
||||||
xe::load_and_swap<uint32_t>(song_base + 12)));
|
song->genre = xe::load_and_swap<std::u16string>(
|
||||||
song->album_artist =
|
memory_->TranslateVirtual(song_descriptor[i].genre_ptr));
|
||||||
xe::load_and_swap<std::u16string>(memory_->TranslateVirtual(
|
song->track_number = song_descriptor[i].track_number;
|
||||||
xe::load_and_swap<uint32_t>(song_base + 16)));
|
song->duration_ms = song_descriptor[i].duration;
|
||||||
song->genre = xe::load_and_swap<std::u16string>(memory_->TranslateVirtual(
|
|
||||||
xe::load_and_swap<uint32_t>(song_base + 20)));
|
|
||||||
song->track_number = xe::load_and_swap<uint32_t>(song_base + 24);
|
|
||||||
song->duration_ms = xe::load_and_swap<uint32_t>(song_base + 28);
|
|
||||||
song->format = static_cast<Song::Format>(
|
song->format = static_cast<Song::Format>(
|
||||||
xe::load_and_swap<uint32_t>(song_base + 32));
|
xe::byte_swap<uint32_t>(song_descriptor[i].song_format));
|
||||||
|
|
||||||
if (out_song_handles) {
|
if (out_song_handles) {
|
||||||
xe::store_and_swap<uint32_t>(
|
xe::store_and_swap<uint32_t>(
|
||||||
memory_->TranslateVirtual(out_song_handles + (i * 4)),
|
memory_->TranslateVirtual(out_song_handles + (i * 4)),
|
||||||
song->handle);
|
song->handle);
|
||||||
}
|
}
|
||||||
playlist->songs.emplace_back(std::move(song));
|
playlist->songs.push_back(std::move(song));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (out_playlist_handle) {
|
if (out_playlist_handle) {
|
||||||
|
@ -352,48 +90,24 @@ X_HRESULT XmpApp::XMPCreateTitlePlaylist(
|
||||||
playlist->handle);
|
playlist->handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto global_lock = global_critical_region_.Acquire();
|
kernel_state_->emulator()->audio_media_player()->AddPlaylist(
|
||||||
playlists_.insert({playlist->handle, playlist.get()});
|
next_playlist_handle_, std::move(playlist));
|
||||||
playlist.release();
|
|
||||||
return X_E_SUCCESS;
|
return X_E_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
X_HRESULT XmpApp::XMPDeleteTitlePlaylist(uint32_t playlist_handle) {
|
X_HRESULT XmpApp::XMPDeleteTitlePlaylist(uint32_t playlist_handle) {
|
||||||
XELOGD("XMPDeleteTitlePlaylist({:08X})", playlist_handle);
|
XELOGD("XMPDeleteTitlePlaylist({:08X})", playlist_handle);
|
||||||
auto global_lock = global_critical_region_.Acquire();
|
kernel_state_->emulator()->audio_media_player()->RemovePlaylist(
|
||||||
auto it = playlists_.find(playlist_handle);
|
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;
|
|
||||||
return X_E_SUCCESS;
|
return X_E_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
X_HRESULT XmpApp::XMPPlayTitlePlaylist(uint32_t playlist_handle,
|
X_HRESULT XmpApp::XMPPlayTitlePlaylist(uint32_t playlist_handle,
|
||||||
uint32_t song_handle) {
|
uint32_t song_handle) {
|
||||||
XELOGD("XMPPlayTitlePlaylist({:08X}, {:08X})", playlist_handle, song_handle);
|
XELOGD("XMPPlayTitlePlaylist({:08X}, {:08X})", playlist_handle, song_handle);
|
||||||
Playlist* playlist = nullptr;
|
kernel_state_->emulator()->audio_media_player()->Play(playlist_handle,
|
||||||
{
|
song_handle, false);
|
||||||
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_->BroadcastNotification(kNotificationXmpPlaybackBehaviorChanged,
|
kernel_state_->BroadcastNotification(kNotificationXmpPlaybackBehaviorChanged,
|
||||||
1);
|
1);
|
||||||
return X_E_SUCCESS;
|
return X_E_SUCCESS;
|
||||||
|
@ -401,75 +115,35 @@ X_HRESULT XmpApp::XMPPlayTitlePlaylist(uint32_t playlist_handle,
|
||||||
|
|
||||||
X_HRESULT XmpApp::XMPContinue() {
|
X_HRESULT XmpApp::XMPContinue() {
|
||||||
XELOGD("XMPContinue()");
|
XELOGD("XMPContinue()");
|
||||||
if (state_ == State::kPaused) {
|
kernel_state_->emulator()->audio_media_player()->Continue();
|
||||||
state_ = State::kPlaying;
|
|
||||||
resume_fence_.Signal();
|
|
||||||
{
|
|
||||||
std::unique_lock<std::mutex> guard(driver_mutex_);
|
|
||||||
if (driver_ != nullptr) driver_->Resume();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
OnStateChanged();
|
|
||||||
return X_E_SUCCESS;
|
return X_E_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
X_HRESULT XmpApp::XMPStop(uint32_t unk) {
|
X_HRESULT XmpApp::XMPStop(uint32_t unk) {
|
||||||
assert_zero(unk);
|
assert_zero(unk);
|
||||||
XELOGD("XMPStop({:08X})", unk);
|
XELOGD("XMPStop({:08X})", unk);
|
||||||
active_playlist_ = nullptr; // ?
|
kernel_state_->emulator()->audio_media_player()->Stop(true, false);
|
||||||
active_song_index_ = 0;
|
|
||||||
state_ = State::kIdle;
|
|
||||||
OnStateChanged();
|
|
||||||
return X_E_SUCCESS;
|
return X_E_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
X_HRESULT XmpApp::XMPPause() {
|
X_HRESULT XmpApp::XMPPause() {
|
||||||
XELOGD("XMPPause()");
|
XELOGD("XMPPause()");
|
||||||
if (state_ == State::kPlaying) {
|
kernel_state_->emulator()->audio_media_player()->Pause();
|
||||||
{
|
|
||||||
std::unique_lock<std::mutex> guard(driver_mutex_);
|
|
||||||
if (driver_ != nullptr) driver_->Pause();
|
|
||||||
}
|
|
||||||
state_ = State::kPaused;
|
|
||||||
}
|
|
||||||
OnStateChanged();
|
|
||||||
return X_E_SUCCESS;
|
return X_E_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
X_HRESULT XmpApp::XMPNext() {
|
X_HRESULT XmpApp::XMPNext() {
|
||||||
XELOGD("XMPNext()");
|
XELOGD("XMPNext()");
|
||||||
if (!active_playlist_) {
|
kernel_state_->emulator()->audio_media_player()->Next();
|
||||||
return X_E_NOTFOUND;
|
|
||||||
}
|
|
||||||
state_ = State::kPlaying;
|
|
||||||
active_song_index_ =
|
|
||||||
(active_song_index_ + 1) % active_playlist_->songs.size();
|
|
||||||
resume_fence_.Signal();
|
|
||||||
OnStateChanged();
|
|
||||||
return X_E_SUCCESS;
|
return X_E_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
X_HRESULT XmpApp::XMPPrevious() {
|
X_HRESULT XmpApp::XMPPrevious() {
|
||||||
XELOGD("XMPPrevious()");
|
XELOGD("XMPPrevious()");
|
||||||
if (!active_playlist_) {
|
kernel_state_->emulator()->audio_media_player()->Previous();
|
||||||
return X_E_NOTFOUND;
|
|
||||||
}
|
|
||||||
state_ = State::kPlaying;
|
|
||||||
if (!active_song_index_) {
|
|
||||||
active_song_index_ = static_cast<int>(active_playlist_->songs.size()) - 1;
|
|
||||||
} else {
|
|
||||||
--active_song_index_;
|
|
||||||
}
|
|
||||||
resume_fence_.Signal();
|
|
||||||
OnStateChanged();
|
|
||||||
return X_E_SUCCESS;
|
return X_E_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
void XmpApp::OnStateChanged() {
|
|
||||||
kernel_state_->BroadcastNotification(kNotificationXmpStateChanged,
|
|
||||||
static_cast<uint32_t>(state_));
|
|
||||||
}
|
|
||||||
|
|
||||||
X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||||
uint32_t buffer_length) {
|
uint32_t buffer_length) {
|
||||||
// NOTE: buffer_length may be zero or valid.
|
// 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})",
|
XELOGD("XMPSetPlaybackBehavior({:08X}, {:08X}, {:08X})",
|
||||||
uint32_t(args->playback_mode), uint32_t(args->repeat_mode),
|
uint32_t(args->playback_mode), uint32_t(args->repeat_mode),
|
||||||
uint32_t(args->flags));
|
uint32_t(args->flags));
|
||||||
playback_mode_ = static_cast<PlaybackMode>(uint32_t(args->playback_mode));
|
|
||||||
repeat_mode_ = static_cast<RepeatMode>(uint32_t(args->repeat_mode));
|
kernel_state_->emulator()->audio_media_player()->SetPlaybackMode(
|
||||||
unknown_flags_ = args->flags;
|
static_cast<PlaybackMode>(uint32_t(args->playback_mode)));
|
||||||
|
kernel_state_->emulator()->audio_media_player()->SetRepeatMode(
|
||||||
|
static_cast<RepeatMode>(uint32_t(args->repeat_mode)));
|
||||||
|
kernel_state_->emulator()->audio_media_player()->SetPlaybackFlags(
|
||||||
|
static_cast<PlaybackFlags>(uint32_t(args->flags)));
|
||||||
|
|
||||||
kernel_state_->BroadcastNotification(
|
kernel_state_->BroadcastNotification(
|
||||||
kNotificationXmpPlaybackBehaviorChanged, 0);
|
kNotificationXmpPlaybackBehaviorChanged, 0);
|
||||||
return X_E_SUCCESS;
|
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);
|
assert_true(args->xmp_client == 0x00000002);
|
||||||
XELOGD("XMPGetVolume({:08X})", uint32_t(args->volume_ptr));
|
XELOGD("XMPGetVolume({:08X})", uint32_t(args->volume_ptr));
|
||||||
xe::store_and_swap<float>(memory_->TranslateVirtual(args->volume_ptr),
|
|
||||||
volume_);
|
xe::store_and_swap<float>(
|
||||||
|
memory_->TranslateVirtual(args->volume_ptr),
|
||||||
|
kernel_state_->emulator()->audio_media_player()->GetVolume());
|
||||||
return X_E_SUCCESS;
|
return X_E_SUCCESS;
|
||||||
}
|
}
|
||||||
case 0x0007000C: {
|
case 0x0007000C: {
|
||||||
|
@ -569,12 +250,9 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||||
static_assert_size(decltype(*args), 8);
|
static_assert_size(decltype(*args), 8);
|
||||||
|
|
||||||
assert_true(args->xmp_client == 0x00000002);
|
assert_true(args->xmp_client == 0x00000002);
|
||||||
XELOGD("XMPSetVolume({:g})", float(args->value));
|
XELOGD("XMPSetVolume({:d}, {:g})", args->xmp_client, float(args->value));
|
||||||
volume_ = args->value;
|
kernel_state_->emulator()->audio_media_player()->SetVolume(
|
||||||
{
|
float(args->value));
|
||||||
std::unique_lock<std::mutex> guard(driver_mutex_);
|
|
||||||
if (driver_ != nullptr) driver_->SetVolume(volume_);
|
|
||||||
}
|
|
||||||
return X_E_SUCCESS;
|
return X_E_SUCCESS;
|
||||||
}
|
}
|
||||||
case 0x0007000D: {
|
case 0x0007000D: {
|
||||||
|
@ -604,9 +282,7 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||||
playlist_name = xe::load_and_swap<std::u16string>(
|
playlist_name = xe::load_and_swap<std::u16string>(
|
||||||
memory_->TranslateVirtual(args->playlist_name_ptr));
|
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,
|
return XMPCreateTitlePlaylist(args->songs_ptr, args->song_count,
|
||||||
args->playlist_name_ptr, playlist_name,
|
args->playlist_name_ptr, playlist_name,
|
||||||
args->flags, args->song_handles_ptr,
|
args->flags, args->song_handles_ptr,
|
||||||
|
@ -621,26 +297,31 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||||
}* args = memory_->TranslateVirtual<decltype(args)>(buffer_ptr);
|
}* args = memory_->TranslateVirtual<decltype(args)>(buffer_ptr);
|
||||||
static_assert_size(decltype(*args), 12);
|
static_assert_size(decltype(*args), 12);
|
||||||
|
|
||||||
auto info = memory_->TranslateVirtual(args->info_ptr);
|
auto info = memory_->TranslateVirtual<XMP_SONGINFO*>(args->info_ptr);
|
||||||
assert_true(args->xmp_client == 0x00000002);
|
assert_true(args->xmp_client == 0x00000002);
|
||||||
assert_zero(args->unk_ptr);
|
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));
|
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;
|
return X_E_FAIL;
|
||||||
}
|
}
|
||||||
auto& song = active_playlist_->songs[active_song_index_];
|
|
||||||
xe::store_and_swap<uint32_t>(info + 0, song->handle);
|
memset(info, 0, sizeof(XMP_SONGINFO));
|
||||||
xe::store_and_swap<std::u16string>(info + 4 + 572 + 0, song->name);
|
|
||||||
xe::store_and_swap<std::u16string>(info + 4 + 572 + 40, song->artist);
|
info->handle = current_song->handle;
|
||||||
xe::store_and_swap<std::u16string>(info + 4 + 572 + 80, song->album);
|
xe::store_and_swap<std::u16string>(info->title, current_song->name);
|
||||||
xe::store_and_swap<std::u16string>(info + 4 + 572 + 120,
|
xe::store_and_swap<std::u16string>(info->artist, current_song->artist);
|
||||||
song->album_artist);
|
xe::store_and_swap<std::u16string>(info->album, current_song->album);
|
||||||
xe::store_and_swap<std::u16string>(info + 4 + 572 + 160, song->genre);
|
xe::store_and_swap<std::u16string>(info->album_artist,
|
||||||
xe::store_and_swap<uint32_t>(info + 4 + 572 + 200, song->track_number);
|
current_song->album_artist);
|
||||||
xe::store_and_swap<uint32_t>(info + 4 + 572 + 204, song->duration_ms);
|
xe::store_and_swap<std::u16string>(info->genre, current_song->genre);
|
||||||
xe::store_and_swap<uint32_t>(info + 4 + 572 + 208,
|
info->track_number = current_song->track_number;
|
||||||
static_cast<uint32_t>(song->format));
|
info->duration = current_song->duration_ms;
|
||||||
|
info->song_format = static_cast<uint32_t>(current_song->format);
|
||||||
return X_E_SUCCESS;
|
return X_E_SUCCESS;
|
||||||
}
|
}
|
||||||
case 0x00070013: {
|
case 0x00070013: {
|
||||||
|
@ -673,9 +354,14 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||||
XELOGD("XMPSetPlaybackController({:08X}, {:08X})",
|
XELOGD("XMPSetPlaybackController({:08X}, {:08X})",
|
||||||
uint32_t(args->controller), uint32_t(args->playback_client));
|
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(
|
kernel_state_->BroadcastNotification(
|
||||||
kNotificationXmpPlaybackControllerChanged, !args->playback_client);
|
kNotificationXmpPlaybackControllerChanged,
|
||||||
|
kernel_state_->emulator()
|
||||||
|
->audio_media_player()
|
||||||
|
->IsTitleInPlaybackControl());
|
||||||
return X_E_SUCCESS;
|
return X_E_SUCCESS;
|
||||||
}
|
}
|
||||||
case 0x0007001B: {
|
case 0x0007001B: {
|
||||||
|
@ -717,7 +403,7 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||||
xe::be<uint32_t> xmp_client;
|
xe::be<uint32_t> xmp_client;
|
||||||
xe::be<uint32_t> playback_mode_ptr;
|
xe::be<uint32_t> playback_mode_ptr;
|
||||||
xe::be<uint32_t> repeat_mode_ptr;
|
xe::be<uint32_t> repeat_mode_ptr;
|
||||||
xe::be<uint32_t> unk3_ptr;
|
xe::be<uint32_t> playback_flags_ptr;
|
||||||
}* args = memory_->TranslateVirtual<decltype(args)>(buffer_ptr);
|
}* args = memory_->TranslateVirtual<decltype(args)>(buffer_ptr);
|
||||||
static_assert_size(decltype(*args), 16);
|
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);
|
args->xmp_client == 0x00000000);
|
||||||
XELOGD("XMPGetPlaybackBehavior({:08X}, {:08X}, {:08X})",
|
XELOGD("XMPGetPlaybackBehavior({:08X}, {:08X}, {:08X})",
|
||||||
uint32_t(args->playback_mode_ptr), uint32_t(args->repeat_mode_ptr),
|
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) {
|
if (args->playback_mode_ptr) {
|
||||||
xe::store_and_swap<uint32_t>(
|
xe::store_and_swap<uint32_t>(
|
||||||
memory_->TranslateVirtual(args->playback_mode_ptr),
|
memory_->TranslateVirtual(args->playback_mode_ptr),
|
||||||
static_cast<uint32_t>(playback_mode_));
|
static_cast<uint32_t>(kernel_state_->emulator()
|
||||||
|
->audio_media_player()
|
||||||
|
->GetPlaybackMode()));
|
||||||
}
|
}
|
||||||
if (args->repeat_mode_ptr) {
|
if (args->repeat_mode_ptr) {
|
||||||
xe::store_and_swap<uint32_t>(
|
xe::store_and_swap<uint32_t>(
|
||||||
memory_->TranslateVirtual(args->repeat_mode_ptr),
|
memory_->TranslateVirtual(args->repeat_mode_ptr),
|
||||||
static_cast<uint32_t>(repeat_mode_));
|
static_cast<uint32_t>(kernel_state_->emulator()
|
||||||
|
->audio_media_player()
|
||||||
|
->GetRepeatMode()));
|
||||||
}
|
}
|
||||||
if (args->unk3_ptr) {
|
if (args->playback_flags_ptr) {
|
||||||
xe::store_and_swap<uint32_t>(memory_->TranslateVirtual(args->unk3_ptr),
|
xe::store_and_swap<uint32_t>(
|
||||||
unknown_flags_);
|
memory_->TranslateVirtual(args->playback_flags_ptr),
|
||||||
|
static_cast<uint32_t>(kernel_state_->emulator()
|
||||||
|
->audio_media_player()
|
||||||
|
->GetPlaybackFlags()));
|
||||||
}
|
}
|
||||||
return X_E_SUCCESS;
|
return X_E_SUCCESS;
|
||||||
}
|
}
|
||||||
|
@ -772,10 +465,22 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||||
return X_E_FAIL;
|
return X_E_FAIL;
|
||||||
}
|
}
|
||||||
case 0x0007003D: {
|
case 0x0007003D: {
|
||||||
// XMPCaptureOutput - not sure how this works :/
|
// XMPCaptureOutput
|
||||||
XELOGD("XMPCaptureOutput(...)");
|
assert_true(!buffer_length || buffer_length == 16);
|
||||||
assert_always("XMP output not unimplemented");
|
struct {
|
||||||
return X_E_FAIL;
|
xe::be<uint32_t> xmp_client;
|
||||||
|
xe::be<uint32_t> callback;
|
||||||
|
xe::be<uint32_t> context;
|
||||||
|
xe::be<uint32_t> title_render;
|
||||||
|
}* args = memory_->TranslateVirtual<decltype(args)>(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<bool>(args->title_render));
|
||||||
|
return X_E_SUCCESS;
|
||||||
}
|
}
|
||||||
case 0x00070044: {
|
case 0x00070044: {
|
||||||
// Called on the start up of all dashboard versions before kinect
|
// Called on the start up of all dashboard versions before kinect
|
||||||
|
|
|
@ -20,8 +20,6 @@
|
||||||
#include "xenia/kernel/kernel_state.h"
|
#include "xenia/kernel/kernel_state.h"
|
||||||
#include "xenia/kernel/xam/app_manager.h"
|
#include "xenia/kernel/xam/app_manager.h"
|
||||||
|
|
||||||
#include "xenia/apu/audio_driver.h"
|
|
||||||
|
|
||||||
namespace xe {
|
namespace xe {
|
||||||
namespace kernel {
|
namespace kernel {
|
||||||
namespace xam {
|
namespace xam {
|
||||||
|
@ -30,6 +28,36 @@ namespace apps {
|
||||||
// Only source of docs for a lot of these functions:
|
// Only source of docs for a lot of these functions:
|
||||||
// https://github.com/oukiar/freestyledash/blob/master/Freestyle/Scenes/Media/Music/ScnMusic.cpp
|
// https://github.com/oukiar/freestyledash/blob/master/Freestyle/Scenes/Media/Music/ScnMusic.cpp
|
||||||
|
|
||||||
|
struct XMP_SONGDESCRIPTOR {
|
||||||
|
xe::be<uint32_t> file_path_ptr;
|
||||||
|
xe::be<uint32_t> title_ptr;
|
||||||
|
xe::be<uint32_t> artist_ptr;
|
||||||
|
xe::be<uint32_t> album_ptr;
|
||||||
|
xe::be<uint32_t> album_artist_ptr;
|
||||||
|
xe::be<uint32_t> genre_ptr;
|
||||||
|
xe::be<uint32_t> track_number;
|
||||||
|
xe::be<uint32_t> duration;
|
||||||
|
xe::be<uint32_t> 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<char16_t> title[kMaxXmpMetadataStringLength];
|
||||||
|
xe::be<char16_t> artist[kMaxXmpMetadataStringLength];
|
||||||
|
xe::be<char16_t> album[kMaxXmpMetadataStringLength];
|
||||||
|
xe::be<char16_t> album_artist[kMaxXmpMetadataStringLength];
|
||||||
|
xe::be<char16_t> genre[kMaxXmpMetadataStringLength];
|
||||||
|
xe::be<uint32_t> track_number;
|
||||||
|
xe::be<uint32_t> duration;
|
||||||
|
xe::be<uint32_t> song_format;
|
||||||
|
};
|
||||||
|
static_assert_size(XMP_SONGINFO, 988);
|
||||||
|
|
||||||
class XmpApp : public App {
|
class XmpApp : public App {
|
||||||
public:
|
public:
|
||||||
enum class State : uint32_t {
|
enum class State : uint32_t {
|
||||||
|
@ -42,12 +70,16 @@ class XmpApp : public App {
|
||||||
kTitle = 1,
|
kTitle = 1,
|
||||||
};
|
};
|
||||||
enum class PlaybackMode : uint32_t {
|
enum class PlaybackMode : uint32_t {
|
||||||
// kInOrder = ?,
|
kInOrder = 0,
|
||||||
kUnknown = 0,
|
kShuffle = 1,
|
||||||
};
|
};
|
||||||
enum class RepeatMode : uint32_t {
|
enum class RepeatMode : uint32_t {
|
||||||
// kNoRepeat = ?,
|
kPlaylist = 0,
|
||||||
kUnknown = 0,
|
kNoRepeat = 1,
|
||||||
|
};
|
||||||
|
enum class PlaybackFlags : uint32_t {
|
||||||
|
kDefault = 0,
|
||||||
|
kAutoPause = 1,
|
||||||
};
|
};
|
||||||
struct Song {
|
struct Song {
|
||||||
enum class Format : uint32_t {
|
enum class Format : uint32_t {
|
||||||
|
@ -95,31 +127,11 @@ class XmpApp : public App {
|
||||||
uint32_t buffer_length) override;
|
uint32_t buffer_length) override;
|
||||||
|
|
||||||
private:
|
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_;
|
xe::global_critical_region global_critical_region_;
|
||||||
std::unordered_map<uint32_t, Playlist*> playlists_;
|
|
||||||
uint32_t next_playlist_handle_;
|
|
||||||
uint32_t next_song_handle_;
|
|
||||||
|
|
||||||
std::atomic<bool> worker_running_ = {false};
|
// TODO: Remove it and replace with guest handles!
|
||||||
std::unique_ptr<xe::threading::Thread> worker_thread_;
|
uint32_t next_playlist_handle_ = 0;
|
||||||
|
uint32_t next_song_handle_ = 0;
|
||||||
bool paused_ = true;
|
|
||||||
xe::threading::Fence resume_fence_; // Signaled when resume requested.
|
|
||||||
std::mutex driver_mutex_ = {};
|
|
||||||
xe::apu::AudioDriver* driver_ = nullptr;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace apps
|
} // namespace apps
|
||||||
|
|
Loading…
Reference in New Issue