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:
zhupengfei 2020-05-11 22:38:17 +08:00 committed by bunnei
parent 370318aba4
commit 5e52853078
20 changed files with 205 additions and 10 deletions

View File

@ -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

View File

@ -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();

View File

@ -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;

View File

@ -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) {

View File

@ -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";

View File

@ -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)

View File

@ -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 =

View File

@ -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 =

View File

@ -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);

View File

@ -19,6 +19,7 @@ jmethodID GetIsPortraitMode();
jmethodID GetLandscapeScreenLayout();
jmethodID GetExitEmulationActivity();
jmethodID GetRequestCameraPermission();
jmethodID GetRequestMicPermission();
} // namespace IDCache

View 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

View 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

View File

@ -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();

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>();