mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-28 22:06:59 +01:00
audio: let the AJM Opus decoder write part of a packet (#1914)
ajm: buffer and resample Opus output
This commit is contained in:
committed by
GitHub
parent
c67352d5d6
commit
c528324d27
@@ -522,6 +522,13 @@ add_executable(ngs2_sampler_tests EXCLUDE_FROM_ALL
|
|||||||
target_link_libraries(ngs2_sampler_tests common fmt::fmt LibAtrac9)
|
target_link_libraries(ngs2_sampler_tests common fmt::fmt LibAtrac9)
|
||||||
target_include_directories(ngs2_sampler_tests PRIVATE ${inc_headers})
|
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
|
add_executable(pad_haptics_tests EXCLUDE_FROM_ALL
|
||||||
"${KYTY_TESTS_DIR}/PadHapticsTests.cpp"
|
"${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_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 audio_out_timing COMMAND $<TARGET_FILE:audio_out_timing_tests>)
|
||||||
add_test(NAME ngs2_sampler COMMAND $<TARGET_FILE:ngs2_sampler_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 pad_haptics COMMAND $<TARGET_FILE:pad_haptics_tests>)
|
||||||
add_test(NAME controller_settings COMMAND $<TARGET_FILE:controller_settings_tests>)
|
add_test(NAME controller_settings COMMAND $<TARGET_FILE:controller_settings_tests>)
|
||||||
add_test(NAME host_input COMMAND $<TARGET_FILE:host_input_tests>)
|
add_test(NAME host_input COMMAND $<TARGET_FILE:host_input_tests>)
|
||||||
@@ -762,6 +770,7 @@ if(BUILD_TESTING)
|
|||||||
audio_out2_port_tests
|
audio_out2_port_tests
|
||||||
audio_out_timing_tests
|
audio_out_timing_tests
|
||||||
ngs2_sampler_tests
|
ngs2_sampler_tests
|
||||||
|
ajm_resample_tests
|
||||||
pad_haptics_tests
|
pad_haptics_tests
|
||||||
controller_settings_tests
|
controller_settings_tests
|
||||||
host_input_tests
|
host_input_tests
|
||||||
|
|||||||
+55
-10
@@ -6,6 +6,7 @@
|
|||||||
#include "libs/ajm/decoder.h"
|
#include "libs/ajm/decoder.h"
|
||||||
#include "libs/ajm/mp3_decoder.h"
|
#include "libs/ajm/mp3_decoder.h"
|
||||||
#include "libs/ajm/opus_decoder.h"
|
#include "libs/ajm/opus_decoder.h"
|
||||||
|
#include "libs/ajm/resampler.h"
|
||||||
#include "libs/audio.h"
|
#include "libs/audio.h"
|
||||||
#include "libs/errno.h"
|
#include "libs/errno.h"
|
||||||
#include "libs/libs.h"
|
#include "libs/libs.h"
|
||||||
@@ -63,12 +64,6 @@ struct AjmSidebandMFrame {
|
|||||||
uint32_t reserved;
|
uint32_t reserved;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct AjmSidebandResampleInfo {
|
|
||||||
float ratio;
|
|
||||||
int32_t num_samples;
|
|
||||||
uint32_t reserved[8];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct AjmGetStatisticsResult {
|
struct AjmGetStatisticsResult {
|
||||||
AjmSidebandResult result;
|
AjmSidebandResult result;
|
||||||
float engine_usage_batch;
|
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_RUN_MULTIPLE_FRAMES = 1ull << 12u;
|
||||||
constexpr uint64_t AJM_FLAG_CONTROL_RESET = 1ull << 13u;
|
constexpr uint64_t AJM_FLAG_CONTROL_RESET = 1ull << 13u;
|
||||||
constexpr uint64_t AJM_FLAG_CONTROL_INITIALIZE = 1ull << 14u;
|
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_RESAMPLE_INFO = 1ull << 44u;
|
||||||
constexpr uint64_t AJM_FLAG_SIDEBAND_GAPLESS = 1ull << 45u;
|
constexpr uint64_t AJM_FLAG_SIDEBAND_GAPLESS = 1ull << 45u;
|
||||||
constexpr uint64_t AJM_FLAG_SIDEBAND_FORMAT = 1ull << 46u;
|
constexpr uint64_t AJM_FLAG_SIDEBAND_FORMAT = 1ull << 46u;
|
||||||
@@ -177,6 +174,7 @@ struct AjmInstanceState {
|
|||||||
uint64_t flags = 0;
|
uint64_t flags = 0;
|
||||||
AjmGaplessState gapless;
|
AjmGaplessState gapless;
|
||||||
std::unique_ptr<AjmDecoder> decoder;
|
std::unique_ptr<AjmDecoder> decoder;
|
||||||
|
AjmResampler resampler;
|
||||||
};
|
};
|
||||||
|
|
||||||
static std::mutex g_ajm_instances_mutex;
|
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) {
|
offset + AJM_SIDEBAND_RESAMPLE_INFO_SIZE <= sideband_output_size) {
|
||||||
auto* info = reinterpret_cast<AjmSidebandResampleInfo*>(
|
auto* info = reinterpret_cast<AjmSidebandResampleInfo*>(
|
||||||
static_cast<uint8_t*>(sideband_output) + offset);
|
static_cast<uint8_t*>(sideband_output) + offset);
|
||||||
info->ratio = 1.0f;
|
*info = decode_result.resample_info;
|
||||||
offset += AJM_SIDEBAND_RESAMPLE_INFO_SIZE;
|
offset += AJM_SIDEBAND_RESAMPLE_INFO_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,6 +423,12 @@ static AjmDecodeResult AjmDecodeInstance(uint32_t instance, const void* input, s
|
|||||||
return result;
|
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,
|
return state->decoder->Decode(input, input_size, output, output_size, multiple_frames,
|
||||||
&state->gapless);
|
&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 = static_cast<const uint8_t*>(sideband_input) + AJM_SIDEBAND_GAPLESS_SIZE;
|
||||||
sideband_input_size -= 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) {
|
if ((flags & AJM_FLAG_CONTROL_RESET) != 0) {
|
||||||
state->gapless.Reset();
|
state->gapless.Reset();
|
||||||
decoder->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 = decoder->Initialize(sideband_input, sideband_input_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.resample_info = state->resampler.GetInfo();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,6 +603,7 @@ static AjmSidebandFormat AjmGetInstanceFormat(uint32_t instance,
|
|||||||
if (out_result != nullptr) {
|
if (out_result != nullptr) {
|
||||||
*out_result = state->decoder->MakeResult();
|
*out_result = state->decoder->MakeResult();
|
||||||
out_result->format = state->decoder->GetFormat();
|
out_result->format = state->decoder->GetFormat();
|
||||||
|
out_result->resample_info = state->resampler.GetInfo();
|
||||||
}
|
}
|
||||||
return state->decoder->GetFormat();
|
return state->decoder->GetFormat();
|
||||||
}
|
}
|
||||||
@@ -921,7 +953,13 @@ int KYTY_SYSV_ABI AjmBatchJobSetResampleParameters(AjmBatchInfo* info, uint32_t
|
|||||||
float ratio, uint32_t flags, void* result) {
|
float ratio, uint32_t flags, void* result) {
|
||||||
PRINT_NAME();
|
PRINT_NAME();
|
||||||
EXIT_NOT_IMPLEMENTED(info == nullptr);
|
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, ¶ms,
|
||||||
|
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,
|
LOGF("\t instance = 0x%08" PRIx32 ", ratio = %f, flags = 0x%08" PRIx32 "\n", instance,
|
||||||
static_cast<double>(ratio), flags);
|
static_cast<double>(ratio), flags);
|
||||||
return AjmAppendJob(info, AJM_JOB_SET_RESAMPLE_PARAMETERS_SIZE, "set-resample");
|
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) {
|
void* result) {
|
||||||
PRINT_NAME();
|
PRINT_NAME();
|
||||||
EXIT_NOT_IMPLEMENTED(info == nullptr);
|
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, ¶ms,
|
||||||
|
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
|
LOGF("\t instance = 0x%08" PRIx32 ", ratio_start = %f, ratio_change = %f, flags = 0x%08" PRIx32
|
||||||
"\n",
|
"\n",
|
||||||
instance, static_cast<double>(ratio_start), static_cast<double>(ratio_change_per_sample),
|
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) {
|
if (result != nullptr) {
|
||||||
auto* resample = reinterpret_cast<AjmSidebandResampleInfo*>(static_cast<uint8_t*>(result) +
|
auto* resample = reinterpret_cast<AjmSidebandResampleInfo*>(static_cast<uint8_t*>(result) +
|
||||||
AJM_SIDEBAND_RESULT_SIZE);
|
AJM_SIDEBAND_RESULT_SIZE);
|
||||||
resample->ratio = 1.0f;
|
*resample = decode_result.resample_info;
|
||||||
}
|
}
|
||||||
LOGF("\t instance = 0x%08" PRIx32 "\n", instance);
|
LOGF("\t instance = 0x%08" PRIx32 "\n", instance);
|
||||||
return AjmAppendJob(info, AJM_JOB_RUN_SIZE, "get-resample-info");
|
return AjmAppendJob(info, AJM_JOB_RUN_SIZE, "get-resample-info");
|
||||||
|
|||||||
+15
-8
@@ -56,15 +56,22 @@ enum class AjmSampleEncoding : uint32_t {
|
|||||||
Float = 2,
|
Float = 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct AjmSidebandResampleInfo {
|
||||||
|
float ratio = 1.0f;
|
||||||
|
int32_t num_samples = 0;
|
||||||
|
uint32_t reserved[8] {};
|
||||||
|
};
|
||||||
|
|
||||||
struct AjmDecodeResult {
|
struct AjmDecodeResult {
|
||||||
int32_t result = OK;
|
int32_t result = OK;
|
||||||
int32_t internal_result = OK;
|
int32_t internal_result = OK;
|
||||||
size_t input_consumed = 0;
|
size_t input_consumed = 0;
|
||||||
size_t output_written = 0;
|
size_t output_written = 0;
|
||||||
uint64_t total_decoded_samples = 0;
|
uint64_t total_decoded_samples = 0;
|
||||||
uint32_t frames = 0;
|
uint32_t frames = 0;
|
||||||
uint32_t frames_per_packet = 0;
|
uint32_t frames_per_packet = 0;
|
||||||
AjmSidebandFormat format {};
|
AjmSidebandFormat format {};
|
||||||
|
AjmSidebandResampleInfo resample_info {};
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr int32_t AJM_RESULT_NOT_INITIALIZED = 0x00000001;
|
constexpr int32_t AJM_RESULT_NOT_INITIALIZED = 0x00000001;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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, ¶ms, 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, ¶ms, 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");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user