feat(ajm): implement sceAjmBatchJobSetResampleParameters and sceAjmBatchJobGetResampleInfo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4b0412fa97
commit
72be77590c
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue