Skip to content

nsyshid: Play Emulated Portal Audio via Mono Audio #1478

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 19, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Cafe/HW/Latte/Core/LatteShaderCache.cpp
Original file line number Diff line number Diff line change
@@ -209,7 +209,7 @@ class BootSoundPlayer

try
{
bootSndAudioDev = IAudioAPI::CreateDeviceFromConfig(true, sampleRate, nChannels, samplesPerBlock, bitsPerSample);
bootSndAudioDev = IAudioAPI::CreateDeviceFromConfig(IAudioAPI::AudioType::TV, sampleRate, nChannels, samplesPerBlock, bitsPerSample);
if(!bootSndAudioDev)
return;
}
46 changes: 34 additions & 12 deletions src/Cafe/OS/libs/nsyshid/Skylander.cpp
Original file line number Diff line number Diff line change
@@ -6,6 +6,8 @@
#include "Backend.h"

#include "Common/FileStream.h"
#include "audio/IAudioAPI.h"
#include "config/CemuConfig.h"

namespace nsyshid
{
@@ -558,6 +560,26 @@ namespace nsyshid

Device::WriteResult SkylanderPortalDevice::Write(WriteMessage* message)
{
if (message->length != 64) {
cemu_assert_error();
}

if (!g_portalAudio)
{
// Portal audio is mono channel, 16 bit audio.
// Audio is unsigned 16 bit, supplied as 64 bytes which is 32 samples per block
g_portalAudio = IAudioAPI::CreateDeviceFromConfig(IAudioAPI::AudioType::Portal, 8000, 32, 16);
}
std::array<sint16, 32> mono_samples;
for (unsigned int i = 0; i < mono_samples.size(); ++i)
{
sint16 sample = static_cast<uint16>(message->data[i * 2 + 1]) << 8 | static_cast<uint16>(message->data[i * 2]);
mono_samples[i] = sample;
}
if (g_portalAudio)
{
g_portalAudio->FeedBlock(mono_samples.data());
}
message->bytesWritten = message->length;
return Device::WriteResult::Success;
}
@@ -604,20 +626,20 @@ namespace nsyshid
*(uint16be*)(currentWritePtr + 7) = 0x001D; // wDescriptorLength
currentWritePtr = currentWritePtr + 9;
// endpoint descriptor 1
*(uint8*)(currentWritePtr + 0) = 7; // bLength
*(uint8*)(currentWritePtr + 1) = 0x05; // bDescriptorType
*(uint8*)(currentWritePtr + 2) = 0x81; // bEndpointAddress
*(uint8*)(currentWritePtr + 3) = 0x03; // bmAttributes
*(uint8*)(currentWritePtr + 0) = 7; // bLength
*(uint8*)(currentWritePtr + 1) = 0x05; // bDescriptorType
*(uint8*)(currentWritePtr + 2) = 0x81; // bEndpointAddress
*(uint8*)(currentWritePtr + 3) = 0x03; // bmAttributes
*(uint16be*)(currentWritePtr + 4) = 0x0040; // wMaxPacketSize
*(uint8*)(currentWritePtr + 6) = 0x01; // bInterval
*(uint8*)(currentWritePtr + 6) = 0x01; // bInterval
currentWritePtr = currentWritePtr + 7;
// endpoint descriptor 2
*(uint8*)(currentWritePtr + 0) = 7; // bLength
*(uint8*)(currentWritePtr + 1) = 0x05; // bDescriptorType
*(uint8*)(currentWritePtr + 2) = 0x02; // bEndpointAddress
*(uint8*)(currentWritePtr + 3) = 0x03; // bmAttributes
*(uint8*)(currentWritePtr + 0) = 7; // bLength
*(uint8*)(currentWritePtr + 1) = 0x05; // bDescriptorType
*(uint8*)(currentWritePtr + 2) = 0x02; // bEndpointAddress
*(uint8*)(currentWritePtr + 3) = 0x03; // bmAttributes
*(uint16be*)(currentWritePtr + 4) = 0x0040; // wMaxPacketSize
*(uint8*)(currentWritePtr + 6) = 0x01; // bInterval
*(uint8*)(currentWritePtr + 6) = 0x01; // bInterval
currentWritePtr = currentWritePtr + 7;

cemu_assert_debug((currentWritePtr - configurationDescriptor) == 0x29);
@@ -628,8 +650,8 @@ namespace nsyshid
}

