Merge a3747ef5b0
into 1ae0b23265
This commit is contained in:
commit
87beeaae96
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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(), ¶ms, &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, ¶ms, 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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()) ?
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -91,6 +91,7 @@ signals:
|
|||
void ShowResourcePackManager();
|
||||
void ShowSkylanderPortal();
|
||||
void ShowInfinityBase();
|
||||
void ShowWiiSpeakWindow();
|
||||
void ConnectWiiRemote(int id);
|
||||
|
||||
#ifdef USE_RETRO_ACHIEVEMENTS
|
||||
|
|
Loading…
Reference in New Issue