android: Microphone support
Based on Tobi's work. Created a new 'real Mic factory' which creates mic interfaces for real devices. There's a default factory set (cubeb/null, depending on whether cubeb exists) in order to avoid changing code of other frontends. Created a factory for the Android frontend which requests mic permission and creates a CubebInput (or null). The permission requesting code is basically the same as camera's. If the user denied the permission, an alert dialog will be shown informing them of the reason. For easier usage, by default the Audio Input Device is set to 'Real Device'. Co-authored-by: FearlessTobi <thm.frey@gmail.com>
This commit is contained in:
parent
34094a70e7
commit
44cbc4076b
@ -17,6 +17,7 @@
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
||||
|
||||
<application
|
||||
|
@ -27,6 +27,7 @@ import org.citra.citra_emu.utils.PermissionsHandler;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import static android.Manifest.permission.CAMERA;
|
||||
import static android.Manifest.permission.RECORD_AUDIO;
|
||||
|
||||
/**
|
||||
* Class which contains methods that interact
|
||||
@ -441,6 +442,39 @@ public final class NativeLibrary {
|
||||
}
|
||||
}
|
||||
|
||||
private static final Object micPermissionLock = new Object();
|
||||
private static boolean micPermissionGranted = false;
|
||||
public static final int REQUEST_CODE_NATIVE_MIC = 900;
|
||||
|
||||
public static boolean RequestMicPermission() {
|
||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[NativeLibrary] EmulationActivity not present");
|
||||
return false;
|
||||
}
|
||||
if (ContextCompat.checkSelfPermission(emulationActivity, RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
|
||||
// Permission already granted
|
||||
return true;
|
||||
}
|
||||
emulationActivity.requestPermissions(new String[]{RECORD_AUDIO}, REQUEST_CODE_NATIVE_MIC);
|
||||
|
||||
// Wait until result is returned
|
||||
synchronized (micPermissionLock) {
|
||||
try {
|
||||
micPermissionLock.wait();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
return micPermissionGranted;
|
||||
}
|
||||
|
||||
public static void MicPermissionResult(boolean granted) {
|
||||
micPermissionGranted = granted;
|
||||
synchronized (micPermissionLock) {
|
||||
micPermissionLock.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// Notifies that the activity is now in foreground and camera devices can now be reloaded
|
||||
public static native void ReloadCameraDevices();
|
||||
|
||||
|
@ -253,6 +253,16 @@ public final class EmulationActivity extends AppCompatActivity {
|
||||
}
|
||||
NativeLibrary.CameraPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
|
||||
break;
|
||||
case NativeLibrary.REQUEST_CODE_NATIVE_MIC:
|
||||
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.microphone)
|
||||
.setMessage(R.string.microphone_permission_needed)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
NativeLibrary.MicPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
|
||||
break;
|
||||
default:
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
break;
|
||||
|
@ -361,8 +361,10 @@ public final class SettingsFragmentPresenter {
|
||||
|
||||
SettingSection audioSection = mSettings.getSection(Settings.SECTION_AUDIO);
|
||||
Setting audioStretch = audioSection.getSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING);
|
||||
Setting micInputType = audioSection.getSetting(SettingsFile.KEY_MIC_INPUT_TYPE);
|
||||
|
||||
sl.add(new CheckBoxSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING, Settings.SECTION_AUDIO, R.string.audio_stretch, R.string.audio_stretch_description, false, audioStretch));
|
||||
sl.add(new SingleChoiceSetting(SettingsFile.KEY_MIC_INPUT_TYPE, Settings.SECTION_AUDIO, R.string.audio_input_type, 0, R.array.audioInputTypeNames, R.array.audioInputTypeValues, 1, micInputType));
|
||||
}
|
||||
|
||||
private void addDebugSettings(ArrayList<SettingsItem> sl) {
|
||||
|
@ -60,6 +60,7 @@ public final class SettingsFile {
|
||||
public static final String KEY_AUDIO_OUTPUT_ENGINE = "output_engine";
|
||||
public static final String KEY_ENABLE_AUDIO_STRETCHING = "enable_audio_stretching";
|
||||
public static final String KEY_VOLUME = "volume";
|
||||
public static final String KEY_MIC_INPUT_TYPE = "mic_input_type";
|
||||
|
||||
public static final String KEY_USE_VIRTUAL_SD = "use_virtual_sd";
|
||||
|
||||
|
@ -18,13 +18,15 @@ add_library(main SHARED
|
||||
game_info.h
|
||||
id_cache.cpp
|
||||
id_cache.h
|
||||
mic.cpp
|
||||
mic.h
|
||||
native.cpp
|
||||
native.h
|
||||
ndk_motion.cpp
|
||||
ndk_motion.h
|
||||
)
|
||||
|
||||
target_link_libraries(main PRIVATE common core input_common network)
|
||||
target_link_libraries(main PRIVATE audio_core common core input_common network)
|
||||
target_link_libraries(main PRIVATE android camera2ndk mediandk jnigraphics EGL glad inih log yuv)
|
||||
|
||||
set(CPACK_PACKAGE_EXECUTABLES ${CPACK_PACKAGE_EXECUTABLES} main)
|
||||
|
@ -170,7 +170,7 @@ void Config::ReadValues() {
|
||||
Settings::values.mic_input_device =
|
||||
sdl2_config->GetString("Audio", "mic_input_device", "Default");
|
||||
Settings::values.mic_input_type =
|
||||
static_cast<Settings::MicInputType>(sdl2_config->GetInteger("Audio", "mic_input_type", 0));
|
||||
static_cast<Settings::MicInputType>(sdl2_config->GetInteger("Audio", "mic_input_type", 1));
|
||||
|
||||
// Data Storage
|
||||
Settings::values.use_virtual_sd =
|
||||
|
@ -201,6 +201,10 @@ enable_audio_stretching =
|
||||
# auto (default): Auto-select
|
||||
output_device =
|
||||
|
||||
# Which mic input type to use.
|
||||
# 0: None, 1 (default): Real device, 2: Static noise
|
||||
mic_input_type =
|
||||
|
||||
# Output volume.
|
||||
# 1.0 (default): 100%, 0.0; mute
|
||||
volume =
|
||||
|
@ -26,6 +26,7 @@ static jmethodID s_is_portrait_mode;
|
||||
static jmethodID s_landscape_screen_layout;
|
||||
static jmethodID s_exit_emulation_activity;
|
||||
static jmethodID s_request_camera_permission;
|
||||
static jmethodID s_request_mic_permission;
|
||||
|
||||
namespace IDCache {
|
||||
|
||||
@ -80,6 +81,10 @@ jmethodID GetRequestCameraPermission() {
|
||||
return s_request_camera_permission;
|
||||
}
|
||||
|
||||
jmethodID GetRequestMicPermission() {
|
||||
return s_request_mic_permission;
|
||||
}
|
||||
|
||||
} // namespace IDCache
|
||||
|
||||
#ifdef __cplusplus
|
||||
@ -120,6 +125,8 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||
env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V");
|
||||
s_request_camera_permission =
|
||||
env->GetStaticMethodID(s_native_library_class, "RequestCameraPermission", "()Z");
|
||||
s_request_mic_permission =
|
||||
env->GetStaticMethodID(s_native_library_class, "RequestMicPermission", "()Z");
|
||||
|
||||
MiiSelector::InitJNI(env);
|
||||
SoftwareKeyboard::InitJNI(env);
|
||||
|
@ -19,6 +19,7 @@ jmethodID GetIsPortraitMode();
|
||||
jmethodID GetLandscapeScreenLayout();
|
||||
jmethodID GetExitEmulationActivity();
|
||||
jmethodID GetRequestCameraPermission();
|
||||
jmethodID GetRequestMicPermission();
|
||||
|
||||
} // namespace IDCache
|
||||
|
||||
|
38
src/android/app/src/main/jni/mic.cpp
Normal file
38
src/android/app/src/main/jni/mic.cpp
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright 2020 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <jni.h>
|
||||
#include "common/logging/log.h"
|
||||
#include "jni/id_cache.h"
|
||||
#include "jni/mic.h"
|
||||
|
||||
#ifdef HAVE_CUBEB
|
||||
#include "audio_core/cubeb_input.h"
|
||||
#endif
|
||||
|
||||
namespace Mic {
|
||||
|
||||
AndroidFactory::~AndroidFactory() = default;
|
||||
|
||||
std::unique_ptr<Frontend::Mic::Interface> AndroidFactory::Create(std::string mic_device_name) {
|
||||
#ifdef HAVE_CUBEB
|
||||
if (!permission_granted) {
|
||||
JNIEnv* env = IDCache::GetEnvForThread();
|
||||
permission_granted = env->CallStaticBooleanMethod(IDCache::GetNativeLibraryClass(),
|
||||
IDCache::GetRequestMicPermission());
|
||||
}
|
||||
|
||||
if (permission_granted) {
|
||||
return std::make_unique<AudioCore::CubebInput>(std::move(mic_device_name));
|
||||
} else {
|
||||
LOG_WARNING(Frontend, "Mic permissions denied");
|
||||
return std::make_unique<Frontend::Mic::NullMic>();
|
||||
}
|
||||
#else
|
||||
LOG_WARNING(Frontend, "No cubeb support");
|
||||
return std::make_unique<Frontend::Mic::NullMic>();
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace Mic
|
21
src/android/app/src/main/jni/mic.h
Normal file
21
src/android/app/src/main/jni/mic.h
Normal file
@ -0,0 +1,21 @@
|
||||
// Copyright 2020 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/frontend/mic.h"
|
||||
|
||||
namespace Mic {
|
||||
|
||||
class AndroidFactory final : public Frontend::Mic::RealMicFactory {
|
||||
public:
|
||||
~AndroidFactory() override;
|
||||
|
||||
std::unique_ptr<Frontend::Mic::Interface> Create(std::string mic_device_name) override;
|
||||
|
||||
private:
|
||||
bool permission_granted = false;
|
||||
};
|
||||
|
||||
} // namespace Mic
|
@ -17,6 +17,7 @@
|
||||
#include "core/core.h"
|
||||
#include "core/frontend/applets/default_applets.h"
|
||||
#include "core/frontend/camera/factory.h"
|
||||
#include "core/frontend/mic.h"
|
||||
#include "core/frontend/scope_acquire_context.h"
|
||||
#include "core/hle/service/am/am.h"
|
||||
#include "core/hle/service/nfc/nfc.h"
|
||||
@ -30,6 +31,7 @@
|
||||
#include "jni/emu_window/emu_window.h"
|
||||
#include "jni/game_info.h"
|
||||
#include "jni/id_cache.h"
|
||||
#include "jni/mic.h"
|
||||
#include "jni/native.h"
|
||||
#include "jni/ndk_motion.h"
|
||||
#include "video_core/renderer_base.h"
|
||||
@ -137,6 +139,9 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) {
|
||||
system.RegisterMiiSelector(std::make_shared<MiiSelector::AndroidMiiSelector>());
|
||||
system.RegisterSoftwareKeyboard(std::make_shared<SoftwareKeyboard::AndroidKeyboard>());
|
||||
|
||||
// Register real Mic factory
|
||||
Frontend::Mic::RegisterRealMicFactory(std::make_unique<Mic::AndroidFactory>());
|
||||
|
||||
InputManager::Init();
|
||||
|
||||
window->MakeCurrent();
|
||||
|
@ -141,4 +141,16 @@
|
||||
<item>2</item>
|
||||
<item>3</item>
|
||||
</integer-array>
|
||||
|
||||
<string-array name="audioInputTypeNames">
|
||||
<item>None</item>
|
||||
<item>Real Device</item>
|
||||
<item>Static Noise</item>
|
||||
</string-array>
|
||||
|
||||
<integer-array name="audioInputTypeValues">
|
||||
<item>0</item>
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
</integer-array>
|
||||
</resources>
|
||||
|
@ -99,6 +99,7 @@
|
||||
<!-- Audio settings strings -->
|
||||
<string name="audio_stretch">Enable audio stretching</string>
|
||||
<string name="audio_stretch_description">Stretches audio to reduce stuttering. When enabled, increases audio latency and slightly reduces performance.</string>
|
||||
<string name="audio_input_type">Audio Input Device</string>
|
||||
|
||||
<!-- Miscellaneous -->
|
||||
<string name="clear">Clear</string>
|
||||
@ -178,4 +179,8 @@
|
||||
<string name="camera_select_image">Select Image</string>
|
||||
<string name="camera">Camera</string>
|
||||
<string name="camera_permission_needed">Citra needs to access your camera to emulate the 3DS\'s cameras.\n\nAlternatively, you can also set \"Image Source\" to \"Still Image\" in Camera Settings.</string>
|
||||
|
||||
<!-- Microphone -->
|
||||
<string name="microphone">Microphone</string>
|
||||
<string name="microphone_permission_needed">Citra needs to access your microphone to emulate the 3DS\'s microphone.\n\nAlternatively, you can also change \"Audio Input Device\" in Audio Settings.</string>
|
||||
</resources>
|
||||
|
@ -189,4 +189,11 @@ std::vector<std::string> ListCubebInputDevices() {
|
||||
cubeb_destroy(ctx);
|
||||
return device_list;
|
||||
}
|
||||
|
||||
CubebFactory::~CubebFactory() = default;
|
||||
|
||||
std::unique_ptr<Frontend::Mic::Interface> CubebFactory::Create(std::string mic_device_name) {
|
||||
return std::make_unique<CubebInput>(std::move(mic_device_name));
|
||||
}
|
||||
|
||||
} // namespace AudioCore
|
||||
|
@ -31,4 +31,11 @@ private:
|
||||
|
||||
std::vector<std::string> ListCubebInputDevices();
|
||||
|
||||
class CubebFactory final : public Frontend::Mic::RealMicFactory {
|
||||
public:
|
||||
~CubebFactory() override;
|
||||
|
||||
std::unique_ptr<Frontend::Mic::Interface> Create(std::string mic_device_name) override;
|
||||
};
|
||||
|
||||
} // namespace AudioCore
|
||||
|
@ -5,6 +5,10 @@
|
||||
#include <array>
|
||||
#include "core/frontend/mic.h"
|
||||
|
||||
#ifdef HAVE_CUBEB
|
||||
#include "audio_core/cubeb_input.h"
|
||||
#endif
|
||||
|
||||
namespace Frontend::Mic {
|
||||
|
||||
constexpr std::array<u8, 16> NOISE_SAMPLE_8_BIT = {0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
@ -57,4 +61,26 @@ Samples StaticMic::Read() {
|
||||
return (sample_size == 8) ? CACHE_8_BIT : CACHE_16_BIT;
|
||||
}
|
||||
|
||||
RealMicFactory::~RealMicFactory() = default;
|
||||
|
||||
NullFactory::~NullFactory() = default;
|
||||
|
||||
std::unique_ptr<Interface> NullFactory::Create([[maybe_unused]] std::string mic_device_name) {
|
||||
return std::make_unique<NullMic>();
|
||||
}
|
||||
|
||||
#ifdef HAVE_CUBEB
|
||||
static std::unique_ptr<RealMicFactory> g_factory = std::make_unique<AudioCore::CubebFactory>();
|
||||
#else
|
||||
static std::unique_ptr<RealMicFactory> g_factory = std::make_unique<NullFactory>();
|
||||
#endif
|
||||
|
||||
void RegisterRealMicFactory(std::unique_ptr<RealMicFactory> factory) {
|
||||
g_factory = std::move(factory);
|
||||
}
|
||||
|
||||
std::unique_ptr<Interface> CreateRealMic(std::string mic_device_name) {
|
||||
return g_factory->Create(std::move(mic_device_name));
|
||||
}
|
||||
|
||||
} // namespace Frontend::Mic
|
||||
|
@ -115,4 +115,23 @@ private:
|
||||
std::vector<u8> CACHE_16_BIT;
|
||||
};
|
||||
|
||||
/// Factory for creating a real Mic input device;
|
||||
class RealMicFactory {
|
||||
public:
|
||||
virtual ~RealMicFactory();
|
||||
|
||||
virtual std::unique_ptr<Interface> Create(std::string mic_device_name) = 0;
|
||||
};
|
||||
|
||||
class NullFactory final : public RealMicFactory {
|
||||
public:
|
||||
~NullFactory() override;
|
||||
|
||||
std::unique_ptr<Interface> Create(std::string mic_device_name) override;
|
||||
};
|
||||
|
||||
void RegisterRealMicFactory(std::unique_ptr<RealMicFactory> factory);
|
||||
|
||||
std::unique_ptr<Interface> CreateRealMic(std::string mic_device_name);
|
||||
|
||||
} // namespace Frontend::Mic
|
||||
|
@ -3,9 +3,6 @@
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <boost/serialization/weak_ptr.hpp>
|
||||
#ifdef HAVE_CUBEB
|
||||
#include "audio_core/cubeb_input.h"
|
||||
#endif
|
||||
#include "common/archives.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "core/core.h"
|
||||
@ -359,11 +356,7 @@ struct MIC_U::Impl {
|
||||
new_mic = std::make_unique<Frontend::Mic::NullMic>();
|
||||
break;
|
||||
case Settings::MicInputType::Real:
|
||||
#if HAVE_CUBEB
|
||||
new_mic = std::make_unique<AudioCore::CubebInput>(Settings::values.mic_input_device);
|
||||
#else
|
||||
new_mic = std::make_unique<Frontend::Mic::NullMic>();
|
||||
#endif
|
||||
new_mic = Frontend::Mic::CreateRealMic(Settings::values.mic_input_device);
|
||||
break;
|
||||
case Settings::MicInputType::Static:
|
||||
new_mic = std::make_unique<Frontend::Mic::StaticMic>();
|
||||
|
Loading…
x
Reference in New Issue
Block a user