diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 180176b3..8ec46660 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -161,6 +161,8 @@ add_library(skyline SHARED ${source_DIR}/skyline/services/am/storage/IStorage.cpp ${source_DIR}/skyline/services/am/storage/IStorageAccessor.cpp ${source_DIR}/skyline/services/am/applet/ILibraryAppletAccessor.cpp + ${source_DIR}/skyline/services/codec/IHardwareOpusDecoder.cpp + ${source_DIR}/skyline/services/codec/IHardwareOpusDecoderManager.cpp ${source_DIR}/skyline/services/hid/IHidServer.cpp ${source_DIR}/skyline/services/hid/IAppletResource.cpp ${source_DIR}/skyline/services/hid/IActiveVibrationDeviceList.cpp diff --git a/app/src/main/cpp/skyline/common.h b/app/src/main/cpp/skyline/common.h index 5e991af7..61a63da7 100644 --- a/app/src/main/cpp/skyline/common.h +++ b/app/src/main/cpp/skyline/common.h @@ -121,6 +121,7 @@ namespace skyline { namespace constant { // Time + constexpr u64 NsInMicrosecond{1000}; //!< The amount of nanoseconds in a microsecond constexpr u64 NsInSecond{1000000000}; //!< The amount of nanoseconds in a second constexpr u64 NsInMillisecond{1000000}; //!< The amount of nanoseconds in a millisecond constexpr u64 NsInDay{86400000000000UL}; //!< The amount of nanoseconds in a day diff --git a/app/src/main/cpp/skyline/services/codec/IHardwareOpusDecoder.cpp b/app/src/main/cpp/skyline/services/codec/IHardwareOpusDecoder.cpp new file mode 100644 index 00000000..16e30fb3 --- /dev/null +++ b/app/src/main/cpp/skyline/services/codec/IHardwareOpusDecoder.cpp @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright © 2021 Skyline Team and Contributors (https://github.com/skyline-emu/) + +#include +#include + +#include "IHardwareOpusDecoder.h" + +namespace skyline::service::codec { + size_t CalculateOutBufferSize(i32 sampleRate, i32 channelCount, i32 frameSize) { + return util::AlignUp(frameSize * channelCount / (OpusFullbandSampleRate / sampleRate), 0x40); + } + + IHardwareOpusDecoder::IHardwareOpusDecoder(const DeviceState &state, ServiceManager &manager, i32 sampleRate, i32 channelCount, u32 workBufferSize, KHandle workBufferHandle) + : BaseService(state, manager), + sampleRate(sampleRate), + channelCount(channelCount), + workBuffer(state.process->GetHandle(workBufferHandle)), + decoderOutputBufferSize(CalculateOutBufferSize(sampleRate, channelCount, MaxFrameSizeNormal)) { + if (workBufferSize < decoderOutputBufferSize) + throw exception("Work Buffer doesn't have adequate space for Opus Decoder: 0x{:X} (Required: 0x{:X})", workBufferSize, decoderOutputBufferSize); + + // We utilize the guest-supplied work buffer for allocating the OpusDecoder object into + decoderState = reinterpret_cast(workBuffer->host.ptr); + + int result{opus_decoder_init(decoderState, sampleRate, channelCount)}; + if (result != OPUS_OK) + throw OpusException(result); + } + + Result IHardwareOpusDecoder::DecodeInterleavedOld(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) { + return DecodeInterleavedImpl(request, response); + } + + Result IHardwareOpusDecoder::DecodeInterleavedWithPerfOld(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) { + return DecodeInterleavedImpl(request, response, true); + } + + Result IHardwareOpusDecoder::DecodeInterleaved(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) { + bool reset{static_cast(request.Pop())}; + if (reset) + ResetContext(); + + return DecodeInterleavedImpl(request, response, true); + } + + void IHardwareOpusDecoder::ResetContext() { + opus_decoder_ctl(decoderState, OPUS_RESET_STATE); + } + + Result IHardwareOpusDecoder::DecodeInterleavedImpl(ipc::IpcRequest &request, ipc::IpcResponse &response, bool writeDecodeTime) { + auto dataIn{request.inputBuf.at(0)}; + auto dataOut{request.outputBuf.at(0).cast()}; + + if (dataIn.size() <= sizeof(OpusDataHeader)) + throw exception("Incorrect Opus data size: 0x{:X} (Should be > 0x{:X})", dataIn.size(), sizeof(OpusDataHeader)); + + u32 opusPacketSize{dataIn.as().GetPacketSize()}; + i32 requiredInSize{static_cast(opusPacketSize + sizeof(OpusDataHeader))}; + if (opusPacketSize > MaxInputBufferSize || dataIn.size() < requiredInSize) + throw exception("Opus packet size mismatch: 0x{:X} (Requested: 0x{:X})", dataIn.size() - sizeof(OpusDataHeader), opusPacketSize); + + // Skip past the header in the input buffer to get the Opus packet + auto sampleDataIn = dataIn.subspan(sizeof(OpusDataHeader)); + + auto perfTimer{timesrv::TimeSpanType::FromNanoseconds(util::GetTimeNs())}; + i32 decodedCount{opus_decode(decoderState, sampleDataIn.data(), opusPacketSize, dataOut.data(), decoderOutputBufferSize, false)}; + perfTimer = timesrv::TimeSpanType::FromNanoseconds(util::GetTimeNs()) - perfTimer; + + if (decodedCount < 0) + throw OpusException(decodedCount); + + response.Push(requiredInSize); // Decoded data size is equal to opus packet size + header + response.Push(decodedCount); + if (writeDecodeTime) + response.Push(perfTimer.Microseconds()); + + return {}; + } +} diff --git a/app/src/main/cpp/skyline/services/codec/IHardwareOpusDecoder.h b/app/src/main/cpp/skyline/services/codec/IHardwareOpusDecoder.h new file mode 100644 index 00000000..a41f474e --- /dev/null +++ b/app/src/main/cpp/skyline/services/codec/IHardwareOpusDecoder.h @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright © 2021 Skyline Team and Contributors (https://github.com/skyline-emu/) + +#pragma once + +#include + +#include +#include +#include + +namespace skyline::service::codec { + /** + * @return The required output buffer size for decoding an Opus stream with the given parameters + */ + size_t CalculateOutBufferSize(i32 sampleRate, i32 channelCount, i32 frameSize); + + static constexpr u32 OpusFullbandSampleRate{48000}; + static constexpr u32 MaxFrameSizeNormal = OpusFullbandSampleRate * 0.040; //!< 40ms frame size limit for normal decoders + static constexpr u32 MaxFrameSizeEx = OpusFullbandSampleRate * 0.120; //!< 120ms frame size limit for ex decoders added in 12.0.0 + static constexpr u32 MaxInputBufferSize{0x600}; //!< Maximum allocated size of the input buffer + + /** + * @note The Switch has a HW Opus Decoder which this service would interface with, we emulate it using libopus with CPU-decoding + * @url https://switchbrew.org/wiki/Audio_services#IHardwareOpusDecoder + */ + class IHardwareOpusDecoder : public BaseService { + private: + std::shared_ptr workBuffer; + OpusDecoder *decoderState{}; + i32 sampleRate; + i32 channelCount; + i32 decoderOutputBufferSize; + + /** + * @brief Holds information about the Opus packet to be decoded + * @note These fields are big-endian + * @url https://github.com/switchbrew/libnx/blob/c5a9a909a91657a9818a3b7e18c9b91ff0cbb6e3/nx/include/switch/services/hwopus.h#L19 + */ + struct OpusDataHeader { + u32 sizeBe; //!< Size of the packet following this header + u32 finalRangeBe; //!< Final range of the codec encoder's entropy coder (can be zero) + + u32 GetPacketSize() { + return util::SwapEndianness(sizeBe); + } + }; + static_assert(sizeof(OpusDataHeader) == 0x8); + + /** + * @brief Resets the Opus decoder's internal state + */ + void ResetContext(); + + /** + * @brief Decodes Opus source data via libopus + */ + Result DecodeInterleavedImpl(ipc::IpcRequest &request, ipc::IpcResponse &response, bool writeDecodeTime = false); + + public: + IHardwareOpusDecoder(const DeviceState &state, ServiceManager &manager, i32 sampleRate, i32 channelCount, u32 workBufferSize, KHandle workBufferHandle); + + /** + * @brief Decodes the Opus source data, returns decoded data size and decoded sample count + * @url https://switchbrew.org/wiki/Audio_services#DecodeInterleavedOld + */ + Result DecodeInterleavedOld(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response); + + /** + * @brief Decodes the Opus source data, returns decoded data size, decoded sample count and decode time in microseconds + * @url https://switchbrew.org/wiki/Audio_services#DecodeInterleavedWithPerfOld + */ + Result DecodeInterleavedWithPerfOld(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response); + + /** + * @brief Decodes the Opus source data, returns decoded data size, decoded sample count and decode time in microseconds + * @note The bool flag indicates whether or not to reset the decoder context + * @url https://switchbrew.org/wiki/Audio_services#DecodeInterleaved + */ + Result DecodeInterleaved(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response); + + SERVICE_DECL( + SFUNC(0x0, IHardwareOpusDecoder, DecodeInterleavedOld), + SFUNC(0x4, IHardwareOpusDecoder, DecodeInterleavedWithPerfOld), + SFUNC(0x6, IHardwareOpusDecoder, DecodeInterleaved), // DecodeInterleavedWithPerfAndResetOld is effectively the same as DecodeInterleaved + SFUNC(0x8, IHardwareOpusDecoder, DecodeInterleaved), + ) + }; + + /** + * @brief A wrapper around libopus error codes for exceptions + */ + class OpusException : public exception { + public: + OpusException(int errorCode) : exception("Opus failed with error code {}: {}", errorCode, opus_strerror(errorCode)) {} + }; +} diff --git a/app/src/main/cpp/skyline/services/codec/IHardwareOpusDecoderManager.cpp b/app/src/main/cpp/skyline/services/codec/IHardwareOpusDecoderManager.cpp new file mode 100644 index 00000000..6b2cbe37 --- /dev/null +++ b/app/src/main/cpp/skyline/services/codec/IHardwareOpusDecoderManager.cpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright © 2021 Skyline Team and Contributors (https://github.com/skyline-emu/) + +#include + +#include "IHardwareOpusDecoderManager.h" +#include "IHardwareOpusDecoder.h" + +namespace skyline::service::codec { + static size_t CalculateBufferSize(i32 sampleRate, i32 channelCount) { + i32 requiredSize{opus_decoder_get_size(channelCount)}; + requiredSize += MaxInputBufferSize + CalculateOutBufferSize(sampleRate, channelCount, MaxFrameSizeNormal); + return requiredSize; + } + + Result IHardwareOpusDecoderManager::OpenHardwareOpusDecoder(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) { + i32 sampleRate{request.Pop()}; + i32 channelCount{request.Pop()}; + u32 workBufferSize{request.Pop()}; + KHandle workBuffer{request.copyHandles.at(0)}; + + state.logger->Debug("Creating Opus decoder: Sample rate: {}, Channel count: {}, Work buffer handle: 0x{:X} (Size: 0x{:X})", sampleRate, channelCount, workBuffer, workBufferSize); + + manager.RegisterService(std::make_shared(state, manager, sampleRate, channelCount, workBufferSize, workBuffer), session, response); + return {}; + } + + Result IHardwareOpusDecoderManager::GetWorkBufferSize(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response) { + i32 sampleRate{request.Pop()}; + i32 channelCount{request.Pop()}; + + response.Push(CalculateBufferSize(sampleRate, channelCount)); + return {}; + } +} diff --git a/app/src/main/cpp/skyline/services/codec/IHardwareOpusDecoderManager.h b/app/src/main/cpp/skyline/services/codec/IHardwareOpusDecoderManager.h new file mode 100644 index 00000000..3d3e3dd8 --- /dev/null +++ b/app/src/main/cpp/skyline/services/codec/IHardwareOpusDecoderManager.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright © 2021 Skyline Team and Contributors (https://github.com/skyline-emu/) + +#pragma once + +#include + +namespace skyline::service::codec { + /** + * @brief Initialization parameters for the Opus multi-stream decoder + * @see opus_multistream_decoder_init() + */ + struct MultiStreamParameters { + i32 sampleRate; + i32 channelCount; + i32 streamCount; + i32 stereoStreamCount; + std::array mappings; //!< Array of channel mappings + }; + static_assert(sizeof(MultiStreamParameters) == 0x110); + + /** + * @brief Manages all instances of IHardwareOpusDecoder + * @url https://switchbrew.org/wiki/Audio_services#hwopus + */ + class IHardwareOpusDecoderManager : public BaseService { + public: + IHardwareOpusDecoderManager(const DeviceState &state, ServiceManager &manager) : BaseService(state, manager) {} + + /** + * @brief Returns an IHardwareOpusDecoder object + * @url https://switchbrew.org/wiki/Audio_services#OpenHardwareOpusDecoder + */ + Result OpenHardwareOpusDecoder(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response); + + /** + * @brief Returns the required size for the decoder's work buffer + * @url https://switchbrew.org/wiki/Audio_services#GetWorkBufferSize + */ + Result GetWorkBufferSize(type::KSession &session, ipc::IpcRequest &request, ipc::IpcResponse &response); + + SERVICE_DECL( + SFUNC(0x0, IHardwareOpusDecoderManager, OpenHardwareOpusDecoder), + SFUNC(0x1, IHardwareOpusDecoderManager, GetWorkBufferSize), + ) + }; +} diff --git a/app/src/main/cpp/skyline/services/serviceman.cpp b/app/src/main/cpp/skyline/services/serviceman.cpp index 2a848cfe..e86e436f 100644 --- a/app/src/main/cpp/skyline/services/serviceman.cpp +++ b/app/src/main/cpp/skyline/services/serviceman.cpp @@ -11,6 +11,7 @@ #include "am/IAllSystemAppletProxiesService.h" #include "audio/IAudioOutManager.h" #include "audio/IAudioRendererManager.h" +#include "codec/IHardwareOpusDecoderManager.h" #include "fatalsrv/IService.h" #include "hid/IHidServer.h" #include "timesrv/IStaticService.h" @@ -66,6 +67,7 @@ namespace skyline::service { SERVICE_CASE(am::IAllSystemAppletProxiesService, "appletAE") SERVICE_CASE(audio::IAudioOutManager, "audout:u") SERVICE_CASE(audio::IAudioRendererManager, "audren:u") + SERVICE_CASE(codec::IHardwareOpusDecoderManager, "hwopus") SERVICE_CASE(hid::IHidServer, "hid") SERVICE_CASE(timesrv::IStaticService, "time:s", globalServiceState->timesrv, timesrv::constant::StaticServiceSystemPermissions) // Both of these would be registered after TimeServiceManager::Setup normally but we call that in the GlobalServiceState constructor so can just list them here directly SERVICE_CASE(timesrv::IStaticService, "time:su", globalServiceState->timesrv, timesrv::constant::StaticServiceSystemUpdatePermissions) diff --git a/app/src/main/cpp/skyline/services/serviceman.h b/app/src/main/cpp/skyline/services/serviceman.h index 0690b0a9..0cc365b3 100644 --- a/app/src/main/cpp/skyline/services/serviceman.h +++ b/app/src/main/cpp/skyline/services/serviceman.h @@ -40,8 +40,6 @@ namespace skyline::service { * @param serviceObject An instance of the service * @param session The session object of the command * @param response The response object to write the handle or virtual handle to - * @param submodule If the registered service is a submodule or not - * @param name The name of the service to register if it's not a submodule - it will be added to the service map */ void RegisterService(std::shared_ptr serviceObject, type::KSession &session, ipc::IpcResponse &response); diff --git a/app/src/main/cpp/skyline/services/timesrv/common.h b/app/src/main/cpp/skyline/services/timesrv/common.h index 50d79094..b8f50f95 100644 --- a/app/src/main/cpp/skyline/services/timesrv/common.h +++ b/app/src/main/cpp/skyline/services/timesrv/common.h @@ -35,6 +35,10 @@ namespace skyline::service::timesrv { return ns; } + constexpr i64 Microseconds() const { + return ns / static_cast(skyline::constant::NsInMicrosecond); + } + constexpr i64 Seconds() const { return ns / static_cast(skyline::constant::NsInSecond); }