Implement an ADPCM decoder for audren

This is used by many retail games, it only supports mono sound.

This implementation is based on Ryu's.
This commit is contained in:
Billy Laws 2020-07-07 15:35:34 +01:00 committed by ◱ PixelyIon
parent b1e15efbab
commit 30936ce6dc
5 changed files with 114 additions and 2 deletions

View File

@ -37,6 +37,7 @@ add_library(skyline SHARED
${source_DIR}/skyline/audio.cpp
${source_DIR}/skyline/audio/track.cpp
${source_DIR}/skyline/audio/resampler.cpp
${source_DIR}/skyline/audio/adpcm_decoder.cpp
${source_DIR}/skyline/gpu.cpp
${source_DIR}/skyline/gpu/texture.cpp
${source_DIR}/skyline/os.cpp

View File

@ -0,0 +1,54 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
#include <array>
#include <common.h>
#include "common.h"
#include "adpcm_decoder.h"
namespace skyline::audio {
AdpcmDecoder::AdpcmDecoder(const std::vector<std::array<i16, 2>> &coefficients) : coefficients(coefficients) {}
std::vector<i16> AdpcmDecoder::Decode(const std::vector<u8> &adpcmData) {
constexpr size_t BytesPerFrame = 0x8; //!< The number of bytes in a single frame
constexpr size_t SamplesPerFrame = 0xe; //!< The number of samples in a single frame
size_t remainingSamples = (adpcmData.size() / BytesPerFrame) * SamplesPerFrame;
std::vector<i16> output;
output.reserve(remainingSamples);
size_t inputOffset{};
while (inputOffset < adpcmData.size()) {
FrameHeader header(adpcmData[inputOffset++]);
size_t frameSamples = std::min(SamplesPerFrame, remainingSamples);
i32 ctx{};
for (size_t index = 0; index < frameSamples; index++) {
i32 sample{};
if (index & 1) {
sample = (ctx << 28) >> 28;
} else {
ctx = adpcmData[inputOffset++];
sample = (ctx << 24) >> 28;
}
i32 prediction = history[0] * coefficients[header.coefficientIndex][0] + history[1] * coefficients[header.coefficientIndex][1];
sample = (sample * (0x800 << header.scale) + prediction + 0x400) >> 11;
auto saturated = audio::Saturate<i16, i32>(sample);
output.push_back(saturated);
history[1] = history[0];
history[0] = saturated;
}
remainingSamples -= frameSamples;
}
return output;
}
}

View File

@ -0,0 +1,42 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
#pragma once
#include <common.h>
namespace skyline::audio {
/**
* @brief The AdpcmDecoder class handles decoding single channel adaptive differential PCM data
*/
class AdpcmDecoder {
private:
/**
* @brief This struct holds a single ADPCM frame header
*/
union FrameHeader {
struct {
u8 scale : 4; //!< The scale factor for this frame
u8 coefficientIndex : 3; //!< The index of the coeffcients to use for this frame
u8 _pad_ :1;
};
u8 raw; //!< The raw value
FrameHeader(u8 raw) : raw(raw) {}
};
static_assert(sizeof(FrameHeader) == 0x1);
std::array<i32, 2> history{}; //!< This contains the history for decoding the ADPCM stream
std::vector<std::array<i16, 2>> coefficients; //!< This contains the coefficients for decoding the ADPCM stream
public:
AdpcmDecoder(const std::vector<std::array<i16, 2>> &coefficients);
/**
* @brief This decodes a buffer of ADPCM data into I16 PCM
* @param adpcmData A buffer containing the raw ADPCM data
* @return A buffer containing decoded single channel I16 PCM data
*/
std::vector<i16> Decode(const std::vector<u8> &adpcmData);
};
}

View File

@ -30,17 +30,24 @@ namespace skyline::service::audio::IAudioRenderer {
return;
if (input.firstUpdate) {
if (input.format != skyline::audio::AudioFormat::Int16)
if (input.format != skyline::audio::AudioFormat::Int16 && input.format != skyline::audio::AudioFormat::ADPCM)
throw exception("Unsupported voice PCM format: {}", input.format);
format = input.format;
sampleRate = input.sampleRate;
if (input.channelCount > 2)
if (input.channelCount > (input.format == skyline::audio::AudioFormat::ADPCM ? 1 : 2))
throw exception("Unsupported voice channel count: {}", input.channelCount);
channelCount = static_cast<u8>(input.channelCount);
if (input.format == skyline::audio::AudioFormat::ADPCM) {
std::vector<std::array<i16, 2>> adpcmCoefficients(input.adpcmCoeffsSize / (sizeof(u16) * 2));
state.process->ReadMemory(adpcmCoefficients.data(), input.adpcmCoeffsPosition, input.adpcmCoeffsSize);
adpcmDecoder = skyline::audio::AdpcmDecoder(adpcmCoefficients);
}
SetWaveBufferIndex(static_cast<u8>(input.baseWaveBufferIndex));
}
@ -60,6 +67,12 @@ namespace skyline::service::audio::IAudioRenderer {
samples.resize(currentBuffer.size / sizeof(i16));
state.process->ReadMemory(samples.data(), currentBuffer.address, currentBuffer.size);
break;
case skyline::audio::AudioFormat::ADPCM: {
std::vector<u8> adpcmData(currentBuffer.size);
state.process->ReadMemory(adpcmData.data(), currentBuffer.address, currentBuffer.size);
samples = adpcmDecoder->Decode(adpcmData);
}
break;
default:
throw exception("Unsupported PCM format used by Voice: {}", format);
}

View File

@ -5,6 +5,7 @@
#include <array>
#include <audio/resampler.h>
#include <audio/adpcm_decoder.h>
#include <audio.h>
#include <common.h>
@ -91,6 +92,7 @@ namespace skyline::service::audio::IAudioRenderer {
std::array<WaveBuffer, 4> waveBuffers; //!< An array containing the state of all four wave buffers
std::vector<i16> samples; //!< A vector containing processed sample data
skyline::audio::Resampler resampler; //!< The resampler object used for changing the sample rate of a stream
std::optional<skyline::audio::AdpcmDecoder> adpcmDecoder; //!< The decoder object used for decoding ADPCM encoded samples
bool acquired{false}; //!< If the voice is in use
bool bufferReload{true}; //!< If the buffer needs to be updated