This commit is contained in:
sepalani 2025-04-20 01:40:11 +03:00 committed by GitHub
commit 87beeaae96
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 1541 additions and 3 deletions

View File

@ -28,6 +28,9 @@
<uses-permission
android:name="android.permission.VIBRATE"
android:required="false"/>
<uses-permission
android:name="android.permission.RECORD_AUDIO"
android:required="false"/>
<application
android:name=".DolphinApplication"

View File

@ -2,6 +2,7 @@
package org.dolphinemu.dolphinemu;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.hardware.usb.UsbManager;
@ -15,13 +16,15 @@ import org.dolphinemu.dolphinemu.utils.VolleyUtil;
public class DolphinApplication extends Application
{
private static DolphinApplication application;
private static ActivityTracker sActivityTracker;
@Override
public void onCreate()
{
super.onCreate();
application = this;
registerActivityLifecycleCallbacks(new ActivityTracker());
sActivityTracker = new ActivityTracker();
registerActivityLifecycleCallbacks(sActivityTracker);
VolleyUtil.init(getApplicationContext());
System.loadLibrary("main");
@ -36,4 +39,9 @@ public class DolphinApplication extends Application
{
return application.getApplicationContext();
}
public static Activity getAppActivity()
{
return sActivityTracker.getCurrentActivity();
}
}

View File

@ -232,6 +232,18 @@ enum class BooleanSetting(
"EmulateInfinityBase",
false
),
MAIN_EMULATE_WII_SPEAK(
Settings.FILE_DOLPHIN,
Settings.SECTION_EMULATED_USB_DEVICES,
"EmulateWiiSpeak",
false
),
MAIN_WII_SPEAK_MUTED(
Settings.FILE_DOLPHIN,
Settings.SECTION_EMULATED_USB_DEVICES,
"WiiSpeakMuted",
true
),
MAIN_SHOW_GAME_TITLES(
Settings.FILE_DOLPHIN,
Settings.SECTION_INI_ANDROID,
@ -917,7 +929,8 @@ enum class BooleanSetting(
MAIN_CUSTOM_RTC_ENABLE,
MAIN_DSP_JIT,
MAIN_EMULATE_SKYLANDER_PORTAL,
MAIN_EMULATE_INFINITY_BASE
MAIN_EMULATE_INFINITY_BASE,
MAIN_EMULATE_WII_SPEAK
)
private val NOT_RUNTIME_EDITABLE: Set<BooleanSetting> =
HashSet(listOf(*NOT_RUNTIME_EDITABLE_ARRAY))

View File

@ -15,6 +15,7 @@ import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.color.MaterialColors
import com.google.android.material.datepicker.CalendarConstraints
@ -59,6 +60,9 @@ class SettingsAdapter(
val settings: Settings?
get() = fragmentView.settings
val fragmentActivity: FragmentActivity
get() = fragmentView.fragmentActivity
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {

View File

@ -892,6 +892,22 @@ class SettingsFragmentPresenter(
0
)
)
sl.add(
SwitchSetting(
context,
BooleanSetting.MAIN_EMULATE_WII_SPEAK,
R.string.emulate_wii_speak,
0
)
)
sl.add(
SwitchSetting(
context,
BooleanSetting.MAIN_WII_SPEAK_MUTED,
R.string.mute_wii_speak,
0
)
)
}
private fun addAdvancedSettings(sl: ArrayList<SettingsItem>) {

View File

@ -2,6 +2,7 @@
package org.dolphinemu.dolphinemu.features.settings.ui.viewholder
import android.app.Activity
import android.view.View
import android.widget.CompoundButton
import org.dolphinemu.dolphinemu.databinding.ListItemSettingSwitchBinding
@ -10,6 +11,7 @@ import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem
import org.dolphinemu.dolphinemu.features.settings.model.view.SwitchSetting
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization
import org.dolphinemu.dolphinemu.utils.PermissionsHandler
import java.io.File
import java.util.*
@ -57,6 +59,13 @@ class SwitchSettingViewHolder(
binding.settingSwitch.isEnabled = false
}
if (setting.setting === BooleanSetting.MAIN_EMULATE_WII_SPEAK && isChecked) {
if (!PermissionsHandler.hasRecordAudioPermission(itemView.context)) {
val currentActivity = adapter.fragmentActivity as Activity
PermissionsHandler.requestRecordAudioPermission(currentActivity)
}
}
adapter.onBooleanClick(setting, binding.settingSwitch.isChecked)
setStyle(binding.textSettingName, setting)

View File

@ -7,12 +7,15 @@ import android.os.Bundle
class ActivityTracker : ActivityLifecycleCallbacks {
val resumedActivities = HashSet<Activity>()
var backgroundExecutionAllowed = false
var currentActivity : Activity? = null
private set
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
override fun onActivityStarted(activity: Activity) {}
override fun onActivityResumed(activity: Activity) {
currentActivity = activity
resumedActivities.add(activity)
if (!backgroundExecutionAllowed && !resumedActivities.isEmpty()) {
backgroundExecutionAllowed = true
@ -21,6 +24,9 @@ class ActivityTracker : ActivityLifecycleCallbacks {
}
override fun onActivityPaused(activity: Activity) {
if (currentActivity === activity) {
currentActivity = null
}
resumedActivities.remove(activity)
if (backgroundExecutionAllowed && resumedActivities.isEmpty()) {
backgroundExecutionAllowed = false

View File

@ -2,6 +2,7 @@
package org.dolphinemu.dolphinemu.utils;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
@ -11,10 +12,16 @@ import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.Manifest.permission.RECORD_AUDIO;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.DolphinApplication;
import org.dolphinemu.dolphinemu.NativeLibrary;
public class PermissionsHandler
{
public static final int REQUEST_CODE_WRITE_PERMISSION = 500;
public static final int REQUEST_CODE_RECORD_AUDIO = 501;
private static boolean sWritePermissionDenied = false;
public static void requestWritePermission(final FragmentActivity activity)
@ -52,4 +59,32 @@ public class PermissionsHandler
{
return sWritePermissionDenied;
}
public static boolean hasRecordAudioPermission(Context context)
{
if (context == null)
context = DolphinApplication.getAppContext();
int hasRecordPermission = ContextCompat.checkSelfPermission(context, RECORD_AUDIO);
return hasRecordPermission == PackageManager.PERMISSION_GRANTED;
}
public static void requestRecordAudioPermission(Activity activity)
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
return;
if (activity == null)
{
// Calling from C++ code
activity = DolphinApplication.getAppActivity();
// Since the emulation (and cubeb) has already started, enabling the microphone permission
// now might require restarting the game to be effective. Warn the user about it.
NativeLibrary.displayAlertMsg(
activity.getString(R.string.wii_speak_permission_warning),
activity.getString(R.string.wii_speak_permission_warning_description),
false, true, false);
}
activity.requestPermissions(new String[]{RECORD_AUDIO}, REQUEST_CODE_RECORD_AUDIO);
}
}

View File

@ -941,4 +941,8 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="incompatible_figure_selected">Incompatible Figure Selected</string>
<string name="select_compatible_figure">Please select a compatible figure file</string>
<string name="emulate_wii_speak">Wii Speak</string>
<string name="mute_wii_speak">Mute Wii Speak</string>
<string name="wii_speak_permission_warning">Missing Microphone Permission</string>
<string name="wii_speak_permission_warning_description">Wii Speak emulation requires microphone permission. You might need to restart the game for the permission to be effective.</string>
</resources>

View File

@ -116,6 +116,10 @@ static jmethodID s_core_device_control_constructor;
static jclass s_input_detector_class;
static jfieldID s_input_detector_pointer;
static jclass s_permission_handler_class;
static jmethodID s_permission_handler_has_record_audio_permission;
static jmethodID s_permission_handler_request_record_audio_permission;
static jmethodID s_runnable_run;
namespace IDCache
@ -538,6 +542,21 @@ jfieldID GetInputDetectorPointer()
return s_input_detector_pointer;
}
jclass GetPermissionHandlerClass()
{
return s_permission_handler_class;
}
jmethodID GetPermissionHandlerHasRecordAudioPermission()
{
return s_permission_handler_has_record_audio_permission;
}
jmethodID GetPermissionHandlerRequestRecordAudioPermission()
{
return s_permission_handler_request_record_audio_permission;
}
jmethodID GetRunnableRun()
{
return s_runnable_run;
@ -765,6 +784,16 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
s_input_detector_pointer = env->GetFieldID(input_detector_class, "pointer", "J");
env->DeleteLocalRef(input_detector_class);
const jclass permission_handler_class =
env->FindClass("org/dolphinemu/dolphinemu/utils/PermissionsHandler");
s_permission_handler_class =
reinterpret_cast<jclass>(env->NewGlobalRef(permission_handler_class));
s_permission_handler_has_record_audio_permission = env->GetStaticMethodID(
permission_handler_class, "hasRecordAudioPermission", "(Landroid/content/Context;)Z");
s_permission_handler_request_record_audio_permission = env->GetStaticMethodID(
permission_handler_class, "requestRecordAudioPermission", "(Landroid/app/Activity;)V");
env->DeleteLocalRef(permission_handler_class);
const jclass runnable_class = env->FindClass("java/lang/Runnable");
s_runnable_run = env->GetMethodID(runnable_class, "run", "()V");
env->DeleteLocalRef(runnable_class);
@ -804,5 +833,6 @@ JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved)
env->DeleteGlobalRef(s_core_device_class);
env->DeleteGlobalRef(s_core_device_control_class);
env->DeleteGlobalRef(s_input_detector_class);
env->DeleteGlobalRef(s_permission_handler_class);
}
}

View File

@ -115,6 +115,10 @@ jmethodID GetCoreDeviceControlConstructor();
jclass GetInputDetectorClass();
jfieldID GetInputDetectorPointer();
jclass GetPermissionHandlerClass();
jmethodID GetPermissionHandlerHasRecordAudioPermission();
jmethodID GetPermissionHandlerRequestRecordAudioPermission();
jmethodID GetRunnableRun();
} // namespace IDCache

View File

@ -16,6 +16,13 @@
#include <cubeb/cubeb.h>
#ifdef _WIN32
#include <Objbase.h>
#include "Common/Event.h"
#include "Common/ScopeGuard.h"
#endif
static ptrdiff_t s_path_cutoff_point = 0;
static void LogCallback(const char* format, ...)
@ -49,7 +56,9 @@ static void DestroyContext(cubeb* ctx)
}
}
std::shared_ptr<cubeb> CubebUtils::GetContext()
namespace CubebUtils
{
std::shared_ptr<cubeb> GetContext()
{
static std::weak_ptr<cubeb> weak;
@ -82,3 +91,146 @@ std::shared_ptr<cubeb> CubebUtils::GetContext()
weak = shared = {ctx, DestroyContext};
return shared;
}
std::vector<std::pair<std::string, std::string>> ListInputDevices()
{
std::vector<std::pair<std::string, std::string>> devices;
cubeb_device_collection collection;
auto cubeb_ctx = 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 GetInputDeviceById(std::string_view id)
{
if (id.empty())
return nullptr;
cubeb_device_collection collection;
auto cubeb_ctx = 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;
}
CoInitSyncWorker::CoInitSyncWorker([[maybe_unused]] std::string_view worker_name)
#ifdef _WIN32
: m_work_queue
{
worker_name, [](const CoInitSyncWorker::FunctionType& f) { f(); }
}
#endif
{
#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
}
CoInitSyncWorker::~CoInitSyncWorker()
{
#ifdef _WIN32
if (m_should_couninit)
{
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();
}
m_coinit_success = false;
#endif
}
bool CoInitSyncWorker::Execute(FunctionType f)
{
#ifdef _WIN32
if (!m_coinit_success)
return false;
Common::Event sync_event;
m_work_queue.EmplaceItem([&sync_event, f] {
Common::ScopeGuard sync_event_guard([&sync_event] { sync_event.Set(); });
#endif
f();
#ifdef _WIN32
});
sync_event.Wait();
#endif
return true;
}
} // namespace CubebUtils

View File

@ -5,10 +5,39 @@
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#ifdef _WIN32
#include "Common/WorkQueueThread.h"
#endif
struct cubeb;
namespace CubebUtils
{
std::shared_ptr<cubeb> GetContext();
std::vector<std::pair<std::string, std::string>> ListInputDevices();
const void* GetInputDeviceById(std::string_view id);
// Helper used to handle Windows COM library for cubeb WASAPI backend
class CoInitSyncWorker
{
public:
using FunctionType = std::function<void()>;
CoInitSyncWorker(std::string_view worker_name);
~CoInitSyncWorker();
bool Execute(FunctionType f);
#ifdef _WIN32
private:
Common::WorkQueueThread<FunctionType> m_work_queue;
bool m_coinit_success = false;
bool m_should_couninit = false;
#endif
};
} // namespace CubebUtils

View File

@ -427,12 +427,16 @@ add_library(core
IOS/USB/Common.h
IOS/USB/Emulated/Infinity.cpp
IOS/USB/Emulated/Infinity.h
IOS/USB/Emulated/Microphone.cpp
IOS/USB/Emulated/Microphone.h
IOS/USB/Emulated/Skylanders/Skylander.cpp
IOS/USB/Emulated/Skylanders/Skylander.h
IOS/USB/Emulated/Skylanders/SkylanderCrypto.cpp
IOS/USB/Emulated/Skylanders/SkylanderCrypto.h
IOS/USB/Emulated/Skylanders/SkylanderFigure.cpp
IOS/USB/Emulated/Skylanders/SkylanderFigure.h
IOS/USB/Emulated/WiiSpeak.cpp
IOS/USB/Emulated/WiiSpeak.h
IOS/USB/Host.cpp
IOS/USB/Host.h
IOS/USB/OH0/OH0.cpp

View File

@ -591,6 +591,16 @@ const Info<bool> MAIN_EMULATE_SKYLANDER_PORTAL{
const Info<bool> MAIN_EMULATE_INFINITY_BASE{
{System::Main, "EmulatedUSBDevices", "EmulateInfinityBase"}, false};
const Info<bool> MAIN_EMULATE_WII_SPEAK{{System::Main, "EmulatedUSBDevices", "EmulateWiiSpeak"},
false};
const Info<std::string> MAIN_WII_SPEAK_MICROPHONE{
{System::Main, "EmulatedUSBDevices", "WiiSpeakMicrophone"}, ""};
const Info<bool> MAIN_WII_SPEAK_MUTED{{System::Main, "EmulatedUSBDevices", "WiiSpeakMuted"}, true};
const Info<s16> MAIN_WII_SPEAK_VOLUME_MODIFIER{
{System::Main, "EmulatedUSBDevices", "WiiSpeakVolumeModifier"}, 0};
// 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.
DiscIO::Region ToGameCubeRegion(DiscIO::Region region)

View File

@ -360,6 +360,10 @@ void SetUSBDeviceWhitelist(const std::set<std::pair<u16, u16>>& devices);
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_MUTED;
extern const Info<s16> MAIN_WII_SPEAK_VOLUME_MODIFIER;
// GameCube path utility functions

View File

@ -0,0 +1,348 @@
// Copyright 2024 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Core/IOS/USB/Emulated/Microphone.h"
#include <span>
#include <cubeb/cubeb.h>
#include "AudioCommon/CubebUtils.h"
#include "Common/Logging/Log.h"
#include "Common/MathUtil.h"
#include "Common/Swap.h"
#include "Core/Config/MainSettings.h"
#include "Core/Core.h"
#include "Core/IOS/USB/Emulated/WiiSpeak.h"
#ifdef _WIN32
#include <Objbase.h>
#endif
#ifdef ANDROID
#include "jni/AndroidCommon/IDCache.h"
#endif
namespace IOS::HLE::USB
{
Microphone::Microphone(const WiiSpeakState& sampler) : m_sampler(sampler)
{
StreamInit();
}
Microphone::~Microphone()
{
StreamTerminate();
}
void Microphone::StreamInit()
{
if (!m_worker.Execute([this] { m_cubeb_ctx = CubebUtils::GetContext(); }))
{
ERROR_LOG_FMT(IOS_USB, "Failed to init Wii Speak stream");
return;
}
// TODO: Not here but rather inside the WiiSpeak device if possible?
StreamStart(m_sampler.DEFAULT_SAMPLING_RATE);
}
void Microphone::StreamTerminate()
{
StopStream();
if (m_cubeb_ctx)
m_worker.Execute([this] { m_cubeb_ctx.reset(); });
}
static void state_callback(cubeb_stream* stream, void* user_data, cubeb_state state)
{
}
void Microphone::StreamStart(u32 sampling_rate)
{
if (!m_cubeb_ctx)
return;
m_worker.Execute([this, sampling_rate] {
#ifdef ANDROID
JNIEnv* env = IDCache::GetEnvForThread();
if (jboolean result = env->CallStaticBooleanMethod(
IDCache::GetPermissionHandlerClass(),
IDCache::GetPermissionHandlerHasRecordAudioPermission(), nullptr);
result == JNI_FALSE)
{
env->CallStaticVoidMethod(IDCache::GetPermissionHandlerClass(),
IDCache::GetPermissionHandlerRequestRecordAudioPermission(),
nullptr);
}
#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");
}
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");
});
}
void Microphone::StopStream()
{
if (!m_cubeb_stream)
return;
m_worker.Execute([this] {
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;
});
}
long Microphone::DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer,
void* /*output_buffer*/, long nframes)
{
// Skip data when core isn't running
if (Core::GetState(Core::System::GetInstance()) != Core::State::Running)
return nframes;
// Skip data when HLE Wii Speak is muted
// TODO: Update cubeb and use cubeb_stream_set_input_mute
if (Config::Get(Config::MAIN_WII_SPEAK_MUTED))
return nframes;
auto* mic = static_cast<Microphone*>(user_data);
const auto& sampler = mic->GetSampler();
// Skip data if sampling is off or mute is on
if (!sampler.sample_on || sampler.mute)
return nframes;
std::lock_guard lk(mic->m_ring_lock);
std::span<const s16> buff_in(static_cast<const s16*>(input_buffer), nframes);
const auto gain = mic->ComputeGain(Config::Get(Config::MAIN_WII_SPEAK_VOLUME_MODIFIER));
auto processed_buff_in = buff_in | std::views::transform([gain](s16 sample) {
return MathUtil::SaturatingCast<s16>(sample * gain);
});
mic->UpdateLoudness(processed_buff_in);
for (s16 le_sample : processed_buff_in)
{
mic->m_stream_buffer[mic->m_stream_wpos] = Common::swap16(le_sample);
mic->m_stream_wpos = (mic->m_stream_wpos + 1) % mic->STREAM_SIZE;
}
mic->m_samples_avail += nframes;
if (mic->m_samples_avail > mic->STREAM_SIZE)
{
WARN_LOG_FMT(IOS_USB, "Wii Speak ring buffer is full, data will be lost!");
mic->m_samples_avail = 0;
}
return nframes;
}
u16 Microphone::ReadIntoBuffer(u8* ptr, u32 size)
{
static constexpr u32 SINGLE_READ_SIZE = BUFF_SIZE_SAMPLES * sizeof(SampleType);
// Avoid buffer overflow during memcpy
static_assert((STREAM_SIZE % BUFF_SIZE_SAMPLES) == 0,
"The STREAM_SIZE isn't a multiple of BUFF_SIZE_SAMPLES");
std::lock_guard lk(m_ring_lock);
u8* begin = ptr;
for (u8* end = begin + size; ptr < end; ptr += SINGLE_READ_SIZE, size -= SINGLE_READ_SIZE)
{
if (size < SINGLE_READ_SIZE || m_samples_avail < BUFF_SIZE_SAMPLES)
break;
SampleType* last_buffer = &m_stream_buffer[m_stream_rpos];
std::memcpy(ptr, last_buffer, SINGLE_READ_SIZE);
m_samples_avail -= BUFF_SIZE_SAMPLES;
m_stream_rpos += BUFF_SIZE_SAMPLES;
m_stream_rpos %= STREAM_SIZE;
}
return static_cast<u16>(ptr - begin);
}
u16 Microphone::GetLoudnessLevel() const
{
if (m_sampler.mute || Config::Get(Config::MAIN_WII_SPEAK_MUTED))
return 0;
return m_loudness_level;
}
// Based on graphical cues on Monster Hunter 3, the level seems properly displayed with values
// between 0 and 0x3a00.
//
// TODO: Proper hardware testing, documentation, formulas...
void Microphone::UpdateLoudness(std::ranges::input_range auto&& samples)
{
// Based on MH3 graphical cues, let's use a 0x4000 window
static const u32 WINDOW = 0x4000;
static const float UNIT = (m_loudness.DB_MAX - m_loudness.DB_MIN) / WINDOW;
m_loudness.Update(samples);
if (m_loudness.samples_count >= m_loudness.SAMPLES_NEEDED)
{
const float amp_db = m_loudness.GetAmplitudeDb();
m_loudness_level = static_cast<u16>((amp_db - m_loudness.DB_MIN) / UNIT);
#ifdef WII_SPEAK_LOG_STATS
m_loudness.LogStats();
#else
DEBUG_LOG_FMT(IOS_USB,
"Wii Speak loudness stats (sample count: {}/{}):\n"
" - min={} max={} amplitude={} dB\n"
" - level={:04x}",
m_loudness.samples_count, m_loudness.SAMPLES_NEEDED, m_loudness.peak_min,
m_loudness.peak_max, amp_db, m_loudness_level);
#endif
m_loudness.Reset();
}
}
bool Microphone::HasData(u32 sample_count = BUFF_SIZE_SAMPLES) const
{
return m_samples_avail >= sample_count;
}
const WiiSpeakState& Microphone::GetSampler() const
{
return m_sampler;
}
Microphone::FloatType Microphone::ComputeGain(FloatType relative_db) const
{
return m_loudness.ComputeGain(relative_db);
}
void Microphone::SetSamplingRate(u32 sampling_rate)
{
StopStream();
StreamStart(sampling_rate);
}
const Microphone::FloatType Microphone::Loudness::DB_MIN =
20 * std::log10(FloatType(1) / MAX_AMPLTIUDE);
const Microphone::FloatType Microphone::Loudness::DB_MAX = 20 * std::log10(FloatType(1));
Microphone::Loudness::SampleType Microphone::Loudness::GetPeak() const
{
return std::max(std::abs(peak_min), std::abs(peak_max));
}
Microphone::FloatType Microphone::Loudness::GetDecibel(FloatType value) const
{
return 20 * std::log10(value);
}
Microphone::FloatType Microphone::Loudness::GetAmplitude() const
{
return GetPeak() / MAX_AMPLTIUDE;
}
Microphone::FloatType Microphone::Loudness::GetAmplitudeDb() const
{
return GetDecibel(GetAmplitude());
}
Microphone::FloatType Microphone::Loudness::GetAbsoluteMean() const
{
return FloatType(absolute_sum) / samples_count;
}
Microphone::FloatType Microphone::Loudness::GetAbsoluteMeanDb() const
{
return GetDecibel(GetAbsoluteMean());
}
Microphone::FloatType Microphone::Loudness::GetRootMeanSquare() const
{
return std::sqrt(square_sum / samples_count);
}
Microphone::FloatType Microphone::Loudness::GetRootMeanSquareDb() const
{
return GetDecibel(GetRootMeanSquare());
}
Microphone::FloatType Microphone::Loudness::GetCrestFactor() const
{
const auto rms = GetRootMeanSquare();
if (rms == 0)
return FloatType(0);
return GetPeak() / rms;
}
Microphone::FloatType Microphone::Loudness::GetCrestFactorDb() const
{
return GetDecibel(GetCrestFactor());
}
Microphone::FloatType Microphone::Loudness::ComputeGain(FloatType db) const
{
return std::pow(FloatType(10), db / 20);
}
void Microphone::Loudness::Reset()
{
samples_count = 0;
absolute_sum = 0;
square_sum = FloatType(0);
peak_min = 0;
peak_max = 0;
}
void Microphone::Loudness::LogStats()
{
const auto amplitude = GetAmplitude();
const auto amplitude_db = GetDecibel(amplitude);
const auto rms = GetRootMeanSquare();
const auto rms_db = GetDecibel(rms);
const auto abs_mean = GetAbsoluteMean();
const auto abs_mean_db = GetDecibel(abs_mean);
const auto crest_factor = GetCrestFactor();
const auto crest_factor_db = GetDecibel(crest_factor);
INFO_LOG_FMT(IOS_USB,
"Wii Speak loudness stats (sample count: {}/{}):\n"
" - min={} max={} amplitude={} ({} dB)\n"
" - rms={} ({} dB) \n"
" - abs_mean={} ({} dB)\n"
" - crest_factor={} ({} dB)",
samples_count, SAMPLES_NEEDED, peak_min, peak_max, amplitude, amplitude_db, rms,
rms_db, abs_mean, abs_mean_db, crest_factor, crest_factor_db);
}
} // namespace IOS::HLE::USB

View File

@ -0,0 +1,123 @@
// Copyright 2024 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <memory>
#include <mutex>
#include <numeric>
#include <ranges>
#include <type_traits>
#include "AudioCommon/CubebUtils.h"
#include "Common/CommonTypes.h"
struct cubeb;
struct cubeb_stream;
namespace IOS::HLE::USB
{
struct WiiSpeakState;
class Microphone final
{
public:
using FloatType = float;
Microphone(const WiiSpeakState& sampler);
~Microphone();
bool HasData(u32 sample_count) const;
u16 ReadIntoBuffer(u8* ptr, u32 size);
u16 GetLoudnessLevel() const;
void UpdateLoudness(std::ranges::input_range auto&& samples);
const WiiSpeakState& GetSampler() const;
FloatType ComputeGain(FloatType relative_db) const;
void SetSamplingRate(u32 sampling_rate);
private:
static long DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer,
void* output_buffer, long nframes);
void StreamInit();
void StreamTerminate();
void StreamStart(u32 sampling_rate);
void StopStream();
using SampleType = s16;
static constexpr u32 BUFF_SIZE_SAMPLES = 32;
static constexpr u32 STREAM_SIZE = BUFF_SIZE_SAMPLES * 500;
std::array<SampleType, STREAM_SIZE> m_stream_buffer{};
u32 m_stream_wpos = 0;
u32 m_stream_rpos = 0;
u32 m_samples_avail = 0;
// TODO: Find how this level is calculated on real hardware
u16 m_loudness_level = 0;
struct Loudness
{
using SampleType = s16;
using UnsignedSampleType = std::make_unsigned_t<SampleType>;
void Update(const auto& samples)
{
samples_count += static_cast<u16>(samples.size());
const auto [min_element, max_element] = std::ranges::minmax_element(samples);
peak_min = std::min<SampleType>(*min_element, peak_min);
peak_max = std::max<SampleType>(*max_element, peak_max);
const auto begin = samples.begin();
const auto end = samples.end();
absolute_sum = std::reduce(begin, end, absolute_sum,
[](u32 a, SampleType b) { return a + std::abs(b); });
square_sum = std::reduce(begin, end, square_sum, [](FloatType a, s16 b) {
return a + std::pow(FloatType(b), FloatType(2));
});
}
SampleType GetPeak() const;
FloatType GetDecibel(FloatType value) const;
FloatType GetAmplitude() const;
FloatType GetAmplitudeDb() const;
FloatType GetAbsoluteMean() const;
FloatType GetAbsoluteMeanDb() const;
FloatType GetRootMeanSquare() const;
FloatType GetRootMeanSquareDb() const;
FloatType GetCrestFactor() const;
FloatType GetCrestFactorDb() const;
FloatType ComputeGain(FloatType db) const;
void Reset();
void LogStats();
// Samples used to compute the loudness level (arbitrarily chosen)
static constexpr u16 SAMPLES_NEEDED = 128;
static_assert((SAMPLES_NEEDED % BUFF_SIZE_SAMPLES) == 0);
static constexpr FloatType MAX_AMPLTIUDE = std::numeric_limits<UnsignedSampleType>::max() / 2;
static const FloatType DB_MIN;
static const FloatType DB_MAX;
u16 samples_count = 0;
u32 absolute_sum = 0;
FloatType square_sum = FloatType(0);
SampleType peak_min = 0;
SampleType peak_max = 0;
};
Loudness m_loudness;
std::mutex m_ring_lock;
std::shared_ptr<cubeb> m_cubeb_ctx = nullptr;
cubeb_stream* m_cubeb_stream = nullptr;
const WiiSpeakState& m_sampler;
CubebUtils::CoInitSyncWorker m_worker{"Wii Speak Worker"};
};
} // namespace IOS::HLE::USB

View File

@ -0,0 +1,423 @@
// Copyright 2024 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Core/IOS/USB/Emulated/WiiSpeak.h"
#include <algorithm>
#include "Core/Config/MainSettings.h"
#include "Core/HW/Memmap.h"
namespace IOS::HLE::USB
{
WiiSpeak::WiiSpeak(IOS::HLE::EmulationKernel& ios) : m_ios(ios)
{
m_id = u64(m_vid) << 32 | u64(m_pid) << 16 | u64(9) << 8 | u64(1);
}
WiiSpeak::~WiiSpeak() = default;
DeviceDescriptor WiiSpeak::GetDeviceDescriptor() const
{
return m_device_descriptor;
}
std::vector<ConfigDescriptor> WiiSpeak::GetConfigurations() const
{
return m_config_descriptor;
}
std::vector<InterfaceDescriptor> WiiSpeak::GetInterfaces(u8 config) const
{
return m_interface_descriptor;
}
std::vector<EndpointDescriptor> WiiSpeak::GetEndpoints(u8 config, u8 interface, u8 alt) const
{
return m_endpoint_descriptor;
}
bool WiiSpeak::Attach()
{
if (m_device_attached)
return true;
DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x}] Opening device", m_vid, m_pid);
if (!m_microphone)
m_microphone = std::make_unique<Microphone>(m_sampler);
m_device_attached = true;
return true;
}
bool WiiSpeak::AttachAndChangeInterface(const u8 interface)
{
if (!Attach())
return false;
if (interface != m_active_interface)
return ChangeInterface(interface) == 0;
return true;
}
int WiiSpeak::CancelTransfer(const u8 endpoint)
{
INFO_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Cancelling transfers (endpoint {:#x})", m_vid, m_pid,
m_active_interface, endpoint);
return IPC_SUCCESS;
}
int WiiSpeak::ChangeInterface(const u8 interface)
{
DEBUG_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Changing interface to {}", m_vid, m_pid,
m_active_interface, interface);
m_active_interface = interface;
return 0;
}
int WiiSpeak::GetNumberOfAltSettings(u8 interface)
{
return 0;
}
int WiiSpeak::SetAltSetting(u8 alt_setting)
{
return 0;
}
int WiiSpeak::SubmitTransfer(std::unique_ptr<CtrlMessage> cmd)
{
DEBUG_LOG_FMT(IOS_USB,
"[{:04x}:{:04x} {}] Control: bRequestType={:02x} bRequest={:02x} wValue={:04x}"
" wIndex={:04x} wLength={:04x}",
m_vid, m_pid, m_active_interface, cmd->request_type, cmd->request, cmd->value,
cmd->index, cmd->length);
switch (cmd->request_type << 8 | cmd->request)
{
case USBHDR(DIR_DEVICE2HOST, TYPE_STANDARD, REC_INTERFACE, REQUEST_GET_INTERFACE):
{
const u8 data{1};
cmd->FillBuffer(&data, sizeof(data));
cmd->ScheduleTransferCompletion(1, 100);
break;
}
case USBHDR(DIR_HOST2DEVICE, TYPE_STANDARD, REC_INTERFACE, REQUEST_SET_INTERFACE):
{
INFO_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] REQUEST_SET_INTERFACE index={:04x} value={:04x}",
m_vid, m_pid, m_active_interface, cmd->index, cmd->value);
if (static_cast<u8>(cmd->index) != m_active_interface)
{
const int ret = ChangeInterface(static_cast<u8>(cmd->index));
if (ret < 0)
{
ERROR_LOG_FMT(IOS_USB, "[{:04x}:{:04x} {}] Failed to change interface to {}", m_vid, m_pid,
m_active_interface, cmd->index);
return ret;
}
}
const int ret = SetAltSetting(static_cast<u8>(cmd->value));
if (ret == 0)
m_ios.EnqueueIPCReply(cmd->ios_request, cmd->length);
return ret;
}
case USBHDR(DIR_HOST2DEVICE, TYPE_VENDOR, REC_INTERFACE, 0):
{
init = false;
m_ios.EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS);
break;
}
case USBHDR(DIR_DEVICE2HOST, TYPE_VENDOR, REC_INTERFACE, REQUEST_GET_DESCRIPTOR):
{
if (!init)
{
const u8 data{0};
cmd->FillBuffer(&data, sizeof(data));
m_ios.EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS);
init = true;
}
else
{
const u8 data{1};
cmd->FillBuffer(&data, sizeof(data));
m_ios.EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS);
}
break;
}
case USBHDR(DIR_HOST2DEVICE, TYPE_VENDOR, REC_INTERFACE, 1):
SetRegister(cmd);
m_ios.EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS);
break;
case USBHDR(DIR_DEVICE2HOST, TYPE_VENDOR, REC_INTERFACE, 2):
GetRegister(cmd);
m_ios.EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS);
break;
default:
NOTICE_LOG_FMT(IOS_USB, "Unknown command");
m_ios.EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS);
}
return IPC_SUCCESS;
}
int WiiSpeak::SubmitTransfer(std::unique_ptr<BulkMessage> cmd)
{
m_ios.EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS);
return IPC_SUCCESS;
}
int WiiSpeak::SubmitTransfer(std::unique_ptr<IntrMessage> cmd)
{
m_ios.EnqueueIPCReply(cmd->ios_request, IPC_SUCCESS);
return IPC_SUCCESS;
}
int WiiSpeak::SubmitTransfer(std::unique_ptr<IsoMessage> cmd)
{
auto& system = m_ios.GetSystem();
auto& memory = system.GetMemory();
u8* packets = memory.GetPointerForRange(cmd->data_address, cmd->length);
if (packets == nullptr)
{
ERROR_LOG_FMT(IOS_USB, "Wii Speak command invalid");
return IPC_EINVAL;
}
switch (cmd->endpoint)
{
case ENDPOINT_AUDIO_IN:
{
// Transfer: Wii Speak -> Wii
u16 size = 0;
if (m_microphone && m_microphone->HasData(cmd->length / sizeof(s16)))
size = m_microphone->ReadIntoBuffer(packets, cmd->length);
for (std::size_t i = 0; i < cmd->num_packets; i++)
{
cmd->SetPacketReturnValue(i, std::min(size, cmd->packet_sizes[i]));
size = (size > cmd->packet_sizes[i]) ? (size - cmd->packet_sizes[i]) : 0;
}
break;
}
case ENDPOINT_AUDIO_OUT:
// Transfer: Wii -> Wii Speak
break;
default:
WARN_LOG_FMT(IOS_USB, "Wii Speak unsupported isochronous transfer (endpoint={:02x})",
cmd->endpoint);
break;
}
// Transferring too slow causes the visual cue to not appear,
// while transferring too fast results in more choppy audio.
DEBUG_LOG_FMT(IOS_USB,
"Wii Speak isochronous transfer: length={:04x} endpoint={:02x} num_packets={:02x}",
cmd->length, cmd->endpoint, cmd->num_packets);
// According to the Wii Speak specs on wiibrew, it's "USB 2.0 Full-speed Device Module",
// so the length of a single frame should be 1 ms.
//
// Monster Hunter 3 and the Wii Speak Channel use cmd->length=0x100, allowing 256/2 samples
// (i.e. 128 samples in 16-bit mono) per frame transfer. The Microphone class is using cubeb
// configured with a sample rate of 8000.
//
// Based on USB sniffing using Wireshark + Dolphin USB passthrough:
// - 125 frames are received per second (i.e. timing 8 ms per frame)
// - however, the cmd->length=0x80 which doesn't match the HLE emulation
// - each frame having 8 packets of 0x10 bytes
//
// Let's sample at a reasonable speed.
const u32 transfer_timing = 2000;
cmd->ScheduleTransferCompletion(IPC_SUCCESS, transfer_timing);
return IPC_SUCCESS;
}
void WiiSpeak::SetRegister(const std::unique_ptr<CtrlMessage>& cmd)
{
auto& system = m_ios.GetSystem();
auto& memory = system.GetMemory();
const u8 reg = memory.Read_U8(cmd->data_address + 1) & ~1;
const u16 arg1 = memory.Read_U16(cmd->data_address + 2);
const u16 arg2 = memory.Read_U16(cmd->data_address + 4);
DEBUG_LOG_FMT(IOS_USB, "Wii Speak register set (reg={:02x}, arg1={:04x}, arg2={:04x})", reg, arg1,
arg2);
switch (reg)
{
case SAMPLER_STATE:
m_sampler.sample_on = !!arg1;
break;
case SAMPLER_FREQ:
switch (arg1)
{
case FREQ_8KHZ:
m_sampler.freq = 8000;
break;
case FREQ_11KHZ:
m_sampler.freq = 11025;
break;
case FREQ_RESERVED:
default:
WARN_LOG_FMT(IOS_USB,
"Wii Speak unsupported SAMPLER_FREQ set (arg1={:04x}, arg2={:04x}) defaulting "
"to FREQ_16KHZ",
arg1, arg2);
[[fallthrough]];
case FREQ_16KHZ:
m_sampler.freq = 16000;
break;
}
if (m_microphone)
m_microphone->SetSamplingRate(m_sampler.freq);
break;
case SAMPLER_GAIN:
WARN_LOG_FMT(IOS_USB, "Wii Speak SAMPLER_GAIN set (arg1={:04x}, arg2={:04x}) not implemented",
arg1, arg2);
switch (arg1 & ~0x300)
{
case GAIN_00dB:
m_sampler.gain = 0;
break;
case GAIN_15dB:
m_sampler.gain = 15;
break;
case GAIN_30dB:
m_sampler.gain = 30;
break;
default:
WARN_LOG_FMT(IOS_USB,
"Wii Speak unsupported SAMPLER_GAIN set (arg1={:04x}, arg2={:04x}) defaulting "
"to GAIN_36dB",
arg1, arg2);
[[fallthrough]];
case GAIN_36dB:
m_sampler.gain = 36;
break;
}
break;
case EC_STATE:
m_sampler.ec_reset = !!arg1;
break;
case SP_STATE:
switch (arg1)
{
case SP_ENABLE:
m_sampler.sp_on = arg2 == 0;
break;
case SP_SIN:
case SP_SOUT:
case SP_RIN:
break;
default:
WARN_LOG_FMT(IOS_USB, "Wii Speak unsupported SP_STATE set (arg1={:04x}, arg2={:04x})", arg1,
arg2);
break;
}
break;
case SAMPLER_MUTE:
m_sampler.mute = !!arg1;
break;
default:
WARN_LOG_FMT(IOS_USB,
"Wii Speak unsupported register set (reg={:02x}, arg1={:04x}, arg2={:04x})", reg,
arg1, arg2);
break;
}
}
void WiiSpeak::GetRegister(const std::unique_ptr<CtrlMessage>& cmd) const
{
auto& system = m_ios.GetSystem();
auto& memory = system.GetMemory();
const u8 reg = memory.Read_U8(cmd->data_address + 1) & ~1;
const u32 arg1 = cmd->data_address + 2;
const u32 arg2 = cmd->data_address + 4;
DEBUG_LOG_FMT(IOS_USB, "Wii Speak register get (reg={:02x}, arg1={:08x}, arg2={:08x})", reg, arg1,
arg2);
switch (reg)
{
case SAMPLER_STATE:
memory.Write_U16(m_sampler.sample_on ? 1 : 0, arg1);
break;
case SAMPLER_FREQ:
switch (m_sampler.freq)
{
case 8000:
memory.Write_U16(FREQ_8KHZ, arg1);
break;
case 11025:
memory.Write_U16(FREQ_11KHZ, arg1);
break;
default:
WARN_LOG_FMT(IOS_USB,
"Wii Speak unsupported SAMPLER_FREQ get (arg1={:04x}, arg2={:04x}) defaulting "
"to FREQ_16KHZ",
arg1, arg2);
[[fallthrough]];
case 16000:
memory.Write_U16(FREQ_16KHZ, arg1);
break;
}
break;
case SAMPLER_GAIN:
switch (m_sampler.gain)
{
case 0:
memory.Write_U16(0x300 | GAIN_00dB, arg1);
break;
case 15:
memory.Write_U16(0x300 | GAIN_15dB, arg1);
break;
case 30:
memory.Write_U16(0x300 | GAIN_30dB, arg1);
break;
default:
WARN_LOG_FMT(IOS_USB,
"Wii Speak unsupported SAMPLER_GAIN get (arg1={:04x}, arg2={:04x}) defaulting "
"to GAIN_36dB",
arg1, arg2);
[[fallthrough]];
case 36:
memory.Write_U16(0x300 | GAIN_36dB, arg1);
break;
}
break;
case EC_STATE:
memory.Write_U16(m_sampler.ec_reset ? 1 : 0, arg1);
break;
case SP_STATE:
switch (memory.Read_U16(arg1))
{
case SP_ENABLE:
memory.Write_U16(1, arg2);
break;
case SP_SIN:
break;
case SP_SOUT:
// TODO: Find how it was measured and how accurate it was
// memory.Write_U16(0x39B0, arg2); // 6dB
memory.Write_U16(m_microphone->GetLoudnessLevel(), arg2);
break;
case SP_RIN:
break;
default:
WARN_LOG_FMT(IOS_USB, "Wii Speak unsupported SP_STATE get (arg1={:04x}, arg2={:04x})", arg1,
arg2);
break;
}
break;
case SAMPLER_MUTE:
memory.Write_U16(m_sampler.mute ? 1 : 0, arg1);
break;
default:
WARN_LOG_FMT(IOS_USB,
"Wii Speak unsupported register get (reg={:02x}, arg1={:08x}, arg2={:08x})", reg,
arg1, arg2);
break;
}
}
} // namespace IOS::HLE::USB