bool SkylanderPortalDevice::SetIdle(uint8 ifIndex,
uint8 reportId,
uint8 duration)
uint8 reportId,
uint8 duration)
{
return true;
}
17 changes: 15 additions & 2 deletions src/Cafe/OS/libs/snd_core/ax_out.cpp
Original file line number Diff line number Diff line change
@@ -404,7 +404,7 @@ namespace snd_core
{
try
{
g_tvAudio = IAudioAPI::CreateDeviceFromConfig(true, 48000, snd_core::AX_SAMPLES_PER_3MS_48KHZ * AX_FRAMES_PER_GROUP, 16);
g_tvAudio = IAudioAPI::CreateDeviceFromConfig(IAudioAPI::AudioType::TV, 48000, snd_core::AX_SAMPLES_PER_3MS_48KHZ * AX_FRAMES_PER_GROUP, 16);
}
catch (std::runtime_error& ex)
{
@@ -417,7 +417,7 @@ namespace snd_core
{
try
{
g_padAudio = IAudioAPI::CreateDeviceFromConfig(false, 48000, snd_core::AX_SAMPLES_PER_3MS_48KHZ * AX_FRAMES_PER_GROUP, 16);
g_padAudio = IAudioAPI::CreateDeviceFromConfig(IAudioAPI::AudioType::Gamepad, 48000, snd_core::AX_SAMPLES_PER_3MS_48KHZ * AX_FRAMES_PER_GROUP, 16);
if(g_padAudio)
g_padVolume = g_padAudio->GetVolume();
}
@@ -442,6 +442,11 @@ namespace snd_core
g_padAudio->Stop();
g_padAudio.reset();
}
if (g_portalAudio)
{
g_portalAudio->Stop();
g_portalAudio.reset();
}
}

void AXOut_updateDevicePlayState(bool isPlaying)
@@ -462,6 +467,14 @@ namespace snd_core
else
g_padAudio->Stop();
}

if (g_portalAudio)
{
if (isPlaying)
g_portalAudio->Play();
else
g_portalAudio->Stop();
}
}

// called periodically to check for AX updates
87 changes: 68 additions & 19 deletions src/audio/IAudioAPI.cpp
Original file line number Diff line number Diff line change
@@ -13,13 +13,14 @@
std::shared_mutex g_audioMutex;
AudioAPIPtr g_tvAudio;
AudioAPIPtr g_padAudio;
AudioAPIPtr g_portalAudio;
std::atomic_int32_t g_padVolume = 0;

uint32 IAudioAPI::s_audioDelay = 2;
std::array<bool, IAudioAPI::AudioAPIEnd> IAudioAPI::s_availableApis{};

IAudioAPI::IAudioAPI(uint32 samplerate, uint32 channels, uint32 samples_per_block, uint32 bits_per_sample)
: m_samplerate(samplerate), m_channels(channels), m_samplesPerBlock(samples_per_block), m_bitsPerSample(bits_per_sample)
: m_samplerate(samplerate), m_channels(channels), m_samplesPerBlock(samples_per_block), m_bitsPerSample(bits_per_sample)
{
m_bytesPerBlock = samples_per_block * channels * (bits_per_sample / 8);
InitWFX(m_samplerate, m_channels, m_bitsPerSample);
@@ -80,7 +81,7 @@ void IAudioAPI::InitializeStatic()
#if BOOST_OS_WINDOWS
s_availableApis[DirectSound] = true;
s_availableApis[XAudio2] = XAudio2API::InitializeStatic();
if(!s_availableApis[XAudio2]) // don't try to initialize the older lib if the newer version is available
if (!s_availableApis[XAudio2]) // don't try to initialize the older lib if the newer version is available
s_availableApis[XAudio27] = XAudio27API::InitializeStatic();
#endif
#if HAS_CUBEB
@@ -97,38 +98,38 @@ bool IAudioAPI::IsAudioAPIAvailable(AudioAPI api)
return false;
}

AudioAPIPtr IAudioAPI::CreateDeviceFromConfig(bool TV, sint32 rate, sint32 samples_per_block, sint32 bits_per_sample)
AudioAPIPtr IAudioAPI::CreateDeviceFromConfig(AudioType type, sint32 rate, sint32 samples_per_block, sint32 bits_per_sample)
{
auto& config = GetConfig();
sint32 channels = CemuConfig::AudioChannelsToNChannels(TV ? config.tv_channels : config.pad_channels);
return CreateDeviceFromConfig(TV, rate, channels, samples_per_block, bits_per_sample);
sint32 channels = CemuConfig::AudioChannelsToNChannels(AudioTypeToChannels(type));
return CreateDeviceFromConfig(type, rate, channels, samples_per_block, bits_per_sample);
}

