mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-19 18:36:53 +02:00
audio: decode PS5 ATRAC9 discrete multichannel streams
Co-authored-by: fxpw <fxpw@outlook.com>
This commit is contained in:
+1
-13
@@ -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<const uint8_t*>(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<const uint8_t*>(config_data), codec_info)) {
|
||||
return AJM_ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
|
||||
|
||||
+146
-49
@@ -5,30 +5,83 @@
|
||||
#include "libs/ajm/decoder.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cinttypes>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
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<uint8_t, ATRAC9_CONFIG_DATA_SIZE> decoder_data {};
|
||||
uint32_t stream_count = 1;
|
||||
Atrac9CodecInfo info {};
|
||||
};
|
||||
|
||||
using AjmAt9Handle = std::unique_ptr<void, decltype(&Atrac9ReleaseHandle)>;
|
||||
|
||||
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<int>(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<uint8_t>(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<int>(ERR_BAD_CONFIG_DATA);
|
||||
}
|
||||
config.info.channels *= static_cast<int>(config.stream_count);
|
||||
config.info.superframeSize *= static_cast<int>(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<uint32_t>(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<uint32_t>(m_codec_info.channels),
|
||||
static_cast<uint32_t>(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<int16_t*>(m_pcm_buffer.data()), bytes_used, 0);
|
||||
return Atrac9Decode(handle, input, reinterpret_cast<int16_t*>(output), bytes_used, 0);
|
||||
case AjmSampleEncoding::S32:
|
||||
return Atrac9DecodeS32(m_handle, input,
|
||||
reinterpret_cast<int32_t*>(m_pcm_buffer.data()), bytes_used,
|
||||
0);
|
||||
return Atrac9DecodeS32(handle, input, reinterpret_cast<int32_t*>(output), bytes_used, 0);
|
||||
case AjmSampleEncoding::Float:
|
||||
return Atrac9DecodeF32(
|
||||
m_handle, input, reinterpret_cast<float*>(m_pcm_buffer.data()), bytes_used, 0);
|
||||
return Atrac9DecodeF32(handle, input, reinterpret_cast<float*>(output), bytes_used, 0);
|
||||
default:
|
||||
EXIT("unsupported AJM PCM sample encoding %u\n",
|
||||
static_cast<uint32_t>(m_sample_encoding));
|
||||
@@ -343,6 +393,48 @@ private:
|
||||
return -1;
|
||||
}
|
||||
|
||||
template <size_t SampleBytes>
|
||||
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<uint32_t>(m_codec_info.superframeSize / channels);
|
||||
const bool last_frame = m_num_frames + 1u ==
|
||||
static_cast<uint32_t>(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<int>(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<uint32_t>(used) > stream_size - stream.bytes_used) {
|
||||
return static_cast<int>(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<Stream> 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<uint8_t> m_pcm_buffer;
|
||||
std::vector<uint8_t> m_stream_pcm;
|
||||
};
|
||||
|
||||
} // namespace Libs::Audio::Ajm
|
||||
|
||||
+1
-10
@@ -1931,16 +1931,7 @@ static bool Ngs2FourCcEquals(const uint8_t* data, const char* four_cc) {
|
||||
|
||||
static bool Ngs2GetAtrac9CodecInfo(const std::array<uint8_t, ATRAC9_CONFIG_DATA_SIZE>& 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) {
|
||||
|
||||
@@ -383,6 +383,186 @@ void TestAtrac9Vibration() {
|
||||
}
|
||||
}
|
||||
|
||||
void TestAtrac9Discrete() {
|
||||
using namespace Libs::Audio::Ajm;
|
||||
constexpr std::array<uint8_t, 4> 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<uint8_t, 4> 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<std::array<std::vector<uint8_t>, 4>, 12> blocks;
|
||||
std::array<size_t, 12> channel_bytes {};
|
||||
std::array<size_t, 4> frame_ends {};
|
||||
std::vector<uint8_t> 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<uint8_t> expected(4 * frame_bytes), mono(256 * sample_bytes);
|
||||
for (unsigned channel = 0; channel < 12; ++channel) {
|
||||
auto reference = std::unique_ptr<void, decltype(&Atrac9ReleaseHandle)>(
|
||||
Atrac9GetHandle(), Atrac9ReleaseHandle);
|
||||
std::array<uint8_t, 4> 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<uint8_t, 64> 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<int16_t*>(mono.data()), &used, 0);
|
||||
break;
|
||||
case AjmSampleEncoding::S32:
|
||||
result = Atrac9DecodeS32(reference.get(), input.data(),
|
||||
reinterpret_cast<int32_t*>(mono.data()), &used, 0);
|
||||
break;
|
||||
case AjmSampleEncoding::Float:
|
||||
result = Atrac9DecodeF32(reference.get(), input.data(),
|
||||
reinterpret_cast<float*>(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<uint8_t> 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<uint8_t, 4> {0x31, 0x72, 0xc0, 0xfe},
|
||||
std::array<uint8_t, 4> {0x30, 0xf2, 0xc0, 0xfe},
|
||||
std::array<uint8_t, 4> {0x30, 0x62, 0xc0, 0xfe},
|
||||
std::array<uint8_t, 4> {0x30, 0x72, 0xe0, 0xfe},
|
||||
std::array<uint8_t, 4> {0x30, 0x72, 0xc1, 0x82},
|
||||
std::array<uint8_t, 4> {0x30, 0x72, 0xc0, 0xfd},
|
||||
std::array<uint8_t, 4> {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<Fixture>(24000);
|
||||
@@ -1425,6 +1605,7 @@ int main() {
|
||||
TestWaveformQueueCapacity();
|
||||
TestWaveformRingReuse();
|
||||
TestAtrac9Vibration();
|
||||
TestAtrac9Discrete();
|
||||
TestPcm16RenderOutput();
|
||||
TestSubmixerSetupAndRouting();
|
||||
TestWaveformBlockGeometry();
|
||||
|
||||
Reference in New Issue
Block a user