From 28766a915b39d466b43dc5a0db8422bd51a659e8 Mon Sep 17 00:00:00 2001 From: nmzik Date: Thu, 15 Oct 2026 01:44:16 +0200 Subject: [PATCH] audio: decode PS5 ATRAC9 discrete multichannel streams Co-authored-by: fxpw --- src/libs/ajm.cpp | 14 +-- src/libs/ajm/atrac9_decoder.h | 195 +++++++++++++++++++++++++--------- src/libs/ngs2.cpp | 11 +- tests/Ngs2SamplerTests.cpp | 181 +++++++++++++++++++++++++++++++ 4 files changed, 329 insertions(+), 72 deletions(-) diff --git a/src/libs/ajm.cpp b/src/libs/ajm.cpp index 276ed57de..8169fddfb 100644 --- a/src/libs/ajm.cpp +++ b/src/libs/ajm.cpp @@ -156,20 +156,8 @@ int KYTY_SYSV_ABI AjmDecAt9ParseConfigData(const void* config_data, return AJM_ERROR_INVALID_PARAMETER; } - auto* handle = Atrac9GetHandle(); - if (handle == nullptr) { - return AJM_ERROR_INVALID_PARAMETER; - } - Atrac9CodecInfo codec_info {}; - const int init_result = AjmAt9InitDecoder(handle, static_cast(config_data)); - const int info_result = - init_result == 0 ? Atrac9GetCodecInfo(handle, &codec_info) : init_result; - Atrac9ReleaseHandle(handle); - - if (info_result != 0 || codec_info.channels <= 0 || codec_info.samplingRate <= 0 || - codec_info.frameSamples <= 0 || codec_info.framesInSuperframe <= 0 || - codec_info.superframeSize <= 0) { + if (!AjmAt9GetCodecInfo(static_cast(config_data), codec_info)) { return AJM_ERROR_INVALID_PARAMETER; } diff --git a/src/libs/ajm/atrac9_decoder.h b/src/libs/ajm/atrac9_decoder.h index 511e6c559..12ea943dd 100644 --- a/src/libs/ajm/atrac9_decoder.h +++ b/src/libs/ajm/atrac9_decoder.h @@ -5,30 +5,83 @@ #include "libs/ajm/decoder.h" #include +#include #include #include #include #include #include +#include #include extern "C" { +#include "error_codes.h" #include "libatrac9.h" } namespace Libs::Audio::Ajm { -inline int AjmAt9InitDecoder(void* handle, const uint8_t* config_data) { - // Keep the guest configuration unchanged for metadata and decoder resets. - uint8_t config[ATRAC9_CONFIG_DATA_SIZE]; - std::memcpy(config, config_data, sizeof(config)); - const auto channel_config = (config[1] >> 1u) & 7u; - if (channel_config >= 6u) { +struct AjmAt9Config { + std::array decoder_data {}; + uint32_t stream_count = 1; + Atrac9CodecInfo info {}; +}; + +using AjmAt9Handle = std::unique_ptr; + +inline int AjmAt9InitDecoder(void* handle, const uint8_t* config_data, AjmAt9Config& config) { + config = {}; + std::memcpy(config.decoder_data.data(), config_data, config.decoder_data.size()); + if (config_data[0] == 0x30) { + // Discrete multichannel config: reserved:1, rate:3, channels-1:6, reserved:1, + // mono frame bytes-1:11, superframe index:2. + const uint32_t bits = (uint32_t{config_data[1]} << 16u) | + (uint32_t{config_data[2]} << 8u) | config_data[3]; + const uint32_t rate = (bits >> 20u) & 7u; + const uint32_t frame_bytes = ((bits >> 2u) & 0x7ffu) + 1u; + if ((bits & ((1u << 23u) | (1u << 13u))) != 0 || + (rate != 1u && rate != 4u && rate != 7u) || frame_bytes > 96u || + ((bits & 3u) != 0u && (bits & 3u) != 2u)) { + return static_cast(ERR_BAD_CONFIG_DATA); + } + config.stream_count = ((bits >> 14u) & 63u) + 1u; + const uint32_t mono = 0xfe000000u | (bits & 0xf00000u) | + (((bits >> 2u) & 0x7ffu) << 5u) | ((bits & 3u) << 3u); + for (uint32_t i = 0; i < config.decoder_data.size(); ++i) { + config.decoder_data[i] = static_cast(mono >> ((3u - i) * 8u)); + } + } else if (config_data[0] == 0xfe && ((config_data[1] >> 1u) & 7u) >= 6u) { // LibAtrac9 lacks vibration layouts; their blocks match ordinary mono/dual-mono. // Translate only the channel field, preserving the other configuration bits. - config[1] -= 6u << 1u; + config.decoder_data[1] -= 6u << 1u; } - return Atrac9InitDecoder(handle, config); + const int initialized = Atrac9InitDecoder(handle, config.decoder_data.data()); + if (initialized != 0) { + return initialized; + } + const int result = Atrac9GetCodecInfo(handle, &config.info); + if (result != 0) { + return result; + } + if (config.info.channels <= 0 || config.info.samplingRate <= 0 || + config.info.frameSamples <= 0 || config.info.framesInSuperframe <= 0 || + config.info.superframeSize <= 0) { + return static_cast(ERR_BAD_CONFIG_DATA); + } + config.info.channels *= static_cast(config.stream_count); + config.info.superframeSize *= static_cast(config.stream_count); + std::memcpy(config.info.configData, config_data, ATRAC9_CONFIG_DATA_SIZE); + return 0; +} + +inline bool AjmAt9GetCodecInfo(const uint8_t* config_data, Atrac9CodecInfo& info) { + AjmAt9Handle handle(Atrac9GetHandle(), Atrac9ReleaseHandle); + AjmAt9Config config; + if (handle == nullptr || AjmAt9InitDecoder(handle.get(), config_data, config) != 0) { + return false; + } + info = config.info; + return true; } struct AjmDecAt9InitializeParameters { @@ -65,14 +118,7 @@ public: AjmAt9Decoder(uint32_t channels, uint32_t sample_rate, AjmSampleEncoding encoding, uint64_t flags) : AjmDecoder(channels, sample_rate, encoding), - m_parse_riff_header((flags & AJM_INSTANCE_FLAG_DEC_AT9_PARSE_RIFF_HEADER) != 0), - m_handle(Atrac9GetHandle()) {} - - ~AjmAt9Decoder() override { - if (m_handle != nullptr) { - Atrac9ReleaseHandle(m_handle); - } - } + m_parse_riff_header((flags & AJM_INSTANCE_FLAG_DEC_AT9_PARSE_RIFF_HEADER) != 0) {} AjmDecodeResult Initialize(const void* codec_parameters, size_t codec_parameters_size) override { @@ -92,16 +138,13 @@ public: } void Reset() override { - if (m_handle != nullptr) { - Atrac9ReleaseHandle(m_handle); - } - m_handle = Atrac9GetHandle(); - m_num_frames = 0; - m_total_decoded_samples = 0; - m_superframe_bytes_remain = - (m_is_initialized ? static_cast(m_codec_info.superframeSize) : 0); + m_streams.clear(); + m_is_initialized = false; + m_num_frames = 0; + m_total_decoded_samples = 0; + m_superframe_bytes_remain = 0; - if (m_has_config && m_handle != nullptr) { + if (m_has_config) { auto result = MakeResult(); (void)InitializeConfig(m_config_data, &result); } @@ -280,33 +323,41 @@ private: if (config_data == nullptr || result == nullptr) { return false; } - if (m_handle == nullptr) { - m_handle = Atrac9GetHandle(); - } - if (m_handle == nullptr) { + m_is_initialized = false; + m_streams.clear(); + m_streams.resize(1); + m_streams[0].handle.reset(Atrac9GetHandle()); + if (m_streams[0].handle == nullptr) { result->result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_FATAL; return false; } - std::memcpy(m_config_data, config_data, ATRAC9_CONFIG_DATA_SIZE); + std::memmove(m_config_data, config_data, ATRAC9_CONFIG_DATA_SIZE); m_has_config = true; - const int init_ret = AjmAt9InitDecoder(m_handle, m_config_data); + AjmAt9Config config; + const int init_ret = AjmAt9InitDecoder(m_streams[0].handle.get(), m_config_data, config); if (init_ret != 0) { - m_is_initialized = false; result->result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_INVALID_DATA; result->internal_result = init_ret; return false; } - - const int info_ret = Atrac9GetCodecInfo(m_handle, &m_codec_info); - if (info_ret != 0 || m_codec_info.channels <= 0 || m_codec_info.frameSamples <= 0 || - m_codec_info.superframeSize <= 0 || m_codec_info.framesInSuperframe <= 0) { - m_is_initialized = false; - result->result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_INVALID_DATA; - result->internal_result = info_ret; - return false; + m_streams.resize(config.stream_count); + for (size_t i = 1; i < m_streams.size(); ++i) { + m_streams[i].handle.reset(Atrac9GetHandle()); + if (m_streams[i].handle == nullptr) { + result->result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_FATAL; + return false; + } + const int initialized = + Atrac9InitDecoder(m_streams[i].handle.get(), config.decoder_data.data()); + if (initialized != 0) { + result->result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_INVALID_DATA; + result->internal_result = initialized; + return false; + } } + m_codec_info = config.info; m_is_initialized = true; m_num_frames = 0; @@ -315,6 +366,9 @@ private: SetFormat(static_cast(m_codec_info.channels), static_cast(m_codec_info.samplingRate), m_sample_encoding); m_pcm_buffer.assign(FrameOutputBytes(), 0); + m_stream_pcm.resize(m_streams.size() > 1 + ? m_codec_info.frameSamples * AjmBytesPerSample(m_sample_encoding) + : 0); LOGF("AJM ATRAC9 initialized: %d Hz, %d ch, frame_samples=%d, superframe=%d bytes/%d " "frames\n", @@ -324,18 +378,14 @@ private: return true; } - int DecodeFrame(const uint8_t* input, int* bytes_used) { + int DecodeStream(void* handle, const uint8_t* input, uint8_t* output, int* bytes_used) { switch (m_sample_encoding) { case AjmSampleEncoding::S16: - return Atrac9Decode(m_handle, input, - reinterpret_cast(m_pcm_buffer.data()), bytes_used, 0); + return Atrac9Decode(handle, input, reinterpret_cast(output), bytes_used, 0); case AjmSampleEncoding::S32: - return Atrac9DecodeS32(m_handle, input, - reinterpret_cast(m_pcm_buffer.data()), bytes_used, - 0); + return Atrac9DecodeS32(handle, input, reinterpret_cast(output), bytes_used, 0); case AjmSampleEncoding::Float: - return Atrac9DecodeF32( - m_handle, input, reinterpret_cast(m_pcm_buffer.data()), bytes_used, 0); + return Atrac9DecodeF32(handle, input, reinterpret_cast(output), bytes_used, 0); default: EXIT("unsupported AJM PCM sample encoding %u\n", static_cast(m_sample_encoding)); @@ -343,6 +393,48 @@ private: return -1; } + template + void InterleaveStream(size_t channel) { + for (int sample = 0; sample < m_codec_info.frameSamples; ++sample) { + std::memcpy(m_pcm_buffer.data() + (sample * m_streams.size() + channel) * SampleBytes, + m_stream_pcm.data() + sample * SampleBytes, SampleBytes); + } + } + + int DecodeFrame(const uint8_t* input, int* bytes_used) { + if (m_streams.size() == 1) { + return DecodeStream(m_streams[0].handle.get(), input, m_pcm_buffer.data(), bytes_used); + } + const auto channels = m_streams.size(); + const auto sample_bytes = AjmBytesPerSample(m_sample_encoding); + const auto stream_size = static_cast(m_codec_info.superframeSize / channels); + const bool last_frame = m_num_frames + 1u == + static_cast(m_codec_info.framesInSuperframe); + *bytes_used = 0; + for (size_t channel = 0; channel < channels; ++channel) { + auto& stream = m_streams[channel]; + if (stream.bytes_used >= stream_size) { + return static_cast(ERR_BAD_CONFIG_DATA); + } + int used = 0; + const int result = DecodeStream(stream.handle.get(), input + *bytes_used, + m_stream_pcm.data(), &used); + if (result != 0) { + return result; + } + if (used <= 0 || static_cast(used) > stream_size - stream.bytes_used) { + return static_cast(ERR_BAD_CONFIG_DATA); + } + // DMC interleaves mono frames. On the last frame each channel's padding + // precedes the next channel, rather than following the aggregate frame. + *bytes_used += last_frame ? stream_size - stream.bytes_used : used; + stream.bytes_used = last_frame ? 0 : stream.bytes_used + used; + if (sample_bytes == 2) InterleaveStream<2>(channel); + else InterleaveStream<4>(channel); + } + return 0; + } + uint64_t CopyDecodedFrame(uint8_t* output, size_t output_size, size_t* output_written, AjmGaplessState* gapless) { EXIT_IF(output_written == nullptr); @@ -535,12 +627,17 @@ private: bool m_parse_riff_header = false; bool m_is_initialized = false; bool m_has_config = false; - void* m_handle = nullptr; + struct Stream { + AjmAt9Handle handle {nullptr, Atrac9ReleaseHandle}; + uint32_t bytes_used = 0; + }; + std::vector m_streams; uint8_t m_config_data[ATRAC9_CONFIG_DATA_SIZE] {}; uint32_t m_superframe_bytes_remain = 0; uint32_t m_num_frames = 0; Atrac9CodecInfo m_codec_info {}; std::vector m_pcm_buffer; + std::vector m_stream_pcm; }; } // namespace Libs::Audio::Ajm diff --git a/src/libs/ngs2.cpp b/src/libs/ngs2.cpp index e92cfb9ca..fbb4ed9f9 100644 --- a/src/libs/ngs2.cpp +++ b/src/libs/ngs2.cpp @@ -1931,16 +1931,7 @@ static bool Ngs2FourCcEquals(const uint8_t* data, const char* four_cc) { static bool Ngs2GetAtrac9CodecInfo(const std::array& config, Atrac9CodecInfo& codec) { - void* decoder = Atrac9GetHandle(); - const bool valid = decoder != nullptr && Ajm::AjmAt9InitDecoder(decoder, config.data()) == 0 && - Atrac9GetCodecInfo(decoder, &codec) == 0 && codec.channels > 0 && - codec.samplingRate > 0 && codec.superframeSize > 0 && - codec.framesInSuperframe > 0 && codec.frameSamples > 0 && - codec.superframeSize % codec.framesInSuperframe == 0; - if (decoder != nullptr) { - Atrac9ReleaseHandle(decoder); - } - return valid; + return Ajm::AjmAt9GetCodecInfo(config.data(), codec); } static int Ngs2ParseAtrac9Riff(const void* data, size_t data_size, Ngs2WaveformInfo* info) { diff --git a/tests/Ngs2SamplerTests.cpp b/tests/Ngs2SamplerTests.cpp index 13b92cdd3..2db528a14 100644 --- a/tests/Ngs2SamplerTests.cpp +++ b/tests/Ngs2SamplerTests.cpp @@ -383,6 +383,186 @@ void TestAtrac9Vibration() { } } +void TestAtrac9Discrete() { + using namespace Libs::Audio::Ajm; + constexpr std::array captured {0x30, 0x72, 0xc0, 0xfe}; + struct Geometry { uint32_t channels, rate_index, rate, samples, frames, bytes; }; + for (const auto [channels, rate_index, rate, samples, frames, bytes]: + {Geometry {12, 7, 48000, 256, 4, 64}, Geometry {16, 4, 24000, 128, 1, 96}, + Geometry {64, 1, 12000, 64, 4, 96}}) { + const uint32_t word = 0x30000000u | (rate_index << 20u) | ((channels - 1u) << 14u) | + ((bytes - 1u) << 2u) | (frames == 4 ? 2u : 0u); + std::array config {uint8_t(word >> 24u), uint8_t(word >> 16u), + uint8_t(word >> 8u), uint8_t(word)}; + const auto original = config; + AjmAt9Decoder decoder(channels, rate, AjmSampleEncoding::S16, 0); + const auto initialized = decoder.Initialize(config.data(), config.size()); + AjmSidebandDecAt9CodecInfo info {}; + decoder.WriteCodecInfo(&info, sizeof(info), initialized); + Atrac9CodecInfo parsed {}; + Check(initialized.result == OK && initialized.format.channel_num == channels && + initialized.format.sampling_frequency == rate && info.frame_samples == samples && + info.frames_in_super_frame == frames && info.super_frame_size == bytes * frames * channels && + Ngs2GetAtrac9CodecInfo(config, parsed) && parsed.channels == channels && + parsed.samplingRate == rate && parsed.frameSamples == samples && + parsed.framesInSuperframe == frames && parsed.superframeSize == bytes * frames * channels && + config == original && (channels != 12 || config == captured), + "ATRAC9 discrete metadata or channel limit is incorrect"); + } + + // The existing mono fixture's constant gradient/scalefactors give 24 nine-bit + // coefficients. Ignored extension bits vary frame lengths independently of PCM. + std::array, 4>, 12> blocks; + std::array channel_bytes {}; + std::array frame_ends {}; + std::vector encoded; + for (unsigned frame = 0; frame < 4; ++frame) { + for (unsigned channel = 0; channel < 12; ++channel) { + auto& block = blocks[channel][frame]; + unsigned position = 0; + auto put = [&](unsigned value, unsigned bits) { + for (unsigned bit = bits; bit != 0; --bit) { + if (position % 8 == 0) block.push_back(0); + block.back() |= ((value >> (bit - 1)) & 1u) << (7u - position++ % 8u); + } + }; + put(frame != 0, 1); // First frame, fresh band parameters, three bands, no BEX. + put(0, 6); + put(0, 14); // Gradient mode/start/end, then constant value 16. + put(16, 5); + put(16, 5); + put(0, 4); + put(1, 1); // Optional ignored extension payload of 0/8/16/24 bits. + put(0, 2); + put((channel % 4) * 8, 5); + put(0, (channel % 4) * 8); + put(1, 2); // CLC scale factors: two bits each, base 24. + put(0, 2); + put(24, 5); + for (unsigned unit = 0; unit < 10; ++unit) put(0, 2); + const int coefficient = int((channel + 1) * (frame + 1) * 4); + put(unsigned(channel % 2 ? -coefficient : coefficient) & 0x1ffu, 9); + for (unsigned coefficient = 1; coefficient < 24; ++coefficient) put(0, 9); + encoded.insert(encoded.end(), block.begin(), block.end()); + channel_bytes[channel] += block.size(); + if (frame == 3) { + Check(channel_bytes[channel] < 256, "ATRAC9 test channel has no padding"); + encoded.insert(encoded.end(), 256 - channel_bytes[channel], 0x01); + } + } + frame_ends[frame] = encoded.size(); + } + Check(encoded.size() == 3072 && blocks[0][0].size() != blocks[3][0].size(), + "ATRAC9 discrete fixture did not exercise distinct channel padding"); + + for (auto encoding: {AjmSampleEncoding::S16, AjmSampleEncoding::S32, + AjmSampleEncoding::Float}) { + const size_t sample_bytes = AjmBytesPerSample(encoding); + const size_t row_bytes = 12 * sample_bytes; + const size_t frame_bytes = 256 * row_bytes; + std::vector expected(4 * frame_bytes), mono(256 * sample_bytes); + for (unsigned channel = 0; channel < 12; ++channel) { + auto reference = std::unique_ptr( + Atrac9GetHandle(), Atrac9ReleaseHandle); + std::array mono_config {0xfe, 0x70, 0x07, 0xf0}; + Check(reference && Atrac9InitDecoder(reference.get(), mono_config.data()) == 0, + "ATRAC9 discrete mono reference initialization failed"); + for (unsigned frame = 0; frame < 4; ++frame) { + int used = 0, result = -1; + std::array input {}; // LibAtrac9's bit reader peeks past the final byte. + std::copy(blocks[channel][frame].begin(), blocks[channel][frame].end(), input.begin()); + switch (encoding) { + case AjmSampleEncoding::S16: + result = Atrac9Decode(reference.get(), input.data(), + reinterpret_cast(mono.data()), &used, 0); + break; + case AjmSampleEncoding::S32: + result = Atrac9DecodeS32(reference.get(), input.data(), + reinterpret_cast(mono.data()), &used, 0); + break; + case AjmSampleEncoding::Float: + result = Atrac9DecodeF32(reference.get(), input.data(), + reinterpret_cast(mono.data()), &used, 0); + break; + } + Check(result == 0 && used == blocks[channel][frame].size(), + "ATRAC9 discrete mono reference did not consume its exact frame"); + for (unsigned sample = 0; sample < 256; ++sample) { + std::memcpy(expected.data() + (frame * 256 + sample) * row_bytes + + channel * sample_bytes, + mono.data() + sample * sample_bytes, sample_bytes); + } + } + } + Check(std::ranges::any_of(expected, [](uint8_t byte) { return byte != 0; }), + "ATRAC9 discrete reference unexpectedly contains only silence"); + AjmAt9Decoder decoder(12, 48000, encoding, 0); + Check(decoder.Initialize(captured.data(), captured.size()).result == OK, + "ATRAC9 discrete decoder initialization failed"); + std::vector actual(expected.size() + 32, 0xa5); + auto decode = [&](size_t input_size, size_t output_size) { + return decoder.Decode(encoded.data(), input_size, actual.data() + 16, + output_size, true, nullptr); + }; + const auto partial = decode(encoded.size() - 1, expected.size()); + const auto short_output = decode(encoded.size(), frame_bytes - 1); + Check(partial.result == AJM_RESULT_PARTIAL_INPUT && partial.frames == 0 && + partial.input_consumed == 0 && partial.output_written == 0 && + short_output.result == AJM_RESULT_NOT_ENOUGH_ROOM && short_output.frames == 0 && + short_output.input_consumed == 0 && short_output.output_written == 0 && + std::ranges::all_of(actual, [](uint8_t byte) { return byte == 0xa5; }), + "ATRAC9 discrete partial input/output consumed data or wrote PCM"); + for (unsigned pass = 0; pass < 2; ++pass) { + if (pass != 0) decoder.Reset(); + const auto result = decode(encoded.size(), expected.size()); + Check(result.result == OK && result.frames == 4 && + result.input_consumed == encoded.size() && + result.output_written == expected.size() && result.total_decoded_samples == 1024 && + std::equal(expected.begin(), expected.end(), actual.begin() + 16) && + std::all_of(actual.begin(), actual.begin() + 16, [](auto b) { return b == 0xa5; }) && + std::all_of(actual.end() - 16, actual.end(), [](auto b) { return b == 0xa5; }), + "ATRAC9 discrete interleave, per-channel padding, reset or PCM bounds differ"); + } + decoder.Reset(); + size_t offset = 0; + for (unsigned frame = 0; frame < 4; ++frame) { + const auto result = decoder.Decode(encoded.data() + offset, encoded.size() - offset, + actual.data() + 16, frame_bytes, false, nullptr); + Check(result.result == OK && result.frames == 1 && + result.input_consumed == frame_ends[frame] - offset && + result.output_written == frame_bytes && + std::equal(expected.begin() + frame * frame_bytes, + expected.begin() + (frame + 1) * frame_bytes, actual.begin() + 16), + "ATRAC9 discrete split calls lost channel histories or padding position"); + offset = frame_ends[frame]; + } + decoder.Reset(); + AjmGaplessState gapless; + gapless.Set({700, 17, 0}, true); + const auto trimmed = decoder.Decode(encoded.data(), encoded.size(), actual.data() + 16, + 700 * row_bytes, true, &gapless); + Check(trimmed.result == OK && trimmed.frames == 4 && + trimmed.input_consumed == encoded.size() && trimmed.output_written == 700 * row_bytes && + trimmed.total_decoded_samples == 700 && gapless.IsEnd() && + std::equal(expected.begin() + 17 * row_bytes, + expected.begin() + 717 * row_bytes, actual.begin() + 16), + "ATRAC9 discrete gapless trim or superframe drain changed channel order"); + } + for (auto invalid: {std::array {0x31, 0x72, 0xc0, 0xfe}, + std::array {0x30, 0xf2, 0xc0, 0xfe}, + std::array {0x30, 0x62, 0xc0, 0xfe}, + std::array {0x30, 0x72, 0xe0, 0xfe}, + std::array {0x30, 0x72, 0xc1, 0x82}, + std::array {0x30, 0x72, 0xc0, 0xfd}, + std::array {0x30, 0x72, 0xc0, 0xff}}) { + Atrac9CodecInfo info {}; + AjmAt9Decoder decoder(12, 48000, AjmSampleEncoding::S16, 0); + Check(!Ngs2GetAtrac9CodecInfo(invalid, info) && + decoder.Initialize(invalid.data(), invalid.size()).result != OK, + "ATRAC9 discrete accepted unsupported configuration fields"); + } +} + void TestAtrac9ResampleAcrossFrames() { const auto make_voice = [] { auto f = std::make_unique(24000); @@ -1425,6 +1605,7 @@ int main() { TestWaveformQueueCapacity(); TestWaveformRingReuse(); TestAtrac9Vibration(); + TestAtrac9Discrete(); TestPcm16RenderOutput(); TestSubmixerSetupAndRouting(); TestWaveformBlockGeometry();