AudioAPIPtr IAudioAPI::CreateDeviceFromConfig(bool TV, sint32 rate, sint32 channels, sint32 samples_per_block, sint32 bits_per_sample)
AudioAPIPtr IAudioAPI::CreateDeviceFromConfig(AudioType type, sint32 rate, sint32 channels, sint32 samples_per_block, sint32 bits_per_sample)
{
AudioAPIPtr audioAPIDev;

auto& config = GetConfig();

const auto audio_api = (IAudioAPI::AudioAPI)config.audio_api;
auto& selectedDevice = TV ? config.tv_device : config.pad_device;
auto selectedDevice = GetDeviceFromType(type);

if(selectedDevice.empty())
if (selectedDevice.empty())
return {};

IAudioAPI::DeviceDescriptionPtr device_description;
if (IAudioAPI::IsAudioAPIAvailable(audio_api))
{
auto devices = IAudioAPI::GetDevices(audio_api);
const auto it = std::find_if(devices.begin(), devices.end(), [&selectedDevice](const auto& d) {return d->GetIdentifier() == selectedDevice; });
const auto it = std::find_if(devices.begin(), devices.end(), [&selectedDevice](const auto& d) { return d->GetIdentifier() == selectedDevice; });
if (it != devices.end())
device_description = *it;
}
if (!device_description)
throw std::runtime_error("failed to find selected device while trying to create audio device");

audioAPIDev = CreateDevice(audio_api, device_description, rate, channels, samples_per_block, bits_per_sample);
audioAPIDev->SetVolume(TV ? config.tv_volume : config.pad_volume);
audioAPIDev->SetVolume(GetVolumeFromType(type));

return audioAPIDev;
}

