Skip to content

Commit

Permalink
IOS/USB: Emulate Wii Speak using cubeb
Browse files Browse the repository at this point in the history
Based on @noahpistilli (Sketch) PR:
dolphin-emu#12567

Fixed the Windows support and the heisenbug caused by uninitialized
members.

Config system integration finalized.
  • Loading branch information
sepalani committed May 12, 2024
1 parent 9bbdfba commit d57577d
Show file tree
Hide file tree
Showing 11 changed files with 429 additions and 276 deletions.
87 changes: 87 additions & 0 deletions Source/Core/AudioCommon/CubebUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,90 @@ std::shared_ptr<cubeb> CubebUtils::GetContext()
weak = shared = {ctx, DestroyContext};
return shared;
}

std::vector<std::pair<std::string, std::string>> CubebUtils::ListInputDevices()
{
std::vector<std::pair<std::string, std::string>> devices;

cubeb_device_collection collection;
auto cubeb_ctx = CubebUtils::GetContext();
int r = cubeb_enumerate_devices(cubeb_ctx.get(), CUBEB_DEVICE_TYPE_INPUT, &collection);

if (r != CUBEB_OK)
{
ERROR_LOG_FMT(AUDIO, "Error listing cubeb input devices");
return devices;
}

INFO_LOG_FMT(AUDIO, "Listing cubeb input devices:");
for (uint32_t i = 0; i < collection.count; i++)
{
auto& info = collection.device[i];
auto& device_state = info.state;
const char* state_name = [device_state] {
switch (device_state)
{
case CUBEB_DEVICE_STATE_DISABLED:
return "disabled";
case CUBEB_DEVICE_STATE_UNPLUGGED:
return "unplugged";
case CUBEB_DEVICE_STATE_ENABLED:
return "enabled";
default:
return "unknown?";
}
}();

INFO_LOG_FMT(AUDIO,
"[{}] Device ID: {}\n"
"\tName: {}\n"
"\tGroup ID: {}\n"
"\tVendor: {}\n"
"\tState: {}",
i, info.device_id, info.friendly_name, info.group_id,
(info.vendor_name == nullptr) ? "(null)" : info.vendor_name, state_name);
if (info.state == CUBEB_DEVICE_STATE_ENABLED)
{
devices.emplace_back(info.device_id, info.friendly_name);
}
}

cubeb_device_collection_destroy(cubeb_ctx.get(), &collection);

return devices;
}

cubeb_devid CubebUtils::GetInputDeviceById(const std::string& id)
{
if (id.empty())
return nullptr;

cubeb_device_collection collection;
auto cubeb_ctx = CubebUtils::GetContext();
int r = cubeb_enumerate_devices(cubeb_ctx.get(), CUBEB_DEVICE_TYPE_INPUT, &collection);

if (r != CUBEB_OK)
{
ERROR_LOG_FMT(AUDIO, "Error enumerating cubeb input devices");
return nullptr;
}

cubeb_devid device_id = nullptr;
for (uint32_t i = 0; i < collection.count; i++)
{
auto& info = collection.device[i];
if (id.compare(info.device_id) == 0)
{
device_id = info.devid;
break;
}
}
if (device_id == nullptr)
{
WARN_LOG_FMT(AUDIO, "Failed to find selected input device, defaulting to system preferences");
}

cubeb_device_collection_destroy(cubeb_ctx.get(), &collection);

return device_id;
}
5 changes: 5 additions & 0 deletions Source/Core/AudioCommon/CubebUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@

#include <functional>
#include <memory>
#include <string>
#include <utility>
#include <vector>

struct cubeb;

