diff --git a/.gitmodules b/.gitmodules index 995853108..0a5f83fb7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,6 +19,9 @@ [submodule "3rdparty/LibAtrac9"] path = 3rdparty/LibAtrac9 url = https://github.com/shadps4-emu/ext-LibAtrac9.git +[submodule "3rdparty/ffmpeg-core"] + path = 3rdparty/ffmpeg-core + url = https://github.com/KytyPS5/ext-ffmpeg-core.git [submodule "3rdparty/freetype"] path = 3rdparty/freetype url = https://github.com/freetype/freetype.git diff --git a/3rdparty/ffmpeg-core b/3rdparty/ffmpeg-core new file mode 160000 index 000000000..9ac4cfd19 --- /dev/null +++ b/3rdparty/ffmpeg-core @@ -0,0 +1 @@ +Subproject commit 9ac4cfd195f192ed8b08566f49c28dbb48d08341 diff --git a/CMakeLists.txt b/CMakeLists.txt index 3751edef8..9236f55fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,11 @@ set(ANYPS5_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") set(CMAKE_EXE_LINKER_FLAGS "") add_subdirectory(3rdparty/SDL2) set(CMAKE_EXE_LINKER_FLAGS "${ANYPS5_EXE_LINKER_FLAGS}") +# Prebuilt FFmpeg (AJM's MP3 decoder), downloaded for the submodule's commit as in Kyty. +set(ARCHITECTURE x86_64) +add_subdirectory(3rdparty/ffmpeg-core) +# The system libraries its pkg-config files name, which ffmpeg-core adds only for MSVC. +target_link_libraries(ffmpeg INTERFACE bcrypt ole32 user32) set(FT_DISABLE_ZLIB ON CACHE BOOL "" FORCE) set(FT_DISABLE_BZIP2 ON CACHE BOOL "" FORCE) diff --git a/core/libs/prx/libSceAjm.native/CMakeLists.txt b/core/libs/prx/libSceAjm.native/CMakeLists.txt index 83bd7327e..6623b3914 100644 --- a/core/libs/prx/libSceAjm.native/CMakeLists.txt +++ b/core/libs/prx/libSceAjm.native/CMakeLists.txt @@ -27,5 +27,5 @@ target_include_directories(libSceAjm.native PRIVATE ${LIBS_INCLUDE_DIR} ${LIBATRAC9_DIR} ) -target_link_libraries(libSceAjm.native PRIVATE atrac9 libc libkernel) +target_link_libraries(libSceAjm.native PRIVATE atrac9 ffmpeg libc libkernel) configure_windows_unwind(libSceAjm.native) diff --git a/core/libs/prx/libSceAjm.native/Export.cpp b/core/libs/prx/libSceAjm.native/Export.cpp index c2150a64a..ff03fa3b4 100644 --- a/core/libs/prx/libSceAjm.native/Export.cpp +++ b/core/libs/prx/libSceAjm.native/Export.cpp @@ -33,18 +33,6 @@ int APS5_VABI sceAjmBatchJobControl(AjmBatchInfo* info, uint32_t instance, uint6 return 0; } -int APS5_VABI sceAjmBatchJobDecode(AjmBatchInfo* info, uint32_t instance, const void* bitstream_input, size_t bitstream_input_size, void* pcm_output, size_t pcm_output_size, void* result) { - (void)info; - (void)instance; - (void)bitstream_input; - (void)bitstream_input_size; - (void)pcm_output; - (void)pcm_output_size; - (void)result; - AjmStub(__func__); - return 0; -} - int APS5_VABI sceAjmBatchJobDecodeSingle(AjmBatchInfo* info, uint32_t instance, const void* bitstream_input, size_t bitstream_input_size, void* pcm_output, size_t pcm_output_size, void* result) { (void)info; (void)instance; @@ -114,20 +102,6 @@ int APS5_VABI sceAjmBatchJobGetResampleInfo(AjmBatchInfo* info, uint32_t instanc return 0; } -int APS5_VABI sceAjmBatchJobRun(AjmBatchInfo* info, uint32_t instance, uint64_t flags, const void* data_input, size_t data_input_size, void* data_output, size_t data_output_size, void* sideband_output, size_t sideband_output_size) { - (void)info; - (void)instance; - (void)flags; - (void)data_input; - (void)data_input_size; - (void)data_output; - (void)data_output_size; - (void)sideband_output; - (void)sideband_output_size; - AjmStub(__func__); - return 0; -} - int APS5_VABI sceAjmBatchJobSetResampleParameters(AjmBatchInfo* info, uint32_t instance, float ratio, uint32_t flags, void* result) { (void)info; (void)instance; diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 1bfc8f760..13c823926 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -3,14 +3,21 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "libatrac9.h" +extern "C" { +#include +#include +} #include #include +#include #include #include #include #include #include #include +#include +#include #include // The batch buffer and its job records are private to this library: the guest only reserves the @@ -29,6 +36,7 @@ constexpr std::int32_t AJM_RESULT_PARTIAL_INPUT = 0x00000008; constexpr std::int32_t AJM_RESULT_NOT_ENOUGH_ROOM = 0x00000010; constexpr std::int32_t AJM_RESULT_CODEC_ERROR = 0x40000000; +constexpr std::uint32_t CODEC_MP3 = 0; constexpr std::uint32_t CODEC_AT9 = 1; // APS5_TRACE_AJM=1 logs every job the title submits and what the library writes back. @@ -89,9 +97,15 @@ struct Instance { std::uint64_t totalDecodedSamples = 0; SidebandGaplessDecode gapless{}; bool flagsReported = false; + // MP3: the FFmpeg decoder (it keeps the bit reservoir between frames) and the last frame's shape. + AVCodecContext* mp3 = nullptr; + std::uint32_t mp3Channels = 0; + std::uint32_t mp3SampleRate = 0; + std::uint32_t mp3Bitrate = 0; ~Instance() { if (decoder) Atrac9ReleaseHandle(decoder); + if (mp3) avcodec_free_context(&mp3); } }; @@ -223,21 +237,99 @@ bool consumedFirstRun(Instance& instance) { return true; } -// Decodes AT9 frames from the concatenated inputs into the concatenated outputs in the instance's PCM -// encoding. -void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { +std::vector JoinInputs(const JobHeader& job, const AjmBuffer* inputs) { std::vector input; for (std::uint32_t index = 0; index < job.inputCount; ++index) { const auto* data = static_cast(inputs[index].ptr); input.insert(input.end(), data, data + inputs[index].size); } - std::size_t outputCapacity = 0; - for (std::uint32_t index = 0; index < job.outputCount; ++index) outputCapacity += outputs[index].size; + return input; +} + +// The job's output buffers, filled in order. +struct PcmOutputs { + const AjmBuffer* buffers; + std::uint32_t count; + std::size_t capacity = 0; + std::size_t produced = 0; + std::uint32_t index = 0; + std::size_t offset = 0; + + PcmOutputs(const AjmBuffer* buffers, std::uint32_t count) : buffers(buffers), count(count) { + for (std::uint32_t i = 0; i < count; ++i) capacity += buffers[i].size; + } + std::size_t Room() const { return capacity - produced; } + void Emit(const std::uint8_t* source, std::size_t bytes) { + while (bytes > 0 && index < count) { + const std::size_t chunk = std::min(buffers[index].size - offset, bytes); + std::memcpy(static_cast(buffers[index].ptr) + offset, source, chunk); + source += chunk; + bytes -= chunk; + produced += chunk; + offset += chunk; + if (offset == buffers[index].size) { + ++index; + offset = 0; + } + } + } +}; + +// Instance flags bits 7..9 select the PCM encoding: 0 16-bit, 1 32-bit integer, 2 float. +std::uint32_t PcmEncoding(const Instance& instance) { + return static_cast((instance.flags >> 7u) & 7u); +} + +// Emits one decoded frame of interleaved PCM after the instance's gapless skip and up to its sample limit. +void EmitFrame(Instance& instance, PcmOutputs& outputs, const std::uint8_t* pcm, std::size_t samples, std::size_t channels, std::size_t sampleBytes) { + std::size_t first = 0; + std::size_t count = samples; + if (instance.gapless.skippedSamples < instance.gapless.skipSamples) { + const std::size_t skip = std::min(count, instance.gapless.skipSamples - instance.gapless.skippedSamples); + instance.gapless.skippedSamples = static_cast(instance.gapless.skippedSamples + skip); + first += skip; + count -= skip; + } + if (instance.gapless.totalSamples != 0) { + const std::uint64_t remaining = instance.gapless.totalSamples > instance.totalDecodedSamples ? instance.gapless.totalSamples - instance.totalDecodedSamples : 0; + count = static_cast(std::min(count, remaining)); + } + outputs.Emit(pcm + first * channels * sampleBytes, count * channels * sampleBytes); + instance.totalDecodedSamples += count; +} + +// The sidebands a run job asked for, in the SDK's order after its result. +void WriteRunSideband(const JobHeader& job, const Instance& instance, std::int32_t result, std::size_t consumed, std::size_t produced, std::uint32_t frames, const SidebandFormat& format) { + auto* sideband = static_cast(job.sideband); + std::size_t offset = 0; + const auto write = [&](const void* value, std::size_t size) { + if (!sideband || offset + size > job.sidebandSize) return; + std::memcpy(sideband + offset, value, size); + offset += size; + }; + const SidebandResult status{result, 0}; + write(&status, sizeof(status)); + if (job.flags & SIDEBAND_STREAM) { + const SidebandStream stream{static_cast(consumed), static_cast(produced), instance.totalDecodedSamples}; + write(&stream, sizeof(stream)); + } + if (job.flags & SIDEBAND_FORMAT) write(&format, sizeof(format)); + if (job.flags & SIDEBAND_GAPLESS_DECODE) write(&instance.gapless, sizeof(instance.gapless)); + if (job.flags & RUN_MULTIPLE_FRAMES) { + const SidebandMultipleFrames multiple{frames, 0}; + write(&multiple, sizeof(multiple)); + } +} + +// Decodes AT9 frames from the concatenated inputs into the concatenated outputs in the instance's PCM +// encoding. +void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { + const auto input = JoinInputs(job, inputs); + PcmOutputs pcmOutputs(outputs, job.outputCount); const auto channels = static_cast(instance.info.channels); const auto frameSamples = static_cast(instance.info.frameSamples); - // Instance flags bits 7..9 select the PCM encoding: 0 16-bit, 1 32-bit integer, 2 float. - const auto encoding = static_cast((instance.flags >> 7u) & 7u); + const auto encoding = PcmEncoding(instance); const std::size_t sampleBytes = encoding == 0 ? sizeof(std::int16_t) : sizeof(std::int32_t); const std::size_t frameBytes = frameSamples * channels * sampleBytes; std::vector pcm(frameBytes); @@ -247,25 +339,7 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c std::int32_t result = 0; int decodeStatus = 0; std::size_t consumed = 0; - std::size_t produced = 0; std::uint32_t frames = 0; - std::uint32_t outputIndex = 0; - std::size_t outputOffset = 0; - const auto emit = [&](const std::uint8_t* source, std::size_t bytes) { - while (bytes > 0 && outputIndex < job.outputCount) { - const std::size_t room = outputs[outputIndex].size - outputOffset; - const std::size_t chunk = std::min(room, bytes); - std::memcpy(static_cast(outputs[outputIndex].ptr) + outputOffset, source, chunk); - source += chunk; - bytes -= chunk; - produced += chunk; - outputOffset += chunk; - if (outputOffset == outputs[outputIndex].size) { - ++outputIndex; - outputOffset = 0; - } - } - }; // A superframe is a fixed superframeSize bytes holding framesInSuperframe frames packed back to back, // each byte aligned but of its own size (the encoder moves bits between the frames of a superframe), @@ -282,7 +356,7 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c if (input.size() != consumed) result |= AJM_RESULT_PARTIAL_INPUT; break; } - if (outputCapacity - produced < frameBytes) { + if (pcmOutputs.Room() < frameBytes) { if (frames == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; break; } @@ -310,54 +384,137 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c } ++frames; - std::size_t first = 0; - std::size_t count = frameSamples; - if (instance.gapless.skippedSamples < instance.gapless.skipSamples) { - const std::size_t skip = std::min(count, instance.gapless.skipSamples - instance.gapless.skippedSamples); - instance.gapless.skippedSamples = static_cast(instance.gapless.skippedSamples + skip); - first += skip; - count -= skip; - } - if (instance.gapless.totalSamples != 0) { - const std::uint64_t remaining = instance.gapless.totalSamples > instance.totalDecodedSamples ? instance.gapless.totalSamples - instance.totalDecodedSamples : 0; - count = static_cast(std::min(count, remaining)); - } - emit(pcm.data() + first * channels * sampleBytes, count * channels * sampleBytes); - instance.totalDecodedSamples += count; + EmitFrame(instance, pcmOutputs, pcm.data(), frameSamples, channels, sampleBytes); if ((job.flags & RUN_MULTIPLE_FRAMES) == 0) break; } if (TraceEnabled()) { std::fprintf(stderr, "[ajm] instance %u run flags 0x%llx: %zu input bytes in %u buffers, %zu output bytes in %u buffers, sideband %llu bytes; superframe %d (%d frames), %d channels, %d samples/frame, %d Hz, config %02x %02x %02x %02x -> result 0x%x, decoder status 0x%x, %u frames, consumed %zu, produced %zu, total samples %llu, gapless total %u skip %u skipped %u, input", - job.instance, static_cast(job.flags), input.size(), job.inputCount, outputCapacity, job.outputCount, static_cast(job.sidebandSize), instance.info.superframeSize, instance.info.framesInSuperframe, instance.info.channels, instance.info.frameSamples, instance.info.samplingRate, - instance.info.configData[0], instance.info.configData[1], instance.info.configData[2], instance.info.configData[3], static_cast(result), static_cast(decodeStatus), frames, consumed, produced, static_cast(instance.totalDecodedSamples), instance.gapless.totalSamples, instance.gapless.skipSamples, instance.gapless.skippedSamples); + job.instance, static_cast(job.flags), input.size(), job.inputCount, pcmOutputs.capacity, job.outputCount, static_cast(job.sidebandSize), instance.info.superframeSize, instance.info.framesInSuperframe, instance.info.channels, instance.info.frameSamples, instance.info.samplingRate, + instance.info.configData[0], instance.info.configData[1], instance.info.configData[2], instance.info.configData[3], static_cast(result), static_cast(decodeStatus), frames, consumed, pcmOutputs.produced, static_cast(instance.totalDecodedSamples), instance.gapless.totalSamples, instance.gapless.skipSamples, instance.gapless.skippedSamples); for (std::size_t i = 0; i < input.size() && i < 16; ++i) std::fprintf(stderr, " %02x", input[i]); std::fprintf(stderr, "\n"); } - auto* sideband = static_cast(job.sideband); - std::size_t offset = 0; - const auto write = [&](const void* value, std::size_t size) { - if (!sideband || offset + size > job.sidebandSize) return; - std::memcpy(sideband + offset, value, size); - offset += size; - }; - const SidebandResult status{result, 0}; - write(&status, sizeof(status)); - if (job.flags & SIDEBAND_STREAM) { - const SidebandStream stream{static_cast(consumed), static_cast(produced), instance.totalDecodedSamples}; - write(&stream, sizeof(stream)); - } - if (job.flags & SIDEBAND_FORMAT) { - const SidebandFormat format{static_cast(channels), ChannelMask(channels), static_cast(instance.info.samplingRate), encoding, 0, 0}; - write(&format, sizeof(format)); - } - if (job.flags & SIDEBAND_GAPLESS_DECODE) write(&instance.gapless, sizeof(instance.gapless)); - if (job.flags & RUN_MULTIPLE_FRAMES) { - const SidebandMultipleFrames multiple{frames, 0}; - write(&multiple, sizeof(multiple)); - } + WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, SidebandFormat{static_cast(channels), ChannelMask(channels), static_cast(instance.info.samplingRate), encoding, 0, 0}); } +// An MPEG audio Layer III frame header: the frame's bytes and PCM shape; false when the bytes do not +// start one (free-format bitrates included). +struct Mp3Frame { + std::size_t bytes; + std::uint32_t channels; + std::uint32_t samples; + std::uint32_t sampleRate; + std::uint32_t bitrate; +}; + +bool ParseMp3Frame(const std::uint8_t* data, std::size_t size, Mp3Frame& frame) { + if (size < 4) return false; + const std::uint32_t header = (std::uint32_t{data[0]} << 24u) | (std::uint32_t{data[1]} << 16u) | (std::uint32_t{data[2]} << 8u) | data[3]; + const auto version = (header >> 19u) & 3u; + const auto layer = (header >> 17u) & 3u; + const auto bitrateIndex = (header >> 12u) & 15u; + const auto rateIndex = (header >> 10u) & 3u; + if ((header & 0xFFE00000u) != 0xFFE00000u || version == 1 || layer != 1 || bitrateIndex == 0 || bitrateIndex == 15 || rateIndex == 3) return false; + static constexpr std::uint32_t rates[4][3] = {{11025, 12000, 8000}, {0, 0, 0}, {22050, 24000, 16000}, {44100, 48000, 32000}}; + static constexpr std::uint32_t mpeg1Kbps[16] = {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0}; + static constexpr std::uint32_t mpeg2Kbps[16] = {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0}; + const bool mpeg1 = version == 3; + frame.sampleRate = rates[version][rateIndex]; + frame.bitrate = (mpeg1 ? mpeg1Kbps : mpeg2Kbps)[bitrateIndex] * 1000u; + frame.samples = mpeg1 ? 1152u : 576u; + frame.channels = ((header >> 6u) & 3u) == 3u ? 1u : 2u; + frame.bytes = (mpeg1 ? 144u : 72u) * frame.bitrate / frame.sampleRate + ((header >> 9u) & 1u); + return true; +} + +void OpenMp3(Instance& instance) { + static const bool quiet = (av_log_set_level(TraceEnabled() ? AV_LOG_WARNING : AV_LOG_QUIET), true); + (void)quiet; + const AVCodec* codec = avcodec_find_decoder(AV_CODEC_ID_MP3); + if (instance.mp3) avcodec_free_context(&instance.mp3); + instance.mp3 = codec ? avcodec_alloc_context3(codec) : nullptr; + if (!instance.mp3 || avcodec_open2(instance.mp3, codec, nullptr) < 0) throw std::runtime_error("AJM: cannot open the FFmpeg MP3 decoder"); +} + +// Decodes whole MP3 frames from the concatenated inputs. Like AT9 superframes, a frame is decoded only +// once all its bytes are present: a trailing partial frame stays unconsumed as partial input. +void RunMp3(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { + const auto input = JoinInputs(job, inputs); + PcmOutputs pcmOutputs(outputs, job.outputCount); + const auto encoding = PcmEncoding(instance); + if (encoding > 2) throw std::runtime_error("AJM MP3: PCM encoding " + std::to_string(encoding) + " is not implemented"); + const std::size_t sampleBytes = encoding == 0 ? sizeof(std::int16_t) : sizeof(std::int32_t); + std::unique_ptr packet(av_packet_alloc(), [](AVPacket* p) { av_packet_free(&p); }); + std::unique_ptr decoded(av_frame_alloc(), [](AVFrame* f) { av_frame_free(&f); }); + if (!packet || !decoded) throw std::bad_alloc(); + std::vector pcm; + + std::int32_t result = 0; + std::size_t consumed = 0; + std::uint32_t frames = 0; + while (consumed < input.size()) { + Mp3Frame frame{}; + if (!ParseMp3Frame(input.data() + consumed, input.size() - consumed, frame)) { + result |= AJM_RESULT_INVALID_DATA; + break; + } + if (input.size() - consumed < frame.bytes) { + result |= AJM_RESULT_PARTIAL_INPUT; + break; + } + if (pcmOutputs.Room() < frame.samples * frame.channels * sampleBytes) { + if (frames == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; + break; + } + // Not reference counted: the decoder copies the frame into a padded buffer of its own. + packet->data = const_cast(input.data() + consumed); + packet->size = static_cast(frame.bytes); + if (avcodec_send_packet(instance.mp3, packet.get()) < 0) { + result |= AJM_RESULT_INVALID_DATA; + break; + } + consumed += frame.bytes; + ++frames; + instance.mp3Channels = frame.channels; + instance.mp3SampleRate = frame.sampleRate; + instance.mp3Bitrate = frame.bitrate; + int status = 0; + while ((status = avcodec_receive_frame(instance.mp3, decoded.get())) == 0) { + if (decoded->format != AV_SAMPLE_FMT_FLTP) throw std::runtime_error("AJM MP3: FFmpeg sample format " + std::to_string(decoded->format) + " is not converted"); + const auto channels = static_cast(decoded->ch_layout.nb_channels); + const auto samples = static_cast(decoded->nb_samples); + pcm.resize(samples * channels * sampleBytes); + for (std::size_t sample = 0; sample < samples; ++sample) { + for (std::size_t channel = 0; channel < channels; ++channel) { + const float value = reinterpret_cast(decoded->extended_data[channel])[sample]; + auto* out = pcm.data() + (sample * channels + channel) * sampleBytes; + if (encoding == 0) { + const auto converted = static_cast(std::clamp(std::lrint(value * 32768.0), -32768L, 32767L)); + std::memcpy(out, &converted, sizeof(converted)); + } else if (encoding == 1) { + const auto converted = static_cast(std::clamp(std::llrint(value * 2147483648.0), -2147483648LL, 2147483647LL)); + std::memcpy(out, &converted, sizeof(converted)); + } else { + std::memcpy(out, &value, sizeof(value)); + } + } + } + EmitFrame(instance, pcmOutputs, pcm.data(), samples, channels, sampleBytes); + av_frame_unref(decoded.get()); + } + if (status != AVERROR(EAGAIN)) { + result |= AJM_RESULT_INVALID_DATA; + break; + } + if ((job.flags & RUN_MULTIPLE_FRAMES) == 0) break; + } + if (consumed == input.size() && frames == 0 && result == 0) result |= AJM_RESULT_PARTIAL_INPUT; + + AJM_TRACE("[ajm] instance %u mp3 run flags 0x%llx: %zu input bytes, %zu output bytes -> result 0x%x, %u frames, consumed %zu, produced %zu, %u channels, %u Hz, %u bps, total samples %llu\n", job.instance, static_cast(job.flags), input.size(), pcmOutputs.capacity, static_cast(result), frames, consumed, pcmOutputs.produced, + instance.mp3Channels, instance.mp3SampleRate, instance.mp3Bitrate, static_cast(instance.totalDecodedSamples)); + WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, SidebandFormat{instance.mp3Channels, ChannelMask(instance.mp3Channels), instance.mp3SampleRate, encoding, instance.mp3Bitrate, 0}); +} void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { if (job.kind == JobKind::GetStatistics) { AJM_TRACE("[ajm] statistics job: sideband %llu bytes\n", static_cast(job.sidebandSize)); @@ -383,6 +540,7 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out instance->totalDecodedSamples = 0; instance->gapless.skippedSamples = 0; if (instance->decoder && instance->initialized) ResetDecoder(*instance); + if (instance->mp3) avcodec_flush_buffers(instance->mp3); WriteResult(job.sideband, job.sidebandSize, 0); break; case JobKind::SetGaplessDecode: { @@ -401,9 +559,10 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out WriteResult(job.sideband, job.sidebandSize, AJM_RESULT_NOT_INITIALIZED); } else if (instance->codec == CODEC_AT9) { RunAt9(*instance, job, inputs, outputs); + } else if (instance->codec == CODEC_MP3) { + RunMp3(*instance, job, inputs, outputs); } else { - AJM_TRACE("[ajm] instance %u run on codec %u without a decoder\n", job.instance, instance->codec); - WriteResult(job.sideband, job.sidebandSize, AJM_RESULT_CODEC_ERROR); + throw std::runtime_error("AJM: decoding codec " + std::to_string(instance->codec) + " is not implemented"); } break; default: @@ -458,12 +617,14 @@ int APS5_VABI sceAjmMemoryUnregister(uint32_t context, void* ptr) { int APS5_VABI sceAjmInstanceCreate(uint32_t context, uint32_t codec, uint64_t flags, uint32_t* instance) { (void)context; if (!instance) return SCE_AJM_ERROR_INVALID_PARAMETER; - static std::atomic reportedCodecs{0}; - if (codec != CODEC_AT9 && codec < 32 && (reportedCodecs.fetch_or(1u << codec) & (1u << codec)) == 0) - std::fprintf(stderr, "[ajm] codec %u has no decoder; its jobs report codec errors\n", codec); auto created = std::make_unique(); created->codec = codec; created->flags = flags; + // MP3 has no codec parameters: the stream's frame headers carry them, so it runs without an initialize job. + if (codec == CODEC_MP3) { + OpenMp3(*created); + created->initialized = true; + } const std::uint32_t id = (codec << 14) | (g_nextInstance.fetch_add(1, std::memory_order_relaxed) & 0x3FFF); std::lock_guard lock(g_lock); g_instances[id] = std::move(created); @@ -532,6 +693,16 @@ int APS5_VABI sceAjmBatchJobRunSplit(AjmBatchInfo* info, uint32_t instance, uint return Append(info, header, input_buffers, output_buffers); } +int APS5_VABI sceAjmBatchJobRun(AjmBatchInfo* info, uint32_t instance, uint64_t flags, const void* data_input, size_t data_input_size, void* data_output, size_t data_output_size, void* sideband_output, size_t sideband_output_size) { + const AjmBuffer input{const_cast(data_input), data_input_size}; + const AjmBuffer output{data_output, data_output_size}; + return sceAjmBatchJobRunSplit(info, instance, flags, &input, 1, &output, 1, sideband_output, sideband_output_size); +} + +int APS5_VABI sceAjmBatchJobDecode(AjmBatchInfo* info, uint32_t instance, const void* bitstream_input, size_t bitstream_input_size, void* pcm_output, size_t pcm_output_size, void* result) { + return sceAjmBatchJobRun(info, instance, SIDEBAND_STREAM, bitstream_input, bitstream_input_size, pcm_output, pcm_output_size, result, sizeof(SidebandResult) + sizeof(SidebandStream)); +} + int APS5_VABI sceAjmBatchJobGetStatistics(AjmBatchInfo* info, float interval, void* result) { (void)interval; return Append(info, MakeHeader(JobKind::GetStatistics, 0, result, 24), nullptr, nullptr); diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index 16d7901f5..c1fccfd6e 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -1,3 +1,5 @@ +#include +#include #include #include #include @@ -329,6 +331,9 @@ int APS5_VABI sceAudioOutOpen(int userId, int type, int index, std::uint32_t len if (type != PORT_TYPE_VIBRATION) { openDevice(port); } + // APS5_TRACE_AUDIOOUT=1 reports the ports a title opens. + static const bool trace = std::getenv("APS5_TRACE_AUDIOOUT") != nullptr; + if (trace) std::fprintf(stderr, "[audioout] port %d: type %d, %u samples at %u Hz, format %d, device %u\n", i + 1, type, len, freq, static_cast(format), static_cast(port.device)); return i + 1; } } @@ -490,4 +495,9 @@ int APS5_VABI sceAudioOutGetPortState(int handle, AudioOutPortState* state) { return 0; } +int APS5_VABI sceAudioOutSetMixLevelPadSpk(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp index 611d3e726..86e9524cb 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp @@ -46,13 +46,13 @@ struct AudioOut2Port { std::uint32_t dataFormat = 0; std::uint32_t samplingFreq = 0; std::uint32_t flags = 0; - // Channel count decoded from dataFormat; 0 when the format is not understood (the port is then - // not rendered). Samples are float. + // Channel count and sample type decoded from dataFormat: float samples, or 16-bit integers. std::uint32_t channels = 0; - // The PCM buffer (one grain, float, interleaved) the title last handed over through the data - // attribute. It is guest memory the title rewrites every tick, so it is read when the context - // mixes, not when it is set. - const float* data = nullptr; + bool int16 = false; + // The PCM buffer (one grain, interleaved) the title last handed over through the data attribute. + // It is guest memory the title rewrites every tick, so it is read when the context mixes, not + // when it is set. + const void* data = nullptr; float volume[AUDIO_OUT2_PORT_CHANNELS_MAX] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; std::uint64_t dataSets = 0; std::uint64_t attributeTraces = 0; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp index 546ac704e..ea2f8a169 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp @@ -1,7 +1,10 @@ #include #include #include +#include #include +#include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -19,11 +22,12 @@ static constexpr std::uint64_t ATTRIBUTE_TRACE_EVERY = 2000; static constexpr std::uint32_t ATTRIBUTE_DATA = 0; static constexpr std::uint32_t ATTRIBUTE_VOLUME = 1; -// data_format bits 8..11 carry the channel count: the title opens 0x100 (mono object ports), -// 0x200 (stereo) and 0x880 (7.1 bed). The buffers behind them are float, as their spacing shows -// (1024 bytes per 256-sample mono grain). The low byte's meaning is not known. +// data_format bits 8..11 carry the channel count, bits 0..6 the sample type (0 float, 1 16-bit +// integer) and bit 7 the standard 8-channel order, as Kyty decodes it: the titles open 0x100 (mono +// object ports), 0x200 (stereo), 0x880 (7.1 bed) and 0x201 (a Bink movie's sound). static constexpr std::uint32_t FORMAT_CHANNELS_SHIFT = 8; static constexpr std::uint32_t FORMAT_CHANNELS_MASK = 0xFu; +static constexpr std::uint32_t FORMAT_TYPE_MASK = 0x7Fu; static std::mutex g_portsLock; // Grows on demand: the title opens its bed ports plus max_object_ports object ports at once. @@ -42,8 +46,12 @@ static constexpr float DOWNMIX_GAIN = 0.7071f; static void AccumulatePort(const AudioOut2Port& port, float* out, std::uint32_t frames) { const auto ch = port.channels; const float* volume = port.volume; + float in[AUDIO_OUT2_PORT_CHANNELS_MAX]; for (std::uint32_t frame = 0; frame < frames; frame++) { - const float* in = port.data + static_cast(frame) * ch; + const auto first = static_cast(frame) * ch; + for (std::uint32_t c = 0; c < ch; c++) { + in[c] = port.int16 ? static_cast(port.data)[first + c] / 32768.0f : static_cast(port.data)[first + c]; + } float left = 0.0f; float right = 0.0f; if (ch == 1) { @@ -116,7 +124,12 @@ int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut2 entry.samplingFreq = params->sampling_freq; entry.flags = params->flags; entry.channels = (params->data_format >> FORMAT_CHANNELS_SHIFT) & FORMAT_CHANNELS_MASK; - if (entry.channels > AUDIO_OUT2_PORT_CHANNELS_MAX) entry.channels = 0; + const auto sampleType = params->data_format & FORMAT_TYPE_MASK; + if (entry.channels == 0 || entry.channels > AUDIO_OUT2_PORT_CHANNELS_MAX || sampleType > 1) { + entry = AudioOut2Port{}; + throw std::runtime_error("sceAudioOut2PortCreate: data format 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%x", params->data_format); return std::string(text); }() + " is not implemented"); + } + entry.int16 = sampleType == 1; *port = static_cast(index) + 1; AUDIOOUT2_TRACE("t=%.3f PortCreate ctx=%llx -> port %llu: type=0x%x data_format=0x%x (%u float ch) sampling_freq=%u flags=0x%x user=%llx\n", AudioOut2TraceSeconds(), static_cast(ctx), static_cast(*port), params->port_type, params->data_format, diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp index 5f9850726..a8da0320c 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp @@ -1,30 +1,107 @@ +#include +#include #include #include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +// Layout of the params titles pass to sceAudioOut2SpeakerArrayCreate. +struct GuestVbapParams { + const AudioOut2Position* positions; + std::uint32_t numSpeakers; + std::uint8_t is3d; + void* memory; + std::size_t memorySize; + std::uint32_t reserved; +}; + +constexpr std::uint32_t MaxSpeakers = 32; +constexpr std::uint32_t AmbisonicsChannelFlag = 0x40; +constexpr std::uint32_t MaxAmbisonicsOrder = 5; + +// Lives in the memory the title allocates from sceAudioOut2GetSpeakerArrayMemorySize; the handle points at it. +struct SpeakerArray { + std::uint32_t magic; + std::uint32_t numSpeakers; + AudioOut2Position directions[MaxSpeakers]; +}; +constexpr std::uint32_t SpeakerArrayMagic = 0x53504b41; + +SpeakerArray& Array(AudioOut2SpeakerArrayHandle handle) { + auto* array = static_cast(handle); + if (array == nullptr || array->magic != SpeakerArrayMagic) throw std::invalid_argument("invalid speaker array handle"); + return *array; +} + +// Real SN3D spherical harmonic for ACN index `acn` (no Condon-Shortley phase), frame x front, y left, z up. +float Sn3d(std::uint32_t acn, const AudioOut2Position& direction) { + const int n = static_cast(std::sqrt(static_cast(acn))); + const int m = static_cast(acn) - n * n - n; + const int am = std::abs(m); + const double length = std::sqrt(double(direction.x) * direction.x + double(direction.y) * direction.y + double(direction.z) * direction.z); + const double sinEl = length > 0 ? direction.z / length : 0; + const double cosEl = std::sqrt(std::max(0.0, 1 - sinEl * sinEl)); + const double azimuth = std::atan2(direction.y, direction.x); + double legendre = 1; + for (int i = 1; i <= am; ++i) legendre *= (2 * i - 1) * cosEl; + if (n > am) { + double previous = legendre; + double current = sinEl * (2 * am + 1) * legendre; + for (int l = am + 2; l <= n; ++l) { + const double next = (sinEl * (2 * l - 1) * current - (l + am - 1) * previous) / (l - am); + previous = current; + current = next; + } + legendre = current; + } + double ratio = 1; + for (int i = n - am + 1; i <= n + am; ++i) ratio /= i; + const double norm = std::sqrt((m == 0 ? 1.0 : 2.0) * ratio); + return static_cast(norm * legendre * (m < 0 ? std::sin(am * azimuth) : std::cos(am * azimuth))); +} + +} + extern "C" { int APS5_VABI sceAudioOut2SpeakerArrayCreate(AudioOut2SpeakerArrayHandle* handle, const void* vbap_params, const void* ambi_params) { - (void)handle; - (void)vbap_params; - (void)ambi_params; - NotImplemented_nid_no_patch(__func__); + const auto* params = static_cast(vbap_params); + if (handle == nullptr || params == nullptr || ambi_params == nullptr || params->positions == nullptr || + params->numSpeakers == 0 || params->numSpeakers > MaxSpeakers || params->memory == nullptr || + params->memorySize < sizeof(SpeakerArray) || reinterpret_cast(params->memory) % alignof(SpeakerArray) != 0) { + APS5_INVALID_ARG_EX; + } + auto* array = new (params->memory) SpeakerArray{SpeakerArrayMagic, params->numSpeakers, {}}; + std::copy_n(params->positions, params->numSpeakers, array->directions); + *handle = array; return 0; } int APS5_VABI sceAudioOut2SpeakerArrayDestroy(AudioOut2SpeakerArrayHandle handle) { - (void)handle; - NotImplemented_nid_no_patch(__func__); + Array(handle).magic = 0; return 0; } +// Basic sampling decoder: speaker gain = sum over channels of Y(speaker) / numSpeakers, so a stereo pair decodes +// first order as two cardioids. Orders the layout cannot resolve (2D rule: 2N+1 speakers) get zero. int APS5_VABI sceAudioOut2GetSpeakerArrayAmbisonicsCoefficients(AudioOut2SpeakerArrayHandle handle, uint32_t ambisonics_channel, float* coefficients, uint32_t num_coefficients) { - (void)handle; - (void)ambisonics_channel; - (void)coefficients; - (void)num_coefficients; - NotImplemented_nid_no_patch(__func__); + const auto& array = Array(handle); + if (coefficients == nullptr || num_coefficients != array.numSpeakers) APS5_INVALID_ARG_EX; + // ponytail: only the 0x40-flagged encoding seen so far (assumed ACN/SN3D); other encodings throw until seen. + const std::uint32_t acn = ambisonics_channel & ~AmbisonicsChannelFlag; + if ((ambisonics_channel & AmbisonicsChannelFlag) == 0 || acn >= (MaxAmbisonicsOrder + 1) * (MaxAmbisonicsOrder + 1)) { + throw std::invalid_argument("unsupported ambisonics channel " + std::to_string(ambisonics_channel)); + } + const auto order = static_cast(std::sqrt(static_cast(acn))); + const std::uint32_t decodable = std::max(1, (array.numSpeakers - 1) / 2); + for (std::uint32_t speaker = 0; speaker < array.numSpeakers; ++speaker) { + coefficients[speaker] = order > decodable ? 0.0f : Sn3d(acn, array.directions[speaker]) / static_cast(array.numSpeakers); + } return 0; } @@ -41,11 +118,10 @@ int APS5_VABI sceAudioOut2GetSpeakerArrayCoefficients(AudioOut2SpeakerArrayHandl } size_t APS5_VABI sceAudioOut2GetSpeakerArrayMemorySize(uint32_t num_speakers, uint8_t is_3d, uint8_t is_ambisonics) { - (void)num_speakers; (void)is_3d; (void)is_ambisonics; - NotImplemented_nid_no_patch(__func__); - return 0; + if (num_speakers == 0 || num_speakers > MaxSpeakers) APS5_INVALID_ARG_EX; + return sizeof(SpeakerArray); } int APS5_VABI sceAudioOut2GetSpeakerInfo(AudioOut2SpeakerInfo* info, uint32_t flags) { diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index 91ae9bd80..44e6e9967 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -1,118 +1,213 @@ #include #include +#include +#include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +// A console with no headset and no voice peers: the library runs its lifecycle and keeps ports and +// their connections, but no voice ever arrives, so output ports stay empty. +namespace { + +constexpr std::int32_t PortInDevice = 0; +constexpr std::int32_t PortInPcm = 1; +constexpr std::int32_t PortInVoice = 2; +constexpr std::int32_t PortOutPcm = 3; +constexpr std::int32_t PortOutVoice = 4; +constexpr std::int32_t PortOutDevice = 5; +constexpr std::int32_t PortStateReady = 1; +constexpr std::uint32_t VoiceFrameMs = 20; + +struct Port { + std::int32_t type; + float volume; + std::uint32_t bitrate; +}; + +struct Voice { + std::mutex mutex; + bool initialized = false; + bool started = false; + std::uint32_t nextId = 0; + std::map ports; + std::set> connections; +}; + +Voice& State() { + static Voice voice; + return voice; +} + +[[noreturn]] void Fail(const char* function, const std::string& why) { + throw std::logic_error(std::string(function) + ": " + why); +} + +Port& RequirePort(Voice& voice, std::uint32_t id, const char* function) { + if (!voice.initialized) Fail(function, "library not initialized"); + const auto found = voice.ports.find(id); + if (found == voice.ports.end()) Fail(function, "unknown port " + std::to_string(id)); + return found->second; +} + +bool IsInput(std::int32_t type) { return type == PortInDevice || type == PortInPcm || type == PortInVoice; } + +} + extern "C" { -int APS5_VABI sceVoiceConnectIPortToOPort(uint32_t input_port_id, uint32_t output_port_id) { - (void)input_port_id; - (void)output_port_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceCreatePort(uint32_t* port_id, const VoicePortParam* param) { - (void)port_id; - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceDeletePort(uint32_t port_id) { - (void)port_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceDisconnectIPortFromOPort(uint32_t input_port_id, uint32_t output_port_id) { - (void)input_port_id; - (void)output_port_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceEnd_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceGetBitRate(uint32_t port_id, uint32_t* bitrate) { - (void)port_id; - (void)bitrate; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceGetPortAttr(uint32_t port_id, int32_t attr, void* value, int32_t size) { - (void)port_id; - (void)attr; - (void)value; - (void)size; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceGetPortInfo(uint32_t port_id, VoicePortInfo* info) { - (void)port_id; - (void)info; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceGetVolume(uint32_t port_id, float* volume) { - (void)port_id; - (void)volume; - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceVoiceInit(VoiceInitParam* param, int32_t version) { - (void)param; - (void)version; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceReadFromOPort(uint32_t output_port_id, void* data, uint32_t* size) { - (void)output_port_id; - (void)data; - (void)size; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)version; + if (param == nullptr) APS5_INVALID_ARG_EX; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (voice.initialized) Fail(__func__, "already initialized"); + voice.initialized = true; + return 0; } int APS5_VABI sceVoiceSetThreadsParams(void* params) { - (void)params; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceSetVolume(uint32_t port_id, float volume) { - (void)port_id; - (void)volume; - NotImplemented_nid_no_patch(__func__); - return 0; + // Thread priorities and affinities for the voice threads; this library runs none. + if (params == nullptr) APS5_INVALID_ARG_EX; + return 0; } int APS5_VABI sceVoiceStart(const VoiceStartParam* param) { - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; + if (param == nullptr) APS5_INVALID_ARG_EX; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) Fail(__func__, "library not initialized"); + voice.started = true; + return 0; } int APS5_VABI sceVoiceStop(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) Fail(__func__, "library not initialized"); + voice.started = false; + return 0; +} + +int APS5_VABI sceVoiceEnd_nid_postfix(void) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) Fail(__func__, "library not initialized"); + voice.ports.clear(); + voice.connections.clear(); + voice.started = false; + voice.initialized = false; + return 0; +} + +int APS5_VABI sceVoiceCreatePort(uint32_t* port_id, const VoicePortParam* param) { + if (port_id == nullptr || param == nullptr) APS5_INVALID_ARG_EX; + if (param->port_type < PortInDevice || param->port_type > PortOutDevice) Fail(__func__, "unknown port type " + std::to_string(param->port_type)); + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) Fail(__func__, "library not initialized"); + const bool voicePort = param->port_type == PortInVoice || param->port_type == PortOutVoice; + const auto id = voice.nextId++; + voice.ports.emplace(id, Port{param->port_type, param->volume, voicePort ? static_cast(param->voice.bitrate) : 0u}); + *port_id = id; + return 0; +} + +int APS5_VABI sceVoiceDeletePort(uint32_t port_id) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + RequirePort(voice, port_id, __func__); + std::erase_if(voice.connections, [port_id](const auto& link) { return link.first == port_id || link.second == port_id; }); + voice.ports.erase(port_id); + return 0; +} + +int APS5_VABI sceVoiceConnectIPortToOPort(uint32_t input_port_id, uint32_t output_port_id) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!IsInput(RequirePort(voice, input_port_id, __func__).type)) Fail(__func__, "first port is not an input port"); + if (IsInput(RequirePort(voice, output_port_id, __func__).type)) Fail(__func__, "second port is not an output port"); + voice.connections.emplace(input_port_id, output_port_id); + return 0; +} + +int APS5_VABI sceVoiceDisconnectIPortFromOPort(uint32_t input_port_id, uint32_t output_port_id) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + RequirePort(voice, input_port_id, __func__); + RequirePort(voice, output_port_id, __func__); + if (voice.connections.erase({input_port_id, output_port_id}) == 0) Fail(__func__, "ports are not connected"); + return 0; +} + +int APS5_VABI sceVoiceGetBitRate(uint32_t port_id, uint32_t* bitrate) { + if (bitrate == nullptr) APS5_INVALID_ARG_EX; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + const auto& port = RequirePort(voice, port_id, __func__); + if (port.type != PortInVoice && port.type != PortOutVoice) Fail(__func__, "port does not carry encoded voice"); + *bitrate = port.bitrate; + return 0; +} + +int APS5_VABI sceVoiceSetVolume(uint32_t port_id, float volume) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + RequirePort(voice, port_id, __func__).volume = volume; + return 0; +} + +int APS5_VABI sceVoiceGetVolume(uint32_t port_id, float* volume) { + if (volume == nullptr) APS5_INVALID_ARG_EX; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + *volume = RequirePort(voice, port_id, __func__).volume; + return 0; +} + +int APS5_VABI sceVoiceGetPortInfo(uint32_t port_id, VoicePortInfo* info) { + if (info == nullptr) APS5_INVALID_ARG_EX; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + const auto& port = RequirePort(voice, port_id, __func__); + *info = VoicePortInfo{}; + info->port_type = port.type; + info->state = PortStateReady; + // Titles pace voice by frame (a title divides by the frame length it derives from this). + // ponytail: the codec's real frame layout is unknown; 20 ms frames at the port's bitrate. + if (port.bitrate != 0) info->frame_size = (port.bitrate * VoiceFrameMs + 7999u) / 8000u; + return 0; +} + +int APS5_VABI sceVoiceGetPortAttr(uint32_t port_id, int32_t attr, void* value, int32_t size) { + (void)value; + (void)size; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + RequirePort(voice, port_id, __func__); + Fail(__func__, "attribute " + std::to_string(attr) + " not implemented"); +} + +int APS5_VABI sceVoiceReadFromOPort(uint32_t output_port_id, void* data, uint32_t* size) { + if (data == nullptr || size == nullptr) APS5_INVALID_ARG_EX; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (IsInput(RequirePort(voice, output_port_id, __func__).type)) Fail(__func__, "port is not an output port"); + // Inputs are the absent microphone or WriteToIPort, which is not implemented: nothing to read. + *size = 0; + return 0; } int APS5_VABI sceVoiceWriteToIPort(uint32_t input_port_id, const void* data, uint32_t* size, int16_t frame_gaps) { - (void)input_port_id; - (void)data; - (void)size; - (void)frame_gaps; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)input_port_id; + (void)data; + (void)size; + (void)frame_gaps; + NotImplemented_nid_no_patch(__func__); + return 0; } }