@@ -137,7 +138,7 @@ AudioAPIPtr IAudioAPI::CreateDevice(AudioAPI api, const DeviceDescriptionPtr& de
if (!IsAudioAPIAvailable(api))
return {};

switch(api)
switch (api)
{
#if BOOST_OS_WINDOWS
case DirectSound:
@@ -157,11 +158,11 @@ AudioAPIPtr IAudioAPI::CreateDevice(AudioAPI api, const DeviceDescriptionPtr& de
}
#endif
#if HAS_CUBEB
case Cubeb:
{
const auto tmp = std::dynamic_pointer_cast<CubebAPI::CubebDeviceDescription>(device);
return std::make_unique<CubebAPI>(tmp->GetDeviceId(), samplerate, channels, samples_per_block, bits_per_sample);
}
case Cubeb:
{
const auto tmp = std::dynamic_pointer_cast<CubebAPI::CubebDeviceDescription>(device);
return std::make_unique<CubebAPI>(tmp->GetDeviceId(), samplerate, channels, samples_per_block, bits_per_sample);
}
#endif
default:
throw std::runtime_error(fmt::format("invalid audio api: {}", api));
@@ -172,8 +173,8 @@ std::vector<IAudioAPI::DeviceDescriptionPtr> IAudioAPI::GetDevices(AudioAPI api)
{
if (!IsAudioAPIAvailable(api))
return {};
switch(api)

switch (api)
{
#if BOOST_OS_WINDOWS
case DirectSound:
@@ -209,3 +210,51 @@ uint32 IAudioAPI::GetAudioDelay() const
{
return m_audioDelayOverride > 0 ? m_audioDelayOverride : s_audioDelay;
}

AudioChannels IAudioAPI::AudioTypeToChannels(AudioType type)
{
auto& config = GetConfig();
switch (type)
{
case TV:
return config.tv_channels;
case Gamepad:
return config.pad_channels;
case Portal:
return kMono;
default:
return kMono;
}
}

std::wstring IAudioAPI::GetDeviceFromType(AudioType type)
{
auto& config = GetConfig();
switch (type)
{
case TV:
return config.tv_device;
case Gamepad:
return config.pad_device;
case Portal:
return config.portal_device;
default:
return L"";
}
}

sint32 IAudioAPI::GetVolumeFromType(AudioType type)
{
auto& config = GetConfig();
switch (type)
{
case TV:
return config.tv_volume;
case Gamepad:
return config.pad_volume;
case Portal:
return config.portal_volume;
default:
return 0;
}
}
18 changes: 16 additions & 2 deletions src/audio/IAudioAPI.h
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@
#include <mmreg.h>
#endif

#include "config/CemuConfig.h"

class IAudioAPI
{
friend class GeneralSettings2;
@@ -30,6 +32,13 @@ class IAudioAPI

using DeviceDescriptionPtr = std::shared_ptr<DeviceDescription>;

enum AudioType
{
TV = 0,
Gamepad,
Portal
};

enum AudioAPI
{
DirectSound = 0,
@@ -62,8 +71,8 @@ class IAudioAPI
static void InitializeStatic();
static bool IsAudioAPIAvailable(AudioAPI api);

static std::unique_ptr<IAudioAPI> CreateDeviceFromConfig(bool TV, sint32 rate, sint32 samples_per_block, sint32 bits_per_sample);
static std::unique_ptr<IAudioAPI> CreateDeviceFromConfig(bool TV, sint32 rate, sint32 channels, sint32 samples_per_block, sint32 bits_per_sample);
static std::unique_ptr<IAudioAPI> CreateDeviceFromConfig(AudioType type, sint32 rate, sint32 samples_per_block, sint32 bits_per_sample);
static std::unique_ptr<IAudioAPI> CreateDeviceFromConfig(AudioType type, sint32 rate, sint32 channels, sint32 samples_per_block, sint32 bits_per_sample);
static std::unique_ptr<IAudioAPI> CreateDevice(AudioAPI api, const DeviceDescriptionPtr& device, sint32 samplerate, sint32 channels, sint32 samples_per_block, sint32 bits_per_sample);
static std::vector<DeviceDescriptionPtr> GetDevices(AudioAPI api);

@@ -84,6 +93,9 @@ class IAudioAPI
private:
static uint32 s_audioDelay;
void InitWFX(sint32 samplerate, sint32 channels, sint32 bits_per_sample);
static AudioChannels AudioTypeToChannels(AudioType type);
static std::wstring GetDeviceFromType(AudioType type);
static sint32 GetVolumeFromType(AudioType type);

};

@@ -93,3 +105,5 @@ extern AudioAPIPtr g_tvAudio;

extern AudioAPIPtr g_padAudio;
extern std::atomic_int32_t g_padVolume;

extern AudioAPIPtr g_portalAudio;
13 changes: 13 additions & 0 deletions src/config/CemuConfig.cpp
Original file line number Diff line number Diff line change
@@ -278,6 +278,7 @@ void CemuConfig::Load(XMLConfigParser& parser)
tv_volume = audio.get("TVVolume", 20);
pad_volume = audio.get("PadVolume", 0);
input_volume = audio.get("InputVolume", 20);
portal_volume = audio.get("PortalVolume", 20);

const auto tv = audio.get("TVDevice", "");
try
@@ -309,6 +310,16 @@ void CemuConfig::Load(XMLConfigParser& parser)
cemuLog_log(LogType::Force, "config load error: can't load input device: {}", input_device_name);
}

const auto portal_device_name = audio.get("PortalDevice", "");
try
{
portal_device = boost::nowide::widen(portal_device_name);
}
catch (const std::exception&)
{
cemuLog_log(LogType::Force, "config load error: can't load input device: {}", portal_device_name);
}

// account
auto acc = parser.get("Account");
account.m_persistent_id = acc.get("PersistentId", account.m_persistent_id);
@@ -511,9 +522,11 @@ void CemuConfig::Save(XMLConfigParser& parser)
audio.set("TVVolume", tv_volume);
audio.set("PadVolume", pad_volume);
audio.set("InputVolume", input_volume);
audio.set("PortalVolume", portal_volume);
audio.set("TVDevice", boost::nowide::narrow(tv_device).c_str());
audio.set("PadDevice", boost::nowide::narrow(pad_device).c_str());
audio.set("InputDevice", boost::nowide::narrow(input_device).c_str());
audio.set("PortalDevice", boost::nowide::narrow(portal_device).c_str());

// account
auto acc = config.set("Account");
4 changes: 2 additions & 2 deletions src/config/CemuConfig.h
Original file line number Diff line number Diff line change
@@ -480,8 +480,8 @@ struct CemuConfig
sint32 audio_api = 0;
sint32 audio_delay = 2;
AudioChannels tv_channels = kStereo, pad_channels = kStereo, input_channels = kMono;
sint32 tv_volume = 50, pad_volume = 0, input_volume = 50;
std::wstring tv_device{ L"default" }, pad_device, input_device;
sint32 tv_volume = 50, pad_volume = 0, input_volume = 50, portal_volume = 50;
std::wstring tv_device{ L"default" }, pad_device, input_device, portal_device;

// account
struct
Loading