diff --git a/.gitmodules b/.gitmodules index 88635e64..07d1d4ef 100644 --- a/.gitmodules +++ b/.gitmodules @@ -98,4 +98,7 @@ [submodule "externals/discord-rpc"] path = externals/discord-rpc url = https://github.com/shadps4-emu/ext-discord-rpc.git - shallow = true \ No newline at end of file + shallow = true +[submodule "externals/LibAtrac9"] + path = externals/LibAtrac9 + url = https://github.com/shadps4-emu/ext-LibAtrac9.git diff --git a/CMakeLists.txt b/CMakeLists.txt index eb085572..d9479e85 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -179,12 +179,25 @@ set(AUDIO_CORE src/audio_core/sdl_audio.cpp src/audio_core/sdl_audio.h ) +set(AJM_LIB src/core/libraries/ajm/ajm.cpp + src/core/libraries/ajm/ajm.h + src/core/libraries/ajm/ajm_at9.cpp + src/core/libraries/ajm/ajm_at9.h + src/core/libraries/ajm/ajm_batch.cpp + src/core/libraries/ajm/ajm_batch.h + src/core/libraries/ajm/ajm_context.cpp + src/core/libraries/ajm/ajm_context.h + src/core/libraries/ajm/ajm_error.h + src/core/libraries/ajm/ajm_instance.cpp + src/core/libraries/ajm/ajm_instance.h + src/core/libraries/ajm/ajm_mp3.cpp + src/core/libraries/ajm/ajm_mp3.h +) + set(AUDIO_LIB src/core/libraries/audio/audioin.cpp src/core/libraries/audio/audioin.h src/core/libraries/audio/audioout.cpp src/core/libraries/audio/audioout.h - src/core/libraries/ajm/ajm.cpp - src/core/libraries/ajm/ajm.h src/core/libraries/ngs2/ngs2.cpp src/core/libraries/ngs2/ngs2.h ) @@ -194,8 +207,7 @@ set(GNM_LIB src/core/libraries/gnmdriver/gnmdriver.cpp src/core/libraries/gnmdriver/gnm_error.h ) -set(KERNEL_LIB - src/core/libraries/kernel/event_flag/event_flag.cpp +set(KERNEL_LIB src/core/libraries/kernel/event_flag/event_flag.cpp src/core/libraries/kernel/event_flag/event_flag.h src/core/libraries/kernel/event_flag/event_flag_obj.cpp src/core/libraries/kernel/event_flag/event_flag_obj.h @@ -494,6 +506,7 @@ set(CORE src/core/aerolib/stubs.cpp src/core/libraries/error_codes.h src/core/libraries/libs.h src/core/libraries/libs.cpp + ${AJM_LIB} ${AUDIO_LIB} ${GNM_LIB} ${KERNEL_LIB} @@ -795,7 +808,7 @@ endif() create_target_directory_groups(shadps4) target_link_libraries(shadps4 PRIVATE magic_enum::magic_enum fmt::fmt toml11::toml11 tsl::robin_map xbyak::xbyak Tracy::TracyClient RenderDoc::API FFmpeg::ffmpeg Dear_ImGui gcn half::half) -target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAllocator sirit Vulkan::Headers xxHash::xxhash Zydis::Zydis glslang::SPIRV glslang::glslang SDL3::SDL3 pugixml::pugixml) +target_link_libraries(shadps4 PRIVATE Boost::headers GPUOpen::VulkanMemoryAllocator LibAtrac9 sirit Vulkan::Headers xxHash::xxhash Zydis::Zydis glslang::SPIRV glslang::glslang SDL3::SDL3 pugixml::pugixml) target_compile_definitions(shadps4 PRIVATE IMGUI_USER_CONFIG="imgui/imgui_config.h") target_compile_definitions(Dear_ImGui PRIVATE IMGUI_USER_CONFIG="${PROJECT_SOURCE_DIR}/src/imgui/imgui_config.h") diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 8fefee0f..17d71087 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -47,6 +47,11 @@ if (NOT TARGET FFmpeg::ffmpeg) add_library(FFmpeg::ffmpeg ALIAS ffmpeg) endif() +# LibAtrac9 +file(GLOB LIBATRAC9_SOURCES LibAtrac9/C/src/*.c) +add_library(LibAtrac9 STATIC ${LIBATRAC9_SOURCES}) +target_include_directories(LibAtrac9 INTERFACE LibAtrac9/C/src) + # Zlib-Ng if (NOT TARGET zlib-ng::zlib) set(ZLIB_ENABLE_TESTS OFF) diff --git a/externals/LibAtrac9 b/externals/LibAtrac9 new file mode 160000 index 00000000..82767fe3 --- /dev/null +++ b/externals/LibAtrac9 @@ -0,0 +1 @@ +Subproject commit 82767fe38823c32536726ea798f392b0b49e66b9 diff --git a/src/common/slot_array.h b/src/common/slot_array.h new file mode 100644 index 00000000..3a57899c --- /dev/null +++ b/src/common/slot_array.h @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +// #include + +#include +#include +#include +#include + +#include +#include + +namespace Common { + +template ::max(), IndexType MinIndex = 0> +class SlotArray { +public: + SlotArray() { + std::iota(m_free_indices.begin(), m_free_indices.end(), MinIndex); + } + + template + std::optional Create(Types&&... args) { + if (!HasFreeSlots()) { + return std::nullopt; + } + const auto index = m_free_indices[m_curr_cursor]; + m_resources[index - MinIndex] = ResourceType(std::forward(args)...); + m_curr_cursor += 1; + return index; + } + + bool Destroy(IndexType index) { + if (!m_resources[index - MinIndex].has_value()) { + return false; + } + m_curr_cursor -= 1; + m_free_indices[m_curr_cursor] = index; + m_resources[index - MinIndex] = std::nullopt; + return true; + } + + ResourceType* Get(IndexType index) { + auto& resource = m_resources[index - MinIndex]; + if (!resource.has_value()) { + return nullptr; + } + return &resource.value(); + } + + bool HasFreeSlots() { + return m_curr_cursor < m_free_indices.size(); + } + +private: + size_t m_curr_cursor = 0; + std::array m_free_indices; + std::array, MaxIndex - MinIndex> m_resources; +}; + +} // namespace Common diff --git a/src/core/libraries/ajm/ajm.cpp b/src/core/libraries/ajm/ajm.cpp index 441a07f6..80681cba 100644 --- a/src/core/libraries/ajm/ajm.cpp +++ b/src/core/libraries/ajm/ajm.cpp @@ -1,18 +1,23 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "ajm.h" -#include "ajm_error.h" - #include "common/logging/log.h" +#include "core/libraries/ajm/ajm.h" +#include "core/libraries/ajm/ajm_context.h" +#include "core/libraries/ajm/ajm_error.h" +#include "core/libraries/ajm/ajm_mp3.h" #include "core/libraries/error_codes.h" #include "core/libraries/libs.h" +#include + namespace Libraries::Ajm { -int PS4_SYSV_ABI sceAjmBatchCancel() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); - return ORBIS_OK; +static std::unique_ptr context{}; + +int PS4_SYSV_ABI sceAjmBatchCancel(const u32 context_id, const u32 batch_id) { + LOG_INFO(Lib_Ajm, "called context_id = {} batch_id = {}", context_id, batch_id); + return context->BatchCancel(batch_id); } int PS4_SYSV_ABI sceAjmBatchErrorDump() { @@ -20,34 +25,56 @@ int PS4_SYSV_ABI sceAjmBatchErrorDump() { return ORBIS_OK; } -int PS4_SYSV_ABI sceAjmBatchJobControlBufferRa() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); - return ORBIS_OK; +void* PS4_SYSV_ABI sceAjmBatchJobControlBufferRa(void* p_buffer, u32 instance_id, u64 flags, + void* p_sideband_input, size_t sideband_input_size, + void* p_sideband_output, + size_t sideband_output_size, + void* p_return_address) { + return BatchJobControlBufferRa(p_buffer, instance_id, flags, p_sideband_input, + sideband_input_size, p_sideband_output, sideband_output_size, + p_return_address); } -int PS4_SYSV_ABI sceAjmBatchJobInlineBuffer() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); - return ORBIS_OK; +void* PS4_SYSV_ABI sceAjmBatchJobInlineBuffer(void* p_buffer, const void* p_data_input, + size_t data_input_size, + const void** pp_batch_address) { + return BatchJobInlineBuffer(p_buffer, p_data_input, data_input_size, pp_batch_address); } -int PS4_SYSV_ABI sceAjmBatchJobRunBufferRa() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); - return ORBIS_OK; +void* PS4_SYSV_ABI sceAjmBatchJobRunBufferRa(void* p_buffer, u32 instance_id, u64 flags, + void* p_data_input, size_t data_input_size, + void* p_data_output, size_t data_output_size, + void* p_sideband_output, size_t sideband_output_size, + void* p_return_address) { + return BatchJobRunBufferRa(p_buffer, instance_id, flags, p_data_input, data_input_size, + p_data_output, data_output_size, p_sideband_output, + sideband_output_size, p_return_address); } -int PS4_SYSV_ABI sceAjmBatchJobRunSplitBufferRa() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); - return ORBIS_OK; +void* PS4_SYSV_ABI sceAjmBatchJobRunSplitBufferRa( + void* p_buffer, u32 instance_id, u64 flags, const AjmBuffer* p_data_input_buffers, + size_t num_data_input_buffers, const AjmBuffer* p_data_output_buffers, + size_t num_data_output_buffers, void* p_sideband_output, size_t sideband_output_size, + void* p_return_address) { + return BatchJobRunSplitBufferRa(p_buffer, instance_id, flags, p_data_input_buffers, + num_data_input_buffers, p_data_output_buffers, + num_data_output_buffers, p_sideband_output, + sideband_output_size, p_return_address); } -int PS4_SYSV_ABI sceAjmBatchStartBuffer() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); - return ORBIS_OK; +int PS4_SYSV_ABI sceAjmBatchStartBuffer(u32 context_id, u8* p_batch, u32 batch_size, + const int priority, AjmBatchError* batch_error, + u32* out_batch_id) { + LOG_TRACE(Lib_Ajm, "called context = {}, batch_size = {:#x}, priority = {}", context_id, + batch_size, priority); + return context->BatchStartBuffer(p_batch, batch_size, priority, batch_error, out_batch_id); } -int PS4_SYSV_ABI sceAjmBatchWait() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); - return ORBIS_OK; +int PS4_SYSV_ABI sceAjmBatchWait(const u32 context_id, const u32 batch_id, const u32 timeout, + AjmBatchError* const batch_error) { + LOG_TRACE(Lib_Ajm, "called context = {}, batch_id = {}, timeout = {}", context_id, batch_id, + timeout); + return context->BatchWait(batch_id, timeout, batch_error); } int PS4_SYSV_ABI sceAjmDecAt9ParseConfigData() { @@ -55,9 +82,9 @@ int PS4_SYSV_ABI sceAjmDecAt9ParseConfigData() { return ORBIS_OK; } -int PS4_SYSV_ABI sceAjmDecMp3ParseFrame() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); - return ORBIS_OK; +int PS4_SYSV_ABI sceAjmDecMp3ParseFrame(const u8* buf, u32 stream_size, int parse_ofl, + AjmDecMp3ParseFrame* frame) { + return AjmMp3Decoder::ParseMp3Header(buf, stream_size, parse_ofl, frame); } int PS4_SYSV_ABI sceAjmFinalize() { @@ -65,8 +92,14 @@ int PS4_SYSV_ABI sceAjmFinalize() { return ORBIS_OK; } -int PS4_SYSV_ABI sceAjmInitialize() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); +int PS4_SYSV_ABI sceAjmInitialize(s64 reserved, u32* p_context_id) { + LOG_INFO(Lib_Ajm, "called reserved = {}", reserved); + ASSERT_MSG(context == nullptr, "Multiple contexts are currently unsupported."); + if (p_context_id == nullptr || reserved != 0) { + return ORBIS_AJM_ERROR_INVALID_PARAMETER; + } + *p_context_id = 1; + context = std::make_unique(); return ORBIS_OK; } @@ -75,14 +108,16 @@ int PS4_SYSV_ABI sceAjmInstanceCodecType() { return ORBIS_OK; } -int PS4_SYSV_ABI sceAjmInstanceCreate() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); - return ORBIS_OK; +int PS4_SYSV_ABI sceAjmInstanceCreate(u32 context_id, AjmCodecType codec_type, + AjmInstanceFlags flags, u32* out_instance) { + LOG_INFO(Lib_Ajm, "called context = {}, codec_type = {}, flags = {:#x}", context_id, + magic_enum::enum_name(codec_type), flags.raw); + return context->InstanceCreate(codec_type, flags, out_instance); } -int PS4_SYSV_ABI sceAjmInstanceDestroy() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); - return ORBIS_OK; +int PS4_SYSV_ABI sceAjmInstanceDestroy(u32 context_id, u32 instance_id) { + LOG_INFO(Lib_Ajm, "called context = {}, instance = {}", context_id, instance_id); + return context->InstanceDestroy(instance_id); } int PS4_SYSV_ABI sceAjmInstanceExtend() { @@ -105,9 +140,12 @@ int PS4_SYSV_ABI sceAjmMemoryUnregister() { return ORBIS_OK; } -int PS4_SYSV_ABI sceAjmModuleRegister() { - LOG_ERROR(Lib_Ajm, "(STUBBED) called"); - return ORBIS_OK; +int PS4_SYSV_ABI sceAjmModuleRegister(u32 context_id, AjmCodecType codec_type, s64 reserved) { + LOG_INFO(Lib_Ajm, "called context = {}, codec_type = {}", context_id, u32(codec_type)); + if (reserved != 0) { + return ORBIS_AJM_ERROR_INVALID_PARAMETER; + } + return context->ModuleRegister(codec_type); } int PS4_SYSV_ABI sceAjmModuleUnregister() { @@ -145,4 +183,4 @@ void RegisterlibSceAjm(Core::Loader::SymbolsResolver* sym) { LIB_FUNCTION("AxhcqVv5AYU", "libSceAjm", 1, "libSceAjm", 1, 1, sceAjmStrError); }; -} // namespace Libraries::Ajm \ No newline at end of file +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm.h b/src/core/libraries/ajm/ajm.h index 8491e519..10e23482 100644 --- a/src/core/libraries/ajm/ajm.h +++ b/src/core/libraries/ajm/ajm.h @@ -3,6 +3,8 @@ #pragma once +#include "common/bit_field.h" +#include "common/enum.h" #include "common/types.h" namespace Core::Loader { @@ -11,28 +13,173 @@ class SymbolsResolver; namespace Libraries::Ajm { -int PS4_SYSV_ABI sceAjmBatchCancel(); +constexpr u32 ORBIS_AT9_CONFIG_DATA_SIZE = 4; +constexpr u32 AJM_INSTANCE_STATISTICS = 0x80000; + +enum class AjmCodecType : u32 { + Mp3Dec = 0, + At9Dec = 1, + M4aacDec = 2, + Max = 23, +}; +DECLARE_ENUM_FLAG_OPERATORS(AjmCodecType); + +struct AjmBatchInfo { + void* pBuffer; + u64 offset; + u64 size; +}; + +struct AjmBatchError { + int error_code; + const void* job_addr; + u32 cmd_offset; + const void* job_ra; +}; + +struct AjmBuffer { + u8* p_address; + u64 size; +}; + +enum class AjmJobControlFlags : u64 { + Reset = 1 << 0, + Initialize = 1 << 1, + Resample = 1 << 2, +}; +DECLARE_ENUM_FLAG_OPERATORS(AjmJobControlFlags) + +enum class AjmJobRunFlags : u64 { + GetCodecInfo = 1 << 0, + MultipleFrames = 1 << 1, +}; +DECLARE_ENUM_FLAG_OPERATORS(AjmJobRunFlags) + +enum class AjmJobSidebandFlags : u64 { + GaplessDecode = 1 << 0, + Format = 1 << 1, + Stream = 1 << 2, +}; +DECLARE_ENUM_FLAG_OPERATORS(AjmJobSidebandFlags) + +union AjmJobFlags { + u64 raw; + struct { + u64 version : 3; + u64 codec : 8; + AjmJobRunFlags run_flags : 2; + AjmJobControlFlags control_flags : 3; + u64 reserved : 29; + AjmJobSidebandFlags sideband_flags : 3; + }; +}; + +struct AjmSidebandResult { + s32 result; + s32 internal_result; +}; + +struct AjmSidebandMFrame { + u32 num_frames; + u32 reserved; +}; + +struct AjmSidebandStream { + s32 input_consumed; + s32 output_written; + u64 total_decoded_samples; +}; + +enum class AjmFormatEncoding : u32 { + S16 = 0, + S32 = 1, + Float = 2, +}; + +struct AjmSidebandFormat { + u32 num_channels; + u32 channel_mask; + u32 sampl_freq; + AjmFormatEncoding sample_encoding; + u32 bitrate; + u32 reserved; +}; + +struct AjmSidebandGaplessDecode { + u32 total_samples; + u16 skip_samples; + u16 skipped_samples; +}; + +struct AjmSidebandResampleParameters { + float ratio; + uint32_t flags; +}; + +struct AjmDecAt9InitializeParameters { + u8 config_data[ORBIS_AT9_CONFIG_DATA_SIZE]; + u32 reserved; +}; + +union AjmSidebandInitParameters { + AjmDecAt9InitializeParameters at9; + u8 reserved[8]; +}; + +union AjmInstanceFlags { + u64 raw; + struct { + u64 version : 3; + u64 channels : 4; + u64 format : 3; + u64 gapless_loop : 1; + u64 : 21; + u64 codec : 28; + }; +}; + +struct AjmDecMp3ParseFrame; + +int PS4_SYSV_ABI sceAjmBatchCancel(const u32 context_id, const u32 batch_id); int PS4_SYSV_ABI sceAjmBatchErrorDump(); -int PS4_SYSV_ABI sceAjmBatchJobControlBufferRa(); -int PS4_SYSV_ABI sceAjmBatchJobInlineBuffer(); -int PS4_SYSV_ABI sceAjmBatchJobRunBufferRa(); -int PS4_SYSV_ABI sceAjmBatchJobRunSplitBufferRa(); -int PS4_SYSV_ABI sceAjmBatchStartBuffer(); -int PS4_SYSV_ABI sceAjmBatchWait(); +void* PS4_SYSV_ABI sceAjmBatchJobControlBufferRa(void* p_buffer, u32 instance_id, u64 flags, + void* p_sideband_input, size_t sideband_input_size, + void* p_sideband_output, + size_t sideband_output_size, + void* p_return_address); +void* PS4_SYSV_ABI sceAjmBatchJobInlineBuffer(void* p_buffer, const void* p_data_input, + size_t data_input_size, + const void** pp_batch_address); +void* PS4_SYSV_ABI sceAjmBatchJobRunBufferRa(void* p_buffer, u32 instance_id, u64 flags, + void* p_data_input, size_t data_input_size, + void* p_data_output, size_t data_output_size, + void* p_sideband_output, size_t sideband_output_size, + void* p_return_address); +void* PS4_SYSV_ABI sceAjmBatchJobRunSplitBufferRa( + void* p_buffer, u32 instance_id, u64 flags, const AjmBuffer* p_data_input_buffers, + size_t num_data_input_buffers, const AjmBuffer* p_data_output_buffers, + size_t num_data_output_buffers, void* p_sideband_output, size_t sideband_output_size, + void* p_return_address); +int PS4_SYSV_ABI sceAjmBatchStartBuffer(u32 context, u8* batch, u32 batch_size, const int priority, + AjmBatchError* batch_error, u32* out_batch_id); +int PS4_SYSV_ABI sceAjmBatchWait(const u32 context, const u32 batch_id, const u32 timeout, + AjmBatchError* const batch_error); int PS4_SYSV_ABI sceAjmDecAt9ParseConfigData(); -int PS4_SYSV_ABI sceAjmDecMp3ParseFrame(); +int PS4_SYSV_ABI sceAjmDecMp3ParseFrame(const u8* stream, u32 stream_size, int parse_ofl, + AjmDecMp3ParseFrame* frame); int PS4_SYSV_ABI sceAjmFinalize(); -int PS4_SYSV_ABI sceAjmInitialize(); +int PS4_SYSV_ABI sceAjmInitialize(s64 reserved, u32* out_context); int PS4_SYSV_ABI sceAjmInstanceCodecType(); -int PS4_SYSV_ABI sceAjmInstanceCreate(); -int PS4_SYSV_ABI sceAjmInstanceDestroy(); +int PS4_SYSV_ABI sceAjmInstanceCreate(u32 context, AjmCodecType codec_type, AjmInstanceFlags flags, + u32* instance); +int PS4_SYSV_ABI sceAjmInstanceDestroy(u32 context, u32 instance); int PS4_SYSV_ABI sceAjmInstanceExtend(); int PS4_SYSV_ABI sceAjmInstanceSwitch(); int PS4_SYSV_ABI sceAjmMemoryRegister(); int PS4_SYSV_ABI sceAjmMemoryUnregister(); -int PS4_SYSV_ABI sceAjmModuleRegister(); +int PS4_SYSV_ABI sceAjmModuleRegister(u32 context, AjmCodecType codec_type, s64 reserved); int PS4_SYSV_ABI sceAjmModuleUnregister(); int PS4_SYSV_ABI sceAjmStrError(); void RegisterlibSceAjm(Core::Loader::SymbolsResolver* sym); -} // namespace Libraries::Ajm \ No newline at end of file +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_at9.cpp b/src/core/libraries/ajm/ajm_at9.cpp new file mode 100644 index 00000000..07647c52 --- /dev/null +++ b/src/core/libraries/ajm/ajm_at9.cpp @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/assert.h" +#include "core/libraries/ajm/ajm_at9.h" +#include "error_codes.h" + +extern "C" { +#include +#include +} + +#include + +namespace Libraries::Ajm { + +AjmAt9Decoder::AjmAt9Decoder() { + m_handle = Atrac9GetHandle(); + ASSERT_MSG(m_handle, "Atrac9GetHandle failed"); + AjmAt9Decoder::Reset(); +} + +AjmAt9Decoder::~AjmAt9Decoder() { + Atrac9ReleaseHandle(m_handle); +} + +void AjmAt9Decoder::Reset() { + Atrac9ReleaseHandle(m_handle); + m_handle = Atrac9GetHandle(); + Atrac9InitDecoder(m_handle, m_config_data); + Atrac9GetCodecInfo(m_handle, &m_codec_info); + + m_num_frames = 0; + m_superframe_bytes_remain = m_codec_info.superframeSize; +} + +void AjmAt9Decoder::Initialize(const void* buffer, u32 buffer_size) { + ASSERT_MSG(buffer_size == sizeof(AjmDecAt9InitializeParameters), + "Incorrect At9 initialization buffer size {}", buffer_size); + const auto params = reinterpret_cast(buffer); + std::memcpy(m_config_data, params->config_data, ORBIS_AT9_CONFIG_DATA_SIZE); + AjmAt9Decoder::Reset(); + m_pcm_buffer.resize(m_codec_info.frameSamples * m_codec_info.channels, 0); +} + +void AjmAt9Decoder::GetInfo(void* out_info) { + auto* info = reinterpret_cast(out_info); + info->super_frame_size = m_codec_info.superframeSize; + info->frames_in_super_frame = m_codec_info.framesInSuperframe; + info->frame_samples = m_codec_info.frameSamples; + info->next_frame_size = static_cast(m_handle)->Config.FrameBytes; +} + +std::tuple AjmAt9Decoder::ProcessData(std::span& in_buf, SparseOutputBuffer& output, + AjmSidebandGaplessDecode& gapless, + u32 max_samples_per_channel) { + int bytes_used = 0; + u32 ret = Atrac9Decode(m_handle, in_buf.data(), m_pcm_buffer.data(), &bytes_used); + ASSERT_MSG(ret == At9Status::ERR_SUCCESS, "Atrac9Decode failed ret = {:#x}", ret); + in_buf = in_buf.subspan(bytes_used); + + m_superframe_bytes_remain -= bytes_used; + std::span pcm_data{m_pcm_buffer}; + + if (gapless.skipped_samples < gapless.skip_samples) { + const auto skipped_samples = std::min(u32(m_codec_info.frameSamples), + u32(gapless.skip_samples - gapless.skipped_samples)); + gapless.skipped_samples += skipped_samples; + pcm_data = pcm_data.subspan(skipped_samples * m_codec_info.channels); + } + + const auto max_samples = max_samples_per_channel == std::numeric_limits::max() + ? max_samples_per_channel + : max_samples_per_channel * m_codec_info.channels; + + const auto pcm_size = std::min(u32(pcm_data.size()), max_samples); + const auto written = output.Write(pcm_data.subspan(0, pcm_size)); + + m_num_frames += 1; + if ((m_num_frames % m_codec_info.framesInSuperframe) == 0) { + if (m_superframe_bytes_remain) { + in_buf = in_buf.subspan(m_superframe_bytes_remain); + } + m_superframe_bytes_remain = m_codec_info.superframeSize; + m_num_frames = 0; + } + + return {1, (written / m_codec_info.channels) / sizeof(s16)}; +} + +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_at9.h b/src/core/libraries/ajm/ajm_at9.h new file mode 100644 index 00000000..e89e69ce --- /dev/null +++ b/src/core/libraries/ajm/ajm_at9.h @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" +#include "core/libraries/ajm/ajm_instance.h" + +#include "libatrac9.h" + +namespace Libraries::Ajm { + +constexpr s32 ORBIS_AJM_DEC_AT9_MAX_CHANNELS = 8; + +struct AjmSidebandDecAt9CodecInfo { + u32 super_frame_size; + u32 frames_in_super_frame; + u32 next_frame_size; + u32 frame_samples; +}; + +struct AjmAt9Decoder final : AjmCodec { + explicit AjmAt9Decoder(); + ~AjmAt9Decoder() override; + + void Reset() override; + void Initialize(const void* buffer, u32 buffer_size) override; + void GetInfo(void* out_info) override; + std::tuple ProcessData(std::span& input, SparseOutputBuffer& output, + AjmSidebandGaplessDecode& gapless, u32 max_samples) override; + +private: + void* m_handle{}; + u8 m_config_data[ORBIS_AT9_CONFIG_DATA_SIZE]{}; + u32 m_superframe_bytes_remain{}; + u32 m_num_frames{}; + Atrac9CodecInfo m_codec_info{}; + std::vector m_pcm_buffer; +}; + +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_batch.cpp b/src/core/libraries/ajm/ajm_batch.cpp new file mode 100644 index 00000000..5c76beae --- /dev/null +++ b/src/core/libraries/ajm/ajm_batch.cpp @@ -0,0 +1,461 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/alignment.h" +#include "core/libraries/ajm/ajm_batch.h" + +namespace Libraries::Ajm { + +enum Identifier : u8 { + AjmIdentJob = 0, + AjmIdentInputRunBuf = 1, + AjmIdentInputControlBuf = 2, + AjmIdentControlFlags = 3, + AjmIdentRunFlags = 4, + AjmIdentReturnAddressBuf = 6, + AjmIdentInlineBuf = 7, + AjmIdentOutputRunBuf = 17, + AjmIdentOutputControlBuf = 18, +}; + +struct AjmChunkHeader { + u32 ident : 6; + u32 payload : 20; + u32 reserved : 6; +}; +static_assert(sizeof(AjmChunkHeader) == 4); + +struct AjmChunkJob { + AjmChunkHeader header; + u32 size; +}; +static_assert(sizeof(AjmChunkJob) == 8); + +struct AjmChunkFlags { + AjmChunkHeader header; + u32 flags_low; +}; +static_assert(sizeof(AjmChunkFlags) == 8); + +struct AjmChunkBuffer { + AjmChunkHeader header; + u32 size; + void* p_address; +}; +static_assert(sizeof(AjmChunkBuffer) == 16); + +class AjmBatchBuffer { +public: + static constexpr size_t s_dynamic_extent = std::numeric_limits::max(); + + AjmBatchBuffer(u8* begin, u8* end) + : m_p_begin(begin), m_p_current(begin), m_size(end - begin) {} + AjmBatchBuffer(u8* begin, size_t size = s_dynamic_extent) + : m_p_begin(begin), m_p_current(m_p_begin), m_size(size) {} + AjmBatchBuffer(std::span data) + : m_p_begin(data.data()), m_p_current(m_p_begin), m_size(data.size()) {} + + AjmBatchBuffer SubBuffer(size_t size = s_dynamic_extent) { + auto current = m_p_current; + if (size != s_dynamic_extent) { + m_p_current += size; + } + return AjmBatchBuffer(current, size); + } + + template + T& Peek() const { + DEBUG_ASSERT(m_size == s_dynamic_extent || + (m_p_current + sizeof(T)) <= (m_p_begin + m_size)); + return *reinterpret_cast(m_p_current); + } + + template + T& Consume() { + auto* const result = reinterpret_cast(m_p_current); + m_p_current += sizeof(T); + DEBUG_ASSERT(m_size == s_dynamic_extent || m_p_current <= (m_p_begin + m_size)); + return *result; + } + + template + void Skip() { + Advance(sizeof(T)); + } + + void Advance(size_t size) { + m_p_current += size; + DEBUG_ASSERT(m_size == s_dynamic_extent || m_p_current <= (m_p_begin + m_size)); + } + + bool IsEmpty() { + return m_size != s_dynamic_extent && m_p_current >= (m_p_begin + m_size); + } + + size_t BytesConsumed() const { + return m_p_current - m_p_begin; + } + + size_t BytesRemaining() const { + if (m_size == s_dynamic_extent) { + return s_dynamic_extent; + } + return m_size - (m_p_current - m_p_begin); + } + + u8* GetCurrent() const { + return m_p_current; + } + +private: + u8* m_p_begin{}; + u8* m_p_current{}; + size_t m_size{}; +}; + +AjmJob AjmJobFromBatchBuffer(u32 instance_id, AjmBatchBuffer batch_buffer) { + std::optional job_flags = {}; + std::optional input_control_buffer = {}; + std::optional output_control_buffer = {}; + std::optional inline_buffer = {}; + + AjmJob job; + job.instance_id = instance_id; + + // Read parameters of a job + while (!batch_buffer.IsEmpty()) { + auto& header = batch_buffer.Peek(); + switch (header.ident) { + case Identifier::AjmIdentInputRunBuf: { + auto& buffer = batch_buffer.Consume(); + u8* p_begin = reinterpret_cast(buffer.p_address); + job.input.buffer.insert(job.input.buffer.end(), p_begin, p_begin + buffer.size); + break; + } + case Identifier::AjmIdentInputControlBuf: { + ASSERT_MSG(!input_control_buffer.has_value(), + "Only one instance of input control buffer is allowed per job"); + input_control_buffer = batch_buffer.Consume(); + break; + } + case Identifier::AjmIdentControlFlags: + case Identifier::AjmIdentRunFlags: { + ASSERT_MSG(!job_flags.has_value(), "Only one instance of job flags is allowed per job"); + auto& chunk = batch_buffer.Consume(); + job_flags = AjmJobFlags{ + .raw = (u64(chunk.header.payload) << 32) + chunk.flags_low, + }; + break; + } + case Identifier::AjmIdentReturnAddressBuf: { + // Ignore return address buffers. + batch_buffer.Skip(); + break; + } + case Identifier::AjmIdentInlineBuf: { + ASSERT_MSG(!output_control_buffer.has_value(), + "Only one instance of inline buffer is allowed per job"); + inline_buffer = batch_buffer.Consume(); + break; + } + case Identifier::AjmIdentOutputRunBuf: { + auto& buffer = batch_buffer.Consume(); + u8* p_begin = reinterpret_cast(buffer.p_address); + job.output.buffers.emplace_back(std::span(p_begin, p_begin + buffer.size)); + break; + } + case Identifier::AjmIdentOutputControlBuf: { + ASSERT_MSG(!output_control_buffer.has_value(), + "Only one instance of output control buffer is allowed per job"); + output_control_buffer = batch_buffer.Consume(); + break; + } + default: + UNREACHABLE_MSG("Unknown chunk: {}", header.ident); + } + } + + job.flags = job_flags.value(); + + // Initialize sideband input parameters + if (input_control_buffer.has_value()) { + AjmBatchBuffer input_batch(reinterpret_cast(input_control_buffer->p_address), + input_control_buffer->size); + + const auto sideband_flags = job_flags->sideband_flags; + if (True(sideband_flags & AjmJobSidebandFlags::Format) && !input_batch.IsEmpty()) { + job.input.format = input_batch.Consume(); + } + if (True(sideband_flags & AjmJobSidebandFlags::GaplessDecode) && !input_batch.IsEmpty()) { + job.input.gapless_decode = input_batch.Consume(); + } + + const auto control_flags = job_flags.value().control_flags; + if (True(control_flags & AjmJobControlFlags::Initialize)) { + job.input.init_params = AjmDecAt9InitializeParameters{}; + std::memcpy(&job.input.init_params.value(), input_batch.GetCurrent(), + input_batch.BytesRemaining()); + } + } + + if (inline_buffer.has_value()) { + AjmBatchBuffer inline_batch(reinterpret_cast(inline_buffer->p_address), + inline_buffer->size); + + const auto control_flags = job_flags.value().control_flags; + if (True(control_flags & AjmJobControlFlags::Resample)) { + job.input.resample_parameters = inline_batch.Consume(); + } + } + + // Initialize sideband output parameters + if (output_control_buffer.has_value()) { + AjmBatchBuffer output_batch(reinterpret_cast(output_control_buffer->p_address), + output_control_buffer->size); + + job.output.p_result = &output_batch.Consume(); + *job.output.p_result = AjmSidebandResult{}; + + const auto sideband_flags = job_flags->sideband_flags; + if (True(sideband_flags & AjmJobSidebandFlags::Stream) && !output_batch.IsEmpty()) { + job.output.p_stream = &output_batch.Consume(); + *job.output.p_stream = AjmSidebandStream{}; + } + if (True(sideband_flags & AjmJobSidebandFlags::Format) && !output_batch.IsEmpty()) { + job.output.p_format = &output_batch.Consume(); + *job.output.p_format = AjmSidebandFormat{}; + } + if (True(sideband_flags & AjmJobSidebandFlags::GaplessDecode) && !output_batch.IsEmpty()) { + job.output.p_gapless_decode = &output_batch.Consume(); + *job.output.p_gapless_decode = AjmSidebandGaplessDecode{}; + } + + const auto run_flags = job_flags->run_flags; + if (True(run_flags & AjmJobRunFlags::MultipleFrames) && !output_batch.IsEmpty()) { + job.output.p_mframe = &output_batch.Consume(); + *job.output.p_mframe = AjmSidebandMFrame{}; + } + if (True(run_flags & AjmJobRunFlags::GetCodecInfo) && !output_batch.IsEmpty()) { + job.output.p_codec_info = output_batch.GetCurrent(); + } + } + + return job; +} + +std::shared_ptr AjmBatch::FromBatchBuffer(std::span data) { + auto batch = std::make_shared(); + + AjmBatchBuffer buffer(data); + while (!buffer.IsEmpty()) { + auto& job_chunk = buffer.Consume(); + ASSERT(job_chunk.header.ident == AjmIdentJob); + auto instance_id = job_chunk.header.payload; + batch->jobs.push_back(AjmJobFromBatchBuffer(instance_id, buffer.SubBuffer(job_chunk.size))); + } + + return batch; +} + +void* BatchJobControlBufferRa(void* p_buffer, u32 instance_id, u64 flags, void* p_sideband_input, + size_t sideband_input_size, void* p_sideband_output, + size_t sideband_output_size, void* p_return_address) { + LOG_TRACE(Lib_Ajm, "called"); + + AjmBatchBuffer buffer(reinterpret_cast(p_buffer)); + + auto& job_chunk = buffer.Consume(); + job_chunk.header.ident = AjmIdentJob; + job_chunk.header.payload = instance_id; + + auto job_buffer = buffer.SubBuffer(); + + if (p_return_address != nullptr) { + auto& chunk_ra = job_buffer.Consume(); + chunk_ra.header.ident = AjmIdentReturnAddressBuf; + chunk_ra.header.payload = 0; + chunk_ra.size = 0; + chunk_ra.p_address = p_return_address; + } + + { + auto& chunk_input = job_buffer.Consume(); + chunk_input.header.ident = AjmIdentInputControlBuf; + chunk_input.header.payload = 0; + chunk_input.size = sideband_input_size; + chunk_input.p_address = p_sideband_input; + } + + { + // 0x0000'0000'C001'8007 (AJM_INSTANCE_STATISTICS): + // | sideband | reserved | statistics | command | codec | revision | + // | 000 | 0000000000000 | 11000000000000011 | 0000 | 00000000 | 111 | + // statistics flags: + // STATISTICS_ENGINE | STATISTICS_ENGINE_PER_CODEC | ??STATISTICS_UNK?? | STATISTICS_MEMORY + + // 0x0000'6000'0000'E7FF: + // | sideband | reserved | control | run | codec | revision | + // | 011 | 00000000000000000000000000000 | 111 | 00 | 11111111 | 111 | + const bool is_statistics = instance_id == AJM_INSTANCE_STATISTICS; + flags &= is_statistics ? 0x0000'0000'C001'8007 : 0x0000'6000'0000'E7FF; + + auto& chunk_flags = job_buffer.Consume(); + chunk_flags.header.ident = AjmIdentControlFlags; + chunk_flags.header.payload = u32(flags >> 32); + chunk_flags.flags_low = u32(flags); + } + + { + auto& chunk_output = job_buffer.Consume(); + chunk_output.header.ident = AjmIdentOutputControlBuf; + chunk_output.header.payload = 0; + chunk_output.size = sideband_output_size; + chunk_output.p_address = p_sideband_output; + } + + job_chunk.size = job_buffer.BytesConsumed(); + return job_buffer.GetCurrent(); +} + +void* BatchJobInlineBuffer(void* p_buffer, const void* p_data_input, size_t data_input_size, + const void** pp_batch_address) { + LOG_TRACE(Lib_Ajm, "called"); + + AjmBatchBuffer buffer(reinterpret_cast(p_buffer)); + + auto& job_chunk = buffer.Consume(); + job_chunk.header.ident = AjmIdentInlineBuf; + job_chunk.header.payload = 0; + job_chunk.size = Common::AlignUp(data_input_size, 8); + *pp_batch_address = buffer.GetCurrent(); + + memcpy(buffer.GetCurrent(), p_data_input, data_input_size); + return buffer.GetCurrent() + job_chunk.size; +} + +void* BatchJobRunBufferRa(void* p_buffer, u32 instance_id, u64 flags, void* p_data_input, + size_t data_input_size, void* p_data_output, size_t data_output_size, + void* p_sideband_output, size_t sideband_output_size, + void* p_return_address) { + LOG_TRACE(Lib_Ajm, "called"); + + AjmBatchBuffer buffer(reinterpret_cast(p_buffer)); + + auto& job_chunk = buffer.Consume(); + job_chunk.header.ident = AjmIdentJob; + job_chunk.header.payload = instance_id; + + auto job_buffer = buffer.SubBuffer(); + + if (p_return_address != nullptr) { + auto& chunk_ra = job_buffer.Consume(); + chunk_ra.header.ident = AjmIdentReturnAddressBuf; + chunk_ra.header.payload = 0; + chunk_ra.size = 0; + chunk_ra.p_address = p_return_address; + } + + { + auto& chunk_input = job_buffer.Consume(); + chunk_input.header.ident = AjmIdentInputRunBuf; + chunk_input.header.payload = 0; + chunk_input.size = data_input_size; + chunk_input.p_address = p_data_input; + } + + { + // 0x0000'E000'0000'1FFF: + // | sideband | reserved | control | run | codec | revision | + // | 111 | 00000000000000000000000000000 | 000 | 11 | 11111111 | 111 | + flags &= 0x0000'E000'0000'1FFF; + + auto& chunk_flags = job_buffer.Consume(); + chunk_flags.header.ident = AjmIdentRunFlags; + chunk_flags.header.payload = u32(flags >> 32); + chunk_flags.flags_low = u32(flags); + } + + { + auto& chunk_output = job_buffer.Consume(); + chunk_output.header.ident = AjmIdentOutputRunBuf; + chunk_output.header.payload = 0; + chunk_output.size = data_output_size; + chunk_output.p_address = p_data_output; + } + + { + auto& chunk_output = job_buffer.Consume(); + chunk_output.header.ident = AjmIdentOutputControlBuf; + chunk_output.header.payload = 0; + chunk_output.size = sideband_output_size; + chunk_output.p_address = p_sideband_output; + } + + job_chunk.size = job_buffer.BytesConsumed(); + return job_buffer.GetCurrent(); +} + +void* BatchJobRunSplitBufferRa(void* p_buffer, u32 instance_id, u64 flags, + const AjmBuffer* p_data_input_buffers, size_t num_data_input_buffers, + const AjmBuffer* p_data_output_buffers, + size_t num_data_output_buffers, void* p_sideband_output, + size_t sideband_output_size, void* p_return_address) { + LOG_TRACE(Lib_Ajm, "called"); + + AjmBatchBuffer buffer(reinterpret_cast(p_buffer)); + + auto& job_chunk = buffer.Consume(); + job_chunk.header.ident = AjmIdentJob; + job_chunk.header.payload = instance_id; + + auto job_buffer = buffer.SubBuffer(); + + if (p_return_address != nullptr) { + auto& chunk_ra = job_buffer.Consume(); + chunk_ra.header.ident = AjmIdentReturnAddressBuf; + chunk_ra.header.payload = 0; + chunk_ra.size = 0; + chunk_ra.p_address = p_return_address; + } + + for (s32 i = 0; i < num_data_input_buffers; i++) { + auto& chunk_input = job_buffer.Consume(); + chunk_input.header.ident = AjmIdentInputRunBuf; + chunk_input.header.payload = 0; + chunk_input.size = p_data_input_buffers[i].size; + chunk_input.p_address = p_data_input_buffers[i].p_address; + } + + { + // 0x0000'E000'0000'1FFF: + // | sideband | reserved | control | run | codec | revision | + // | 111 | 00000000000000000000000000000 | 000 | 11 | 11111111 | 111 | + flags &= 0x0000'E000'0000'1FFF; + + auto& chunk_flags = job_buffer.Consume(); + chunk_flags.header.ident = AjmIdentRunFlags; + chunk_flags.header.payload = u32(flags >> 32); + chunk_flags.flags_low = u32(flags); + } + + for (s32 i = 0; i < num_data_output_buffers; i++) { + auto& chunk_output = job_buffer.Consume(); + chunk_output.header.ident = AjmIdentOutputRunBuf; + chunk_output.header.payload = 0; + chunk_output.size = p_data_output_buffers[i].size; + chunk_output.p_address = p_data_output_buffers[i].p_address; + } + + { + auto& chunk_output = job_buffer.Consume(); + chunk_output.header.ident = AjmIdentOutputControlBuf; + chunk_output.header.payload = 0; + chunk_output.size = sideband_output_size; + chunk_output.p_address = p_sideband_output; + } + + job_chunk.size = job_buffer.BytesConsumed(); + return job_buffer.GetCurrent(); +} + +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_batch.h b/src/core/libraries/ajm/ajm_batch.h new file mode 100644 index 00000000..65110ee7 --- /dev/null +++ b/src/core/libraries/ajm/ajm_batch.h @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/assert.h" +#include "common/types.h" +#include "core/libraries/ajm/ajm.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace Libraries::Ajm { + +struct AjmJob { + struct Input { + std::optional init_params; + std::optional resample_parameters; + std::optional format; + std::optional gapless_decode; + std::vector buffer; + }; + + struct Output { + boost::container::small_vector, 8> buffers; + AjmSidebandResult* p_result = nullptr; + AjmSidebandStream* p_stream = nullptr; + AjmSidebandFormat* p_format = nullptr; + AjmSidebandGaplessDecode* p_gapless_decode = nullptr; + AjmSidebandMFrame* p_mframe = nullptr; + u8* p_codec_info = nullptr; + }; + + u32 instance_id{}; + AjmJobFlags flags{}; + Input input; + Output output; +}; + +struct AjmBatch { + u32 id{}; + std::atomic_bool waiting{}; + std::atomic_bool canceled{}; + std::binary_semaphore finished{0}; + boost::container::small_vector jobs; + + static std::shared_ptr FromBatchBuffer(std::span buffer); +}; + +void* BatchJobControlBufferRa(void* p_buffer, u32 instance_id, u64 flags, void* p_sideband_input, + size_t sideband_input_size, void* p_sideband_output, + size_t sideband_output_size, void* p_return_address); + +void* BatchJobInlineBuffer(void* p_buffer, const void* p_data_input, size_t data_input_size, + const void** pp_batch_address); + +void* BatchJobRunBufferRa(void* p_buffer, u32 instance_id, u64 flags, void* p_data_input, + size_t data_input_size, void* p_data_output, size_t data_output_size, + void* p_sideband_output, size_t sideband_output_size, + void* p_return_address); + +void* BatchJobRunSplitBufferRa(void* p_buffer, u32 instance_id, u64 flags, + const AjmBuffer* p_data_input_buffers, size_t num_data_input_buffers, + const AjmBuffer* p_data_output_buffers, + size_t num_data_output_buffers, void* p_sideband_output, + size_t sideband_output_size, void* p_return_address); + +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_context.cpp b/src/core/libraries/ajm/ajm_context.cpp new file mode 100644 index 00000000..d831ab48 --- /dev/null +++ b/src/core/libraries/ajm/ajm_context.cpp @@ -0,0 +1,175 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/assert.h" +#include "common/logging/log.h" +#include "core/libraries/ajm/ajm.h" +#include "core/libraries/ajm/ajm_at9.h" +#include "core/libraries/ajm/ajm_context.h" +#include "core/libraries/ajm/ajm_error.h" +#include "core/libraries/ajm/ajm_instance.h" +#include "core/libraries/ajm/ajm_mp3.h" +#include "core/libraries/error_codes.h" + +#include +#include + +namespace Libraries::Ajm { + +static constexpr u32 ORBIS_AJM_WAIT_INFINITE = -1; + +AjmContext::AjmContext() { + worker_thread = std::jthread([this](std::stop_token stop) { this->WorkerThread(stop); }); +} + +bool AjmContext::IsRegistered(AjmCodecType type) const { + return registered_codecs[std::to_underlying(type)]; +} + +s32 AjmContext::BatchCancel(const u32 batch_id) { + std::shared_ptr batch{}; + { + std::shared_lock guard(batches_mutex); + const auto p_batch = batches.Get(batch_id); + if (p_batch == nullptr) { + return ORBIS_AJM_ERROR_INVALID_BATCH; + } + batch = *p_batch; + } + + batch->canceled = true; + return ORBIS_OK; +} + +s32 AjmContext::ModuleRegister(AjmCodecType type) { + if (std::to_underlying(type) >= NumAjmCodecs) { + return ORBIS_AJM_ERROR_INVALID_PARAMETER; + } + if (IsRegistered(type)) { + return ORBIS_AJM_ERROR_CODEC_ALREADY_REGISTERED; + } + registered_codecs[std::to_underlying(type)] = true; + return ORBIS_OK; +} + +void AjmContext::WorkerThread(std::stop_token stop) { + while (!stop.stop_requested()) { + auto batch = batch_queue.PopWait(stop); + if (batch != nullptr) { + ProcessBatch(batch->id, batch->jobs); + batch->finished.release(); + } + } +} + +void AjmContext::ProcessBatch(u32 id, std::span jobs) { + // Perform operation requested by control flags. + for (auto& job : jobs) { + LOG_TRACE(Lib_Ajm, "Processing job {} for instance {}. flags = {:#x}", id, job.instance_id, + job.flags.raw); + + std::shared_ptr instance; + { + std::shared_lock lock(instances_mutex); + auto* p_instance = instances.Get(job.instance_id); + ASSERT_MSG(p_instance != nullptr, "Attempting to execute job on null instance"); + instance = *p_instance; + } + + instance->ExecuteJob(job); + } +} + +s32 AjmContext::BatchWait(const u32 batch_id, const u32 timeout, AjmBatchError* const batch_error) { + std::shared_ptr batch{}; + { + std::shared_lock guard(batches_mutex); + const auto p_batch = batches.Get(batch_id); + if (p_batch == nullptr) { + return ORBIS_AJM_ERROR_INVALID_BATCH; + } + batch = *p_batch; + } + + bool expected = false; + if (!batch->waiting.compare_exchange_strong(expected, true)) { + return ORBIS_AJM_ERROR_BUSY; + } + + if (timeout == ORBIS_AJM_WAIT_INFINITE) { + batch->finished.acquire(); + } else if (!batch->finished.try_acquire_for(std::chrono::milliseconds(timeout))) { + batch->waiting = false; + return ORBIS_AJM_ERROR_IN_PROGRESS; + } + + { + std::unique_lock guard(batches_mutex); + batches.Destroy(batch_id); + } + + if (batch->canceled) { + return ORBIS_AJM_ERROR_CANCELLED; + } + + return ORBIS_OK; +} + +int AjmContext::BatchStartBuffer(u8* p_batch, u32 batch_size, const int priority, + AjmBatchError* batch_error, u32* out_batch_id) { + if ((batch_size & 7) != 0) { + LOG_ERROR(Lib_Ajm, "ORBIS_AJM_ERROR_MALFORMED_BATCH"); + return ORBIS_AJM_ERROR_MALFORMED_BATCH; + } + + const auto batch_info = AjmBatch::FromBatchBuffer({p_batch, batch_size}); + std::optional batch_id; + { + std::unique_lock guard(batches_mutex); + batch_id = batches.Create(batch_info); + } + if (!batch_id.has_value()) { + return ORBIS_AJM_ERROR_OUT_OF_MEMORY; + } + *out_batch_id = batch_id.value(); + batch_info->id = *out_batch_id; + + batch_queue.EmplaceWait(batch_info); + + return ORBIS_OK; +} + +s32 AjmContext::InstanceCreate(AjmCodecType codec_type, AjmInstanceFlags flags, u32* out_instance) { + if (codec_type >= AjmCodecType::Max) { + return ORBIS_AJM_ERROR_INVALID_PARAMETER; + } + if (flags.version == 0) { + return ORBIS_AJM_ERROR_WRONG_REVISION_FLAG; + } + if (!IsRegistered(codec_type)) { + return ORBIS_AJM_ERROR_CODEC_NOT_REGISTERED; + } + ASSERT_MSG(flags.format == 0, "Only signed 16-bit PCM output is supported currently!"); + std::optional opt_index; + { + std::unique_lock lock(instances_mutex); + opt_index = instances.Create(std::move(std::make_unique(codec_type, flags))); + } + if (!opt_index.has_value()) { + return ORBIS_AJM_ERROR_OUT_OF_RESOURCES; + } + *out_instance = opt_index.value(); + + LOG_INFO(Lib_Ajm, "instance = {}", *out_instance); + return ORBIS_OK; +} + +s32 AjmContext::InstanceDestroy(u32 instance) { + std::unique_lock lock(instances_mutex); + if (!instances.Destroy(instance)) { + return ORBIS_AJM_ERROR_INVALID_INSTANCE; + } + return ORBIS_OK; +} + +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_context.h b/src/core/libraries/ajm/ajm_context.h new file mode 100644 index 00000000..e51ea4fc --- /dev/null +++ b/src/core/libraries/ajm/ajm_context.h @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/bounded_threadsafe_queue.h" +#include "common/slot_array.h" +#include "common/types.h" +#include "core/libraries/ajm/ajm.h" +#include "core/libraries/ajm/ajm_batch.h" +#include "core/libraries/ajm/ajm_instance.h" + +#include +#include +#include +#include +#include +#include + +namespace Libraries::Ajm { + +class AjmContext { +public: + AjmContext(); + + s32 InstanceCreate(AjmCodecType codec_type, AjmInstanceFlags flags, u32* out_instance_id); + s32 InstanceDestroy(u32 instance_id); + + s32 BatchCancel(const u32 batch_id); + s32 ModuleRegister(AjmCodecType type); + s32 BatchWait(const u32 batch_id, const u32 timeout, AjmBatchError* const p_batch_error); + s32 BatchStartBuffer(u8* p_batch, u32 batch_size, const int priority, + AjmBatchError* p_batch_error, u32* p_batch_id); + + void WorkerThread(std::stop_token stop); + void ProcessBatch(u32 id, std::span jobs); + +private: + static constexpr u32 MaxInstances = 0x2fff; + static constexpr u32 MaxBatches = 0x0400; + static constexpr u32 NumAjmCodecs = std::to_underlying(AjmCodecType::Max); + + [[nodiscard]] bool IsRegistered(AjmCodecType type) const; + + std::array registered_codecs{}; + + std::shared_mutex instances_mutex; + Common::SlotArray, MaxInstances, 1> instances; + + std::shared_mutex batches_mutex; + Common::SlotArray, MaxBatches, 1> batches; + + std::jthread worker_thread{}; + Common::MPSCQueue> batch_queue; +}; + +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_instance.cpp b/src/core/libraries/ajm/ajm_instance.cpp new file mode 100644 index 00000000..259fbc26 --- /dev/null +++ b/src/core/libraries/ajm/ajm_instance.cpp @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "core/libraries/ajm/ajm_at9.h" +#include "core/libraries/ajm/ajm_instance.h" +#include "core/libraries/ajm/ajm_mp3.h" + +#include + +namespace Libraries::Ajm { + +AjmInstance::AjmInstance(AjmCodecType codec_type, AjmInstanceFlags flags) : m_flags(flags) { + switch (codec_type) { + case AjmCodecType::At9Dec: { + m_codec = std::make_unique(); + break; + } + case AjmCodecType::Mp3Dec: { + m_codec = std::make_unique(); + break; + } + default: + UNREACHABLE_MSG("Unimplemented codec type {}", magic_enum::enum_name(codec_type)); + } +} + +void AjmInstance::ExecuteJob(AjmJob& job) { + const auto control_flags = job.flags.control_flags; + if (True(control_flags & AjmJobControlFlags::Reset)) { + LOG_TRACE(Lib_Ajm, "Resetting instance {}", job.instance_id); + m_format = {}; + m_gapless = {}; + m_resample_parameters = {}; + m_gapless_samples = 0; + m_total_samples = 0; + m_codec->Reset(); + } + if (job.input.init_params.has_value()) { + LOG_TRACE(Lib_Ajm, "Initializing instance {}", job.instance_id); + auto& params = job.input.init_params.value(); + m_codec->Initialize(¶ms, sizeof(params)); + } + if (job.input.resample_parameters.has_value()) { + UNREACHABLE_MSG("Unimplemented: resample parameters"); + m_resample_parameters = job.input.resample_parameters.value(); + } + if (job.input.format.has_value()) { + UNREACHABLE_MSG("Unimplemented: format parameters"); + m_format = job.input.format.value(); + } + if (job.input.gapless_decode.has_value()) { + auto& params = job.input.gapless_decode.value(); + m_gapless.total_samples = params.total_samples; + m_gapless.skip_samples = params.skip_samples; + } + + if (!job.input.buffer.empty() && !job.output.buffers.empty()) { + u32 frames_decoded = 0; + std::span in_buf(job.input.buffer); + SparseOutputBuffer out_buf(job.output.buffers); + + auto in_size = in_buf.size(); + auto out_size = out_buf.Size(); + while (!in_buf.empty() && !out_buf.IsEmpty() && !IsGaplessEnd()) { + const u32 samples_remain = m_gapless.total_samples != 0 + ? m_gapless.total_samples - m_gapless_samples + : std::numeric_limits::max(); + const auto [nframes, nsamples] = + m_codec->ProcessData(in_buf, out_buf, m_gapless, samples_remain); + frames_decoded += nframes; + m_total_samples += nsamples; + m_gapless_samples += nsamples; + } + if (job.output.p_mframe) { + job.output.p_mframe->num_frames = frames_decoded; + } + if (job.output.p_stream) { + job.output.p_stream->input_consumed = in_size - in_buf.size(); + job.output.p_stream->output_written = out_size - out_buf.Size(); + job.output.p_stream->total_decoded_samples = m_total_samples; + } + } + + if (m_flags.gapless_loop && m_gapless.total_samples != 0 && + m_gapless_samples >= m_gapless.total_samples) { + m_gapless_samples = 0; + m_gapless.skipped_samples = 0; + m_codec->Reset(); + } + if (job.output.p_gapless_decode != nullptr) { + *job.output.p_gapless_decode = m_gapless; + } + if (job.output.p_codec_info != nullptr) { + m_codec->GetInfo(job.output.p_codec_info); + } +} + +bool AjmInstance::IsGaplessEnd() { + return m_gapless.total_samples != 0 && m_gapless_samples >= m_gapless.total_samples; +} + +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_instance.h b/src/core/libraries/ajm/ajm_instance.h new file mode 100644 index 00000000..7c00c43a --- /dev/null +++ b/src/core/libraries/ajm/ajm_instance.h @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/enum.h" +#include "common/types.h" +#include "core/libraries/ajm/ajm.h" +#include "core/libraries/ajm/ajm_batch.h" + +#include +#include +#include + +namespace Libraries::Ajm { + +constexpr int ORBIS_AJM_RESULT_NOT_INITIALIZED = 0x00000001; +constexpr int ORBIS_AJM_RESULT_INVALID_DATA = 0x00000002; +constexpr int ORBIS_AJM_RESULT_INVALID_PARAMETER = 0x00000004; +constexpr int ORBIS_AJM_RESULT_PARTIAL_INPUT = 0x00000008; +constexpr int ORBIS_AJM_RESULT_NOT_ENOUGH_ROOM = 0x00000010; +constexpr int ORBIS_AJM_RESULT_STREAM_CHANGE = 0x00000020; +constexpr int ORBIS_AJM_RESULT_TOO_MANY_CHANNELS = 0x00000040; +constexpr int ORBIS_AJM_RESULT_UNSUPPORTED_FLAG = 0x00000080; +constexpr int ORBIS_AJM_RESULT_SIDEBAND_TRUNCATED = 0x00000100; +constexpr int ORBIS_AJM_RESULT_PRIORITY_PASSED = 0x00000200; +constexpr int ORBIS_AJM_RESULT_CODEC_ERROR = 0x40000000; +constexpr int ORBIS_AJM_RESULT_FATAL = 0x80000000; + +class SparseOutputBuffer { +public: + SparseOutputBuffer(std::span> chunks) + : m_chunks(chunks), m_current(m_chunks.begin()) {} + + template + size_t Write(std::span pcm) { + size_t bytes_written = 0; + while (!pcm.empty() && !IsEmpty()) { + auto size = std::min(pcm.size() * sizeof(T), m_current->size()); + std::memcpy(m_current->data(), pcm.data(), size); + bytes_written += size; + pcm = pcm.subspan(size / sizeof(T)); + *m_current = m_current->subspan(size); + if (m_current->empty()) { + ++m_current; + } + } + return bytes_written; + } + + bool IsEmpty() { + return m_current == m_chunks.end(); + } + + size_t Size() { + size_t result = 0; + for (auto it = m_current; it != m_chunks.end(); ++it) { + result += it->size(); + } + return result; + } + +private: + std::span> m_chunks; + std::span>::iterator m_current; +}; + +struct DecodeResult { + u32 bytes_consumed{}; + u32 bytes_written{}; +}; + +class AjmCodec { +public: + virtual ~AjmCodec() = default; + + virtual void Initialize(const void* buffer, u32 buffer_size) = 0; + virtual void Reset() = 0; + virtual void GetInfo(void* out_info) = 0; + virtual std::tuple ProcessData(std::span& input, SparseOutputBuffer& output, + AjmSidebandGaplessDecode& gapless, + u32 max_samples) = 0; +}; + +class AjmInstance { +public: + AjmInstance(AjmCodecType codec_type, AjmInstanceFlags flags); + + void ExecuteJob(AjmJob& job); + +private: + bool IsGaplessEnd(); + + AjmInstanceFlags m_flags{}; + AjmSidebandFormat m_format{}; + AjmSidebandGaplessDecode m_gapless{}; + AjmSidebandResampleParameters m_resample_parameters{}; + + u32 m_gapless_samples{}; + u32 m_total_samples{}; + + std::unique_ptr m_codec; + + // AjmCodecType codec_type; + // u32 index{}; +}; + +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_mp3.cpp b/src/core/libraries/ajm/ajm_mp3.cpp new file mode 100644 index 00000000..542b31be --- /dev/null +++ b/src/core/libraries/ajm/ajm_mp3.cpp @@ -0,0 +1,166 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "common/assert.h" +#include "core/libraries/ajm/ajm_error.h" +#include "core/libraries/ajm/ajm_mp3.h" +#include "core/libraries/error_codes.h" + +extern "C" { +#include +#include +#include +} + +namespace Libraries::Ajm { + +// Following tables have been reversed from AJM library +static constexpr std::array, 3> SamplerateTable = {{ + {0x5622, 0x5DC0, 0x3E80}, + {0xAC44, 0xBB80, 0x7D00}, + {0x2B11, 0x2EE0, 0x1F40}, +}}; + +static constexpr std::array, 2> BitrateTable = {{ + {0, 0x20, 0x28, 0x30, 0x38, 0x40, 0x50, 0x60, 0x70, 0x80, 0xA0, 0xC0, 0xE0, 0x100, 0x140}, + {0, 0x8, 0x10, 0x18, 0x20, 0x28, 0x30, 0x38, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xA0}, +}}; + +static constexpr std::array UnkTable = {0x48, 0x90}; + +SwrContext* swr_context{}; + +AVFrame* ConvertAudioFrame(AVFrame* frame) { + auto pcm16_frame = av_frame_clone(frame); + pcm16_frame->format = AV_SAMPLE_FMT_S16; + + if (swr_context) { + swr_free(&swr_context); + swr_context = nullptr; + } + AVChannelLayout in_ch_layout = frame->ch_layout; + AVChannelLayout out_ch_layout = pcm16_frame->ch_layout; + swr_alloc_set_opts2(&swr_context, &out_ch_layout, AV_SAMPLE_FMT_S16, frame->sample_rate, + &in_ch_layout, AVSampleFormat(frame->format), frame->sample_rate, 0, + nullptr); + swr_init(swr_context); + const auto res = swr_convert_frame(swr_context, pcm16_frame, frame); + if (res < 0) { + LOG_ERROR(Lib_AvPlayer, "Could not convert to S16: {}", av_err2str(res)); + return nullptr; + } + av_frame_free(&frame); + return pcm16_frame; +} + +AjmMp3Decoder::AjmMp3Decoder() { + m_codec = avcodec_find_decoder(AV_CODEC_ID_MP3); + ASSERT_MSG(m_codec, "MP3 m_codec not found"); + m_parser = av_parser_init(m_codec->id); + ASSERT_MSG(m_parser, "Parser not found"); + AjmMp3Decoder::Reset(); +} + +AjmMp3Decoder::~AjmMp3Decoder() { + avcodec_free_context(&m_codec_context); +} + +void AjmMp3Decoder::Reset() { + if (m_codec_context) { + avcodec_free_context(&m_codec_context); + } + m_codec_context = avcodec_alloc_context3(m_codec); + ASSERT_MSG(m_codec_context, "Could not allocate audio m_codec context"); + int ret = avcodec_open2(m_codec_context, m_codec, nullptr); + ASSERT_MSG(ret >= 0, "Could not open m_codec"); +} + +void AjmMp3Decoder::GetInfo(void* out_info) { + auto* info = reinterpret_cast(out_info); +} + +std::tuple AjmMp3Decoder::ProcessData(std::span& in_buf, SparseOutputBuffer& output, + AjmSidebandGaplessDecode& gapless, + u32 max_samples) { + AVPacket* pkt = av_packet_alloc(); + + int ret = av_parser_parse2(m_parser, m_codec_context, &pkt->data, &pkt->size, in_buf.data(), + in_buf.size(), AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0); + ASSERT_MSG(ret >= 0, "Error while parsing {}", ret); + in_buf = in_buf.subspan(ret); + + u32 frames_decoded = 0; + u32 samples_decoded = 0; + + if (pkt->size) { + // Send the packet with the compressed data to the decoder + pkt->pts = m_parser->pts; + pkt->dts = m_parser->dts; + pkt->flags = (m_parser->key_frame == 1) ? AV_PKT_FLAG_KEY : 0; + ret = avcodec_send_packet(m_codec_context, pkt); + ASSERT_MSG(ret >= 0, "Error submitting the packet to the decoder {}", ret); + + // Read all the output frames (in general there may be any number of them + while (ret >= 0) { + AVFrame* frame = av_frame_alloc(); + ret = avcodec_receive_frame(m_codec_context, frame); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { + break; + } else if (ret < 0) { + UNREACHABLE_MSG("Error during decoding"); + } + if (frame->format != AV_SAMPLE_FMT_S16) { + frame = ConvertAudioFrame(frame); + } + + frames_decoded += 1; + samples_decoded += frame->nb_samples; + const auto size = frame->ch_layout.nb_channels * frame->nb_samples * sizeof(u16); + std::span pcm_data(reinterpret_cast(frame->data[0]), size >> 1); + if (gapless.skipped_samples < gapless.skip_samples) { + const auto skipped_samples = std::min( + u32(frame->nb_samples), u32(gapless.skip_samples - gapless.skipped_samples)); + gapless.skipped_samples += skipped_samples; + pcm_data = pcm_data.subspan(skipped_samples * frame->ch_layout.nb_channels); + samples_decoded -= skipped_samples; + } + + const auto pcm_size = std::min(u32(pcm_data.size()), max_samples); + output.Write(pcm_data.subspan(0, pcm_size)); + + av_frame_free(&frame); + } + } + + av_packet_free(&pkt); + + return {frames_decoded, samples_decoded}; +} + +int AjmMp3Decoder::ParseMp3Header(const u8* buf, u32 stream_size, int parse_ofl, + AjmDecMp3ParseFrame* frame) { + LOG_INFO(Lib_Ajm, "called stream_size = {} parse_ofl = {}", stream_size, parse_ofl); + if (buf == nullptr || stream_size < 4 || frame == nullptr) { + return ORBIS_AJM_ERROR_INVALID_PARAMETER; + } + if ((buf[0] & SYNCWORDH) != SYNCWORDH || (buf[1] & SYNCWORDL) != SYNCWORDL) { + return ORBIS_AJM_ERROR_INVALID_PARAMETER; + } + + const u32 unk_idx = buf[1] >> 3 & 1; + const s32 version_idx = (buf[1] >> 3 & 3) ^ 2; + const s32 sr_idx = buf[2] >> 2 & 3; + const s32 br_idx = (buf[2] >> 4) & 0xf; + const s32 padding_bit = (buf[2] >> 1) & 0x1; + + frame->sample_rate = SamplerateTable[version_idx][sr_idx]; + frame->bitrate = BitrateTable[version_idx != 1][br_idx] * 1000; + frame->num_channels = (buf[3] < 0xc0) + 1; + frame->frame_size = (UnkTable[unk_idx] * frame->bitrate) / frame->sample_rate + padding_bit; + frame->samples_per_channel = UnkTable[unk_idx] * 8; + frame->encoder_delay = 0; + + return ORBIS_OK; +} + +} // namespace Libraries::Ajm diff --git a/src/core/libraries/ajm/ajm_mp3.h b/src/core/libraries/ajm/ajm_mp3.h new file mode 100644 index 00000000..d2acf7ab --- /dev/null +++ b/src/core/libraries/ajm/ajm_mp3.h @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" +#include "core/libraries/ajm/ajm_instance.h" + +extern "C" { +struct AVCodec; +struct AVCodecContext; +struct AVCodecParserContext; +struct AVFrame; +struct AVPacket; +} + +namespace Libraries::Ajm { + +enum class AjmDecMp3OflType : u32 { None = 0, Lame = 1, Vbri = 2, Fgh = 3, VbriAndFgh = 4 }; + +// 11-bit syncword if MPEG 2.5 extensions are enabled +static constexpr u8 SYNCWORDH = 0xff; +static constexpr u8 SYNCWORDL = 0xe0; + +struct AjmDecMp3ParseFrame { + u64 frame_size; + u32 num_channels; + u32 samples_per_channel; + u32 bitrate; + u32 sample_rate; + u32 encoder_delay; + u32 num_frames; + u32 total_samples; + AjmDecMp3OflType ofl_type; +}; + +enum class ChannelMode : u8 { + Stereo = 0, + JointStero = 1, + Dual = 2, + Mono = 3, +}; + +struct AjmSidebandDecMp3CodecInfo { + u32 header; + bool has_crc; + ChannelMode channel_mode; + u8 mode_extension; + u8 copyright; + u8 original; + u8 emphasis; + u16 reserved[3]; +}; + +class AjmMp3Decoder : public AjmCodec { +public: + explicit AjmMp3Decoder(); + ~AjmMp3Decoder() override; + + void Reset() override; + void Initialize(const void* buffer, u32 buffer_size) override {} + void GetInfo(void* out_info) override; + std::tuple ProcessData(std::span& input, SparseOutputBuffer& output, + AjmSidebandGaplessDecode& gapless, u32 max_samples) override; + + static int ParseMp3Header(const u8* buf, u32 stream_size, int parse_ofl, + AjmDecMp3ParseFrame* frame); + +private: + const AVCodec* m_codec = nullptr; + AVCodecContext* m_codec_context = nullptr; + AVCodecParserContext* m_parser = nullptr; +}; + +} // namespace Libraries::Ajm