From 72be77590c646b7057384c0afa5fc5e16c7eef48 Mon Sep 17 00:00:00 2001 From: raghav314 Date: Wed, 7 Oct 2026 04:55:18 +0530 Subject: [PATCH] feat(ajm): implement sceAjmBatchJobSetResampleParameters and sceAjmBatchJobGetResampleInfo Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAjm.native/Export.cpp | 18 -- core/libs/prx/libSceAjm.native/src/Ajm.cpp | 204 ++++++++++++++++--- core/libs/tests/GuestAjm.cpp | 216 +++++++++++++++++++++ 3 files changed, 390 insertions(+), 48 deletions(-) diff --git a/core/libs/prx/libSceAjm.native/Export.cpp b/core/libs/prx/libSceAjm.native/Export.cpp index 4664c379b..3585560a0 100644 --- a/core/libs/prx/libSceAjm.native/Export.cpp +++ b/core/libs/prx/libSceAjm.native/Export.cpp @@ -38,24 +38,6 @@ int APS5_VABI sceAjmBatchJobEncode(AjmBatchInfo* info, uint32_t instance, const return 0; } -int APS5_VABI sceAjmBatchJobGetResampleInfo(AjmBatchInfo* info, uint32_t instance, void* result) { - (void)info; - (void)instance; - (void)result; - AjmStub(__func__); - return 0; -} - -int APS5_VABI sceAjmBatchJobSetResampleParameters(AjmBatchInfo* info, uint32_t instance, float ratio, uint32_t flags, void* result) { - (void)info; - (void)instance; - (void)ratio; - (void)flags; - (void)result; - AjmStub(__func__); - return 0; -} - int APS5_VABI sceAjmBatchJobSetResampleParametersEx(AjmBatchInfo* info, uint32_t instance, float ratio_start, float ratio_change_per_sample, 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 1cd4f3f3e..bff995503 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -100,6 +100,18 @@ struct SidebandAt9CodecInfo { std::uint32_t frameSamples; }; +struct SidebandResampleInfo { + float ratio; + std::int32_t samples; + std::uint32_t reserved[8]; +}; + +struct Resampler { + float ratio = 1.0f; + std::vector frames; + double position = 1.0; +}; + struct Instance { std::uint32_t codec = 0; std::uint64_t flags = 0; @@ -119,6 +131,7 @@ struct Instance { std::uint32_t opusChannels = 0; std::uint32_t opusSampleRate = 0; std::vector opusPending; + Resampler resampler; ~Instance() { if (decoder) Atrac9ReleaseHandle(decoder); @@ -142,6 +155,8 @@ enum class JobKind : std::uint32_t { Run = 4, GetStatistics = 5, Control = 6, + SetResampleParameters = 7, + GetResampleInfo = 8, }; struct JobHeader { @@ -236,7 +251,13 @@ bool ResetDecoder(Instance& instance) { void OpenOpus(Instance& instance); +void ClearResampler(Instance& instance) { + instance.resampler.frames.clear(); + instance.resampler.position = 1.0; +} + std::int32_t InitializeInstance(Instance& instance, const std::uint8_t* parameters, std::uint64_t size) { + ClearResampler(instance); if (instance.codec == CODEC_OPUS) { if (size < 8) return AJM_RESULT_INVALID_PARAMETER; std::uint32_t channels = 0; @@ -336,6 +357,82 @@ std::uint32_t PcmEncoding(const Instance& instance) { return static_cast((instance.flags >> 7u) & 7u); } +double PcmValue(const std::uint8_t* at, std::uint32_t encoding) { + if (encoding == 0) { + std::int16_t value = 0; + std::memcpy(&value, at, sizeof(value)); + return value / 32768.0; + } + if (encoding == 1) { + std::int32_t value = 0; + std::memcpy(&value, at, sizeof(value)); + return value / 2147483648.0; + } + float value = 0; + std::memcpy(&value, at, sizeof(value)); + return value; +} + +void StorePcm(double value, std::uint8_t* out, std::uint32_t encoding) { + 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 { + const auto converted = static_cast(value); + std::memcpy(out, &converted, sizeof(converted)); + } +} + +bool Resampling(const Instance& instance) { + return instance.resampler.ratio != 1.0f || !instance.resampler.frames.empty(); +} + +std::size_t ResamplerHeld(const Instance& instance, std::size_t channels) { + const auto& resampler = instance.resampler; + if (resampler.frames.empty() || channels == 0) return 0; + const std::size_t frames = resampler.frames.size() / channels; + const auto next = static_cast(resampler.position); + return frames > next ? frames - next : 0; +} + +void ResamplerPush(Instance& instance, const double* samples, std::size_t frames, std::size_t channels) { + auto& stored = instance.resampler.frames; + if (frames == 0) return; + if (stored.empty()) stored.insert(stored.end(), samples, samples + channels); + stored.insert(stored.end(), samples, samples + frames * channels); +} + +void ResamplerProduce(Instance& instance, PcmOutputs& outputs, std::size_t channels, std::uint32_t encoding, std::size_t sampleBytes) { + auto& resampler = instance.resampler; + if (resampler.frames.empty() || channels == 0) return; + const std::size_t frameBytes = channels * sampleBytes; + const std::size_t available = resampler.frames.size() / channels; + std::vector frame(frameBytes); + while (outputs.Room() >= frameBytes) { + const auto index = static_cast(resampler.position); + if (index + 2 >= available) break; + const double t = resampler.position - static_cast(index); + for (std::size_t channel = 0; channel < channels; ++channel) { + const double p0 = resampler.frames[(index - 1) * channels + channel]; + const double p1 = resampler.frames[index * channels + channel]; + const double p2 = resampler.frames[(index + 1) * channels + channel]; + const double p3 = resampler.frames[(index + 2) * channels + channel]; + const double value = p1 + 0.5 * t * (p2 - p0 + t * (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3 + t * (3.0 * (p1 - p2) + p3 - p0))); + StorePcm(value, frame.data() + channel * sampleBytes, encoding); + } + outputs.Emit(frame.data(), frameBytes); + resampler.position += resampler.ratio; + } + const auto index = std::min(static_cast(resampler.position), available); + if (index > 1) { + resampler.frames.erase(resampler.frames.begin(), resampler.frames.begin() + static_cast((index - 1) * channels)); + resampler.position -= static_cast(index - 1); + } +} + 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; @@ -349,8 +446,16 @@ void EmitFrame(Instance& instance, PcmOutputs& outputs, const std::uint8_t* pcm, 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; + if (!Resampling(instance)) { + outputs.Emit(pcm + first * channels * sampleBytes, count * channels * sampleBytes); + return; + } + const auto encoding = PcmEncoding(instance); + std::vector values(count * channels); + for (std::size_t index = 0; index < values.size(); ++index) values[index] = PcmValue(pcm + (first * channels + index) * sampleBytes, encoding); + ResamplerPush(instance, values.data(), count, channels); + ResamplerProduce(instance, outputs, channels, encoding, sampleBytes); } SidebandFormat CurrentFormat(const Instance& instance) { @@ -419,6 +524,7 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c // reported as partial input, and the title resubmits it from the consumed offset. const auto superframeSize = static_cast(instance.info.superframeSize); const auto framesInSuperframe = static_cast(std::max(1, instance.info.framesInSuperframe)); + if (Resampling(instance)) ResamplerProduce(instance, pcmOutputs, channels, encoding, sampleBytes); for (;;) { if (instance.superframeRemaining == 0) consumed += RiffDataOffset(input.data() + consumed, input.size() - consumed); const std::size_t needed = instance.superframeRemaining == 0 ? superframeSize : instance.superframeRemaining; @@ -426,8 +532,8 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c if (input.size() != consumed) result |= AJM_RESULT_PARTIAL_INPUT; break; } - if (pcmOutputs.Room() < frameBytes) { - if (frames == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; + if (Resampling(instance) ? pcmOutputs.Room() < channels * sampleBytes : pcmOutputs.Room() < frameBytes) { + if (frames == 0 && pcmOutputs.produced == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; break; } if (instance.superframeRemaining == 0) { @@ -647,6 +753,7 @@ void RunMp3(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c std::int32_t result = 0; std::size_t consumed = 0; std::uint32_t frames = 0; + if (Resampling(instance)) ResamplerProduce(instance, pcmOutputs, instance.mp3Channels, encoding, sampleBytes); while (consumed < input.size()) { Mp3Frame frame{}; if (!ParseMp3Frame(input.data() + consumed, input.size() - consumed, frame)) { @@ -657,8 +764,8 @@ void RunMp3(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c result |= AJM_RESULT_PARTIAL_INPUT; break; } - if (pcmOutputs.Room() < frame.samples * frame.channels * sampleBytes) { - if (frames == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; + if (pcmOutputs.Room() < (Resampling(instance) ? 1 : frame.samples) * frame.channels * sampleBytes) { + if (frames == 0 && pcmOutputs.produced == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; break; } packet->data = const_cast(input.data() + consumed); @@ -704,7 +811,7 @@ void RunMp3(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c } if ((job.flags & RUN_MULTIPLE_FRAMES) == 0) break; } - if (consumed == input.size() && frames == 0 && result == 0) result |= AJM_RESULT_PARTIAL_INPUT; + if (consumed == input.size() && frames == 0 && pcmOutputs.produced == 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)); @@ -757,12 +864,19 @@ void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, std::int32_t result = 0; std::size_t consumed = 0; std::uint32_t frames = 0; + const std::size_t frameBytes = channels * sampleBytes; + if (Resampling(instance) && !instance.opusPending.empty()) { + std::vector values(instance.opusPending.size() / sampleBytes); + for (std::size_t index = 0; index < values.size(); ++index) values[index] = PcmValue(instance.opusPending.data() + index * sampleBytes, encoding); + ResamplerPush(instance, values.data(), values.size() / channels, channels); + instance.opusPending.clear(); + } + if (Resampling(instance)) ResamplerProduce(instance, pcmOutputs, channels, encoding, sampleBytes); if (!instance.opusPending.empty()) { const std::size_t bytes = std::min(instance.opusPending.size(), pcmOutputs.Room()); pcmOutputs.Emit(instance.opusPending.data(), bytes); instance.opusPending.erase(instance.opusPending.begin(), instance.opusPending.begin() + static_cast(bytes)); } - const std::size_t frameBytes = channels * sampleBytes; while (consumed < input.size() && instance.opusPending.empty()) { if (input.size() - consumed < 2) { result |= AJM_RESULT_PARTIAL_INPUT; @@ -797,24 +911,11 @@ void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, if (format != AV_SAMPLE_FMT_FLTP && format != AV_SAMPLE_FMT_FLT) throw std::runtime_error("AJM Opus: FFmpeg sample format " + std::to_string(decoded->format) + " is not converted"); const auto frameChannels = static_cast(decoded->ch_layout.nb_channels); const auto frameSamples = static_cast(decoded->nb_samples); - pcm.assign(frameSamples * channels * sampleBytes, 0); - for (std::size_t sample = 0; sample < frameSamples; ++sample) { - for (std::size_t channel = 0; channel < channels; ++channel) { - const std::size_t sourceChannel = std::min(channel, frameChannels - 1); - const float value = format == AV_SAMPLE_FMT_FLTP ? reinterpret_cast(decoded->extended_data[sourceChannel])[sample] - : reinterpret_cast(decoded->data[0])[sample * frameChannels + sourceChannel]; - 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)); - } - } - } + const auto value = [&](std::size_t sample, std::size_t channel) { + const std::size_t sourceChannel = std::min(channel, frameChannels - 1); + return format == AV_SAMPLE_FMT_FLTP ? reinterpret_cast(decoded->extended_data[sourceChannel])[sample] + : reinterpret_cast(decoded->data[0])[sample * frameChannels + sourceChannel]; + }; std::size_t first = 0; std::size_t count = frameSamples; if (instance.gapless.skippedSamples < instance.gapless.skipSamples) { @@ -827,12 +928,23 @@ void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const std::uint64_t remaining = instance.gapless.totalSamples > instance.totalDecodedSamples ? instance.gapless.totalSamples - instance.totalDecodedSamples : 0; count = static_cast(std::min(count, remaining)); } - const auto* start = pcm.data() + first * frameBytes; - const std::size_t total = count * frameBytes; - const std::size_t fits = std::min(total, pcmOutputs.Room() / frameBytes * frameBytes); - pcmOutputs.Emit(start, fits); - instance.opusPending.insert(instance.opusPending.end(), start + fits, start + total); instance.totalDecodedSamples += count; + if (Resampling(instance)) { + std::vector values(count * channels); + for (std::size_t sample = 0; sample < count; ++sample) { + for (std::size_t channel = 0; channel < channels; ++channel) values[sample * channels + channel] = value(first + sample, channel); + } + ResamplerPush(instance, values.data(), count, channels); + ResamplerProduce(instance, pcmOutputs, channels, encoding, sampleBytes); + } else { + pcm.assign(count * frameBytes, 0); + for (std::size_t sample = 0; sample < count; ++sample) { + for (std::size_t channel = 0; channel < channels; ++channel) StorePcm(value(first + sample, channel), pcm.data() + (sample * channels + channel) * sampleBytes, encoding); + } + const std::size_t fits = std::min(pcm.size(), pcmOutputs.Room() / frameBytes * frameBytes); + pcmOutputs.Emit(pcm.data(), fits); + instance.opusPending.insert(instance.opusPending.end(), pcm.begin() + static_cast(fits), pcm.end()); + } av_frame_unref(decoded.get()); } if (status != AVERROR(EAGAIN)) { @@ -854,6 +966,7 @@ void ClearContext(Instance& instance) { if (instance.mp3) avcodec_flush_buffers(instance.mp3); if (instance.opus) avcodec_flush_buffers(instance.opus); instance.opusPending.clear(); + ClearResampler(instance); } std::size_t ControlInitializeSize(std::uint32_t codec) { @@ -884,6 +997,13 @@ void Control(Instance& instance, const JobHeader& job, const AjmBuffer* inputs) WriteResult(job.sideband, job.sidebandSize, result); } +std::size_t HeldSamples(const Instance& instance) { + const std::size_t channels = instance.codec == CODEC_OPUS ? instance.opusChannels : instance.codec == CODEC_MP3 ? instance.mp3Channels : static_cast(std::max(0, instance.info.channels)); + if (Resampling(instance)) return ResamplerHeld(instance, channels); + const std::size_t sampleBytes = PcmEncoding(instance) == 0 ? sizeof(std::int16_t) : sizeof(std::int32_t); + return channels == 0 ? 0 : instance.opusPending.size() / (channels * sampleBytes); +} + 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)); @@ -922,6 +1042,17 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out case JobKind::Control: Control(*instance, job, inputs); break; + case JobKind::SetResampleParameters: + std::memcpy(&instance->resampler.ratio, job.parameters, sizeof(instance->resampler.ratio)); + AJM_TRACE("[ajm] instance %u set resample ratio %f\n", job.instance, static_cast(instance->resampler.ratio)); + WriteResult(job.sideband, job.sidebandSize, 0); + break; + case JobKind::GetResampleInfo: { + const SidebandResampleInfo resample{instance->resampler.ratio, static_cast(HeldSamples(*instance)), {}}; + WriteResult(job.sideband, job.sidebandSize, 0); + if (job.sideband && job.sidebandSize >= sizeof(SidebandResult) + sizeof(resample)) std::memcpy(static_cast(job.sideband) + sizeof(SidebandResult), &resample, sizeof(resample)); + break; + } case JobKind::Run: if (!instance->initialized) { AJM_TRACE("[ajm] instance %u run before initialize\n", job.instance); @@ -1094,6 +1225,19 @@ int APS5_VABI sceAjmBatchJobRun(AjmBatchInfo* info, uint32_t instance, uint64_t return sceAjmBatchJobRunSplit(info, instance, flags, &input, 1, &output, 1, sideband_output, sideband_output_size); } +int APS5_VABI sceAjmBatchJobSetResampleParameters(AjmBatchInfo* info, uint32_t instance, float ratio, uint32_t flags, void* result) { + (void)flags; + if (!std::isfinite(ratio) || ratio <= 0.0f) return SCE_AJM_ERROR_INVALID_PARAMETER; + auto header = MakeHeader(JobKind::SetResampleParameters, instance, result, sizeof(SidebandResult)); + std::memcpy(header.parameters, &ratio, sizeof(ratio)); + header.parameterSize = sizeof(ratio); + return Append(info, header, nullptr, nullptr); +} + +int APS5_VABI sceAjmBatchJobGetResampleInfo(AjmBatchInfo* info, uint32_t instance, void* result) { + return Append(info, MakeHeader(JobKind::GetResampleInfo, instance, result, sizeof(SidebandResult) + sizeof(SidebandResampleInfo)), nullptr, nullptr); +} + 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)); } diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 6d0a6593b..b2878b8a9 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -28,6 +28,9 @@ int APS5_VABI sceAjmBatchJobGetInfo(AjmBatchInfo*, std::uint32_t, void*); int APS5_VABI sceAjmBatchStart(std::uint32_t, const AjmBatchInfo*, int, AjmBatchError*, std::uint32_t*); int APS5_VABI sceAjmBatchWait(std::uint32_t, std::uint32_t, std::uint32_t, AjmBatchError*); int APS5_VABI sceAjmBatchCancel(std::uint32_t, std::uint32_t); +int APS5_VABI sceAjmBatchJobClearContext(AjmBatchInfo*, std::uint32_t, void*); +int APS5_VABI sceAjmBatchJobSetResampleParameters(AjmBatchInfo*, std::uint32_t, float, std::uint32_t, void*); +int APS5_VABI sceAjmBatchJobGetResampleInfo(AjmBatchInfo*, std::uint32_t, void*); } static void Require(bool value) { if (!value) std::abort(); } @@ -663,6 +666,216 @@ void TestControlOpus(std::uint32_t context) { Require(sceAjmInstanceDestroy(context, instance) == 0); } +struct ResampleInfo { + std::int32_t result; + std::int32_t internalResult; + float ratio; + std::int32_t samples; + std::uint32_t reserved[8]; +}; +static_assert(sizeof(ResampleInfo) == 48); + +std::uint32_t CreateOpus(std::uint32_t context) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); + const std::uint32_t parameters[3] = {2, 48000, 0}; + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t result[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, parameters, sizeof(parameters), result) == 0); + Submit(context, info); + Require(result[0] == 0); + return instance; +} + +ResampleInfo GetResampleInfo(std::uint32_t context, std::uint32_t instance) { + ResampleInfo resample; + std::memset(&resample, 0xAA, sizeof(resample)); + std::vector batch(4096); + AjmBatchInfo info{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobGetResampleInfo(&info, instance, &resample) == 0); + Submit(context, info); + return resample; +} + +struct ResampledStream { + std::vector pcm; + std::size_t jobs = 0; + std::size_t shortJobs = 0; + ResampleInfo afterFirstJob{}; + ResampleInfo atEnd{}; +}; + +ResampledStream Stream(std::uint32_t context, std::uint32_t instance, const std::uint8_t* stream, std::size_t size, std::size_t channels, std::size_t frames, float ratio) { + ResampledStream out; + std::vector pcm(frames * channels); + std::size_t offset = 0; + for (int guard = 0; guard < 4096; ++guard) { + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t setResult[2] = {-1, -1}; + DecodeSideband sideband{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + if (ratio > 0) Require(sceAjmBatchJobSetResampleParameters(&info, instance, ratio, 1, setResult) == 0); + Require(sceAjmBatchJobDecode(&info, instance, stream + offset, size - offset, pcm.data(), pcm.size() * sizeof(std::int16_t), &sideband) == 0); + Submit(context, info); + if (ratio > 0) Require(setResult[0] == 0); + Require(sideband.result == 0 || (offset == size && sideband.outputWritten == 0)); + offset += static_cast(sideband.inputConsumed); + if (offset < size && static_cast(sideband.outputWritten) < pcm.size() * sizeof(std::int16_t)) ++out.shortJobs; + out.pcm.insert(out.pcm.end(), pcm.begin(), pcm.begin() + sideband.outputWritten / static_cast(sizeof(std::int16_t))); + if (++out.jobs == 1) out.afterFirstJob = GetResampleInfo(context, instance); + if (offset == size && sideband.outputWritten == 0) break; + } + Require(offset == size); + out.atEnd = GetResampleInfo(context, instance); + return out; +} + +void RequireDecimated(const std::vector& reference, const std::vector& resampled, std::size_t channels, std::size_t step) { + const std::size_t frames = reference.size() / channels; + Require(resampled.size() / channels == (frames - 3) / step + 1); + for (std::size_t frame = 0; frame < resampled.size() / channels; ++frame) { + for (std::size_t channel = 0; channel < channels; ++channel) Require(resampled[frame * channels + channel] == reference[frame * step * channels + channel]); + } +} + +void RequireInterpolated(const std::vector& reference, const std::vector& resampled, std::size_t channels) { + const std::size_t frames = reference.size() / channels; + Require(resampled.size() / channels == 2 * frames - 4); + const auto at = [&](std::ptrdiff_t frame, std::size_t channel) { return static_cast(reference[static_cast(std::max(frame, 0)) * channels + channel]); }; + for (std::size_t frame = 0; frame < resampled.size() / channels; ++frame) { + for (std::size_t channel = 0; channel < channels; ++channel) { + const auto source = static_cast(frame / 2); + const double midpoint = (-at(source - 1, channel) + 9.0 * at(source, channel) + 9.0 * at(source + 1, channel) - at(source + 2, channel)) / 16.0; + if (frame % 2 == 0) Require(resampled[frame * channels + channel] == reference[frame / 2 * channels + channel]); + else Require(std::fabs(resampled[frame * channels + channel] - midpoint) <= 1.5); + } + } +} + +void TestResampleOpus(std::uint32_t context) { + const std::uint32_t plain = CreateOpus(context); + const auto reference = Stream(context, plain, OPUS_STEREO, sizeof(OPUS_STEREO), 2, 960, 0); + Require(reference.pcm.size() / 2 >= 960 * 4); + + const std::uint32_t unity = CreateOpus(context); + const auto passthrough = Stream(context, unity, OPUS_STEREO, sizeof(OPUS_STEREO), 2, 512, 1.0f); + Require(passthrough.pcm == reference.pcm); + Require(passthrough.afterFirstJob.result == 0 && passthrough.afterFirstJob.internalResult == 0 && passthrough.afterFirstJob.ratio == 1.0f && passthrough.afterFirstJob.samples == 960 - 512); + for (const auto word : passthrough.afterFirstJob.reserved) Require(word == 0); + Require(passthrough.atEnd.samples == 0); + + const std::uint32_t faster = CreateOpus(context); + const auto decimated = Stream(context, faster, OPUS_STEREO, sizeof(OPUS_STEREO), 2, 512, 2.0f); + RequireDecimated(reference.pcm, decimated.pcm, 2, 2); + Require(decimated.afterFirstJob.ratio == 2.0f && decimated.afterFirstJob.samples == 2); + Require(decimated.atEnd.result == 0 && decimated.atEnd.samples >= 0 && decimated.atEnd.samples <= 2); + + const std::uint32_t slower = CreateOpus(context); + const auto interpolated = Stream(context, slower, OPUS_STEREO, sizeof(OPUS_STEREO), 2, 512, 0.5f); + RequireInterpolated(reference.pcm, interpolated.pcm, 2); + Require(interpolated.shortJobs == 0 && passthrough.shortJobs == 0); + Require(interpolated.afterFirstJob.ratio == 0.5f && interpolated.afterFirstJob.samples == 960 - 256); + Require(interpolated.atEnd.samples >= 0 && interpolated.atEnd.samples <= 2); + + const std::uint32_t hades = CreateOpus(context); + const auto pitched = Stream(context, hades, OPUS_STEREO, sizeof(OPUS_STEREO), 2, 512, 0.890899f); + const double expected = static_cast(reference.pcm.size() / 2 - 2) / 0.890899; + Require(std::fabs(static_cast(pitched.pcm.size() / 2) - expected) <= 2.0); + + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t result[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + for (const float bad : {0.0f, -1.0f, std::nanf(""), INFINITY}) Require(sceAjmBatchJobSetResampleParameters(&info, slower, bad, 1, result) == static_cast(0x80930005)); + Require(info.offset == 0); + + const std::size_t firstPacket = 2 + (OPUS_STEREO[0] | (std::size_t{OPUS_STEREO[1]} << 8u)); + const auto clear = [&] { + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobClearContext(&info, slower, result) == 0); + Submit(context, info); + Require(result[0] == 0 && GetResampleInfo(context, slower).samples == 0); + }; + clear(); + const auto before = Stream(context, slower, OPUS_STEREO, firstPacket, 2, 512, 0.5f); + Require(before.pcm.size() / 2 == 2 * 960 - 4); + clear(); + const auto after = Stream(context, slower, OPUS_STEREO, firstPacket, 2, 512, 0.5f); + Require(after.pcm == before.pcm); + + const std::uint32_t switched = CreateOpus(context); + const auto head = Stream(context, switched, OPUS_STEREO, 2 + (OPUS_STEREO[0] | (std::size_t{OPUS_STEREO[1]} << 8u)), 2, 512, 1.0f); + Require(head.jobs >= 1 && head.pcm.size() == 960 * 2); + const std::uint32_t resumed = CreateOpus(context); + std::vector pcm(512 * 2); + DecodeSideband first{}; + RunDecode(context, resumed, OPUS_STEREO, sizeof(OPUS_STEREO), pcm.data(), pcm.size() * sizeof(std::int16_t), first); + Require(first.result == 0 && first.outputWritten == 512 * 2 * 2 && GetResampleInfo(context, resumed).samples == 960 - 512); + std::vector rest(pcm.begin(), pcm.end()); + const auto tail = Stream(context, resumed, OPUS_STEREO + first.inputConsumed, sizeof(OPUS_STEREO) - static_cast(first.inputConsumed), 2, 512, 2.0f); + const std::vector later(reference.pcm.begin() + 512 * 2, reference.pcm.end()); + RequireDecimated(later, tail.pcm, 2, 2); + Require(std::equal(rest.begin(), rest.end(), reference.pcm.begin())); + + const auto unknown = GetResampleInfo(context, 0x3FFF); + Require(unknown.result == 4); + + for (const std::uint32_t instance : {plain, unity, faster, slower, hades, switched, resumed}) Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +void TestResampleMp3(std::uint32_t context) { + std::uint32_t plain = 0; + std::uint32_t faster = 0; + std::uint32_t slower = 0; + Require(sceAjmInstanceCreate(context, 0, 0, &plain) == 0 && sceAjmInstanceCreate(context, 0, 0, &faster) == 0 && sceAjmInstanceCreate(context, 0, 0, &slower) == 0); + const auto reference = Stream(context, plain, MP3_MONO, sizeof(MP3_MONO), 1, 1152, 0); + Require(reference.pcm.size() == 1152 * 6); + RequireDecimated(reference.pcm, Stream(context, faster, MP3_MONO, sizeof(MP3_MONO), 1, 512, 2.0f).pcm, 1, 2); + const auto interpolated = Stream(context, slower, MP3_MONO, sizeof(MP3_MONO), 1, 512, 0.5f); + RequireInterpolated(reference.pcm, interpolated.pcm, 1); + Require(interpolated.shortJobs == 0); + for (const std::uint32_t instance : {plain, faster, slower}) Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +const std::uint8_t AT9_MONO_SILENT_SUPERFRAME[] = { + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +void TestResampleAt9(std::uint32_t context) { + const auto create = [&] { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0); + const std::uint8_t config[8] = {0xFE, 0x70, 0x1F, 0xF0}; + std::vector batch(4096); + AjmBatchInfo info{}; + std::int32_t result[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, config, sizeof(config), result) == 0); + Submit(context, info); + Require(result[0] == 0); + return instance; + }; + std::vector stream(1024 * 3, 0); + for (std::size_t superframe = 0; superframe < 3; ++superframe) std::memcpy(stream.data() + superframe * 1024, AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); + const std::uint32_t plain = create(); + const std::uint32_t faster = create(); + const std::uint32_t slower = create(); + const auto reference = Stream(context, plain, stream.data(), stream.size(), 1, 256, 0); + Require(reference.pcm.size() == 3 * 4 * 256); + const auto decimated = Stream(context, faster, stream.data(), stream.size(), 1, 512, 2.0f); + RequireDecimated(reference.pcm, decimated.pcm, 1, 2); + Require(decimated.atEnd.samples == 2); + const auto interpolated = Stream(context, slower, stream.data(), stream.size(), 1, 256, 0.5f); + RequireInterpolated(reference.pcm, interpolated.pcm, 1); + Require(interpolated.shortJobs == 0 && interpolated.afterFirstJob.samples == 256 - 256 / 2 && interpolated.atEnd.samples == 2); + for (const std::uint32_t instance : {plain, faster, slower}) Require(sceAjmInstanceDestroy(context, instance) == 0); +} + } int main() { @@ -692,5 +905,8 @@ int main() { TestControlAt9(context); TestControlMp3(context); TestControlOpus(context); + TestResampleOpus(context); + TestResampleMp3(context); + TestResampleAt9(context); Require(sceAjmFinalize(context) == 0); }