View File

@ -0,0 +1,103 @@
// Copyright 2024 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#include <vector>
#include "Common/CommonTypes.h"
#include "Core/IOS/USB/Common.h"
#include "Core/IOS/USB/Emulated/Microphone.h"
#include "Core/System.h"
namespace IOS::HLE::USB
{
struct WiiSpeakState
{
bool sample_on;
bool mute;
int freq;
int gain;
bool ec_reset;
bool sp_on;
static constexpr u32 DEFAULT_SAMPLING_RATE = 16000;
};
class WiiSpeak final : public Device
{
public:
WiiSpeak(EmulationKernel& ios);
~WiiSpeak();
DeviceDescriptor GetDeviceDescriptor() const override;
std::vector<ConfigDescriptor> GetConfigurations() const override;
std::vector<InterfaceDescriptor> GetInterfaces(u8 config) const override;
std::vector<EndpointDescriptor> GetEndpoints(u8 config, u8 interface, u8 alt) const override;
bool Attach() override;
bool AttachAndChangeInterface(u8 interface) override;
int CancelTransfer(u8 endpoint) override;
int ChangeInterface(u8 interface) override;
int GetNumberOfAltSettings(u8 interface) override;
int SetAltSetting(u8 alt_setting) override;
int SubmitTransfer(std::unique_ptr<CtrlMessage> message) override;
int SubmitTransfer(std::unique_ptr<BulkMessage> message) override;
int SubmitTransfer(std::unique_ptr<IntrMessage> message) override;
int SubmitTransfer(std::unique_ptr<IsoMessage> message) override;
private:
WiiSpeakState m_sampler{};
enum Registers
{
SAMPLER_STATE = 0,
SAMPLER_MUTE = 0x0c,
SAMPLER_FREQ = 2,
FREQ_8KHZ = 0,
FREQ_11KHZ = 1,
FREQ_RESERVED = 2,
FREQ_16KHZ = 3, // default
SAMPLER_GAIN = 4,
GAIN_00dB = 0,
GAIN_15dB = 1,
GAIN_30dB = 2,
GAIN_36dB = 3, // default
EC_STATE = 0x14,
SP_STATE = 0x38,
SP_ENABLE = 0x1010,
SP_SIN = 0x2001,
SP_SOUT = 0x2004,
SP_RIN = 0x200d
};
void GetRegister(const std::unique_ptr<CtrlMessage>& cmd) const;
void SetRegister(const std::unique_ptr<CtrlMessage>& cmd);
EmulationKernel& m_ios;
const u16 m_vid = 0x057E;
const u16 m_pid = 0x0308;
u8 m_active_interface = 0;
bool m_device_attached = false;
bool init = false;
std::unique_ptr<Microphone> m_microphone{};
const DeviceDescriptor m_device_descriptor{0x12, 0x1, 0x200, 0, 0, 0, 0x10,
0x57E, 0x0308, 0x0214, 0x1, 0x2, 0x0, 0x1};
const std::vector<ConfigDescriptor> m_config_descriptor{
{0x9, 0x2, 0x0030, 0x1, 0x1, 0x0, 0x80, 0x32}};
const std::vector<InterfaceDescriptor> m_interface_descriptor{
{0x9, 0x4, 0x0, 0x0, 0x0, 0xFF, 0xFF, 0xFF, 0x0},
{0x9, 0x4, 0x0, 0x01, 0x03, 0xFF, 0xFF, 0xFF, 0x0}};
static constexpr u8 ENDPOINT_AUDIO_IN = 0x81;
static constexpr u8 ENDPOINT_AUDIO_OUT = 0x3;
static constexpr u8 ENDPOINT_DATA_OUT = 0x2;
const std::vector<EndpointDescriptor> m_endpoint_descriptor{
{0x7, 0x5, ENDPOINT_AUDIO_IN, 0x1, 0x0020, 0x1},
{0x7, 0x5, ENDPOINT_DATA_OUT, 0x2, 0x0020, 0},
{0x7, 0x5, ENDPOINT_AUDIO_OUT, 0x1, 0x0040, 0x1}};
};
} // namespace IOS::HLE::USB