namespace CubebUtils
{
std::shared_ptr<cubeb> GetContext();
std::vector<std::pair<std::string, std::string>> ListInputDevices();
const void* GetInputDeviceById(const std::string& id);
} // namespace CubebUtils
7 changes: 5 additions & 2 deletions Source/Core/Core/Config/MainSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -575,8 +575,11 @@ const Info<bool> MAIN_EMULATE_INFINITY_BASE{
const Info<bool> MAIN_EMULATE_WII_SPEAK{{System::Main, "EmulatedUSBDevices", "EmulateWiiSpeak"},
false};

const Info<std::string> MAIN_WII_SPEAK_MICROPHONE{{System::Main, "General", "WiiSpeakMicrophone"},
""};
const Info<std::string> MAIN_WII_SPEAK_MICROPHONE{
{System::Main, "EmulatedUSBDevices", "WiiSpeakMicrophone"}, ""};

const Info<bool> MAIN_WII_SPEAK_CONNECTED{{System::Main, "EmulatedUSBDevices", "WiiSpeakConnected"},
false};

// The reason we need this function is because some memory card code
// expects to get a non-NTSC-K region even if we're emulating an NTSC-K Wii.
Expand Down
1 change: 1 addition & 0 deletions Source/Core/Core/Config/MainSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ extern const Info<bool> MAIN_EMULATE_SKYLANDER_PORTAL;
extern const Info<bool> MAIN_EMULATE_INFINITY_BASE;
extern const Info<bool> MAIN_EMULATE_WII_SPEAK;
extern const Info<std::string> MAIN_WII_SPEAK_MICROPHONE;
extern const Info<bool> MAIN_WII_SPEAK_CONNECTED;

// GameCube path utility functions

Expand Down
212 changes: 169 additions & 43 deletions Source/Core/Core/IOS/USB/Emulated/Microphone.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,89 +3,215 @@

#include "Core/IOS/USB/Emulated/Microphone.h"

#include <algorithm>

#include <cubeb/cubeb.h>

#include "AudioCommon/CubebUtils.h"
#include "Common/Event.h"
#include "Common/Logging/Log.h"
#include "Common/ScopeGuard.h"
#include "Common/Swap.h"
#include "Core/Config/MainSettings.h"

#include <algorithm>
#ifdef _WIN32
#include <Objbase.h>
#endif

namespace IOS::HLE::USB
{
std::vector<std::string> Microphone::ListDevices()
Microphone::Microphone()
{
#ifdef _WIN32
Common::Event sync_event;
m_work_queue.EmplaceItem([this, &sync_event] {
Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); });
auto result = ::CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE);
m_coinit_success = result == S_OK;
m_should_couninit = result == S_OK || result == S_FALSE;
});
sync_event.Wait();
#endif

StreamInit();
}

Microphone::~Microphone()
{
std::vector<std::string> devices{};
const ALchar* pDeviceList = alcGetString(nullptr, ALC_CAPTURE_DEVICE_SPECIFIER);
while (*pDeviceList)
StreamTerminate();

#ifdef _WIN32
if (m_should_couninit)
{
devices.emplace_back(pDeviceList);
pDeviceList += strlen(pDeviceList) + 1;
Common::Event sync_event;
m_work_queue.EmplaceItem([this, &sync_event] {
Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); });
m_should_couninit = false;
CoUninitialize();
});
sync_event.Wait();
}

return devices;
m_coinit_success = false;
#endif
}

int Microphone::OpenMicrophone()
void Microphone::StreamInit()
{
m_device = alcCaptureOpenDevice(nullptr, SAMPLING_RATE, AL_FORMAT_MONO16, BUFFER_SIZE);
m_dsp_data.resize(BUFFER_SIZE, 0);
m_temp_buffer.resize(BUFFER_SIZE, 0);
return static_cast<int>(alcGetError(m_device));
#ifdef _WIN32
if (!m_coinit_success)
{
ERROR_LOG_FMT(IOS_USB, "Failed to init Wii Speak stream");
return;
}
Common::Event sync_event;
m_work_queue.EmplaceItem([this, &sync_event] {
Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); });
#endif
m_cubeb_ctx = CubebUtils::GetContext();
#ifdef _WIN32
});
sync_event.Wait();
#endif

// TODO: Not here but rather inside the WiiSpeak device if possible?
StreamStart();
}

int Microphone::StartCapture()
void Microphone::StreamTerminate()
{
alcCaptureStart(m_device);
return static_cast<int>(alcGetError(m_device));
StopStream();

if (m_cubeb_ctx)
{
#ifdef _WIN32
if (!m_coinit_success)
return;
Common::Event sync_event;
m_work_queue.EmplaceItem([this, &sync_event] {
Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); });
#endif
m_cubeb_ctx.reset();
#ifdef _WIN32
});
sync_event.Wait();
#endif
}
}

void Microphone::StopCapture()
static void state_callback(cubeb_stream* stream, void* user_data, cubeb_state state)
{
alcCaptureStop(m_device);
}

void Microphone::PerformAudioCapture()
void Microphone::StreamStart()
{
m_num_of_samples = BUFFER_SIZE / 2;

ALCint samples_in{};
alcGetIntegerv(m_device, ALC_CAPTURE_SAMPLES, 1, &samples_in);
m_num_of_samples = std::min(m_num_of_samples, static_cast<u32>(samples_in));
if (!m_cubeb_ctx)
return;

if (m_num_of_samples == 0)
#ifdef _WIN32
if (!m_coinit_success)
return;
Common::Event sync_event;
m_work_queue.EmplaceItem([this, &sync_event] {
Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); });
#endif

