mirror of
https://github.com/izzy2lost/dolphin.git
synced 2026-06-19 01:16:48 -07:00
Merge pull request #12769 from sepalani/wii-speak
IOS/USB: Emulate Wii Speak using cubeb
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+14
-1
@@ -244,6 +244,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,
|
||||
@@ -930,7 +942,8 @@ enum class BooleanSetting(
|
||||
MAIN_DSP_JIT,
|
||||
MAIN_TIME_TRACKING,
|
||||
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))
|
||||
|
||||
+4
@@ -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) {
|
||||
|
||||
+16
@@ -896,6 +896,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>) {
|
||||
|
||||
+9
@@ -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)
|
||||
|
||||
@@ -9,12 +9,15 @@ class ActivityTracker : ActivityLifecycleCallbacks {
|
||||
private val resumedActivities = HashSet<Activity>()
|
||||
private var backgroundExecutionAllowed = false
|
||||
private var firstStart = true
|
||||
var currentActivity : Activity? = null
|
||||
private set
|
||||
|
||||
private fun isMainActivity(activity: Activity): Boolean {
|
||||
return activity is MainView
|
||||
}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
|
||||
currentActivity = activity
|
||||
if (isMainActivity(activity)) {
|
||||
firstStart = bundle == null
|
||||
}
|
||||
@@ -26,6 +29,7 @@ class ActivityTracker : ActivityLifecycleCallbacks {
|
||||
}
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
currentActivity = activity
|
||||
resumedActivities.add(activity)
|
||||
if (!backgroundExecutionAllowed && !resumedActivities.isEmpty()) {
|
||||
backgroundExecutionAllowed = true
|
||||
@@ -34,6 +38,9 @@ class ActivityTracker : ActivityLifecycleCallbacks {
|
||||
}
|
||||
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
if (currentActivity === activity) {
|
||||
currentActivity = null
|
||||
}
|
||||
resumedActivities.remove(activity)
|
||||
if (backgroundExecutionAllowed && resumedActivities.isEmpty()) {
|
||||
backgroundExecutionAllowed = false
|
||||
|
||||
+35
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -948,4 +948,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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,6 +115,10 @@ jmethodID GetCoreDeviceControlConstructor();
|
||||
jclass GetInputDetectorClass();
|
||||
jfieldID GetInputDetectorPointer();
|
||||
|
||||
jclass GetPermissionHandlerClass();
|
||||
jmethodID GetPermissionHandlerHasRecordAudioPermission();
|
||||
jmethodID GetPermissionHandlerRequestRecordAudioPermission();
|
||||
|
||||
jmethodID GetRunnableRun();
|
||||
|
||||
} // namespace IDCache
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
|
||||
#include <cubeb/cubeb.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <Objbase.h>
|
||||
#endif
|
||||
|
||||
static void LogCallback(const char* format, ...)
|
||||
{
|
||||
auto* instance = Common::Log::LogManager::GetInstance();
|
||||
@@ -47,7 +51,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;
|
||||
|
||||
@@ -72,3 +78,143 @@ 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();
|
||||
const 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++)
|
||||
{
|
||||
const auto& info = collection.device[i];
|
||||
const 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?";
|
||||
}
|
||||
}();
|
||||
|
||||
// According to cubeb_device_info definition in cubeb.h:
|
||||
// > "Optional vendor name, may be NULL."
|
||||
// In practice, it seems some other fields might be NULL as well.
|
||||
static constexpr auto fmt_str = [](const char* ptr) constexpr -> const char* {
|
||||
return (ptr == nullptr) ? "(null)" : ptr;
|
||||
};
|
||||
|
||||
INFO_LOG_FMT(AUDIO,
|
||||
"[{}] Device ID: {}\n"
|
||||
"\tName: {}\n"
|
||||
"\tGroup ID: {}\n"
|
||||
"\tVendor: {}\n"
|
||||
"\tState: {}",
|
||||
i, fmt_str(info.device_id), fmt_str(info.friendly_name), fmt_str(info.group_id),
|
||||
fmt_str(info.vendor_name), state_name);
|
||||
|
||||
if (info.device_id == nullptr)
|
||||
continue; // Shouldn't happen
|
||||
|
||||
if (info.state == CUBEB_DEVICE_STATE_ENABLED)
|
||||
{
|
||||
devices.emplace_back(info.device_id, fmt_str(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 = CubebUtils::GetContext();
|
||||
const 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++)
|
||||
{
|
||||
const 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 worker_name)
|
||||
#ifdef _WIN32
|
||||
: m_work_queue{std::move(worker_name)}
|
||||
#endif
|
||||
{
|
||||
#ifdef _WIN32
|
||||
m_work_queue.PushBlocking([this] {
|
||||
const auto result = ::CoInitializeEx(nullptr, COINIT_MULTITHREADED | COINIT_DISABLE_OLE1DDE);
|
||||
m_coinit_success = result == S_OK;
|
||||
m_should_couninit = m_coinit_success || result == S_FALSE;
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
CoInitSyncWorker::~CoInitSyncWorker()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (m_should_couninit)
|
||||
{
|
||||
m_work_queue.PushBlocking([this] {
|
||||
m_should_couninit = false;
|
||||
CoUninitialize();
|
||||
});
|
||||
}
|
||||
m_coinit_success = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool CoInitSyncWorker::Execute(FunctionType f)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (!m_coinit_success)
|
||||
return false;
|
||||
|
||||
m_work_queue.PushBlocking(f);
|
||||
#else
|
||||
f();
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
} // namespace CubebUtils
|
||||
|
||||
@@ -3,11 +3,41 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#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 worker_name);
|
||||
~CoInitSyncWorker();
|
||||
|
||||
bool Execute(FunctionType f);
|
||||
|
||||
#ifdef _WIN32
|
||||
private:
|
||||
Common::AsyncWorkThread m_work_queue;
|
||||
bool m_coinit_success = false;
|
||||
bool m_should_couninit = false;
|
||||
#endif
|
||||
};
|
||||
} // namespace CubebUtils
|
||||
|
||||
@@ -429,12 +429,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
|
||||
|
||||
@@ -596,6 +596,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)
|
||||
|
||||
@@ -364,6 +364,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
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ constexpr std::array<const char*, NUM_HOTKEYS> s_hotkey_labels{{
|
||||
_trans("Connect Balance Board"),
|
||||
_trans("Toggle SD Card"),
|
||||
_trans("Toggle USB Keyboard"),
|
||||
_trans("Toggle Wii Speak Mute"),
|
||||
|
||||
_trans("Next Profile"),
|
||||
_trans("Previous Profile"),
|
||||
@@ -345,7 +346,7 @@ constexpr std::array<HotkeyGroupInfo, NUM_HOTKEY_GROUPS> s_groups_info = {
|
||||
{_trans("Stepping"), HK_STEP, HK_SKIP},
|
||||
{_trans("Program Counter"), HK_SHOW_PC, HK_SET_PC},
|
||||
{_trans("Breakpoint"), HK_BP_TOGGLE, HK_MBP_ADD},
|
||||
{_trans("Wii"), HK_TRIGGER_SYNC_BUTTON, HK_TOGGLE_USB_KEYBOARD},
|
||||
{_trans("Wii"), HK_TRIGGER_SYNC_BUTTON, HK_TOGGLE_WII_SPEAK_MUTE},
|
||||
{_trans("Controller Profile 1"), HK_NEXT_WIIMOTE_PROFILE_1, HK_PREV_GAME_WIIMOTE_PROFILE_1},
|
||||
{_trans("Controller Profile 2"), HK_NEXT_WIIMOTE_PROFILE_2, HK_PREV_GAME_WIIMOTE_PROFILE_2},
|
||||
{_trans("Controller Profile 3"), HK_NEXT_WIIMOTE_PROFILE_3, HK_PREV_GAME_WIIMOTE_PROFILE_3},
|
||||
|
||||
@@ -74,6 +74,7 @@ enum Hotkey
|
||||
HK_BALANCEBOARD_CONNECT,
|
||||
HK_TOGGLE_SD_CARD,
|
||||
HK_TOGGLE_USB_KEYBOARD,
|
||||
HK_TOGGLE_WII_SPEAK_MUTE,
|
||||
|
||||
HK_NEXT_WIIMOTE_PROFILE_1,
|
||||
HK_PREV_WIIMOTE_PROFILE_1,
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
// Copyright 2025 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "Core/IOS/USB/Emulated/Microphone.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <ranges>
|
||||
#include <span>
|
||||
|
||||
#ifdef HAVE_CUBEB
|
||||
#include <cubeb/cubeb.h>
|
||||
|
||||
#include "AudioCommon/CubebUtils.h"
|
||||
#endif
|
||||
|
||||
#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"
|
||||
#include "Core/System.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();
|
||||
}
|
||||
|
||||
#ifndef HAVE_CUBEB
|
||||
void Microphone::StreamInit()
|
||||
{
|
||||
}
|
||||
|
||||
void Microphone::StreamStart([[maybe_unused]] u32 sampling_rate)
|
||||
{
|
||||
}
|
||||
|
||||
void Microphone::StreamStop()
|
||||
{
|
||||
}
|
||||
|
||||
void Microphone::StreamTerminate()
|
||||
{
|
||||
}
|
||||
#else
|
||||
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()
|
||||
{
|
||||
StreamStop();
|
||||
|
||||
if (m_cubeb_ctx)
|
||||
m_worker.Execute([this] { m_cubeb_ctx.reset(); });
|
||||
}
|
||||
|
||||
static void StateCallback(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(), ¶ms, &minimum_latency) != CUBEB_OK)
|
||||
{
|
||||
WARN_LOG_FMT(IOS_USB, "Error getting minimum latency");
|
||||
minimum_latency = 16;
|
||||
}
|
||||
|
||||
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, ¶ms, nullptr, nullptr,
|
||||
std::max<u32>(16, minimum_latency), CubebDataCallback, StateCallback,
|
||||
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::StreamStop()
|
||||
{
|
||||
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::CubebDataCallback(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);
|
||||
return mic->DataCallback(static_cast<const SampleType*>(input_buffer), nframes);
|
||||
}
|
||||
|
||||
long Microphone::DataCallback(const SampleType* input_buffer, long nframes)
|
||||
{
|
||||
std::lock_guard lock(m_ring_lock);
|
||||
|
||||
// Skip data if sampling is off or mute is on
|
||||
if (!m_sampler.sample_on || m_sampler.mute)
|
||||
return nframes;
|
||||
|
||||
std::span<const SampleType> buffer(input_buffer, nframes);
|
||||
const auto gain = ComputeGain(Config::Get(Config::MAIN_WII_SPEAK_VOLUME_MODIFIER));
|
||||
const auto apply_gain = [gain](SampleType sample) {
|
||||
return MathUtil::SaturatingCast<SampleType>(sample * gain);
|
||||
};
|
||||
|
||||
for (const SampleType le_sample : std::ranges::transform_view(buffer, apply_gain))
|
||||
{
|
||||
UpdateLoudness(le_sample);
|
||||
m_stream_buffer[m_stream_wpos] = Common::swap16(le_sample);
|
||||
m_stream_wpos = (m_stream_wpos + 1) % STREAM_SIZE;
|
||||
}
|
||||
|
||||
m_samples_avail += nframes;
|
||||
if (m_samples_avail > STREAM_SIZE)
|
||||
{
|
||||
WARN_LOG_FMT(IOS_USB, "Wii Speak ring buffer is full, data will be lost!");
|
||||
m_samples_avail = STREAM_SIZE;
|
||||
}
|
||||
|
||||
return nframes;
|
||||
}
|
||||
#endif
|
||||
|
||||
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 lock(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(const SampleType sample)
|
||||
{
|
||||
// Based on MH3 graphical cues, let's use a 0x4000 window
|
||||
static const u32 WINDOW = 0x4000;
|
||||
static const FloatType UNIT = (m_loudness.DB_MAX - m_loudness.DB_MIN) / WINDOW;
|
||||
|
||||
m_loudness.Update(sample);
|
||||
|
||||
if (m_loudness.samples_count >= m_loudness.SAMPLES_NEEDED)
|
||||
{
|
||||
const FloatType 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();
|
||||
#endif
|
||||
|
||||
m_loudness.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
bool Microphone::HasData(u32 sample_count = BUFF_SIZE_SAMPLES) const
|
||||
{
|
||||
std::lock_guard lock(m_ring_lock);
|
||||
return m_samples_avail >= sample_count;
|
||||
}
|
||||
|
||||
Microphone::FloatType Microphone::ComputeGain(FloatType relative_db) const
|
||||
{
|
||||
return m_loudness.ComputeGain(relative_db);
|
||||
}
|
||||
|
||||
void Microphone::SetSamplingRate(u32 sampling_rate)
|
||||
{
|
||||
StreamStop();
|
||||
StreamStart(sampling_rate);
|
||||
}
|
||||
|
||||
const Microphone::FloatType Microphone::Loudness::DB_MIN =
|
||||
20 * std::log10(FloatType(1) / MAX_AMPLITUDE);
|
||||
const Microphone::FloatType Microphone::Loudness::DB_MAX = 20 * std::log10(FloatType(1));
|
||||
|
||||
void Microphone::Loudness::Update(const SampleType sample)
|
||||
{
|
||||
++samples_count;
|
||||
|
||||
peak_min = std::min(sample, peak_min);
|
||||
peak_max = std::max(sample, peak_max);
|
||||
absolute_sum += std::abs(sample);
|
||||
square_sum += std::pow(FloatType(sample), FloatType(2));
|
||||
}
|
||||
|
||||
Microphone::SampleType Microphone::Loudness::GetPeak() const
|
||||
{
|
||||
return std::max(std::abs(peak_min), std::abs(peak_max));
|
||||
}
|
||||
|
||||
Microphone::FloatType Microphone::Loudness::GetDecibel(FloatType value)
|
||||
{
|
||||
return 20 * std::log10(value);
|
||||
}
|
||||
|
||||
Microphone::FloatType Microphone::Loudness::GetAmplitude() const
|
||||
{
|
||||
return GetPeak() / MAX_AMPLITUDE;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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
|
||||
@@ -0,0 +1,113 @@
|
||||
// Copyright 2025 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <type_traits>
|
||||
|
||||
#include "AudioCommon/CubebUtils.h"
|
||||
#include "Common/CommonTypes.h"
|
||||
|
||||
#ifdef HAVE_CUBEB
|
||||
#include "AudioCommon/CubebUtils.h"
|
||||
|
||||
struct cubeb;
|
||||
struct cubeb_stream;
|
||||
#endif
|
||||
|
||||
namespace IOS::HLE::USB
|
||||
{
|
||||
struct WiiSpeakState;
|
||||
|
||||
class Microphone final
|
||||
{
|
||||
public:
|
||||
using FloatType = float;
|
||||
using SampleType = s16;
|
||||
using UnsignedSampleType = std::make_unsigned_t<SampleType>;
|
||||
|
||||
Microphone(const WiiSpeakState& sampler);
|
||||
~Microphone();
|
||||
|
||||
bool HasData(u32 sample_count) const;
|
||||
u16 ReadIntoBuffer(u8* ptr, u32 size);
|
||||
u16 GetLoudnessLevel() const;
|
||||
FloatType ComputeGain(FloatType relative_db) const;
|
||||
void SetSamplingRate(u32 sampling_rate);
|
||||
|
||||
private:
|
||||
#ifdef HAVE_CUBEB
|
||||
static long CubebDataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer,
|
||||
void* output_buffer, long nframes);
|
||||
#endif
|
||||
|
||||
long DataCallback(const SampleType* input_buffer, long nframes);
|
||||
void UpdateLoudness(SampleType sample);
|
||||
|
||||
void StreamInit();
|
||||
void StreamTerminate();
|
||||
void StreamStart(u32 sampling_rate);
|
||||
void StreamStop();
|
||||
|
||||
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
|
||||
std::atomic<u16> m_loudness_level = 0;
|
||||
struct Loudness
|
||||
{
|
||||
void Update(SampleType sample);
|
||||
|
||||
SampleType GetPeak() const;
|
||||
static FloatType GetDecibel(FloatType value);
|
||||
FloatType GetAmplitude() const;
|
||||
FloatType GetAmplitudeDb() const;
|
||||
FloatType GetAbsoluteMean() const;
|
||||
FloatType GetAbsoluteMeanDb() const;
|
||||
FloatType GetRootMeanSquare() const;
|
||||
FloatType GetRootMeanSquareDb() const;
|
||||
FloatType GetCrestFactor() const;
|
||||
FloatType GetCrestFactorDb() const;
|
||||
static FloatType ComputeGain(FloatType db);
|
||||
|
||||
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_AMPLITUDE =
|
||||
UnsignedSampleType{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;
|
||||
|
||||
mutable std::mutex m_ring_lock;
|
||||
|
||||
const WiiSpeakState& m_sampler;
|
||||
|
||||
#ifdef HAVE_CUBEB
|
||||
std::shared_ptr<cubeb> m_cubeb_ctx = nullptr;
|
||||
cubeb_stream* m_cubeb_stream = nullptr;
|
||||
CubebUtils::CoInitSyncWorker m_worker{"Wii Speak Worker"};
|
||||
#endif
|
||||
};
|
||||
} // namespace IOS::HLE::USB
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user