audio: let the AJM Opus decoder write part of a packet (#1914)

ajm: buffer and resample Opus output
This commit is contained in:
Devran Cosmo Uenal authored and GitHub committed 2026-10-28 21:31:16 +01:00
1 parent c67352d5d6
commit c528324d27
5 files changed
+621 -18

No files matched your search

+9
View File
@@ -522,6 +522,13 @@ add_executable(ngs2_sampler_tests EXCLUDE_FROM_ALL
target_link_libraries(ngs2_sampler_tests common fmt::fmt LibAtrac9)
target_include_directories(ngs2_sampler_tests PRIVATE ${inc_headers})
add_executable(ajm_resample_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/AjmResampleTests.cpp"
"${KYTY_SOURCE_DIR}/loader/timer.cpp"
)
target_link_libraries(ajm_resample_tests common fmt::fmt FFmpeg::ffmpeg LibAtrac9 opus)
target_include_directories(ajm_resample_tests PRIVATE ${inc_headers})
add_executable(pad_haptics_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/PadHapticsTests.cpp"
)
@@ -668,6 +675,7 @@ if(BUILD_TESTING)
add_test(NAME audio_out2_port COMMAND $<TARGET_FILE:audio_out2_port_tests>)
add_test(NAME audio_out_timing COMMAND $<TARGET_FILE:audio_out_timing_tests>)
add_test(NAME ngs2_sampler COMMAND $<TARGET_FILE:ngs2_sampler_tests>)
add_test(NAME ajm_resample COMMAND $<TARGET_FILE:ajm_resample_tests>)
add_test(NAME pad_haptics COMMAND $<TARGET_FILE:pad_haptics_tests>)
add_test(NAME controller_settings COMMAND $<TARGET_FILE:controller_settings_tests>)
add_test(NAME host_input COMMAND $<TARGET_FILE:host_input_tests>)
@@ -762,6 +770,7 @@ if(BUILD_TESTING)
audio_out2_port_tests
audio_out_timing_tests
ngs2_sampler_tests
ajm_resample_tests
pad_haptics_tests
controller_settings_tests
host_input_tests
+55 -10
View File
@@ -6,6 +6,7 @@
#include "libs/ajm/decoder.h"
#include "libs/ajm/mp3_decoder.h"
#include "libs/ajm/opus_decoder.h"
#include "libs/ajm/resampler.h"
#include "libs/audio.h"
#include "libs/errno.h"
#include "libs/libs.h"
@@ -63,12 +64,6 @@ struct AjmSidebandMFrame {
uint32_t reserved;
};
struct AjmSidebandResampleInfo {
float ratio;
int32_t num_samples;
uint32_t reserved[8];
};
struct AjmGetStatisticsResult {
AjmSidebandResult result;
float engine_usage_batch;
@@ -104,6 +99,8 @@ constexpr uint64_t AJM_FLAG_RUN_GET_CODEC_INFO = 1ull << 11u;
constexpr uint64_t AJM_FLAG_RUN_MULTIPLE_FRAMES = 1ull << 12u;
constexpr uint64_t AJM_FLAG_CONTROL_RESET = 1ull << 13u;
constexpr uint64_t AJM_FLAG_CONTROL_INITIALIZE = 1ull << 14u;
constexpr uint64_t AJM_FLAG_CONTROL_RESAMPLE = 1ull << 15u;
constexpr uint64_t AJM_FLAG_CONTROL_RESAMPLE_EX = 1ull << 16u;
constexpr uint64_t AJM_FLAG_SIDEBAND_RESAMPLE_INFO = 1ull << 44u;
constexpr uint64_t AJM_FLAG_SIDEBAND_GAPLESS = 1ull << 45u;
constexpr uint64_t AJM_FLAG_SIDEBAND_FORMAT = 1ull << 46u;
@@ -177,6 +174,7 @@ struct AjmInstanceState {
uint64_t flags = 0;
AjmGaplessState gapless;
std::unique_ptr<AjmDecoder> decoder;
AjmResampler resampler;
};
static std::mutex g_ajm_instances_mutex;
@@ -358,7 +356,7 @@ static void AjmWriteSideband(uint64_t flags, void* sideband_output, size_t sideb
offset + AJM_SIDEBAND_RESAMPLE_INFO_SIZE <= sideband_output_size) {
auto* info = reinterpret_cast<AjmSidebandResampleInfo*>(
static_cast<uint8_t*>(sideband_output) + offset);
info->ratio = 1.0f;
*info = decode_result.resample_info;
offset += AJM_SIDEBAND_RESAMPLE_INFO_SIZE;
}
@@ -425,6 +423,12 @@ static AjmDecodeResult AjmDecodeInstance(uint32_t instance, const void* input, s
return result;
}
// The resampler requires one complete decoded packet per decoder call.
if (state->codec == static_cast<uint32_t>(AjmCodec::DecOpus) &&
(state->flags & AJM_INSTANCE_FLAG_RESAMPLE) != 0) {
return state->resampler.Decode(*state->decoder, state->flags, input, input_size, output,
output_size, multiple_frames, &state->gapless);
}
return state->decoder->Decode(input, input_size, output, output_size, multiple_frames,
&state->gapless);
}
@@ -514,6 +518,32 @@ static AjmDecodeResult AjmControlInstance(uint32_t instance, uint64_t flags,
sideband_input = static_cast<const uint8_t*>(sideband_input) + AJM_SIDEBAND_GAPLESS_SIZE;
sideband_input_size -= AJM_SIDEBAND_GAPLESS_SIZE;
}
for (const auto flag : {AJM_FLAG_CONTROL_RESAMPLE, AJM_FLAG_CONTROL_RESAMPLE_EX}) {
if ((flags & flag) == 0) {
continue;
}
const bool extended = flag == AJM_FLAG_CONTROL_RESAMPLE_EX;
const size_t size = extended ? 12 : 8;
if (sideband_input == nullptr || sideband_input_size < size ||
(state->flags & AJM_INSTANCE_FLAG_RESAMPLE) == 0) {
result.result = AJM_RESULT_INVALID_PARAMETER;
return result;
}
float ratio, change = 0;
uint32_t resample_flags;
const auto* data = static_cast<const uint8_t*>(sideband_input);
std::memcpy(&ratio, data, sizeof(ratio));
if (extended) {
std::memcpy(&change, data + sizeof(ratio), sizeof(change));
}
std::memcpy(&resample_flags, data + size - sizeof(resample_flags), sizeof(resample_flags));
result.result = state->resampler.SetParameters(ratio, change, resample_flags);
if (result.result != OK) {
return result;
}
sideband_input = data + size;
sideband_input_size -= size;
}
if ((flags & AJM_FLAG_CONTROL_RESET) != 0) {
state->gapless.Reset();
decoder->Reset();
@@ -524,6 +554,7 @@ static AjmDecodeResult AjmControlInstance(uint32_t instance, uint64_t flags,
result = decoder->Initialize(sideband_input, sideband_input_size);
}
result.resample_info = state->resampler.GetInfo();
return result;
}
@@ -572,6 +603,7 @@ static AjmSidebandFormat AjmGetInstanceFormat(uint32_t instance,
if (out_result != nullptr) {
*out_result = state->decoder->MakeResult();
out_result->format = state->decoder->GetFormat();
out_result->resample_info = state->resampler.GetInfo();
}
return state->decoder->GetFormat();
}
@@ -921,7 +953,13 @@ int KYTY_SYSV_ABI AjmBatchJobSetResampleParameters(AjmBatchInfo* info, uint32_t
float ratio, uint32_t flags, void* result) {
PRINT_NAME();
EXIT_NOT_IMPLEMENTED(info == nullptr);
AjmWriteResult(result, AJM_SIDEBAND_RESULT_SIZE);
const struct {
float ratio;
uint32_t flags;
} params {ratio, flags};
const auto decoded = AjmControlInstance(instance, AJM_FLAG_CONTROL_RESAMPLE, &params,
sizeof(params), nullptr, nullptr);
AjmWriteResult(result, AJM_SIDEBAND_RESULT_SIZE, decoded);
LOGF("\t instance = 0x%08" PRIx32 ", ratio = %f, flags = 0x%08" PRIx32 "\n", instance,
static_cast<double>(ratio), flags);
return AjmAppendJob(info, AJM_JOB_SET_RESAMPLE_PARAMETERS_SIZE, "set-resample");
@@ -933,7 +971,14 @@ int KYTY_SYSV_ABI AjmBatchJobSetResampleParametersEx(AjmBatchInfo* info, uint32_
void* result) {
PRINT_NAME();
EXIT_NOT_IMPLEMENTED(info == nullptr);
AjmWriteResult(result, AJM_SIDEBAND_RESULT_SIZE);
const struct {
float ratio;
float change;
uint32_t flags;
} params {ratio_start, ratio_change_per_sample, flags};
const auto decoded = AjmControlInstance(instance, AJM_FLAG_CONTROL_RESAMPLE_EX, &params,
sizeof(params), nullptr, nullptr);
AjmWriteResult(result, AJM_SIDEBAND_RESULT_SIZE, decoded);
LOGF("\t instance = 0x%08" PRIx32 ", ratio_start = %f, ratio_change = %f, flags = 0x%08" PRIx32
"\n",
instance, static_cast<double>(ratio_start), static_cast<double>(ratio_change_per_sample),
@@ -952,7 +997,7 @@ int KYTY_SYSV_ABI AjmBatchJobGetResampleInfo(AjmBatchInfo* info, uint32_t instan
if (result != nullptr) {
auto* resample = reinterpret_cast<AjmSidebandResampleInfo*>(static_cast<uint8_t*>(result) +
AJM_SIDEBAND_RESULT_SIZE);
resample->ratio = 1.0f;
*resample = decode_result.resample_info;
}
LOGF("\t instance = 0x%08" PRIx32 "\n", instance);
return AjmAppendJob(info, AJM_JOB_RUN_SIZE, "get-resample-info");
+15 -8
View File
@@ -56,15 +56,22 @@ enum class AjmSampleEncoding : uint32_t {
Float = 2,
};
struct AjmSidebandResampleInfo {
float ratio = 1.0f;
int32_t num_samples = 0;
uint32_t reserved[8] {};
};
struct AjmDecodeResult {
int32_t result = OK;
int32_t internal_result = OK;
size_t input_consumed = 0;
size_t output_written = 0;
uint64_t total_decoded_samples = 0;
uint32_t frames = 0;
uint32_t frames_per_packet = 0;
AjmSidebandFormat format {};
int32_t result = OK;
int32_t internal_result = OK;
size_t input_consumed = 0;
size_t output_written = 0;
uint64_t total_decoded_samples = 0;
uint32_t frames = 0;
uint32_t frames_per_packet = 0;
AjmSidebandFormat format {};
AjmSidebandResampleInfo resample_info {};
};
constexpr int32_t AJM_RESULT_NOT_INITIALIZED = 0x00000001;
+182
View File
@@ -0,0 +1,182 @@
#pragma once
#include "libs/ajm/decoder.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <vector>
namespace Libs::Audio::Ajm {
constexpr uint64_t AJM_INSTANCE_FLAG_GAPLESS_LOOP = 1ull << 10u;
constexpr uint64_t AJM_INSTANCE_FLAG_RESAMPLE = 1ull << 12u;
constexpr uint64_t AJM_INSTANCE_FLAG_SEAMLESS_LOOP = 1ull << 13u;
constexpr uint32_t AJM_RESAMPLE_FLAG_RESET = 1u;
constexpr uint32_t AJM_RESAMPLE_FLAG_END = 2u;
constexpr uint32_t AJM_RESAMPLE_FLAG_IGNORE_RATIO_PARAMETER = 8u;
class AjmResampler {
public:
int32_t SetParameters(float ratio, float change, uint32_t flags) {
if ((flags & AJM_RESAMPLE_FLAG_IGNORE_RATIO_PARAMETER) == 0 &&
(!std::isfinite(ratio) || ratio < 0 || ratio > 4 || !std::isfinite(change))) {
return AJM_RESULT_INVALID_PARAMETER;
}
if ((flags & AJM_RESAMPLE_FLAG_RESET) != 0) {
m_pcm.clear();
m_position = 0;
m_end = false;
}
if ((flags & AJM_RESAMPLE_FLAG_IGNORE_RATIO_PARAMETER) == 0) {
m_ratio = ratio;
m_change = std::abs(change) < 0.000001f ? 0 : change;
}
m_end |= (flags & AJM_RESAMPLE_FLAG_END) != 0;
return OK;
}
[[nodiscard]] AjmSidebandResampleInfo GetInfo() const {
AjmSidebandResampleInfo info;
info.ratio = static_cast<float>(m_ratio);
info.num_samples = m_end && m_position >= Samples()
? -1
: static_cast<int32_t>(std::ceil(Samples() - m_position));
return info;
}
AjmDecodeResult Decode(AjmDecoder& decoder, uint64_t flags, const void* input,
size_t input_size, void* output, size_t output_size,
bool multiple_frames, AjmGaplessState* gapless) {
auto result = decoder.MakeResult();
if (result.format.channel_num == 0) {
return decoder.Decode(input, input_size, output, output_size, multiple_frames, gapless);
}
bool stop_decoding = false;
for (;;) {
const auto format = decoder.GetFormat();
const auto encoding = static_cast<AjmSampleEncoding>(format.sample_encoding);
const auto sample_bytes = format.channel_num * AjmBytesPerSample(encoding);
if (!m_pcm.empty() && sample_bytes != m_sample_bytes) {
result.result = AJM_RESULT_INVALID_PARAMETER;
break;
}
if (output == nullptr || output_size - result.output_written < sample_bytes) {
if (m_position < Samples() || result.input_consumed < input_size) {
result.result |= AJM_RESULT_NOT_ENOUGH_ROOM;
}
break;
}
const auto index = static_cast<size_t>(m_position);
const auto fraction = m_position - static_cast<double>(index);
const bool need_next = fraction != 0 && index + 1 >= Samples();
if (index >= Samples() || need_next) {
Compact();
if (!stop_decoding && input != nullptr && result.input_consumed < input_size &&
(gapless == nullptr || !gapless->IsEnd())) {
// A decode job can produce up to six 20 ms Opus frames in one packet.
const auto channels = std::max<uint32_t>(format.channel_num, flags & 0x7fu);
m_frame.resize(5760 * channels * AjmBytesPerSample(encoding));
auto decoded =
decoder.Decode(static_cast<const uint8_t*>(input) + result.input_consumed,
input_size - result.input_consumed, m_frame.data(),
m_frame.size(), false, gapless);
result.input_consumed += decoded.input_consumed;
result.frames += decoded.frames;
result.frames_per_packet = decoded.frames_per_packet;
result.total_decoded_samples = decoded.total_decoded_samples;
result.format = decoded.format;
result.result |= decoded.result;
result.internal_result = decoded.internal_result;
m_sample_bytes = decoded.format.channel_num * AjmBytesPerSample(encoding);
m_pcm.insert(m_pcm.end(), m_frame.begin(),
m_frame.begin() + decoded.output_written);
stop_decoding =
!multiple_frames || decoded.result != OK || decoded.input_consumed == 0;
if (gapless != nullptr && gapless->IsEnd() &&
(flags & AJM_INSTANCE_FLAG_GAPLESS_LOOP) != 0) {
gapless->Reset();
stop_decoding |= (flags & AJM_INSTANCE_FLAG_SEAMLESS_LOOP) == 0;
}
continue;
}
if (!m_end || m_position >= Samples()) {
if (!m_end && !stop_decoding && (gapless == nullptr || !gapless->IsEnd())) {
result.result |= AJM_RESULT_PARTIAL_INPUT;
}
break;
}
}
const auto current = static_cast<size_t>(m_position);
const auto next = std::min(current + 1, Samples() - 1);
auto* dst = static_cast<uint8_t*>(output) + result.output_written;
const auto* a = m_pcm.data() + current * sample_bytes;
const auto* b = m_pcm.data() + next * sample_bytes;
const auto weight = m_position - static_cast<double>(current);
if (weight == 0) {
std::memcpy(dst, a, sample_bytes);
} else {
switch (encoding) {
case AjmSampleEncoding::S16:
Interpolate<int16_t>(dst, a, b, format.channel_num, weight);
break;
case AjmSampleEncoding::S32:
Interpolate<int32_t>(dst, a, b, format.channel_num, weight);
break;
case AjmSampleEncoding::Float:
Interpolate<float>(dst, a, b, format.channel_num, weight);
break;
}
}
result.output_written += sample_bytes;
m_position += m_ratio;
m_ratio = std::clamp(m_ratio + m_change, 0.0, 4.0);
if (m_ratio == 0 || m_ratio == 4) {
m_change = 0;
}
}
Compact();
result.resample_info = GetInfo();
return result;
}
private:
template <typename T>
static void Interpolate(uint8_t* dst, const uint8_t* a, const uint8_t* b, uint32_t channels,
double weight) {
for (uint32_t channel = 0; channel < channels; ++channel) {
T first, second;
std::memcpy(&first, a + channel * sizeof(T), sizeof(T));
std::memcpy(&second, b + channel * sizeof(T), sizeof(T));
const auto value =
static_cast<T>(first + (static_cast<double>(second) - first) * weight);
std::memcpy(dst + channel * sizeof(T), &value, sizeof(T));
}
}
[[nodiscard]] size_t Samples() const {
return m_sample_bytes == 0 ? 0 : m_pcm.size() / m_sample_bytes;
}
void Compact() {
const auto consumed = std::min(static_cast<size_t>(m_position), Samples());
const auto bytes = consumed * m_sample_bytes;
if (bytes != 0) {
std::memmove(m_pcm.data(), m_pcm.data() + bytes, m_pcm.size() - bytes);
m_pcm.resize(m_pcm.size() - bytes);
m_position -= static_cast<double>(consumed);
}
}
std::vector<uint8_t> m_pcm;
std::vector<uint8_t> m_frame;
size_t m_sample_bytes = 0;
double m_position = 0;
double m_ratio = 1;
double m_change = 0;
bool m_end = false;
};
} // namespace Libs::Audio::Ajm
+360
View File
@@ -0,0 +1,360 @@
#include "libs/ajm.cpp"
#include <array>
#include <cmath>
#include <cstdlib>
#include <fstream>
#include <opus.h>
using namespace Libs::Audio::Ajm;
namespace {
void Check(bool condition, const char* message) {
if (!condition) {
std::fprintf(stderr, "AjmResampleTests: %s\n", message);
std::abort();
}
}
struct DecodeResult {
AjmSidebandResult result;
AjmSidebandStream stream;
AjmSidebandMFrame frames;
};
struct Fixture {
uint32_t instance = 0;
uint32_t channels;
std::array<uint8_t, 4096> batch_data {};
AjmBatchInfo batch {};
explicit Fixture(uint32_t channels = 1, AjmSampleEncoding encoding = AjmSampleEncoding::S16,
uint64_t flags = 0x1000)
: channels(channels) {
Check(AjmInstanceCreate(1, 24, flags | channels | (uint64_t(encoding) << 7), &instance) ==
OK,
"create Opus instance");
Initialize();
}
~Fixture() { AjmInstanceDestroy(1, instance); }
AjmBatchInfo* Batch() {
batch = {batch_data.data(), 0, batch_data.size(), nullptr, nullptr};
return &batch;
}
void Initialize() {
AjmDecOpusInitializeParameters params {channels, 48000, 0};
AjmSidebandResult result {};
AjmBatchJobInitialize(Batch(), instance, &params, sizeof(params), &result);
Check(result.result == OK, "initialize Opus instance");
}
void Parameters(float ratio, uint32_t flags, float change = 0, bool extended = false) {
AjmSidebandResult result {};
if (extended) {
AjmBatchJobSetResampleParametersEx(Batch(), instance, ratio, change, flags, &result);
} else {
AjmBatchJobSetResampleParameters(Batch(), instance, ratio, flags, &result);
}
Check(result.result == OK, "set resampling parameters");
}
AjmSidebandResampleInfo Info() {
struct {
AjmSidebandResult result;
AjmSidebandResampleInfo info;
} result {};
AjmBatchJobGetResampleInfo(Batch(), instance, &result);
Check(result.result.result == OK, "get resampling info");
return result.info;
}
void Gapless(uint32_t samples, uint16_t skip) {
AjmSidebandGaplessDecode params {samples, skip, 0};
AjmSidebandResult result {};
AjmBatchJobSetGaplessDecode(Batch(), instance, &params, 1, &result);
Check(result.result == OK, "set gapless parameters");
}
DecodeResult Decode(const void* input, size_t size, void* output, size_t capacity) {
DecodeResult result {};
AjmBatchJobDecode(Batch(), instance, input, size, output, capacity, &result);
Check((result.result.result & ~(AJM_RESULT_NOT_ENOUGH_ROOM | AJM_RESULT_PARTIAL_INPUT)) ==
0,
"decode returned unexpected error");
return result;
}
};
std::vector<uint8_t> Encode(uint32_t channels, int packets) {
int error = 0;
auto* encoder = opus_encoder_create(48000, channels, OPUS_APPLICATION_AUDIO, &error);
Check(encoder != nullptr && error == OPUS_OK, "create test encoder");
std::vector<uint8_t> stream;
std::vector<int16_t> pcm(960 * channels);
std::array<uint8_t, 4096> packet;
for (int p = 0; p < packets; ++p) {
for (size_t i = 0; i < pcm.size(); ++i) {
pcm[i] = static_cast<int16_t>(
12000 * std::sin((i / channels + p * 960) * 0.041 + (i % channels)));
}
const int size = opus_encode(encoder, pcm.data(), 960, packet.data(), packet.size());
Check(size > 0, "encode test packet");
stream.push_back(static_cast<uint8_t>(size));
stream.push_back(static_cast<uint8_t>(size >> 8));
stream.insert(stream.end(), packet.begin(), packet.begin() + size);
}
opus_encoder_destroy(encoder);
return stream;
}
void SmallOutputs(const std::vector<uint8_t>& packet, uint16_t skip = 0) {
Fixture f;
f.Gapless(960 - skip, skip);
f.Parameters(1, 3);
std::array<uint8_t, 1030> output;
output.fill(0x5a);
auto first = f.Decode(packet.data(), packet.size(), output.data(), 1024);
Check(first.stream.size_consumed == static_cast<int>(packet.size()),
"small output must consume packet");
Check(first.stream.size_produced == 1024, "small output must produce 1024 bytes");
Check(first.frames.num_frames == 1 && first.stream.total_decoded_samples == 960 - skip,
"decoded frame counted once before resampling");
Check(f.Info().num_samples == 448 - skip, "report retained source samples");
Check(std::all_of(output.begin() + 1024, output.end(), [](auto v) { return v == 0x5a; }),
"do not write past output capacity");
const auto last = f.Decode(nullptr, 0, output.data(), 1024);
Check(last.result.result == OK && last.stream.size_consumed == 0 &&
last.stream.size_produced == 896 - skip * 2 && last.frames.num_frames == 0 &&
last.stream.total_decoded_samples == first.stream.total_decoded_samples,
"END drains retained PCM without decoding or reporting partial input");
Check(f.Decode(nullptr, 0, output.data(), 1024).stream.size_produced == 0,
"END drains only once");
}
void UnityMatchesWholeDecode(uint32_t channels, AjmSampleEncoding encoding) {
const auto stream = Encode(channels, 6);
const auto sample_bytes = channels * AjmBytesPerSample(encoding);
Fixture reference(channels, encoding, 0);
reference.Gapless(5328, 312);
std::vector<uint8_t> expected(5760 * sample_bytes);
const auto full =
reference.Decode(stream.data(), stream.size(), expected.data(), expected.size());
expected.resize(full.stream.size_produced);
Check(expected.size() == 5328 * sample_bytes, "reference gapless length");
Fixture f(channels, encoding);
f.Gapless(5328, 312);
f.Parameters(1, 3);
std::vector<uint8_t> actual;
std::array<uint8_t, 1025> output;
size_t offset = 0;
uint32_t frames = 0;
for (int i = 0; i < 100; ++i) {
const auto decoded = f.Decode(offset < stream.size() ? stream.data() + offset : nullptr,
stream.size() - offset, output.data(), output.size());
offset += decoded.stream.size_consumed;
frames += decoded.frames.num_frames;
actual.insert(actual.end(), output.begin(), output.begin() + decoded.stream.size_produced);
if (decoded.stream.size_produced == 0) {
break;
}
}
Check(actual == expected && frames == 6 && offset == stream.size(),
"chunked PCM must exactly match whole decode, without loss or duplication");
}
void ChunkedRates(const std::vector<uint8_t>& stream, uint32_t samples, uint16_t skip) {
for (float ratio: {0.125f, 0.749153f, 1.334840f}) {
Fixture reference;
reference.Gapless(samples, skip);
reference.Parameters(ratio, 3);
std::vector<uint8_t> expected(
(static_cast<size_t>(std::ceil(samples / double(ratio))) + 1) * 2);
const auto full =
reference.Decode(stream.data(), stream.size(), expected.data(), expected.size());
expected.resize(full.stream.size_produced);
Check(expected.size() == static_cast<size_t>(std::ceil(samples / double(ratio))) * 2,
"fractional resampling rounds up final output sample count");
Fixture f;
f.Gapless(samples, skip);
f.Parameters(ratio, 1);
std::vector<uint8_t> actual;
std::array<uint8_t, 1024> output;
size_t offset = 0;
for (int i = 0; i < 1000; ++i) {
if (offset == stream.size()) {
// The caller can signal END only after submitting the last packet.
f.Parameters(0, 2 | 8);
}
const auto decoded = f.Decode(offset < stream.size() ? stream.data() + offset : nullptr,
stream.size() - offset, output.data(), output.size());
offset += decoded.stream.size_consumed;
actual.insert(actual.end(), output.begin(),
output.begin() + decoded.stream.size_produced);
if (decoded.stream.size_produced == 0) {
break;
}
}
Check(actual == expected && offset == stream.size(),
"non-unity PCM is independent of output chunk boundaries and deferred END");
}
}
void LoopBoundaries() {
const auto loop = Encode(1, 2);
auto stream = loop;
stream.insert(stream.end(), loop.begin(), loop.end());
for (uint64_t flags: {0x1400u, 0x3400u}) {
Fixture f(1, AjmSampleEncoding::S16, flags);
f.Gapless(1500, 100);
f.Parameters(1, 1);
std::array<uint8_t, 8192> pcm;
const auto first = f.Decode(stream.data(), stream.size(), pcm.data(), 1024);
Check(first.stream.size_produced == 1024, "loop first partial output");
const auto second =
f.Decode(stream.data() + first.stream.size_consumed,
stream.size() - first.stream.size_consumed, pcm.data(), pcm.size());
const bool seamless = flags == 0x3400;
Check(second.stream.size_produced == (seamless ? 6000 - 1024 : 3000 - 1024),
"non-seamless loop stops job at stream boundary");
const auto consumed = first.stream.size_consumed + second.stream.size_consumed;
Check(consumed == static_cast<int>(seamless ? stream.size() : loop.size()),
"loop boundary input consumption");
if (!seamless) {
Check(
f.Decode(stream.data() + consumed, stream.size() - consumed, pcm.data(), pcm.size())
.stream.size_produced == 3000,
"next job resumes gapless loop");
}
}
}
void RatesAndControls(const std::vector<uint8_t>& packet) {
for (float ratio: {0.125f, 0.5f, 1.0f, 1.25f, 2.0f, 4.0f}) {
Fixture f;
f.Parameters(ratio, 3);
std::vector<int16_t> pcm(8000);
const auto result = f.Decode(packet.data(), packet.size(), pcm.data(), pcm.size() * 2);
Check(result.stream.size_produced == static_cast<int>(std::ceil(960.0 / ratio)) * 2,
"resampling ratio controls output duration");
Check(result.stream.total_decoded_samples == 960,
"ratio does not change decoded sample count");
}
Fixture f;
f.Parameters(1, 1);
std::array<int16_t, 512> pcm;
f.Decode(packet.data(), packet.size(), pcm.data(), sizeof(pcm));
f.Initialize();
Check(f.Info().num_samples == 448, "codec initialize must preserve resampler PCM");
f.Parameters(0, 0);
const auto frozen = f.Decode(nullptr, 0, pcm.data(), sizeof(pcm));
Check(
frozen.stream.size_produced == sizeof(pcm) && f.Info().num_samples == 448 &&
std::all_of(pcm.begin(), pcm.end(), [&](auto sample) { return sample == pcm.front(); }),
"zero ratio repeats one sample without advancing input");
f.Parameters(1, 0, 0.25f, true);
f.Parameters(std::numeric_limits<float>::quiet_NaN(), 8);
f.Decode(nullptr, 0, pcm.data(), 4 * sizeof(int16_t));
Check(f.Info().ratio == 2.0f, "ignore preserves ratio ramp");
f.Parameters(1, 1);
Check(f.Info().num_samples == 0, "resample RESET discards retained PCM");
Check(f.Decode(nullptr, 0, pcm.data(), sizeof(pcm)).stream.size_produced == 0,
"RESET leaves no stale audio");
}
void UnbufferedAndSplit(const std::vector<uint8_t>& packet) {
Fixture raw(1, AjmSampleEncoding::S16, 0);
std::array<uint8_t, 1024> pcm;
const auto too_small = raw.Decode(packet.data(), packet.size(), pcm.data(), pcm.size());
Check(too_small.result.result == AJM_RESULT_NOT_ENOUGH_ROOM &&
too_small.stream.size_consumed == 0 && too_small.stream.size_produced == 0,
"ordinary decoding still requires room for a whole packet");
Fixture f;
f.Parameters(1, 3);
AjmBuffer input[] {{const_cast<uint8_t*>(packet.data()), 1},
{const_cast<uint8_t*>(packet.data()) + 1, packet.size() - 1}};
AjmBuffer output[] {{pcm.data(), 257}, {pcm.data() + 257, 767}};
DecodeResult result {};
AjmBatchJobDecodeSplit(f.Batch(), f.instance, input, 2, output, 2, &result);
Check(result.stream.size_produced == 1024 &&
result.stream.size_consumed == static_cast<int>(packet.size()),
"split buffers retain PCM across jobs");
}
void FormatChangeRequiresReset() {
Fixture f(2);
f.Parameters(1, 1);
const auto stereo = Encode(2, 1);
std::array<uint8_t, 4096> pcm;
f.Decode(stereo.data(), stereo.size(), pcm.data(), 1024);
f.channels = 1;
f.Initialize();
DecodeResult result {};
AjmBatchJobDecode(f.Batch(), f.instance, nullptr, 0, pcm.data(), pcm.size(), &result);
Check(result.result.result == AJM_RESULT_INVALID_PARAMETER && result.stream.size_produced == 0,
"format change cannot reinterpret retained PCM with a different channel stride");
f.Parameters(1, 3);
const auto mono = Encode(1, 1);
Check(f.Decode(mono.data(), mono.size(), pcm.data(), pcm.size()).stream.size_produced == 1920,
"resampler RESET permits a new stream format");
}
void EndPersistsUntilReset(const std::vector<uint8_t>& packet) {
Fixture f;
f.Parameters(0.5f, 1);
std::array<int16_t, 64> pcm;
auto decoded = f.Decode(packet.data(), packet.size(), pcm.data(), sizeof(pcm));
size_t produced = decoded.stream.size_produced;
f.Parameters(0.5f, 2);
for (int i = 0; i < 40; ++i) {
f.Parameters(0.5f, 0);
decoded = f.Decode(nullptr, 0, pcm.data(), sizeof(pcm));
produced += decoded.stream.size_produced;
Check((decoded.result.result & AJM_RESULT_PARTIAL_INPUT) == 0,
"END remains active across subsequent ratio controls");
if (f.Info().num_samples < 0) {
break;
}
}
Check(produced == 3840 && f.Info().num_samples < 0,
"fully drained END reports a negative retained sample count");
f.Parameters(1, 1);
Check(f.Info().num_samples == 0, "RESET clears end-of-stream state");
}
} // namespace
int main(int argc, char** argv) {
const auto packet = Encode(1, 1);
SmallOutputs(packet);
SmallOutputs(packet, 312);
for (uint32_t channels: {1u, 2u}) {
for (auto encoding:
{AjmSampleEncoding::S16, AjmSampleEncoding::S32, AjmSampleEncoding::Float}) {
UnityMatchesWholeDecode(channels, encoding);
}
}
RatesAndControls(packet);
UnbufferedAndSplit(packet);
ChunkedRates(Encode(1, 6), 5328, 312);
LoopBoundaries();
FormatChangeRequiresReset();
EndPersistsUntilReset(packet);
if (argc >= 2) {
std::ifstream file(argv[1], std::ios::binary);
std::vector<uint8_t> real_packet((std::istreambuf_iterator<char>(file)), {});
Check(real_packet.size() == 209, "expected real 209-byte packet fixture");
SmallOutputs(real_packet, 312);
}
if (argc >= 3) {
std::ifstream file(argv[2], std::ios::binary);
std::vector<uint8_t> real_stream((std::istreambuf_iterator<char>(file)), {});
Check(real_stream.size() == 4048, "expected complete real stream fixture");
ChunkedRates(real_stream, 25920, 312);
}
std::puts("AjmResampleTests: passed");
}