cubeb_stream_params params{};
params.format = CUBEB_SAMPLE_S16LE;
params.rate = SAMPLING_RATE;
params.channels = 1;
params.layout = CUBEB_LAYOUT_MONO;

u32 minimum_latency;
if (cubeb_get_min_latency(m_cubeb_ctx.get(), &params, &minimum_latency) != CUBEB_OK)
{
WARN_LOG_FMT(IOS_USB, "Error getting minimum latency");
}

alcCaptureSamples(m_device, m_dsp_data.data(), m_num_of_samples);
cubeb_devid input_device =
CubebUtils::GetInputDeviceById(Config::Get(Config::MAIN_WII_SPEAK_MICROPHONE));
if (cubeb_stream_init(m_cubeb_ctx.get(), &m_cubeb_stream, "Dolphin Emulated Wii Speak",
input_device, &params, nullptr, nullptr,
std::max<u32>(16, minimum_latency), DataCallback, state_callback,
this) != CUBEB_OK)
{
ERROR_LOG_FMT(IOS_USB, "Error initializing cubeb stream");
return;
}

if (cubeb_stream_start(m_cubeb_stream) != CUBEB_OK)
{
ERROR_LOG_FMT(IOS_USB, "Error starting cubeb stream");
return;
}

INFO_LOG_FMT(IOS_USB, "started cubeb stream");
#ifdef _WIN32
});
sync_event.Wait();
#endif
}

void Microphone::ByteSwap(const void* src, void* dst) const
void Microphone::StopStream()
{
*static_cast<u16*>(dst) = Common::swap16(*static_cast<const u16*>(src));
if (m_cubeb_stream)
{
#ifdef _WIN32
Common::Event sync_event;
m_work_queue.EmplaceItem([this, &sync_event] {
Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); });
#endif
if (cubeb_stream_stop(m_cubeb_stream) != CUBEB_OK)
ERROR_LOG_FMT(IOS_USB, "Error stopping cubeb stream");
cubeb_stream_destroy(m_cubeb_stream);
m_cubeb_stream = nullptr;
#ifdef _WIN32
});
sync_event.Wait();
#endif
}
}

void Microphone::GetSoundData()
long Microphone::DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer,
void* /*output_buffer*/, long nframes)
{
if (m_num_of_samples == 0)
return;
auto* mic = static_cast<Microphone*>(user_data);

u8* ptr = const_cast<u8*>(m_temp_buffer.data());
// Convert LE to BE
for (u32 i = 0; i < m_num_of_samples; i++)
std::lock_guard lk(mic->m_ring_lock);

const s16* buff_in = static_cast<const s16*>(input_buffer);
for (long i = 0; i < nframes; i++)
{
for (u32 indchan = 0; indchan < 1; indchan++)
{
const u32 curindex = (i * 2) + indchan * (16 / 8);
ByteSwap(m_dsp_data.data() + curindex, ptr + curindex);
}
mic->m_stream_buffer[mic->m_stream_wpos] = Common::swap16(buff_in[i]);
mic->m_stream_wpos = (mic->m_stream_wpos + 1) % mic->STREAM_SIZE;
}

m_rbuf_dsp.write_bytes(ptr, m_num_of_samples * 2);
mic->m_samples_avail += nframes;
if (mic->m_samples_avail > mic->STREAM_SIZE)
{
mic->m_samples_avail = 0;
}

return nframes;
}

void Microphone::ReadIntoBuffer(u8* dst, u32 size)
{
m_rbuf_dsp.read_bytes(dst, size);
std::lock_guard lk(m_ring_lock);

if (m_samples_avail >= BUFF_SIZE_SAMPLES)
{
u8* last_buffer = reinterpret_cast<u8*>(&m_stream_buffer[m_stream_rpos]);
std::memcpy(dst, static_cast<u8*>(last_buffer), size);

m_samples_avail -= BUFF_SIZE_SAMPLES;

m_stream_rpos += BUFF_SIZE_SAMPLES;
m_stream_rpos %= STREAM_SIZE;
}
}

bool Microphone::HasData() const
{
return m_num_of_samples != 0;
return m_samples_avail > 0 && Config::Get(Config::MAIN_WII_SPEAK_CONNECTED);
}
} // namespace IOS::HLE::USB

0 comments on commit d57577d

Please sign in to comment.