View File

@ -24,6 +24,7 @@
#include "Core/IOS/USB/Common.h"
#include "Core/IOS/USB/Emulated/Infinity.h"
#include "Core/IOS/USB/Emulated/Skylanders/Skylander.h"
#include "Core/IOS/USB/Emulated/WiiSpeak.h"
#include "Core/IOS/USB/LibusbDevice.h"
#include "Core/NetPlayProto.h"
#include "Core/System.h"
@ -195,6 +196,11 @@ void USBHost::AddEmulatedDevices(std::set<u64>& new_devices, DeviceChangeHooks&
auto infinity_base = std::make_unique<USB::InfinityUSB>(GetEmulationKernel());
CheckAndAddDevice(std::move(infinity_base), new_devices, hooks, always_add_hooks);
}
if (Config::Get(Config::MAIN_EMULATE_WII_SPEAK) && !NetPlay::IsNetPlayRunning())
{
auto wii_speak = std::make_unique<USB::WiiSpeak>(GetEmulationKernel());
CheckAndAddDevice(std::move(wii_speak), new_devices, hooks, always_add_hooks);
}
}
void USBHost::CheckAndAddDevice(std::unique_ptr<USB::Device> device, std::set<u64>& new_devices,

View File

@ -401,9 +401,11 @@
<ClInclude Include="Core\IOS\USB\Bluetooth\WiimoteHIDAttr.h" />
<ClInclude Include="Core\IOS\USB\Common.h" />
<ClInclude Include="Core\IOS\USB\Emulated\Infinity.h" />
<ClInclude Include="Core\IOS\USB\Emulated\Microphone.h" />
<ClInclude Include="Core\IOS\USB\Emulated\Skylanders\Skylander.h" />
<ClInclude Include="Core\IOS\USB\Emulated\Skylanders\SkylanderCrypto.h" />
<ClInclude Include="Core\IOS\USB\Emulated\Skylanders\SkylanderFigure.h" />
<ClInclude Include="Core\IOS\USB\Emulated\WiiSpeak.h" />
<ClInclude Include="Core\IOS\USB\Host.h" />
<ClInclude Include="Core\IOS\USB\LibusbDevice.h" />
<ClInclude Include="Core\IOS\USB\OH0\OH0.h" />
@ -1064,9 +1066,11 @@
<ClCompile Include="Core\IOS\USB\Bluetooth\WiimoteHIDAttr.cpp" />
<ClCompile Include="Core\IOS\USB\Common.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\Infinity.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\Microphone.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\Skylanders\Skylander.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\Skylanders\SkylanderCrypto.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\Skylanders\SkylanderFigure.cpp" />
<ClCompile Include="Core\IOS\USB\Emulated\WiiSpeak.cpp" />
<ClCompile Include="Core\IOS\USB\Host.cpp" />
<ClCompile Include="Core\IOS\USB\LibusbDevice.cpp" />
<ClCompile Include="Core\IOS\USB\OH0\OH0.cpp" />

View File

@ -247,6 +247,8 @@ add_executable(dolphin-emu
DiscordHandler.h
DiscordJoinRequestDialog.cpp
DiscordJoinRequestDialog.h
EmulatedUSB/WiiSpeakWindow.cpp
EmulatedUSB/WiiSpeakWindow.h
FIFO/FIFOAnalyzer.cpp
FIFO/FIFOAnalyzer.h
FIFO/FIFOPlayerWindow.cpp

View File

@ -157,6 +157,7 @@
<ClCompile Include="Debugger\WatchWidget.cpp" />
<ClCompile Include="DiscordHandler.cpp" />
<ClCompile Include="DiscordJoinRequestDialog.cpp" />
<ClCompile Include="EmulatedUSB\WiiSpeakWindow.cpp" />
<ClCompile Include="FIFO\FIFOAnalyzer.cpp" />
<ClCompile Include="FIFO\FIFOPlayerWindow.cpp" />
<ClCompile Include="GameList\GameList.cpp" />
@ -374,6 +375,7 @@
<QtMoc Include="Debugger\WatchWidget.h" />
<QtMoc Include="DiscordHandler.h" />
<QtMoc Include="DiscordJoinRequestDialog.h" />
<QtMoc Include="EmulatedUSB\WiiSpeakWindow.h" />
<QtMoc Include="FIFO\FIFOAnalyzer.h" />
<QtMoc Include="FIFO\FIFOPlayerWindow.h" />
<QtMoc Include="GameList\GameList.h" />

View File

@ -0,0 +1,143 @@
// Copyright 2024 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/EmulatedUSB/WiiSpeakWindow.h"
#include <algorithm>
#include <limits>
#include <QCheckBox>
#include <QComboBox>
#include <QGridLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QString>
#include <QVBoxLayout>
#include "AudioCommon/CubebUtils.h"
#include "Core/Config/MainSettings.h"
#include "Core/Core.h"
#include "Core/System.h"
#include "DolphinQt/Settings.h"
WiiSpeakWindow::WiiSpeakWindow(QWidget* parent) : QWidget(parent)
{
// i18n: Window for managing the Wii Speak microphone
setWindowTitle(tr("Wii Speak Manager"));
setObjectName(QStringLiteral("wii_speak_manager"));
setMinimumSize(QSize(700, 200));
CreateMainWindow();
connect(&Settings::Instance(), &Settings::EmulationStateChanged, this,
&WiiSpeakWindow::OnEmulationStateChanged);
installEventFilter(this);
OnEmulationStateChanged(Core::GetState(Core::System::GetInstance()));
};
WiiSpeakWindow::~WiiSpeakWindow() = default;
void WiiSpeakWindow::CreateMainWindow()
{
auto* main_layout = new QVBoxLayout();
auto* label = new QLabel();
label->setText(QStringLiteral("<center><i>%1</i></center>")
.arg(tr("Some settings cannot be changed when emulation is running.")));
main_layout->addWidget(label);
auto* checkbox_group = new QGroupBox();
auto* checkbox_layout = new QHBoxLayout();
checkbox_layout->setAlignment(Qt::AlignHCenter);
m_checkbox_enabled = new QCheckBox(tr("Emulate Wii Speak"), this);
m_checkbox_enabled->setChecked(Config::Get(Config::MAIN_EMULATE_WII_SPEAK));
connect(m_checkbox_enabled, &QCheckBox::toggled, this, &WiiSpeakWindow::EmulateWiiSpeak);
checkbox_layout->addWidget(m_checkbox_enabled);
checkbox_group->setLayout(checkbox_layout);
main_layout->addWidget(checkbox_group);
m_config_group = new QGroupBox(tr("Microphone Configuration"));
auto* config_layout = new QHBoxLayout();
auto checkbox_mic_muted = new QCheckBox(tr("Mute"), this);
checkbox_mic_muted->setChecked(Config::Get(Config::MAIN_WII_SPEAK_MUTED));
connect(checkbox_mic_muted, &QCheckBox::toggled, this,
&WiiSpeakWindow::SetWiiSpeakConnectionState);
checkbox_mic_muted->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
config_layout->addWidget(checkbox_mic_muted);
auto* volume_layout = new QGridLayout();
static constexpr int FILTER_MIN = -50;
static constexpr int FILTER_MAX = 50;
const int volume_modifier =
std::clamp<int>(Config::Get(Config::MAIN_WII_SPEAK_VOLUME_MODIFIER), FILTER_MIN, FILTER_MAX);
auto filter_slider = new QSlider(Qt::Horizontal, this);
m_slider_label = new QLabel(tr("Volume modifier (value: %1dB)").arg(volume_modifier));
connect(filter_slider, &QSlider::valueChanged, this, [this](int value) {
Config::SetBaseOrCurrent(Config::MAIN_WII_SPEAK_VOLUME_MODIFIER, value);
m_slider_label->setText(tr("Volume modifier (value: %1dB)").arg(value));
});
filter_slider->setMinimum(FILTER_MIN);
filter_slider->setMaximum(FILTER_MAX);
filter_slider->setValue(volume_modifier);
filter_slider->setTickPosition(QSlider::TicksBothSides);
filter_slider->setTickInterval(10);
filter_slider->setSingleStep(1);
volume_layout->addWidget(new QLabel(QStringLiteral("%1dB").arg(FILTER_MIN)), 0, 0, Qt::AlignLeft);
volume_layout->addWidget(m_slider_label, 0, 1, Qt::AlignCenter);
volume_layout->addWidget(new QLabel(QStringLiteral("%1dB").arg(FILTER_MAX)), 0, 2,
Qt::AlignRight);
volume_layout->addWidget(filter_slider, 1, 0, 1, 3);
config_layout->addLayout(volume_layout);
config_layout->setStretch(1, 3);
m_combobox_microphones = new QComboBox();
m_combobox_microphones->addItem(QLatin1String("(%1)").arg(tr("Autodetect preferred microphone")),
QString{});
for (auto& [device_id, device_name] : CubebUtils::ListInputDevices())
{
const auto user_data = QString::fromStdString(device_id);
m_combobox_microphones->addItem(QString::fromStdString(device_name), user_data);
}
connect(m_combobox_microphones, &QComboBox::currentIndexChanged, this,
&WiiSpeakWindow::OnInputDeviceChange);
auto current_device_id = QString::fromStdString(Config::Get(Config::MAIN_WII_SPEAK_MICROPHONE));
m_combobox_microphones->setCurrentIndex(m_combobox_microphones->findData(current_device_id));
config_layout->addWidget(m_combobox_microphones);
m_config_group->setLayout(config_layout);
main_layout->addWidget(m_config_group);
setLayout(main_layout);
}
void WiiSpeakWindow::EmulateWiiSpeak(bool emulate)
{
Config::SetBaseOrCurrent(Config::MAIN_EMULATE_WII_SPEAK, emulate);
}
void WiiSpeakWindow::SetWiiSpeakConnectionState(bool muted)
{
Config::SetBaseOrCurrent(Config::MAIN_WII_SPEAK_MUTED, muted);
}
void WiiSpeakWindow::OnEmulationStateChanged(Core::State state)
{
const bool running = state != Core::State::Uninitialized;
m_checkbox_enabled->setEnabled(!running);
m_combobox_microphones->setEnabled(!running);
}
void WiiSpeakWindow::OnInputDeviceChange()
{
auto user_data = m_combobox_microphones->currentData();
if (!user_data.isValid())
return;
const std::string device_id = user_data.toString().toStdString();
Config::SetBaseOrCurrent(Config::MAIN_WII_SPEAK_MICROPHONE, device_id);
}

View File

@ -0,0 +1,34 @@
// Copyright 2024 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QWidget>
#include "Common/CommonTypes.h"
#include "Core/Core.h"
class QCheckBox;
class QComboBox;
class QGroupBox;
class QLabel;
class WiiSpeakWindow : public QWidget
{
Q_OBJECT
public:
explicit WiiSpeakWindow(QWidget* parent = nullptr);
~WiiSpeakWindow() override;
private:
void CreateMainWindow();
void OnEmulationStateChanged(Core::State state);
void EmulateWiiSpeak(bool emulate);
void SetWiiSpeakConnectionState(bool connected);
void OnInputDeviceChange();
QCheckBox* m_checkbox_enabled;
QComboBox* m_combobox_microphones;
QGroupBox* m_config_group;
QLabel* m_slider_label;
};

View File

@ -93,6 +93,7 @@
#include "DolphinQt/Debugger/ThreadWidget.h"
#include "DolphinQt/Debugger/WatchWidget.h"
#include "DolphinQt/DiscordHandler.h"
#include "DolphinQt/EmulatedUSB/WiiSpeakWindow.h"
#include "DolphinQt/FIFO/FIFOPlayerWindow.h"
#include "DolphinQt/GCMemcardManager.h"
#include "DolphinQt/GameList/GameList.h"
@ -572,6 +573,7 @@ void MainWindow::ConnectMenuBar()
connect(m_menu_bar, &MenuBar::ShowFIFOPlayer, this, &MainWindow::ShowFIFOPlayer);
connect(m_menu_bar, &MenuBar::ShowSkylanderPortal, this, &MainWindow::ShowSkylanderPortal);
connect(m_menu_bar, &MenuBar::ShowInfinityBase, this, &MainWindow::ShowInfinityBase);
connect(m_menu_bar, &MenuBar::ShowWiiSpeakWindow, this, &MainWindow::ShowWiiSpeakWindow);
connect(m_menu_bar, &MenuBar::ConnectWiiRemote, this, &MainWindow::OnConnectWiiRemote);
#ifdef USE_RETRO_ACHIEVEMENTS
@ -1413,6 +1415,18 @@ void MainWindow::ShowInfinityBase()
m_infinity_window->activateWindow();
}
void MainWindow::ShowWiiSpeakWindow()
{
if (!m_wii_speak_window)
{
m_wii_speak_window = new WiiSpeakWindow();
}
m_wii_speak_window->show();
m_wii_speak_window->raise();
m_wii_speak_window->activateWindow();
}
void MainWindow::StateLoad()
{
QString dialog_path = (Config::Get(Config::MAIN_CURRENT_STATE_PATH).empty()) ?

View File

@ -52,6 +52,7 @@ class ThreadWidget;
class ToolBar;
class WatchWidget;
class WiiTASInputWindow;
class WiiSpeakWindow;
struct WindowSystemInfo;
namespace Core
@ -173,6 +174,7 @@ private:
void ShowFIFOPlayer();
void ShowSkylanderPortal();
void ShowInfinityBase();
void ShowWiiSpeakWindow();
void ShowMemcardManager();
void ShowResourcePackManager();
void ShowCheatsManager();
@ -245,6 +247,7 @@ private:
FIFOPlayerWindow* m_fifo_window = nullptr;
SkylanderPortalWindow* m_skylander_window = nullptr;
InfinityBaseWindow* m_infinity_window = nullptr;
WiiSpeakWindow* m_wii_speak_window = nullptr;
MappingWindow* m_hotkey_window = nullptr;
FreeLookWindow* m_freelook_window = nullptr;

View File

@ -274,6 +274,7 @@ void MenuBar::AddToolsMenu()
auto* usb_device_menu = new QMenu(tr("Emulated USB Devices"), tools_menu);
usb_device_menu->addAction(tr("&Skylanders Portal"), this, &MenuBar::ShowSkylanderPortal);
usb_device_menu->addAction(tr("&Infinity Base"), this, &MenuBar::ShowInfinityBase);
usb_device_menu->addAction(tr("&Wii Speak"), this, &MenuBar::ShowWiiSpeakWindow);
tools_menu->addMenu(usb_device_menu);
tools_menu->addSeparator();

View File

@ -91,6 +91,7 @@ signals:
void ShowResourcePackManager();
void ShowSkylanderPortal();
void ShowInfinityBase();
void ShowWiiSpeakWindow();
void ConnectWiiRemote(int id);
#ifdef USE_RETRO_ACHIEVEMENTS