Add support for audio renderer services

Audren is used by many games and homebrews to make use of more advanced
audio features than audout.
This commit is contained in:
Billy Laws 2020-01-24 22:04:16 +00:00 committed by ◱ PixelyIon
parent ddd1fc61ef
commit bbe61e37f2
15 changed files with 922 additions and 0 deletions

View File

@ -53,6 +53,10 @@ add_library(skyline SHARED
${source_DIR}/skyline/services/sm/sm.cpp ${source_DIR}/skyline/services/sm/sm.cpp
${source_DIR}/skyline/services/fatal/fatal.cpp ${source_DIR}/skyline/services/fatal/fatal.cpp
${source_DIR}/skyline/services/audout/audout.cpp ${source_DIR}/skyline/services/audout/audout.cpp
${source_DIR}/skyline/services/audren/IAudioRendererManager.cpp
${source_DIR}/skyline/services/audren/IAudioRenderer.cpp
${source_DIR}/skyline/services/audren/voice.cpp
${source_DIR}/skyline/services/audren/memoryPool.cpp
${source_DIR}/skyline/services/set/sys.cpp ${source_DIR}/skyline/services/set/sys.cpp
${source_DIR}/skyline/services/apm/apm.cpp ${source_DIR}/skyline/services/apm/apm.cpp
${source_DIR}/skyline/services/am/applet.cpp ${source_DIR}/skyline/services/am/applet.cpp

View File

@ -0,0 +1,186 @@
#include "IAudioRenderer.h"
#include <kernel/types/KProcess.h>
namespace skyline::service::audren {
IAudioRenderer::IAudioRenderer(const DeviceState &state, ServiceManager &manager, AudioRendererParams &params) : releaseEvent(std::make_shared<type::KEvent>(state)), rendererParams(params), BaseService(state, manager, false, Service::IAudioRenderer, {
{0x0, SFUNC(IAudioRenderer::GetSampleRate)},
{0x1, SFUNC(IAudioRenderer::GetSampleCount)},
{0x2, SFUNC(IAudioRenderer::GetMixBufferCount)},
{0x3, SFUNC(IAudioRenderer::GetState)},
{0x4, SFUNC(IAudioRenderer::RequestUpdate)},
{0x5, SFUNC(IAudioRenderer::Start)},
{0x6, SFUNC(IAudioRenderer::Stop)},
{0x7, SFUNC(IAudioRenderer::QuerySystemEvent)},
}) {
track = state.audio->OpenTrack(audio::constant::ChannelCount, params.sampleRate, [this]() { this->releaseEvent->Signal(); });
track->Start();
samplesPerBuffer = state.settings->GetInt("audren_buffer_size");
memoryPoolCount = rendererParams.effectCount + rendererParams.voiceCount * 4;
memoryPools.resize(memoryPoolCount);
effects.resize(rendererParams.effectCount);
voices.resize(rendererParams.voiceCount, Voice(state));
// Fill track with empty samples that we will triple buffer
track->AppendBuffer(std::vector<i16>(), 0);
track->AppendBuffer(std::vector<i16>(), 1);
track->AppendBuffer(std::vector<i16>(), 2);
}
IAudioRenderer::~IAudioRenderer() {
state.audio->CloseTrack(track);
}
void IAudioRenderer::GetSampleRate(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
response.Push<u32>(rendererParams.sampleRate);
}
void IAudioRenderer::GetSampleCount(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
response.Push<u32>(rendererParams.sampleCount);
}
void IAudioRenderer::GetMixBufferCount(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
response.Push<u32>(rendererParams.subMixCount);
}
void IAudioRenderer::GetState(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
response.Push(static_cast<u32>(playbackState));
}
void IAudioRenderer::RequestUpdate(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
u64 inputAddress = request.inputBuf.at(0).address;
auto inputHeader = state.process->GetObject<UpdateDataHeader>(inputAddress);
revisionInfo.SetUserRevision(inputHeader.revision);
inputAddress += sizeof(UpdateDataHeader);
inputAddress += inputHeader.behaviorSize; // Unused
std::vector<MemoryPoolIn> memoryPoolsIn(memoryPoolCount);
state.process->ReadMemory(memoryPoolsIn.data(), inputAddress, memoryPoolCount * sizeof(MemoryPoolIn));
inputAddress += inputHeader.memoryPoolSize;
for (int i = 0; i < memoryPoolsIn.size(); i++)
memoryPools[i].ProcessInput(memoryPoolsIn[i]);
inputAddress += inputHeader.voiceResourceSize;
std::vector<VoiceIn> voicesIn(rendererParams.voiceCount);
state.process->ReadMemory(voicesIn.data(), inputAddress, rendererParams.voiceCount * sizeof(VoiceIn));
inputAddress += inputHeader.voiceSize;
for (int i = 0; i < voicesIn.size(); i++)
voices[i].ProcessInput(voicesIn[i]);
std::vector<EffectIn> effectsIn(rendererParams.effectCount);
state.process->ReadMemory(effectsIn.data(), inputAddress, rendererParams.effectCount * sizeof(EffectIn));
for (int i = 0; i < effectsIn.size(); i++)
effects[i].ProcessInput(effectsIn[i]);
UpdateAudio();
UpdateDataHeader outputHeader {
.revision = constant::RevMagic,
.behaviorSize = 0xb0,
.memoryPoolSize = (rendererParams.effectCount + rendererParams.voiceCount * 4) * static_cast<u32>(sizeof(MemoryPoolOut)),
.voiceSize = rendererParams.voiceCount * static_cast<u32>(sizeof(VoiceOut)),
.effectSize = rendererParams.effectCount * static_cast<u32>(sizeof(EffectOut)),
.sinkSize = rendererParams.sinkCount * 0x20,
.performanceManagerSize = 0x10,
.elapsedFrameCountInfoSize = 0x0
};
if (revisionInfo.ElapsedFrameCountSupported())
outputHeader.elapsedFrameCountInfoSize = 0x10;
outputHeader.totalSize = sizeof(UpdateDataHeader) +
outputHeader.behaviorSize +
outputHeader.memoryPoolSize +
outputHeader.voiceSize +
outputHeader.effectSize +
outputHeader.sinkSize +
outputHeader.performanceManagerSize +
outputHeader.elapsedFrameCountInfoSize;
u64 outputAddress = request.outputBuf.at(0).address;
state.process->WriteMemory(outputHeader, outputAddress);
outputAddress += sizeof(UpdateDataHeader);
for (auto &memoryPool : memoryPools) {
state.process->WriteMemory(memoryPool.output, outputAddress);
outputAddress += sizeof(MemoryPoolOut);
}
for (auto &voice : voices) {
state.process->WriteMemory(voice.output, outputAddress);
outputAddress += sizeof(VoiceOut);
}
for (auto &effect : effects) {
state.process->WriteMemory(effect.output, outputAddress);
outputAddress += sizeof(EffectOut);
}
}
void IAudioRenderer::UpdateAudio() {
std::vector<u64> released = track->GetReleasedBuffers(2);
for (auto &tag : released) {
MixFinalBuffer();
track->AppendBuffer(sampleBuffer, tag);
}
}
void IAudioRenderer::MixFinalBuffer() {
int setIndex = 0;
sampleBuffer.resize(samplesPerBuffer * audio::constant::ChannelCount);
for (auto &voice : voices) {
if (!voice.Playable())
continue;
int bufferOffset = 0;
int pendingSamples = samplesPerBuffer;
while (pendingSamples > 0) {
int voiceBufferSize = 0;
int voiceBufferOffset = 0;
std::vector<i16> &voiceSamples = voice.GetBufferData(pendingSamples, voiceBufferOffset, voiceBufferSize);
if (voiceBufferSize == 0)
break;
pendingSamples -= voiceBufferSize / audio::constant::ChannelCount;
for (int i = voiceBufferOffset; i < voiceBufferOffset + voiceBufferSize; i++) {
if (setIndex == bufferOffset) {
sampleBuffer[bufferOffset] = static_cast<i16>(std::clamp(static_cast<int>(static_cast<float>(voiceSamples[i]) *
voice.volume), static_cast<int>(std::numeric_limits<i16>::min()), static_cast<int>(std::numeric_limits<i16>::max())));
setIndex++;
} else {
sampleBuffer[bufferOffset] += static_cast<i16>(std::clamp(static_cast<int>(sampleBuffer[voiceSamples[i]]) +
static_cast<int>(static_cast<float>(voiceSamples[i]) * voice.volume),
static_cast<int>(std::numeric_limits<i16>::min()), static_cast<int>(std::numeric_limits<i16>::max())));
}
bufferOffset++;
}
}
}
}
void IAudioRenderer::Start(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
playbackState = audio::AudioOutState::Started;
}
void IAudioRenderer::Stop(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
playbackState = audio::AudioOutState::Stopped;
}
void IAudioRenderer::QuerySystemEvent(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
auto handle = state.process->InsertItem(releaseEvent);
state.logger->Debug("Audren Buffer Release Event Handle: 0x{:X}", handle);
response.copyHandles.push_back(handle);
}
}

View File

@ -0,0 +1,135 @@
#pragma once
#include <audio.h>
#include <services/base_service.h>
#include <services/serviceman.h>
#include <kernel/types/KEvent.h>
#include "memoryPool.h"
#include "effect.h"
#include "voice.h"
#include "revisionInfo.h"
namespace skyline::service::audren {
namespace constant {
constexpr int BufferAlignment = 0x40; //!< The alignment for all audren buffers
}
/**
* @brief The parameters used to configure an IAudioRenderer
*/
struct AudioRendererParams {
u32 sampleRate; //!< The sample rate to use for the renderer
u32 sampleCount; //!< The buffer sample count
u32 mixBufferCount; //!< The amount of mix buffers to use
u32 subMixCount; //!< The amount of sub mixes to use
u32 voiceCount; //!< The amount of voices to use
u32 sinkCount; //!< The amount of sinks to use
u32 effectCount; //!< The amount of effects to use
u32 performanceManagerCount; //!< The amount of performance managers to use
u32 voiceDropEnable; //!< Whether to enable voice drop
u32 splitterCount; //!< The amount of splitters to use
u32 splitterDestinationDataCount; //!< The amount of splitter destination outputs to use
u32 _unk0_;
u32 revision; //!< The revision of audren to use
};
static_assert(sizeof(AudioRendererParams) == 0x34);
/**
* @brief Header containing information about the software side audren implementation
*/
struct UpdateDataHeader {
u32 revision; //!< Revision of the software implementation
u32 behaviorSize; //!< The total size of the behaviour info
u32 memoryPoolSize; //!< The total size of all MemoryPoolIn structs
u32 voiceSize; //!< The total size of all VoiceIn structs
u32 voiceResourceSize; //!< The total size of the voice resources
u32 effectSize; //!< The total size of all EffectIn structs
u32 mixSize; //!< The total size of all mixer descriptors in the input
u32 sinkSize; //!< The total size of all sink descriptors in the input
u32 performanceManagerSize; //!< The total size of all performance manager descriptors in the input
u32 _unk0_;
u32 elapsedFrameCountInfoSize; //!< The total size of all the elapsed frame info
u32 _unk1_[4];
u32 totalSize; //!< The total size of the whole input
};
static_assert(sizeof(UpdateDataHeader) == 0x40);
/**
* @brief IAudioRenderer is used to control an audio renderer output (https://switchbrew.org/wiki/Audio_services#IAudioRenderer)
*/
class IAudioRenderer : public BaseService {
private:
std::shared_ptr<audio::AudioTrack> track; //!< The audio track associated with the audio renderer
AudioRendererParams rendererParams; //!< The parameters to use for the renderer
std::shared_ptr<type::KEvent> releaseEvent; //!< The KEvent that is signalled when a buffer has been released
std::vector<MemoryPool> memoryPools; //!< An vector of all memory pools that the guest may need
std::vector<Effect> effects; //!< An vector of all effects that the guest may need
std::vector<Voice> voices; //!< An vector of all voices that the guest may need
std::vector<i16> sampleBuffer; //!< The final output data that is appended to the stream
RevisionInfo revisionInfo{}; //!< Stores info about supported features for the audren revision used
audio::AudioOutState playbackState{audio::AudioOutState::Stopped}; //!< The current state of playback
size_t memoryPoolCount{}; //!< The amount of memory pools the guest may need
int samplesPerBuffer{}; //!< The amount of samples each appended buffer should contain
/**
* @brief Obtains new sample data from voices and mixes it together into the sample buffer
*/
void MixFinalBuffer();
/**
* @brief Appends all released buffers with new mixed sample data
*/
void UpdateAudio();
public:
/**
* @param params The parameters to use for rendering
*/
IAudioRenderer(const DeviceState &state, ServiceManager &manager, AudioRendererParams &params);
/**
* @brief Closes the audio track
*/
~IAudioRenderer();
/**
* @brief Returns the sample rate (https://switchbrew.org/wiki/Audio_services#GetSampleRate)
*/
void GetSampleRate(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response);
/**
* @brief Returns the sample count (https://switchbrew.org/wiki/Audio_services#GetSampleCount)
*/
void GetSampleCount(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response);
/**
* @brief Returns the number of mix buffers (https://switchbrew.org/wiki/Audio_services#GetMixBufferCount)
*/
void GetMixBufferCount(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response);
/**
* @brief Returns the state of the renderer (https://switchbrew.org/wiki/Audio_services#GetAudioRendererState) (stubbed)?
*/
void GetState(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response);
/**
* @brief Updates the audio renderer state and appends new data to playback buffers
*/
void RequestUpdate(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response);
/**
* @brief Start the audio stream from the renderer
*/
void Start(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response);
/**
* @brief Stop the audio stream from the renderer
*/
void Stop(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response);
/**
* @brief Returns a handle to the sample release KEvent
*/
void QuerySystemEvent(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response);
};
}

View File

@ -0,0 +1,108 @@
#include "IAudioRenderer.h"
#include "IAudioRendererManager.h"
#include <kernel/types/KProcess.h>
namespace skyline::service::audren {
IAudioRendererManager::IAudioRendererManager(const DeviceState &state, ServiceManager &manager) : BaseService(state, manager, false, Service::IAudioRendererManager, {
{0x0, SFUNC(IAudioRendererManager::OpenAudioRenderer)},
{0x1, SFUNC(IAudioRendererManager::GetAudioRendererWorkBufferSize)}
}) {}
void IAudioRendererManager::OpenAudioRenderer(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
AudioRendererParams params = request.Pop<AudioRendererParams>();
state.logger->Debug("IAudioRendererManager: Opening a rev {} IAudioRenderer with sample rate: {}, voice count: {}, effect count: {}",
ExtractVersionFromRevision(params.revision), params.sampleRate, params.voiceCount, params.effectCount);
manager.RegisterService(std::make_shared<IAudioRenderer>(state, manager, params), session, response);
}
void IAudioRendererManager::GetAudioRendererWorkBufferSize(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) {
AudioRendererParams params = request.Pop<AudioRendererParams>();
RevisionInfo revisionInfo;
revisionInfo.SetUserRevision(params.revision);
u32 totalMixCount = params.subMixCount + 1;
i64 size = utils::AlignUp(params.mixBufferCount * 4, constant::BufferAlignment) +
params.subMixCount * 0x400 +
totalMixCount * 0x940 +
params.voiceCount * 0x3F0 +
utils::AlignUp(totalMixCount * 8, 16) +
utils::AlignUp(params.voiceCount * 8, 16) +
utils::AlignUp(((params.sinkCount + params.subMixCount) * 0x3C0 + params.sampleCount * 4) * (params.mixBufferCount + 6), constant::BufferAlignment) +
(params.sinkCount + params.subMixCount) * 0x2C0 +
(params.effectCount + params.voiceCount * 4) * 0x30 +
0x50;
if (revisionInfo.SplitterSupported()) {
i32 nodeStateWorkSize = utils::AlignUp(totalMixCount, constant::BufferAlignment);
if (nodeStateWorkSize < 0)
nodeStateWorkSize |= 7;
nodeStateWorkSize = 4 * (totalMixCount * totalMixCount) + 12 * totalMixCount + 2 * (nodeStateWorkSize / 8);
i32 edgeMatrixWorkSize = utils::AlignUp(totalMixCount * totalMixCount, constant::BufferAlignment);
if (edgeMatrixWorkSize < 0)
edgeMatrixWorkSize |= 7;
edgeMatrixWorkSize /= 8;
size += utils::AlignUp(edgeMatrixWorkSize + nodeStateWorkSize, 16);
}
i64 splitterWorkSize = 0;
if (revisionInfo.SplitterSupported()) {
splitterWorkSize += params.splitterDestinationDataCount * 0xE0 +
params.splitterCount * 0x20;
if (revisionInfo.SplitterBugFixed())
splitterWorkSize += utils::AlignUp(4 * params.splitterDestinationDataCount, 16);
}
size = params.sinkCount * 0x170 +
(params.sinkCount + params.subMixCount) * 0x280 +
params.effectCount * 0x4C0 +
((size + splitterWorkSize + 0x30 * params.effectCount + (4 * params.voiceCount) + 0x8F) & ~0x3Fl) +
((params.voiceCount << 8) | 0x40);
if (params.performanceManagerCount > 0) {
i64 performanceMetricsBufferSize;
if (revisionInfo.UsesPerformanceMetricDataFormatV2()) {
performanceMetricsBufferSize = (params.voiceCount +
params.effectCount +
totalMixCount +
params.sinkCount) + 0x990;
} else {
performanceMetricsBufferSize = ((static_cast<i64>((params.voiceCount +
params.effectCount +
totalMixCount +
params.sinkCount)) << 32) >> 0x1C) + 0x658;
}
size += (performanceMetricsBufferSize * (params.performanceManagerCount + 1) + 0xFF) & ~0x3Fl;
}
if (revisionInfo.VaradicCommandBufferSizeSupported()) {
size += params.effectCount * 0x840 +
params.subMixCount * 0x5A38 +
params.sinkCount * 0x148 +
params.splitterDestinationDataCount * 0x540 +
(params.splitterCount * 0x68 + 0x2E0) * params.voiceCount +
((params.voiceCount + params.subMixCount + params.effectCount + params.sinkCount + 0x65) << 6) +
0x3F8 +
0x7E;
} else {
size += 0x1807E;
}
size = utils::AlignUp(size, 0x1000);
state.logger->Debug("IAudioRendererManager: Work buffer size: 0x{:X}", size);
response.Push<i64>(size);
}
}

View File

@ -0,0 +1,26 @@
#pragma once
#include <audio.h>
#include <services/base_service.h>
#include <services/serviceman.h>
#include <kernel/types/KEvent.h>
namespace skyline::service::audren {
/**
* @brief audren:u or IAudioRendererManager is used to manage audio renderer outputs (https://switchbrew.org/wiki/Audio_services#audren:u)
*/
class IAudioRendererManager : public BaseService {
public:
IAudioRendererManager(const DeviceState &state, ServiceManager &manager);
/**
* @brief Creates a new audrenU::IAudioRenderer object and returns a handle to it
*/
void OpenAudioRenderer(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response);
/**
* @brief Calculates the size of the buffer the guest needs to allocate for audren
*/
void GetAudioRendererWorkBufferSize(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response);
};
}

View File

@ -0,0 +1,45 @@
#pragma once
#include <common.h>
namespace skyline::service::audren {
/**
* @brief This enumerates various states an effect can be in
*/
enum class EffectState : u8 {
None = 0, //!< The effect isn't being used
New = 1 //!< The effect is new
};
/**
* @brief This is in input containing information on what effects to use on an audio stream
*/
struct EffectIn {
u8 _unk0_;
u8 isNew; //!< Whether the effect was used in the previous samples
u8 _unk1_[0xbe];
};
static_assert(sizeof(EffectIn) == 0xc0);
/**
* @brief This is returned to inform the guest of the state of an effect
*/
struct EffectOut {
EffectState state; //!< The state of the effect
u8 _pad0_[15];
};
static_assert(sizeof(EffectOut) == 0x10);
/**
* @brief The Effect class stores the state of audio post processing effects
*/
class Effect {
public:
EffectOut output{}; //!< The current output state
inline void ProcessInput(const EffectIn &input) {
if (input.isNew)
output.state = EffectState::New;
}
};
}

View File

@ -0,0 +1,11 @@
#include <common.h>
#include "memoryPool.h"
namespace skyline::service::audren {
void MemoryPool::ProcessInput(const MemoryPoolIn &input) {
if (input.state == MemoryPoolState::RequestAttach)
output.state = MemoryPoolState::Attached;
else if (input.state == MemoryPoolState::RequestDetach)
output.state = MemoryPoolState::Detached;
}
}

View File

@ -0,0 +1,54 @@
#pragma once
#include <common.h>
namespace skyline::service::audren {
/**
* @brief This enumerates various states a memory pool can be in
*/
enum class MemoryPoolState : u32 {
Invalid = 0, //!< The memory pool is invalid
Unknown = 1, //!< The memory pool is in an unknown state
RequestDetach = 2, //!< The memory pool should be detached from
Detached = 3, //!< The memory pool has been detached from
RequestAttach = 4, //!< The memory pool should be attached to
Attached = 5, //!< The memory pool has been attached to
Released = 6 //!< The memory pool has been released
};
/**
* @brief This is in input containing information about a memory pool for use by the dsp
*/
struct MemoryPoolIn {
u64 address; //!< The address of the memory pool
u64 size; //!< The size of the memory pool
MemoryPoolState state; //!< The state that is requested for the memory pool
u32 _unk0_;
u64 _unk1_;
};
static_assert(sizeof(MemoryPoolIn) == 0x20);
/**
* @brief This is returned to inform the guest of the state of a memory pool
*/
struct MemoryPoolOut {
MemoryPoolState state = MemoryPoolState::Detached; //!< The state of the memory pool
u32 _unk0_;
u64 _unk1_;
};
static_assert(sizeof(MemoryPoolOut) == 0x10);
/**
* @brief The MemoryPool class stores the state of a memory pool
*/
class MemoryPool {
public:
MemoryPoolOut output{}; //!< The current output state
/**
* @brief Processes the input memory pool data from the guest and sets internal data based off it
* @param input The input data struct from guest
*/
void ProcessInput(const MemoryPoolIn &input);
};
}

View File

@ -0,0 +1,83 @@
#pragma once
#include <common.h>
namespace skyline::service::audren {
namespace constant {
constexpr u32 SupportedRevision = 7; //!< The audren revision our implementation supports
constexpr u32 Rev0Magic = ('R' << 0) | ('E' << 8) | ('V' << 16) | ('0' << 24); //!< The HOS 1.0 revision magic
constexpr u32 RevMagic = Rev0Magic + (SupportedRevision << 24); //!< The revision magic for our supported revision
namespace supportTags {
constexpr u32 Splitter = 2; //!< The revision splitter support was added
constexpr u32 SplitterBugFix = 5; //!< The revision the splitter buffer was made aligned
constexpr u32 PerformanceMetricsDataFormatV2 = 5; //!< The revision a new performance metrics format is used
constexpr u32 VaradicCommandBufferSize = 5; //!< The revision support for varying command buffer sizes was added
constexpr u32 ElapsedFrameCount = 5; //!< The revision support for counting elapsed frames was added
};
}
/**
* @brief Extracts the audren version from a revision magic
* @param revision The revision magic
* @return The revision number from the magic
*/
inline u32 ExtractVersionFromRevision(u32 revision) {
return (revision - constant::Rev0Magic) >> 24;
}
/**
* @brief The RevisionInfo class is used to query the supported features of various audren revisions
*/
class RevisionInfo {
private:
u32 userRevision; //!< The current audren revision of the guest
public:
/**
* @brief Extracts the audren revision from the magic and sets the behaviour revision to it
* @param revision The revision magic from guest
*/
inline void SetUserRevision(u32 revision) {
userRevision = ExtractVersionFromRevision(revision);
if (userRevision > constant::SupportedRevision)
throw exception("Unsupported audren revision: {}", userRevision);
}
/**
* @return Whether the splitter is supported
*/
inline bool SplitterSupported() {
return userRevision >= constant::supportTags::Splitter;
}
/**
* @return Whether the splitter is fixed
*/
inline bool SplitterBugFixed() {
return userRevision >= constant::supportTags::SplitterBugFix;
}
/**
* @return Whether the new performance metrics format is used
*/
inline bool UsesPerformanceMetricDataFormatV2() {
return userRevision >= constant::supportTags::PerformanceMetricsDataFormatV2;
}
/**
* @return Whether varying command buffer sizes are supported
*/
inline bool VaradicCommandBufferSizeSupported() {
return userRevision >= constant::supportTags::VaradicCommandBufferSize;
}
/**
* @return Whether elapsed frame counting is supported
*/
inline bool ElapsedFrameCountSupported() {
return userRevision >= constant::supportTags::ElapsedFrameCount;
}
};
}

View File

@ -0,0 +1,110 @@
#include <common.h>
#include "voice.h"
#include <kernel/types/KProcess.h>
namespace skyline::service::audren {
Voice::Voice(const DeviceState &state) : state(state) {}
void Voice::ProcessInput(const VoiceIn &input) {
// Voice no longer in use, reset it
if (acquired && !input.acquired) {
bufferReload = true;
bufferIndex = 0;
sampleOffset = 0;
output.playedSamplesCount = 0;
output.playedWaveBuffersCount = 0;
output.voiceDropsCount = 0;
}
acquired = input.acquired;
if (!acquired)
return;
if (input.firstUpdate) {
if (input.pcmFormat != audio::PcmFormat::Int16)
throw exception("Unsupported voice PCM format: {}", input.pcmFormat);
pcmFormat = input.pcmFormat;
sampleRate = input.sampleRate;
if (input.channelCount > 2)
throw exception("Unsupported voice channel count: {}", input.channelCount);
channelCount = input.channelCount;
SetWaveBufferIndex(input.baseWaveBufferIndex);
}
waveBuffers = input.waveBuffers;
volume = input.volume;
playbackState = input.playbackState;
}
void Voice::UpdateBuffers() {
WaveBuffer &currentBuffer = waveBuffers.at(bufferIndex);
if (currentBuffer.size == 0)
return;
switch (pcmFormat) {
case audio::PcmFormat::Int16:
sampleBuffer.resize(currentBuffer.size / sizeof(i16));
state.process->ReadMemory(sampleBuffer.data(), currentBuffer.position, currentBuffer.size);
break;
default:
throw exception("Unsupported voice PCM format: {}", pcmFormat);
}
if (sampleRate != audio::constant::SampleRate)
sampleBuffer = resampler.ResampleBuffer(sampleBuffer, static_cast<double>(sampleRate) / audio::constant::SampleRate, channelCount);
if (channelCount == 1 && audio::constant::ChannelCount != channelCount) {
size_t originalSize = sampleBuffer.size();
sampleBuffer.resize((originalSize / channelCount) * audio::constant::ChannelCount);
for (size_t monoIndex = originalSize - 1, targetIndex = sampleBuffer.size(); monoIndex > 0; monoIndex--)
for (uint i = 0; i < audio::constant::ChannelCount; i++)
sampleBuffer[--targetIndex] = sampleBuffer[monoIndex];
}
}
std::vector<i16> &Voice::GetBufferData(int maxSamples, int &outOffset, int &outSize) {
WaveBuffer &currentBuffer = waveBuffers.at(bufferIndex);
if (!acquired || playbackState != audio::AudioOutState::Started) {
outSize = 0;
return sampleBuffer;
}
if (bufferReload) {
bufferReload = false;
UpdateBuffers();
}
outOffset = sampleOffset;
outSize = std::min(maxSamples * audio::constant::ChannelCount, static_cast<int>(sampleBuffer.size() - sampleOffset));
output.playedSamplesCount += outSize / audio::constant::ChannelCount;
sampleOffset += outSize;
if (sampleOffset == sampleBuffer.size()) {
sampleOffset = 0;
if (currentBuffer.lastBuffer)
playbackState = audio::AudioOutState::Paused;
if (!currentBuffer.looping)
SetWaveBufferIndex(bufferIndex + 1);
output.playedWaveBuffersCount++;
}
return sampleBuffer;
}
void Voice::SetWaveBufferIndex(uint index) {
bufferIndex = index & 3;
bufferReload = true;
}
}

View File

@ -0,0 +1,138 @@
#pragma once
#include <common.h>
#include <array>
#include <audio.h>
#include <audio/resampler.h>
namespace skyline::service::audren {
/**
* @brief This stores data for configuring a biquadratic filter
*/
struct BiquadFilter {
u8 enable; //!< Whether to enable the filter
u8 _pad0_;
u16 b0; //!< The b0 constant
u16 b1; //!< The b1 constant
u16 b2; //!< The b2 constant
u16 a1; //!< The a1 constant
u16 a2; //!< The a2 constant
};
static_assert(sizeof(BiquadFilter) == 0xc);
/**
* @brief This stores information of a wave buffer of samples
*/
struct WaveBuffer {
u64 position; //!< The position of the wave buffer in guest memory
u64 size; //!< The size of the wave buffer
u32 firstSampleOffset; //!< The offset of the first sample in the buffer
u32 lastSampleOffset; //!< The offset of the last sample in the buffer
u8 looping; //!< Whether to loop the buffer
u8 lastBuffer; //!< Whether this is the last populated buffer
u16 _unk0_;
u32 _unk1_;
u64 adpcmLoopContextPosition; //!< The position of the ADPCM loop context data
u64 adpcmLoopContextSize; //!< The size of the ADPCM loop context data
u64 _unk2_;
};
static_assert(sizeof(WaveBuffer) == 0x38);
/**
* @brief This is in input containing the configuration of a voice
*/
struct VoiceIn {
u32 slot; //!< The slot of the voice
u32 nodeId; //!< The node ID of the voice
u8 firstUpdate; //!< Whether this voice is new
u8 acquired; //!< Whether the sample is in use
audio::AudioOutState playbackState; //!< The playback state
audio::PcmFormat pcmFormat; //!< The sample format
u32 sampleRate; //!< The sample rate
u32 priority; //!< The priority for this voice
u32 _unk0_;
u32 channelCount; //!< The amount of channels the wave buffers contain
float pitch; //!< The pitch to play wave data at
float volume; //!< The volume to play wave data at
std::array<BiquadFilter, 2> biquadFilters; //!< Biquadratic filter configurations
u32 appendedWaveBuffersCount; //!< The amount of buffers appended
u32 baseWaveBufferIndex; //!< The start index of the buffer to use
u32 _unk1_;
u64 adpcmCoeffsPosition; //!< The ADPCM coefficient position in wave data
u64 adpcmCoeffsSize; //!< The size of the ADPCM coefficient configuration data
u32 destination; //!< The voice destination address
u32 _pad0_;
std::array<WaveBuffer, 4> waveBuffers; //!< The wave buffers for the voice
std::array<u32, 6> voiceChannelResourceIds; //!< A list of IDs corresponding to each channel
u32 _pad1_[6];
};
static_assert(sizeof(VoiceIn) == 0x170);
/**
* @brief This is returned to inform the guest of the state of a voice
*/
struct VoiceOut {
u64 playedSamplesCount; //!< The amount of samples played
u32 playedWaveBuffersCount; //!< The amount of wave buffers fully played
u32 voiceDropsCount; //!< The amount of time audio frames have been dropped due to the rendering time limit
};
static_assert(sizeof(VoiceOut) == 0x10);
/**
* @brief The Voice class manages an audio voice
*/
class Voice {
private:
const DeviceState &state; //!< The emulator state object
std::array<WaveBuffer, 4> waveBuffers; //!< An array containing the state of all four wave buffers
std::vector<i16> sampleBuffer; //!< A buffer containing processed sample data
audio::Resampler resampler; //!< The resampler object used for changing the sample rate of a stream
bool acquired{false}; //!< If the voice is in use
bool bufferReload{true}; //!< If the buffer needs to be updated
int bufferIndex{}; //!< The index of the wave buffer currently in use
int sampleOffset{}; //!< The offset in the sample data of the current wave buffer
int sampleRate{}; //!< The sample rate of the sample data
int channelCount{}; //!< The amount of channels in the sample data
audio::AudioOutState playbackState{audio::AudioOutState::Stopped}; //!< The playback state of the voice
audio::PcmFormat pcmFormat{audio::PcmFormat::Invalid}; //!< The PCM format used for guest audio data
/**
* @brief Updates the sample buffer with data from the current wave buffer and processes it
*/
void UpdateBuffers();
/**
* @brief Sets the current wave buffer index to use
* @param index The index to use
*/
void SetWaveBufferIndex(uint index);
public:
VoiceOut output{}; //!< The current output state
float volume{}; //!< The volume of the voice
Voice(const DeviceState &state);
/**
* @brief Reads the input voice data from the guest and sets internal data based off it
* @param input The input data struct from guest
*/
void ProcessInput(const VoiceIn &input);
/**
* @brief Obtains the voices audio sample data, updating it if required
* @param maxSamples The maximum amount of samples the output buffer should contain
* @return A vector of I16 PCM sample data
*/
std::vector<i16> &GetBufferData(int maxSamples, int &outOffset, int &outSize);
/**
* @return Whether the voice is currently playable
*/
inline bool Playable() {
return acquired && playbackState == audio::AudioOutState::Started && waveBuffers[bufferIndex].size != 0;
}
};
}

View File

@ -38,6 +38,8 @@ namespace skyline::service {
am_IAppletCommonFunctions, am_IAppletCommonFunctions,
audout_u, audout_u,
audout_IAudioOut, audout_IAudioOut,
IAudioRendererManager,
IAudioRenderer,
hid, hid,
hid_IAppletResource, hid_IAppletResource,
time, time,
@ -79,6 +81,7 @@ namespace skyline::service {
{"am:IAppletCommonFunctions", Service::am_IAppletCommonFunctions}, {"am:IAppletCommonFunctions", Service::am_IAppletCommonFunctions},
{"audout:u", Service::audout_u}, {"audout:u", Service::audout_u},
{"audout:IAudioOut", Service::audout_IAudioOut}, {"audout:IAudioOut", Service::audout_IAudioOut},
{"audren:u", Service::IAudioRendererManager},
{"hid", Service::hid}, {"hid", Service::hid},
{"hid:IAppletResource", Service::hid_IAppletResource}, {"hid:IAppletResource", Service::hid_IAppletResource},
{"time:s", Service::time}, {"time:s", Service::time},

View File

@ -1,5 +1,6 @@
#include "serviceman.h" #include "serviceman.h"
#include <kernel/types/KProcess.h> #include <kernel/types/KProcess.h>
#include <services/audren/IAudioRendererManager.h>
#include "sm/sm.h" #include "sm/sm.h"
#include "set/sys.h" #include "set/sys.h"
#include "apm/apm.h" #include "apm/apm.h"
@ -86,6 +87,9 @@ namespace skyline::service {
case Service::audout_u: case Service::audout_u:
serviceObj = std::make_shared<audout::audoutU>(state, *this); serviceObj = std::make_shared<audout::audoutU>(state, *this);
break; break;
case Service::IAudioRendererManager:
serviceObj = std::make_shared<audren::IAudioRendererManager>(state, *this);
break;
case Service::hid: case Service::hid:
serviceObj = std::make_shared<hid::hid>(state, *this); serviceObj = std::make_shared<hid::hid>(state, *this);
break; break;

View File

@ -35,4 +35,7 @@
<string name="docked_enabled">The system will emulate being in docked mode</string> <string name="docked_enabled">The system will emulate being in docked mode</string>
<string name="theme">Theme</string> <string name="theme">Theme</string>
<string name="searching_roms">Searching for ROMs</string> <string name="searching_roms">Searching for ROMs</string>
<string name="audio">Audio</string>
<string name="audren_buffer_size">Audio Buffer Size</string>
<string name="audren_buffer_desc">The size of the buffer used to store audio samples for ROMs using audren. A lower value will result in less latency but potentially increased audio stutter depending on the performance of the device</string>
</resources> </resources>

View File

@ -54,4 +54,16 @@
app:key="operation_mode" app:key="operation_mode"
app:title="@string/use_docked" /> app:title="@string/use_docked" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory
android:key="category_audio"
android:title="@string/audio">
<SeekBarPreference
app:key="audren_buffer_size"
app:title="@string/audren_buffer_size"
app:summary="@string/audren_buffer_desc"
app:showSeekBarValue="true"
app:min="320"
android:defaultValue="960"
android:max="2880"/>
</PreferenceCategory>
</androidx.preference.PreferenceScreen> </androidx.preference.PreferenceScreen>