feat(audio): AJM MP3 through FFmpeg, 16-bit and ambisonic AudioOut2, voice

- AJM codec 0 (MP3) decodes with FFmpeg, prebuilt through the
  ffmpeg-core submodule as in Kyty; sceAjmBatchJobDecode and the Run job;
  the PCM encoding follows the job flags; other codecs throw
- AudioOut2 ports of 16-bit integer samples, and the ambisonics speaker
  array decode for the reported layout
- the voice library for a console with no headset, with a frame size for
  voice ports

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
STL1te 2026-09-29 07:54:05 +03:00
parent 2476bfda56
commit 028d465e98
11 changed files with 564 additions and 216 deletions

3
.gitmodules vendored
View File

@ -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

1
3rdparty/ffmpeg-core vendored Submodule

@ -0,0 +1 @@
Subproject commit 9ac4cfd195f192ed8b08566f49c28dbb48d08341

View File

@ -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)

View File

@ -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)

View File

@ -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;

View File

@ -3,14 +3,21 @@
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include "libatrac9.h"
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/log.h>
}
#include <algorithm>
#include <atomic>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <map>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <string>
#include <vector>
// 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<std::uint8_t> JoinInputs(const JobHeader& job, const AjmBuffer* inputs) {
std::vector<std::uint8_t> input;
for (std::uint32_t index = 0; index < job.inputCount; ++index) {
const auto* data = static_cast<const std::uint8_t*>(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<std::uint8_t*>(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<std::uint32_t>((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<std::size_t>(count, instance.gapless.skipSamples - instance.gapless.skippedSamples);
instance.gapless.skippedSamples = static_cast<std::uint16_t>(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::size_t>(std::min<std::uint64_t>(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<std::uint8_t*>(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<std::int32_t>(consumed), static_cast<std::int32_t>(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<std::size_t>(instance.info.channels);
const auto frameSamples = static_cast<std::size_t>(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<std::uint32_t>((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<std::uint8_t> 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<std::uint8_t*>(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<std::size_t>(count, instance.gapless.skipSamples - instance.gapless.skippedSamples);
instance.gapless.skippedSamples = static_cast<std::uint16_t>(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::size_t>(std::min<std::uint64_t>(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<unsigned long long>(job.flags), input.size(), job.inputCount, outputCapacity, job.outputCount, static_cast<unsigned long long>(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<unsigned>(result), static_cast<unsigned>(decodeStatus), frames, consumed, produced, static_cast<unsigned long long>(instance.totalDecodedSamples), instance.gapless.totalSamples, instance.gapless.skipSamples, instance.gapless.skippedSamples);
job.instance, static_cast<unsigned long long>(job.flags), input.size(), job.inputCount, pcmOutputs.capacity, job.outputCount, static_cast<unsigned long long>(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<unsigned>(result), static_cast<unsigned>(decodeStatus), frames, consumed, pcmOutputs.produced, static_cast<unsigned long long>(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<std::uint8_t*>(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<std::int32_t>(consumed), static_cast<std::int32_t>(produced), instance.totalDecodedSamples};
write(&stream, sizeof(stream));
}
if (job.flags & SIDEBAND_FORMAT) {
const SidebandFormat format{static_cast<std::uint32_t>(channels), ChannelMask(channels), static_cast<std::uint32_t>(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<std::uint32_t>(channels), ChannelMask(channels), static_cast<std::uint32_t>(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<AVPacket, void (*)(AVPacket*)> packet(av_packet_alloc(), [](AVPacket* p) { av_packet_free(&p); });
std::unique_ptr<AVFrame, void (*)(AVFrame*)> decoded(av_frame_alloc(), [](AVFrame* f) { av_frame_free(&f); });
if (!packet || !decoded) throw std::bad_alloc();
std::vector<std::uint8_t> 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<std::uint8_t*>(input.data() + consumed);
packet->size = static_cast<int>(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<std::size_t>(decoded->ch_layout.nb_channels);
const auto samples = static_cast<std::size_t>(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<const float*>(decoded->extended_data[channel])[sample];
auto* out = pcm.data() + (sample * channels + channel) * sampleBytes;
if (encoding == 0) {
const auto converted = static_cast<std::int16_t>(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::int32_t>(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<unsigned long long>(job.flags), input.size(), pcmOutputs.capacity, static_cast<unsigned>(result), frames, consumed, pcmOutputs.produced,
instance.mp3Channels, instance.mp3SampleRate, instance.mp3Bitrate, static_cast<unsigned long long>(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<unsigned long long>(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<std::uint32_t> 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<Instance>();
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<void*>(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);

View File

@ -1,3 +1,5 @@
#include <cstdlib>
#include <cstdio>
#include <algorithm>
#include <array>
#include <atomic>
@ -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<int>(format), static_cast<unsigned>(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;
}
}

View File

@ -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;

View File

@ -1,7 +1,10 @@
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <cstdio>
#include <mutex>
#include <stdexcept>
#include <string>
#include <vector>
#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<std::size_t>(frame) * ch;
const auto first = static_cast<std::size_t>(frame) * ch;
for (std::uint32_t c = 0; c < ch; c++) {
in[c] = port.int16 ? static_cast<const std::int16_t*>(port.data)[first + c] / 32768.0f : static_cast<const float*>(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<AudioOut2PortHandle>(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<unsigned long long>(ctx), static_cast<unsigned long long>(*port), params->port_type, params->data_format,

View File

@ -1,30 +1,107 @@
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <new>
#include <stdexcept>
#include <string>
#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<SpeakerArray*>(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<int>(std::sqrt(static_cast<double>(acn)));
const int m = static_cast<int>(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<float>(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<const GuestVbapParams*>(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<std::uintptr_t>(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::uint32_t>(std::sqrt(static_cast<double>(acn)));
const std::uint32_t decodable = std::max<std::uint32_t>(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<float>(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) {

View File

@ -1,118 +1,213 @@
#include <cstdint>
#include <cstddef>
#include <map>
#include <mutex>
#include <set>
#include <stdexcept>
#include <string>
#include <utility>
#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<std::uint32_t, Port> ports;
std::set<std::pair<std::uint32_t, std::uint32_t>> 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<std::uint32_t>(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;
}
}