feat(ajm): implement sceAjmBatchJobSetResampleParameters and sceAjmBatchJobGetResampleInfo

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
raghav314 2026-10-07 04:55:18 +05:30
parent 4b0412fa97
commit 72be77590c
3 changed files with 390 additions and 48 deletions

View File

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

View File

@ -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<double> 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<std::uint8_t> 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<std::uint32_t>((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::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 {
const auto converted = static_cast<float>(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<std::size_t>(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<std::uint8_t> frame(frameBytes);
while (outputs.Room() >= frameBytes) {
const auto index = static_cast<std::size_t>(resampler.position);
if (index + 2 >= available) break;
const double t = resampler.position - static_cast<double>(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<std::size_t>(resampler.position), available);
if (index > 1) {
resampler.frames.erase(resampler.frames.begin(), resampler.frames.begin() + static_cast<std::ptrdiff_t>((index - 1) * channels));
resampler.position -= static_cast<double>(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::size_t>(std::min<std::uint64_t>(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<double> 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<std::size_t>(instance.info.superframeSize);
const auto framesInSuperframe = static_cast<std::uint32_t>(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<std::uint8_t*>(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<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));
@ -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<double> 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<std::ptrdiff_t>(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<std::size_t>(decoded->ch_layout.nb_channels);
const auto frameSamples = static_cast<std::size_t>(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<const float*>(decoded->extended_data[sourceChannel])[sample]
: reinterpret_cast<const float*>(decoded->data[0])[sample * frameChannels + sourceChannel];
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));
}
}
}
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<const float*>(decoded->extended_data[sourceChannel])[sample]
: reinterpret_cast<const float*>(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::size_t>(std::min<std::uint64_t>(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<double> 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<std::ptrdiff_t>(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::size_t>(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<unsigned long long>(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<double>(instance->resampler.ratio));
WriteResult(job.sideband, job.sidebandSize, 0);
break;
case JobKind::GetResampleInfo: {
const SidebandResampleInfo resample{instance->resampler.ratio, static_cast<std::int32_t>(HeldSamples(*instance)), {}};
WriteResult(job.sideband, job.sidebandSize, 0);
if (job.sideband && job.sidebandSize >= sizeof(SidebandResult) + sizeof(resample)) std::memcpy(static_cast<std::uint8_t*>(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));
}

View File

@ -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<std::uint8_t> 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<std::uint8_t> 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<std::int16_t> 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<std::int16_t> pcm(frames * channels);
std::size_t offset = 0;
for (int guard = 0; guard < 4096; ++guard) {
std::vector<std::uint8_t> 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<std::size_t>(sideband.inputConsumed);
if (offset < size && static_cast<std::size_t>(sideband.outputWritten) < pcm.size() * sizeof(std::int16_t)) ++out.shortJobs;
out.pcm.insert(out.pcm.end(), pcm.begin(), pcm.begin() + sideband.outputWritten / static_cast<std::int32_t>(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<std::int16_t>& reference, const std::vector<std::int16_t>& 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<std::int16_t>& reference, const std::vector<std::int16_t>& 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<double>(reference[static_cast<std::size_t>(std::max<std::ptrdiff_t>(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<std::ptrdiff_t>(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<double>(reference.pcm.size() / 2 - 2) / 0.890899;
Require(std::fabs(static_cast<double>(pitched.pcm.size() / 2) - expected) <= 2.0);
std::vector<std::uint8_t> 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<int>(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<std::int16_t> 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<std::int16_t> rest(pcm.begin(), pcm.end());
const auto tail = Stream(context, resumed, OPUS_STEREO + first.inputConsumed, sizeof(OPUS_STEREO) - static_cast<std::size_t>(first.inputConsumed), 2, 512, 2.0f);
const std::vector<std::int16_t> 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<std::uint8_t> 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<std::uint8_t> 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);
}