[XAM] XMP: Implement playback
This commit is contained in:
parent
5a76cac218
commit
764f230dd9
src/xenia/kernel
|
@ -16,9 +16,15 @@ 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",
|
||||||
|
|
|
@ -12,8 +12,29 @@
|
||||||
|
|
||||||
#include "xenia/base/logging.h"
|
#include "xenia/base/logging.h"
|
||||||
#include "xenia/base/threading.h"
|
#include "xenia/base/threading.h"
|
||||||
|
#include "xenia/emulator.h"
|
||||||
#include "xenia/xbox.h"
|
#include "xenia/xbox.h"
|
||||||
|
|
||||||
|
#include "xenia/apu/audio_driver.h"
|
||||||
|
#include "xenia/apu/audio_system.h"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#if XE_COMPILER_MSVC
|
||||||
|
#pragma warning(push)
|
||||||
|
#pragma warning(disable : 4101 4244 5033)
|
||||||
|
#endif
|
||||||
|
#include "third_party/FFmpeg/libavcodec/avcodec.h"
|
||||||
|
#include "third_party/FFmpeg/libavformat/avformat.h"
|
||||||
|
#include "third_party/FFmpeg/libavutil/opt.h"
|
||||||
|
#if XE_COMPILER_MSVC
|
||||||
|
#pragma warning(pop)
|
||||||
|
#endif
|
||||||
|
} // extern "C"
|
||||||
|
|
||||||
|
DEFINE_bool(enable_xmp, true, "Enables Music Player playback.", "APU");
|
||||||
|
DEFINE_int32(xmp_default_volume, 70,
|
||||||
|
"Default music volume if game doesn't set it [0-100].", "APU");
|
||||||
|
|
||||||
namespace xe {
|
namespace xe {
|
||||||
namespace kernel {
|
namespace kernel {
|
||||||
namespace xam {
|
namespace xam {
|
||||||
|
@ -26,11 +47,247 @@ XmpApp::XmpApp(KernelState* kernel_state)
|
||||||
playback_mode_(PlaybackMode::kUnknown),
|
playback_mode_(PlaybackMode::kUnknown),
|
||||||
repeat_mode_(RepeatMode::kUnknown),
|
repeat_mode_(RepeatMode::kUnknown),
|
||||||
unknown_flags_(0),
|
unknown_flags_(0),
|
||||||
volume_(1.0f),
|
volume_(cvars::xmp_default_volume / 100.0f),
|
||||||
active_playlist_(nullptr),
|
active_playlist_(nullptr),
|
||||||
active_song_index_(0),
|
active_song_index_(0),
|
||||||
next_playlist_handle_(1),
|
next_playlist_handle_(1),
|
||||||
next_song_handle_(1) {}
|
next_song_handle_(1) {
|
||||||
|
if (cvars::enable_xmp) {
|
||||||
|
worker_running_ = true;
|
||||||
|
worker_thread_ = threading::Thread::Create({}, [&] { WorkerThreadMain(); });
|
||||||
|
worker_thread_->set_name("Music Player");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VFSContext {
|
||||||
|
xe::vfs::File* file;
|
||||||
|
size_t byte_offset;
|
||||||
|
};
|
||||||
|
|
||||||
|
static int xenia_vfs_read(void* opaque, uint8_t* buf, int buf_size) {
|
||||||
|
auto ctx = static_cast<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()) {
|
||||||
|
@ -39,7 +296,7 @@ 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})", state_ptr);
|
XELOGD("XMPGetStatus({:08X}) -> {:d}", state_ptr, (uint32_t)state_);
|
||||||
xe::store_and_swap<uint32_t>(memory_->TranslateVirtual(state_ptr),
|
xe::store_and_swap<uint32_t>(memory_->TranslateVirtual(state_ptr),
|
||||||
static_cast<uint32_t>(state_));
|
static_cast<uint32_t>(state_));
|
||||||
return X_E_SUCCESS;
|
return X_E_SUCCESS;
|
||||||
|
@ -132,16 +389,10 @@ X_HRESULT XmpApp::XMPPlayTitlePlaylist(uint32_t playlist_handle,
|
||||||
playlist = it->second;
|
playlist = it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playback_client_ == PlaybackClient::kSystem) {
|
|
||||||
XELOGW("XMPPlayTitlePlaylist: System playback is enabled!");
|
|
||||||
return X_E_SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start playlist?
|
|
||||||
XELOGW("Playlist playback not supported");
|
|
||||||
active_playlist_ = playlist;
|
active_playlist_ = playlist;
|
||||||
active_song_index_ = 0;
|
active_song_index_ = 0;
|
||||||
state_ = State::kPlaying;
|
state_ = State::kPlaying;
|
||||||
|
resume_fence_.Signal();
|
||||||
OnStateChanged();
|
OnStateChanged();
|
||||||
kernel_state_->BroadcastNotification(kNotificationXmpPlaybackBehaviorChanged,
|
kernel_state_->BroadcastNotification(kNotificationXmpPlaybackBehaviorChanged,
|
||||||
1);
|
1);
|
||||||
|
@ -152,6 +403,11 @@ X_HRESULT XmpApp::XMPContinue() {
|
||||||
XELOGD("XMPContinue()");
|
XELOGD("XMPContinue()");
|
||||||
if (state_ == State::kPaused) {
|
if (state_ == State::kPaused) {
|
||||||
state_ = State::kPlaying;
|
state_ = State::kPlaying;
|
||||||
|
resume_fence_.Signal();
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> guard(driver_mutex_);
|
||||||
|
if (driver_ != nullptr) driver_->Resume();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
OnStateChanged();
|
OnStateChanged();
|
||||||
return X_E_SUCCESS;
|
return X_E_SUCCESS;
|
||||||
|
@ -170,6 +426,10 @@ X_HRESULT XmpApp::XMPStop(uint32_t unk) {
|
||||||
X_HRESULT XmpApp::XMPPause() {
|
X_HRESULT XmpApp::XMPPause() {
|
||||||
XELOGD("XMPPause()");
|
XELOGD("XMPPause()");
|
||||||
if (state_ == State::kPlaying) {
|
if (state_ == State::kPlaying) {
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> guard(driver_mutex_);
|
||||||
|
if (driver_ != nullptr) driver_->Pause();
|
||||||
|
}
|
||||||
state_ = State::kPaused;
|
state_ = State::kPaused;
|
||||||
}
|
}
|
||||||
OnStateChanged();
|
OnStateChanged();
|
||||||
|
@ -184,6 +444,7 @@ X_HRESULT XmpApp::XMPNext() {
|
||||||
state_ = State::kPlaying;
|
state_ = State::kPlaying;
|
||||||
active_song_index_ =
|
active_song_index_ =
|
||||||
(active_song_index_ + 1) % active_playlist_->songs.size();
|
(active_song_index_ + 1) % active_playlist_->songs.size();
|
||||||
|
resume_fence_.Signal();
|
||||||
OnStateChanged();
|
OnStateChanged();
|
||||||
return X_E_SUCCESS;
|
return X_E_SUCCESS;
|
||||||
}
|
}
|
||||||
|
@ -199,6 +460,7 @@ X_HRESULT XmpApp::XMPPrevious() {
|
||||||
} else {
|
} else {
|
||||||
--active_song_index_;
|
--active_song_index_;
|
||||||
}
|
}
|
||||||
|
resume_fence_.Signal();
|
||||||
OnStateChanged();
|
OnStateChanged();
|
||||||
return X_E_SUCCESS;
|
return X_E_SUCCESS;
|
||||||
}
|
}
|
||||||
|
@ -309,6 +571,10 @@ X_HRESULT XmpApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
|
||||||
assert_true(args->xmp_client == 0x00000002);
|
assert_true(args->xmp_client == 0x00000002);
|
||||||
XELOGD("XMPSetVolume({:g})", float(args->value));
|
XELOGD("XMPSetVolume({:g})", float(args->value));
|
||||||
volume_ = args->value;
|
volume_ = 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: {
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
#define XENIA_KERNEL_XAM_APPS_XMP_APP_H_
|
#define XENIA_KERNEL_XAM_APPS_XMP_APP_H_
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
@ -19,6 +20,8 @@
|
||||||
#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 {
|
||||||
|
@ -93,6 +96,8 @@ class XmpApp : public App {
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void OnStateChanged();
|
void OnStateChanged();
|
||||||
|
void WorkerThreadMain();
|
||||||
|
bool PlayFile(std::string_view filename);
|
||||||
|
|
||||||
State state_;
|
State state_;
|
||||||
PlaybackClient playback_client_;
|
PlaybackClient playback_client_;
|
||||||
|
@ -107,6 +112,14 @@ class XmpApp : public App {
|
||||||
std::unordered_map<uint32_t, Playlist*> playlists_;
|
std::unordered_map<uint32_t, Playlist*> playlists_;
|
||||||
uint32_t next_playlist_handle_;
|
uint32_t next_playlist_handle_;
|
||||||
uint32_t next_song_handle_;
|
uint32_t next_song_handle_;
|
||||||
|
|
||||||
|
std::atomic<bool> worker_running_ = {false};
|
||||||
|
std::unique_ptr<xe::threading::Thread> worker_thread_;
|
||||||
|
|
||||||
|
bool paused_ = true;
|
||||||
|
xe::threading::Fence resume_fence_; // Signaled when resume requested.
|
||||||
|
std::mutex driver_mutex_ = {};
|
||||||
|
xe::apu::AudioDriver* driver_ = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace apps
|
} // namespace apps
|
||||||
|
|
Loading…
Reference in New Issue