From e9cb7fda422e40b1466743a8b04c80f8ba21f798 Mon Sep 17 00:00:00 2001 From: AlexandreRouma Date: Sun, 13 Feb 2022 17:25:43 +0100 Subject: [PATCH] Backend abstraction + added android support + partial high DPI scaling + added missing files --- android/.gitignore | 12 + android/app/build.gradle | 64 +++ android/app/src/main/AndroidManifest.xml | 34 ++ android/app/src/main/java/DeviceManager.kt | 32 ++ android/app/src/main/java/MainActivity.kt | 192 +++++++ .../app/src/main/res/mipmap/ic_launcher.png | Bin 0 -> 21113 bytes .../app/src/main/res/xml/device_filter.xml | 53 ++ android/build.gradle | 22 + android/gradle.properties | 1 + android/settings.gradle | 1 + core/backends/android/android_backend.h | 17 + core/backends/android/backend.cpp | 490 ++++++++++++++++++ .../android/imgui/imgui_impl_android.cpp | 187 +++++++ .../android/imgui/imgui_impl_android.h | 27 + core/backends/android/keybinds.h | 27 + .../android_audio_sink/CMakeLists.txt | 13 + sink_modules/android_audio_sink/src/main.cpp | 195 +++++++ 17 files changed, 1367 insertions(+) create mode 100644 android/.gitignore create mode 100644 android/app/build.gradle create mode 100644 android/app/src/main/AndroidManifest.xml create mode 100644 android/app/src/main/java/DeviceManager.kt create mode 100644 android/app/src/main/java/MainActivity.kt create mode 100644 android/app/src/main/res/mipmap/ic_launcher.png create mode 100644 android/app/src/main/res/xml/device_filter.xml create mode 100644 android/build.gradle create mode 100644 android/gradle.properties create mode 100644 android/settings.gradle create mode 100644 core/backends/android/android_backend.h create mode 100644 core/backends/android/backend.cpp create mode 100644 core/backends/android/imgui/imgui_impl_android.cpp create mode 100644 core/backends/android/imgui/imgui_impl_android.h create mode 100644 core/backends/android/keybinds.h create mode 100644 sink_modules/android_audio_sink/CMakeLists.txt create mode 100644 sink_modules/android_audio_sink/src/main.cpp diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 00000000..3c7a6191 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,12 @@ +.cxx +.externalNativeBuild +build/ +*.iml + +.idea +.gradle +local.properties + +# Android Studio puts a Gradle wrapper here, that we don't want: +gradle/ +gradlew* diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 00000000..ed4054db --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,64 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 28 + buildToolsVersion "30.0.3" + ndkVersion "23.0.7599858" + defaultConfig { + applicationId "org.sdrpp.sdrpp" + minSdkVersion 28 + targetSdkVersion 28 + versionCode 1 + versionName "1.1.0" + + externalNativeBuild { + cmake { + arguments "-DOPT_BACKEND_GLFW=OFF", "-DOPT_BACKEND_ANDROID=ON", "-DOPT_BUILD_SOAPY_SOURCE=OFF", "-DOPT_BUILD_ANDROID_AUDIO_SINK=ON", "-DOPT_BUILD_AUDIO_SINK=OFF", "-DOPT_BUILD_DISCORD_PRESENCE=OFF", "-DUSE_INTERNAL_LIBCORRECT=OFF" + } + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt') + } + } + + externalNativeBuild { + cmake { + version "3.18.1" + path "../../CMakeLists.txt" + } + } + + sourceSets { + main { + assets.srcDirs += ['assets'] + } + } +} + +task deleteTempAssets (type: Delete) { + delete 'assets' +} + +task copyResources(type: Copy) { + description = 'Copy resources...' + from '../../root/' + into 'assets/' + include('**/*') +} + +repositories { + mavenCentral() +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.appcompat:appcompat:1.0.2' +} + +copyResources.dependsOn deleteTempAssets +preBuild.dependsOn copyResources \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..3ed585e1 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/DeviceManager.kt b/android/app/src/main/java/DeviceManager.kt new file mode 100644 index 00000000..f53f1742 --- /dev/null +++ b/android/app/src/main/java/DeviceManager.kt @@ -0,0 +1,32 @@ +package org.sdrpp.sdrpp; + +import android.app.NativeActivity; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.hardware.usb.*; +import android.Manifest; +import android.os.Bundle; +import android.view.View; +import android.view.KeyEvent; +import android.view.inputmethod.InputMethodManager; +import android.util.Log; +import android.content.res.AssetManager; + +import androidx.core.app.ActivityCompat; + +import androidx.core.content.PermissionChecker; + +import java.util.concurrent.LinkedBlockingQueue; +import java.io.*; + +class DeviceManager { + public fun init() { + + } +} \ No newline at end of file diff --git a/android/app/src/main/java/MainActivity.kt b/android/app/src/main/java/MainActivity.kt new file mode 100644 index 00000000..9fbf5ec4 --- /dev/null +++ b/android/app/src/main/java/MainActivity.kt @@ -0,0 +1,192 @@ +package org.sdrpp.sdrpp; + +import android.app.NativeActivity; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.hardware.usb.*; +import android.Manifest; +import android.os.Bundle; +import android.view.View; +import android.view.KeyEvent; +import android.view.inputmethod.InputMethodManager; +import android.util.Log; +import android.content.res.AssetManager; + +import androidx.core.app.ActivityCompat; + +import androidx.core.content.PermissionChecker; + +import java.util.concurrent.LinkedBlockingQueue; +import java.io.*; + +private const val ACTION_USB_PERMISSION = "org.sdrpp.sdrpp.USB_PERMISSION"; + +private val usbReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (ACTION_USB_PERMISSION == intent.action) { + synchronized(this) { + var _this = context as MainActivity; + _this.SDR_device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE) + if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { + _this.SDR_conn = _this.usbManager!!.openDevice(_this.SDR_device); + + // Save SDR info + _this.SDR_VID = _this.SDR_device!!.getVendorId(); + _this.SDR_PID = _this.SDR_device!!.getProductId() + _this.SDR_FD = _this.SDR_conn!!.getFileDescriptor(); + } + + // Whatever the hell this does + context.unregisterReceiver(this); + + // Hide again the system bars + _this.hideSystemBars(); + } + } + } +} + +class MainActivity : NativeActivity() { + private val TAG : String = "SDR++"; + public var usbManager : UsbManager? = null; + public var SDR_device : UsbDevice? = null; + public var SDR_conn : UsbDeviceConnection? = null; + public var SDR_VID : Int = -1; + public var SDR_PID : Int = -1; + public var SDR_FD : Int = -1; + + fun checkAndAsk(permission: String) { + if (PermissionChecker.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, arrayOf(permission), 1); + } + } + + public fun hideSystemBars() { + val decorView = getWindow().getDecorView(); + val uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + decorView.setSystemUiVisibility(uiOptions); + } + + public override fun onCreate(savedInstanceState: Bundle?) { + // Hide bars + hideSystemBars(); + + // Ask for required permissions, without these the app cannot run. + checkAndAsk(Manifest.permission.WRITE_EXTERNAL_STORAGE); + checkAndAsk(Manifest.permission.READ_EXTERNAL_STORAGE); + + // TODO: Have the main code wait until these two permissions are available + + // Register events + usbManager = getSystemService(Context.USB_SERVICE) as UsbManager; + val permissionIntent = PendingIntent.getBroadcast(this, 0, Intent(ACTION_USB_PERMISSION), 0) + val filter = IntentFilter(ACTION_USB_PERMISSION) + registerReceiver(usbReceiver, filter) + + // Get permission for all USB devices + val devList = usbManager!!.getDeviceList(); + for ((name, dev) in devList) { + usbManager!!.requestPermission(dev, permissionIntent); + } + + // Ask for internet permission + checkAndAsk(Manifest.permission.INTERNET); + + super.onCreate(savedInstanceState) + } + + public override fun onResume() { + // Hide bars again + hideSystemBars(); + super.onResume(); + } + + fun showSoftInput() { + val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager; + inputMethodManager.showSoftInput(window.decorView, 0); + } + + fun hideSoftInput() { + val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager; + inputMethodManager.hideSoftInputFromWindow(window.decorView.windowToken, 0); + hideSystemBars(); + } + + // Queue for the Unicode characters to be polled from native code (via pollUnicodeChar()) + private var unicodeCharacterQueue: LinkedBlockingQueue = LinkedBlockingQueue() + + // We assume dispatchKeyEvent() of the NativeActivity is actually called for every + // KeyEvent and not consumed by any View before it reaches here + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (event.action == KeyEvent.ACTION_DOWN) { + unicodeCharacterQueue.offer(event.getUnicodeChar(event.metaState)) + } + return super.dispatchKeyEvent(event) + } + + fun pollUnicodeChar(): Int { + return unicodeCharacterQueue.poll() ?: 0 + } + + public fun createIfDoesntExist(path: String) { + // This is a directory, create it in the filesystem + var folder = File(path); + var success = true; + if (!folder.exists()) { + success = folder.mkdirs(); + } + if (!success) { + Log.e(TAG, "Could not create folder with path " + path); + } + } + + public fun extractDir(aman: AssetManager, local: String, rsrc: String): Int { + val flist = aman.list(rsrc); + var ecount = 0; + for (fp in flist) { + val lpath = local + "/" + fp; + val rpath = rsrc + "/" + fp; + + Log.w(TAG, "Extracting '" + rpath + "' to '" + lpath + "'"); + + // Create local path if non-existent + createIfDoesntExist(local); + + // Create if directory + val ext = extractDir(aman, lpath, rpath); + + // Extract if file + if (ext == 0) { + // This is a file, extract it + val _os = FileOutputStream(lpath); + val _is = aman.open(rpath); + val ilen = _is.available(); + var fbuf = ByteArray(ilen); + _is.read(fbuf, 0, ilen); + _os.write(fbuf); + _os.close(); + _is.close(); + } + + ecount++; + } + return ecount; + } + + public fun getAppDir(): String { + val fdir = getFilesDir().getAbsolutePath(); + + // Extract all resources to the app directory + val aman = getAssets(); + extractDir(aman, fdir + "/res", "res"); + createIfDoesntExist(fdir + "/modules"); + + return fdir; + } +} diff --git a/android/app/src/main/res/mipmap/ic_launcher.png b/android/app/src/main/res/mipmap/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..03cf61838dddb018ecb2e3cb51ece291b80101a1 GIT binary patch literal 21113 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/xml/device_filter.xml b/android/app/src/main/res/xml/device_filter.xml new file mode 100644 index 00000000..88165de3 --- /dev/null +++ b/android/app/src/main/res/xml/device_filter.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 00000000..24f9e99a --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,22 @@ +buildscript { + ext.kotlin_version = '1.4.31' + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 00000000..2d8d1e4d --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1 @@ +android.useAndroidX=true \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 00000000..e7b4def4 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/core/backends/android/android_backend.h b/core/backends/android/android_backend.h new file mode 100644 index 00000000..50fdf515 --- /dev/null +++ b/core/backends/android/android_backend.h @@ -0,0 +1,17 @@ +#pragma once +#include +#include + +namespace backend { + struct DevVIDPID { + uint16_t vid; + uint16_t pid; + }; + + extern const std::vector AIRSPY_VIDPIDS; + extern const std::vector AIRSPYHF_VIDPIDS; + extern const std::vector HACKRF_VIDPIDS; + extern const std::vector RTL_SDR_VIDPIDS; + + int getDeviceFD(int& vid, int& pid, const std::vector& allowedVidPids); +} \ No newline at end of file diff --git a/core/backends/android/backend.cpp b/core/backends/android/backend.cpp new file mode 100644 index 00000000..16ca2479 --- /dev/null +++ b/core/backends/android/backend.cpp @@ -0,0 +1,490 @@ +#include +#include "android_backend.h" +#include +#include +#include "imgui.h" +#include "imgui_impl_android.h" +#include "imgui_impl_opengl3.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Credit to the ImGui android OpenGL3 example for a lot of this code! + +namespace backend { + struct android_app* app = NULL; + EGLDisplay _EglDisplay = EGL_NO_DISPLAY; + EGLSurface _EglSurface = EGL_NO_SURFACE; + EGLContext _EglContext = EGL_NO_CONTEXT; + bool _Initialized = false; + char _LogTag[] = "SDR++"; + bool initialized = false; + bool pauseRendering = false; + bool exited = false; + + // Forward declaration + int ShowSoftKeyboardInput(); + int PollUnicodeChars(); + + void doPartialInit() { + backend::init(); + style::loadFonts(options::opts.root + "/res"); // TODO: Don't hardcode, use config + icons::load(options::opts.root + "/res"); + thememenu::applyTheme(); + ImGui::GetStyle().ScaleAllSizes(style::uiScale); + gui::mainWindow.setFirstMenuRender(); + } + + void handleAppCmd(struct android_app* app, int32_t appCmd) { + switch (appCmd) { + case APP_CMD_SAVE_STATE: + spdlog::warn("APP_CMD_SAVE_STATE"); + break; + case APP_CMD_INIT_WINDOW: + spdlog::warn("APP_CMD_INIT_WINDOW"); + if (pauseRendering && !exited) { + doPartialInit(); + pauseRendering = false; + } + exited = false; + break; + case APP_CMD_TERM_WINDOW: + spdlog::warn("APP_CMD_TERM_WINDOW"); + pauseRendering = true; + backend::end(); + break; + case APP_CMD_GAINED_FOCUS: + spdlog::warn("APP_CMD_GAINED_FOCUS"); + break; + case APP_CMD_LOST_FOCUS: + spdlog::warn("APP_CMD_LOST_FOCUS"); + break; + } + } + + int32_t handleInputEvent(struct android_app* app, AInputEvent* inputEvent) { + return ImGui_ImplAndroid_HandleInputEvent(inputEvent); + } + + int aquireWindow() { + while (!app->window) { + spdlog::warn("Waiting on the shitty window thing"); std::this_thread::sleep_for(std::chrono::milliseconds(30)); + int out_events; + struct android_poll_source* out_data; + + while (ALooper_pollAll(0, NULL, &out_events, (void**)&out_data) >= 0) { + // Process one event + if (out_data != NULL) { out_data->process(app, out_data); } + + // Exit the app by returning from within the infinite loop + if (app->destroyRequested != 0) { + return -1; + } + } + } + ANativeWindow_acquire(app->window); + return 0; + } + + int init(std::string resDir) { + spdlog::warn("Backend init"); + + // Get window + aquireWindow(); + + // EGL Init + { + _EglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if (_EglDisplay == EGL_NO_DISPLAY) + __android_log_print(ANDROID_LOG_ERROR, _LogTag, "%s", "eglGetDisplay(EGL_DEFAULT_DISPLAY) returned EGL_NO_DISPLAY"); + + if (eglInitialize(_EglDisplay, 0, 0) != EGL_TRUE) + __android_log_print(ANDROID_LOG_ERROR, _LogTag, "%s", "eglInitialize() returned with an error"); + + const EGLint egl_attributes[] = { EGL_BLUE_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_RED_SIZE, 8, EGL_DEPTH_SIZE, 24, EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_NONE }; + EGLint num_configs = 0; + if (eglChooseConfig(_EglDisplay, egl_attributes, nullptr, 0, &num_configs) != EGL_TRUE) + __android_log_print(ANDROID_LOG_ERROR, _LogTag, "%s", "eglChooseConfig() returned with an error"); + if (num_configs == 0) + __android_log_print(ANDROID_LOG_ERROR, _LogTag, "%s", "eglChooseConfig() returned 0 matching config"); + + // Get the first matching config + EGLConfig egl_config; + eglChooseConfig(_EglDisplay, egl_attributes, &egl_config, 1, &num_configs); + EGLint egl_format; + eglGetConfigAttrib(_EglDisplay, egl_config, EGL_NATIVE_VISUAL_ID, &egl_format); + ANativeWindow_setBuffersGeometry(app->window, 0, 0, egl_format); + + const EGLint egl_context_attributes[] = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE }; + _EglContext = eglCreateContext(_EglDisplay, egl_config, EGL_NO_CONTEXT, egl_context_attributes); + + if (_EglContext == EGL_NO_CONTEXT) + __android_log_print(ANDROID_LOG_ERROR, _LogTag, "%s", "eglCreateContext() returned EGL_NO_CONTEXT"); + + _EglSurface = eglCreateWindowSurface(_EglDisplay, egl_config, app->window, NULL); + eglMakeCurrent(_EglDisplay, _EglSurface, _EglSurface, _EglContext); + } + + // Setup Dear ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + (void)io; + + // Disable loading/saving of .ini file from disk. + // FIXME: Consider using LoadIniSettingsFromMemory() / SaveIniSettingsToMemory() to save in appropriate location for Android. + io.IniFilename = NULL; + + // Setup Platform/Renderer backends + ImGui_ImplAndroid_Init(app->window); + ImGui_ImplOpenGL3_Init("#version 300 es"); + + return 0; + } + + void beginFrame() { + // Start the Dear ImGui frame + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplAndroid_NewFrame(); + ImGui::NewFrame(); + } + + void render(bool vsync) { + // Rendering + ImGui::Render(); + auto dSize = ImGui::GetIO().DisplaySize; + glViewport(0, 0, dSize.x, dSize.y); + glClearColor(gui::themeManager.clearColor.x, gui::themeManager.clearColor.y, gui::themeManager.clearColor.z, gui::themeManager.clearColor.w); + glClear(GL_COLOR_BUFFER_BIT); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + eglSwapBuffers(_EglDisplay, _EglSurface); + } + + // No screen pos to detect + void getMouseScreenPos(double& x, double& y) { x = 0; y = 0; } + void setMouseScreenPos(double x, double y) {} + + int renderLoop() { + spdlog::warn("BRUH: {0}", (void*)backend::app->window); + while (true) { + int out_events; + struct android_poll_source* out_data; + + while (ALooper_pollAll(0, NULL, &out_events, (void**)&out_data) >= 0) { + // Process one event + if (out_data != NULL) { out_data->process(app, out_data); } + + // Exit the app by returning from within the infinite loop + if (app->destroyRequested != 0) { + spdlog::warn("ASKED TO EXIT"); + exited = true; + + // Stop SDR + gui::mainWindow.setPlayState(false); + return 0; + } + } + + if (_EglDisplay == EGL_NO_DISPLAY) { continue; } + + if (!pauseRendering) { + // Initiate a new frame + ImGuiIO& io = ImGui::GetIO(); + auto dsize = io.DisplaySize; + + // Poll Unicode characters via JNI + // FIXME: do not call this every frame because of JNI overhead + PollUnicodeChars(); + + // Open on-screen (soft) input if requested by Dear ImGui + static bool WantTextInputLast = false; + if (io.WantTextInput && !WantTextInputLast) + ShowSoftKeyboardInput(); + WantTextInputLast = io.WantTextInput; + + // Render + beginFrame(); + + if (dsize.x > 0 && dsize.y > 0) { + ImGui::SetNextWindowPos(ImVec2(0, 0)); + ImGui::SetNextWindowSize(ImVec2(dsize.x, dsize.y)); + gui::mainWindow.draw(); + } + render(); + } + else { + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + } + } + + return 0; + } + + int end() { + // Cleanup + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplAndroid_Shutdown(); + ImGui::DestroyContext(); + + // Destroy all + if (_EglDisplay != EGL_NO_DISPLAY) { + eglMakeCurrent(_EglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + if (_EglContext != EGL_NO_CONTEXT) { eglDestroyContext(_EglDisplay, _EglContext); } + if (_EglSurface != EGL_NO_SURFACE) { eglDestroySurface(_EglDisplay, _EglSurface); } + eglTerminate(_EglDisplay); + } + + _EglDisplay = EGL_NO_DISPLAY; + _EglContext = EGL_NO_CONTEXT; + _EglSurface = EGL_NO_SURFACE; + + if (app->window) { ANativeWindow_release(app->window); } + + return 0; + } + + int ShowSoftKeyboardInput() { + JavaVM* java_vm = app->activity->vm; + JNIEnv* java_env = NULL; + + jint jni_return = java_vm->GetEnv((void**)&java_env, JNI_VERSION_1_6); + if (jni_return == JNI_ERR) + return -1; + + jni_return = java_vm->AttachCurrentThread(&java_env, NULL); + if (jni_return != JNI_OK) + return -2; + + jclass native_activity_clazz = java_env->GetObjectClass(app->activity->clazz); + if (native_activity_clazz == NULL) + return -3; + + jmethodID method_id = java_env->GetMethodID(native_activity_clazz, "showSoftInput", "()V"); + if (method_id == NULL) + return -4; + + java_env->CallVoidMethod(app->activity->clazz, method_id); + + jni_return = java_vm->DetachCurrentThread(); + if (jni_return != JNI_OK) + return -5; + + return 0; + } + + int getDeviceFD(int& vid, int& pid, const std::vector& allowedVidPids) { + JavaVM* java_vm = app->activity->vm; + JNIEnv* java_env = NULL; + + jint jni_return = java_vm->GetEnv((void**)&java_env, JNI_VERSION_1_6); + if (jni_return == JNI_ERR) + return -1; + + jni_return = java_vm->AttachCurrentThread(&java_env, NULL); + if (jni_return != JNI_OK) + return -1; + + jclass native_activity_clazz = java_env->GetObjectClass(app->activity->clazz); + if (native_activity_clazz == NULL) + return -1; + + jfieldID fd_field_id = java_env->GetFieldID(native_activity_clazz, "SDR_FD", "I"); + jfieldID vid_field_id = java_env->GetFieldID(native_activity_clazz, "SDR_VID", "I"); + jfieldID pid_field_id = java_env->GetFieldID(native_activity_clazz, "SDR_PID", "I"); + + if (!vid_field_id || !vid_field_id || !pid_field_id) + return -1; + + int fd = java_env->GetIntField(app->activity->clazz, fd_field_id); + vid = java_env->GetIntField(app->activity->clazz, vid_field_id); + pid = java_env->GetIntField(app->activity->clazz, pid_field_id); + + jni_return = java_vm->DetachCurrentThread(); + if (jni_return != JNI_OK) + return -1; + + // If no vid/pid was given, just return successfully + if (allowedVidPids.empty()) { + return fd; + } + + // Otherwise, check that the vid/pid combo is allowed + for (auto const& vp : allowedVidPids) { + if (vp.vid != vid || vp.pid != pid) { continue; } + return fd; + } + + return -1; + } + + // Unfortunately, the native KeyEvent implementation has no getUnicodeChar() function. + // Therefore, we implement the processing of KeyEvents in MainActivity.kt and poll + // the resulting Unicode characters here via JNI and send them to Dear ImGui. + int PollUnicodeChars() { + JavaVM* java_vm = app->activity->vm; + JNIEnv* java_env = NULL; + + jint jni_return = java_vm->GetEnv((void**)&java_env, JNI_VERSION_1_6); + if (jni_return == JNI_ERR) + return -1; + + jni_return = java_vm->AttachCurrentThread(&java_env, NULL); + if (jni_return != JNI_OK) + return -2; + + jclass native_activity_clazz = java_env->GetObjectClass(app->activity->clazz); + if (native_activity_clazz == NULL) + return -3; + + jmethodID method_id = java_env->GetMethodID(native_activity_clazz, "pollUnicodeChar", "()I"); + if (method_id == NULL) + return -4; + + // Send the actual characters to Dear ImGui + ImGuiIO& io = ImGui::GetIO(); + jint unicode_character; + while ((unicode_character = java_env->CallIntMethod(app->activity->clazz, method_id)) != 0) + io.AddInputCharacter(unicode_character); + + jni_return = java_vm->DetachCurrentThread(); + if (jni_return != JNI_OK) + return -5; + + return 0; + } + + std::string getAppFilesDir() { + JavaVM* java_vm = app->activity->vm; + JNIEnv* java_env = NULL; + + jint jni_return = java_vm->GetEnv((void**)&java_env, JNI_VERSION_1_6); + if (jni_return == JNI_ERR) + throw std::runtime_error("Could not get JNI environement"); + + jni_return = java_vm->AttachCurrentThread(&java_env, NULL); + if (jni_return != JNI_OK) + throw std::runtime_error("Could not attach to thread"); + + jclass native_activity_clazz = java_env->GetObjectClass(app->activity->clazz); + if (native_activity_clazz == NULL) + throw std::runtime_error("Could not get MainActivity class"); + + jmethodID method_id = java_env->GetMethodID(native_activity_clazz, "getAppDir", "()Ljava/lang/String;"); + if (method_id == NULL) + throw std::runtime_error("Could not get methode ID"); + + jstring jstr = (jstring)java_env->CallObjectMethod(app->activity->clazz, method_id); + + const char* _str = java_env->GetStringUTFChars(jstr, NULL); + std::string str(_str); + java_env->ReleaseStringUTFChars(jstr, _str); + + jni_return = java_vm->DetachCurrentThread(); + if (jni_return != JNI_OK) + throw std::runtime_error("Could not detach from thread"); + + + return str; + } + + const std::vector AIRSPY_VIDPIDS = { + { 0x1d50, 0x60a1 } + }; + + const std::vector AIRSPYHF_VIDPIDS = { + { 0x03EB, 0x800C } + }; + + const std::vector HACKRF_VIDPIDS = { + { 0x1d50, 0x604b }, + { 0x1d50, 0x6089 }, + { 0x1d50, 0xcc15 } + }; + + const std::vector RTL_SDR_VIDPIDS = { + { 0x0bda, 0x2832 }, + { 0x0bda, 0x2838 }, + { 0x0413, 0x6680 }, + { 0x0413, 0x6f0f }, + { 0x0458, 0x707f }, + { 0x0ccd, 0x00a9 }, + { 0x0ccd, 0x00b3 }, + { 0x0ccd, 0x00b4 }, + { 0x0ccd, 0x00b5 }, + { 0x0ccd, 0x00b7 }, + { 0x0ccd, 0x00b8 }, + { 0x0ccd, 0x00b9 }, + { 0x0ccd, 0x00c0 }, + { 0x0ccd, 0x00c6 }, + { 0x0ccd, 0x00d3 }, + { 0x0ccd, 0x00d7 }, + { 0x0ccd, 0x00e0 }, + { 0x1554, 0x5020 }, + { 0x15f4, 0x0131 }, + { 0x15f4, 0x0133 }, + { 0x185b, 0x0620 }, + { 0x185b, 0x0650 }, + { 0x185b, 0x0680 }, + { 0x1b80, 0xd393 }, + { 0x1b80, 0xd394 }, + { 0x1b80, 0xd395 }, + { 0x1b80, 0xd397 }, + { 0x1b80, 0xd398 }, + { 0x1b80, 0xd39d }, + { 0x1b80, 0xd3a4 }, + { 0x1b80, 0xd3a8 }, + { 0x1b80, 0xd3af }, + { 0x1b80, 0xd3b0 }, + { 0x1d19, 0x1101 }, + { 0x1d19, 0x1102 }, + { 0x1d19, 0x1103 }, + { 0x1d19, 0x1104 }, + { 0x1f4d, 0xa803 }, + { 0x1f4d, 0xb803 }, + { 0x1f4d, 0xc803 }, + { 0x1f4d, 0xd286 }, + { 0x1f4d, 0xd803 } + }; +} + +extern "C" { + void android_main(struct android_app* app) { + // Save app instance + app->onAppCmd = backend::handleAppCmd; + app->onInputEvent = backend::handleInputEvent; + backend::app = app; + + // Check if this is the first time we run or not + if (backend::initialized) { + spdlog::warn("android_main called again"); + backend::doPartialInit(); + backend::pauseRendering = false; + backend::renderLoop(); + return; + } + backend::initialized = true; + + // prepare spdlog + auto console_sink = std::make_shared("SDR++"); + auto logger = std::shared_ptr(new spdlog::logger("", { console_sink })); + spdlog::set_default_logger(logger); + + // Grab files dir + std::string appdir = backend::getAppFilesDir(); + + // Call main + char* rootpath = new char[appdir.size() + 1]; + strcpy(rootpath, appdir.c_str()); + char* dummy[] = { "", "-r", rootpath }; + sdrpp_main(3, dummy); + } +} \ No newline at end of file diff --git a/core/backends/android/imgui/imgui_impl_android.cpp b/core/backends/android/imgui/imgui_impl_android.cpp new file mode 100644 index 00000000..3201bee2 --- /dev/null +++ b/core/backends/android/imgui/imgui_impl_android.cpp @@ -0,0 +1,187 @@ +// dear imgui: Platform Binding for Android native app +// This needs to be used along with the OpenGL 3 Renderer (imgui_impl_opengl3) + +// Implemented features: +// [X] Platform: Keyboard arrays indexed using AKEYCODE_* codes, e.g. ImGui::IsKeyPressed(AKEYCODE_SPACE). +// Missing features: +// [ ] Platform: Clipboard support. +// [ ] Platform: Gamepad support. Enable with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'. +// [ ] Platform: Mouse cursor shape and visibility. Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'. FIXME: Check if this is even possible with Android. +// Important: +// - FIXME: On-screen keyboard currently needs to be enabled by the application (see examples/ and issue #3446) +// - FIXME: Unicode character inputs needs to be passed by Dear ImGui by the application (see examples/ and issue #3446) + +// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. +// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. +// If you are new to Dear ImGui, read documentation from the docs/ folder + read the top of imgui.cpp. +// Read online: https://github.com/ocornut/imgui/tree/master/docs + +// CHANGELOG +// (minor and older changes stripped away, please see git history for details) +// 2021-03-04: Initial version. + +#include "imgui.h" +#include "imgui_impl_android.h" +#include +#include +#include +#include +#include +#include +#include + +// Android data +static double g_Time = 0.0; +static ANativeWindow* g_Window; +static char g_LogTag[] = "ImGuiExample"; +static std::map> g_KeyEventQueues; // FIXME: Remove dependency on map and queue once we use upcoming input queue. + +int32_t ImGui_ImplAndroid_HandleInputEvent(AInputEvent* input_event) +{ + ImGuiIO& io = ImGui::GetIO(); + int32_t event_type = AInputEvent_getType(input_event); + switch (event_type) + { + case AINPUT_EVENT_TYPE_KEY: + { + int32_t event_key_code = AKeyEvent_getKeyCode(input_event); + int32_t event_action = AKeyEvent_getAction(input_event); + int32_t event_meta_state = AKeyEvent_getMetaState(input_event); + + io.KeyCtrl = ((event_meta_state & AMETA_CTRL_ON) != 0); + io.KeyShift = ((event_meta_state & AMETA_SHIFT_ON) != 0); + io.KeyAlt = ((event_meta_state & AMETA_ALT_ON) != 0); + + switch (event_action) + { + // FIXME: AKEY_EVENT_ACTION_DOWN and AKEY_EVENT_ACTION_UP occur at once as soon as a touch pointer + // goes up from a key. We use a simple key event queue/ and process one event per key per frame in + // ImGui_ImplAndroid_NewFrame()...or consider using IO queue, if suitable: https://github.com/ocornut/imgui/issues/2787 + case AKEY_EVENT_ACTION_DOWN: + case AKEY_EVENT_ACTION_UP: + g_KeyEventQueues[event_key_code].push(event_action); + break; + default: + break; + } + break; + } + case AINPUT_EVENT_TYPE_MOTION: + { + int32_t event_action = AMotionEvent_getAction(input_event); + int32_t event_pointer_index = (event_action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT; + event_action &= AMOTION_EVENT_ACTION_MASK; + switch (event_action) + { + case AMOTION_EVENT_ACTION_DOWN: + case AMOTION_EVENT_ACTION_UP: + // Physical mouse buttons (and probably other physical devices) also invoke the actions AMOTION_EVENT_ACTION_DOWN/_UP, + // but we have to process them separately to identify the actual button pressed. This is done below via + // AMOTION_EVENT_ACTION_BUTTON_PRESS/_RELEASE. Here, we only process "FINGER" input (and "UNKNOWN", as a fallback). + if((AMotionEvent_getToolType(input_event, event_pointer_index) == AMOTION_EVENT_TOOL_TYPE_FINGER) + || (AMotionEvent_getToolType(input_event, event_pointer_index) == AMOTION_EVENT_TOOL_TYPE_STYLUS) || (AMotionEvent_getToolType(input_event, event_pointer_index) == AMOTION_EVENT_TOOL_TYPE_UNKNOWN)) + { + io.MouseDown[0] = (event_action == AMOTION_EVENT_ACTION_DOWN); + io.MousePos = ImVec2(AMotionEvent_getX(input_event, event_pointer_index), AMotionEvent_getY(input_event, event_pointer_index)); + } + break; + case AMOTION_EVENT_ACTION_BUTTON_PRESS: + case AMOTION_EVENT_ACTION_BUTTON_RELEASE: + { + int32_t button_state = AMotionEvent_getButtonState(input_event); + io.MouseDown[0] = ((button_state & AMOTION_EVENT_BUTTON_PRIMARY) != 0); + io.MouseDown[1] = ((button_state & AMOTION_EVENT_BUTTON_SECONDARY) != 0); + io.MouseDown[2] = ((button_state & AMOTION_EVENT_BUTTON_TERTIARY) != 0); + } + break; + case AMOTION_EVENT_ACTION_HOVER_MOVE: // Hovering: Tool moves while NOT pressed (such as a physical mouse) + case AMOTION_EVENT_ACTION_MOVE: // Touch pointer moves while DOWN + io.MousePos = ImVec2(AMotionEvent_getX(input_event, event_pointer_index), AMotionEvent_getY(input_event, event_pointer_index)); + break; + case AMOTION_EVENT_ACTION_SCROLL: + io.MouseWheel = AMotionEvent_getAxisValue(input_event, AMOTION_EVENT_AXIS_VSCROLL, event_pointer_index); + io.MouseWheelH = AMotionEvent_getAxisValue(input_event, AMOTION_EVENT_AXIS_HSCROLL, event_pointer_index); + break; + default: + break; + } + } + return 1; + default: + break; + } + + return 0; +} + +bool ImGui_ImplAndroid_Init(ANativeWindow* window) +{ + g_Window = window; + g_Time = 0.0; + + // Setup backend capabilities flags + ImGuiIO& io = ImGui::GetIO(); + io.BackendPlatformName = "imgui_impl_android"; + + // Keyboard mapping. Dear ImGui will use those indices to peek into the io.KeysDown[] array. + io.KeyMap[ImGuiKey_Tab] = AKEYCODE_TAB; + io.KeyMap[ImGuiKey_LeftArrow] = AKEYCODE_DPAD_LEFT; // also covers physical keyboard arrow key + io.KeyMap[ImGuiKey_RightArrow] = AKEYCODE_DPAD_RIGHT; // also covers physical keyboard arrow key + io.KeyMap[ImGuiKey_UpArrow] = AKEYCODE_DPAD_UP; // also covers physical keyboard arrow key + io.KeyMap[ImGuiKey_DownArrow] = AKEYCODE_DPAD_DOWN; // also covers physical keyboard arrow key + io.KeyMap[ImGuiKey_PageUp] = AKEYCODE_PAGE_UP; + io.KeyMap[ImGuiKey_PageDown] = AKEYCODE_PAGE_DOWN; + io.KeyMap[ImGuiKey_Home] = AKEYCODE_MOVE_HOME; + io.KeyMap[ImGuiKey_End] = AKEYCODE_MOVE_END; + io.KeyMap[ImGuiKey_Insert] = AKEYCODE_INSERT; + io.KeyMap[ImGuiKey_Delete] = AKEYCODE_FORWARD_DEL; + io.KeyMap[ImGuiKey_Backspace] = AKEYCODE_DEL; + io.KeyMap[ImGuiKey_Space] = AKEYCODE_SPACE; + io.KeyMap[ImGuiKey_Enter] = AKEYCODE_ENTER; + io.KeyMap[ImGuiKey_Escape] = AKEYCODE_ESCAPE; + io.KeyMap[ImGuiKey_KeyPadEnter] = AKEYCODE_NUMPAD_ENTER; + io.KeyMap[ImGuiKey_A] = AKEYCODE_A; + io.KeyMap[ImGuiKey_C] = AKEYCODE_C; + io.KeyMap[ImGuiKey_V] = AKEYCODE_V; + io.KeyMap[ImGuiKey_X] = AKEYCODE_X; + io.KeyMap[ImGuiKey_Y] = AKEYCODE_Y; + io.KeyMap[ImGuiKey_Z] = AKEYCODE_Z; + + return true; +} + +void ImGui_ImplAndroid_Shutdown() +{ +} + +void ImGui_ImplAndroid_NewFrame() +{ + ImGuiIO& io = ImGui::GetIO(); + + // Process queued key events + // FIXME: This is a workaround for multiple key event actions occurring at once (see above) and can be removed once we use upcoming input queue. + for (auto& key_queue : g_KeyEventQueues) + { + if (key_queue.second.empty()) + continue; + io.KeysDown[key_queue.first] = (key_queue.second.front() == AKEY_EVENT_ACTION_DOWN); + key_queue.second.pop(); + } + + // Setup display size (every frame to accommodate for window resizing) + int32_t window_width = ANativeWindow_getWidth(g_Window); + int32_t window_height = ANativeWindow_getHeight(g_Window); + int display_width = window_width; + int display_height = window_height; + + io.DisplaySize = ImVec2((float)window_width, (float)window_height); + if (window_width > 0 && window_height > 0) + io.DisplayFramebufferScale = ImVec2((float)display_width / window_width, (float)display_height / window_height); + + // Setup time step + struct timespec current_timespec; + clock_gettime(CLOCK_MONOTONIC, ¤t_timespec); + double current_time = (double)(current_timespec.tv_sec) + (current_timespec.tv_nsec / 1000000000.0); + io.DeltaTime = g_Time > 0.0 ? (float)(current_time - g_Time) : (float)(1.0f / 60.0f); + g_Time = current_time; +} diff --git a/core/backends/android/imgui/imgui_impl_android.h b/core/backends/android/imgui/imgui_impl_android.h new file mode 100644 index 00000000..92b466b6 --- /dev/null +++ b/core/backends/android/imgui/imgui_impl_android.h @@ -0,0 +1,27 @@ +// dear imgui: Platform Binding for Android native app +// This needs to be used along with the OpenGL 3 Renderer (imgui_impl_opengl3) + +// Implemented features: +// [X] Platform: Keyboard arrays indexed using AKEYCODE_* codes, e.g. ImGui::IsKeyPressed(AKEYCODE_SPACE). +// Missing features: +// [ ] Platform: Clipboard support. +// [ ] Platform: Gamepad support. Enable with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'. +// [ ] Platform: Mouse cursor shape and visibility. Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'. FIXME: Check if this is even possible with Android. +// Important: +// - FIXME: On-screen keyboard currently needs to be enabled by the application (see examples/ and issue #3446) +// - FIXME: Unicode character inputs needs to be passed by Dear ImGui by the application (see examples/ and issue #3446) + +// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. +// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. +// If you are new to Dear ImGui, read documentation from the docs/ folder + read the top of imgui.cpp. +// Read online: https://github.com/ocornut/imgui/tree/master/docs + +#pragma once + +struct ANativeWindow; +struct AInputEvent; + +IMGUI_IMPL_API bool ImGui_ImplAndroid_Init(ANativeWindow* window); +IMGUI_IMPL_API int32_t ImGui_ImplAndroid_HandleInputEvent(AInputEvent* input_event); +IMGUI_IMPL_API void ImGui_ImplAndroid_Shutdown(); +IMGUI_IMPL_API void ImGui_ImplAndroid_NewFrame(); diff --git a/core/backends/android/keybinds.h b/core/backends/android/keybinds.h new file mode 100644 index 00000000..83bc6a66 --- /dev/null +++ b/core/backends/android/keybinds.h @@ -0,0 +1,27 @@ +#pragma once + +// No keyboard controls +#define KB_KEY_HOME -1 +#define KB_KEY_MENU -1 +#define KB_KEY_END -1 +#define KB_KEY_ESC -1 +#define KB_KEY_PG_UP -1 +#define KB_KEY_PG_DOWN -1 + +#define KB_KEY_ENTER -1 +#define KB_KEY_KP_ENTER -1 +#define KB_KEY_DEL -1 +#define KB_KEY_BACKSPACE -1 + +#define KB_KEY_LEFT -1 +#define KB_KEY_RIGHT -1 +#define KB_KEY_UP -1 +#define KB_KEY_DOWN -1 + +#define KB_KEY_LCTRL -1 +#define KB_KEY_RCTRL -1 +#define KB_KEY_LSHIFT -1 +#define KB_KEY_RSHIFT -1 + +#define KB_KEY_A -1 +#define KB_KEY_R -1 \ No newline at end of file diff --git a/sink_modules/android_audio_sink/CMakeLists.txt b/sink_modules/android_audio_sink/CMakeLists.txt new file mode 100644 index 00000000..92b2a473 --- /dev/null +++ b/sink_modules/android_audio_sink/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.13) +project(audio_sink) + +file(GLOB SRC "src/*.cpp") + +add_library(audio_sink SHARED ${SRC}) +target_link_libraries(audio_sink PRIVATE sdrpp_core) +set_target_properties(audio_sink PROPERTIES PREFIX "") + +target_include_directories(audio_sink PRIVATE "src/") + +target_compile_options(audio_sink PRIVATE -O3 -std=c++17) +target_link_libraries(audio_sink PRIVATE aaudio) \ No newline at end of file diff --git a/sink_modules/android_audio_sink/src/main.cpp b/sink_modules/android_audio_sink/src/main.cpp new file mode 100644 index 00000000..a4837dd2 --- /dev/null +++ b/sink_modules/android_audio_sink/src/main.cpp @@ -0,0 +1,195 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CONCAT(a, b) ((std::string(a) + b).c_str()) + +SDRPP_MOD_INFO{ + /* Name: */ "audio_sink", + /* Description: */ "Android audio sink module for SDR++", + /* Author: */ "Ryzerth", + /* Version: */ 0, 1, 0, + /* Max instances */ 1 +}; + +ConfigManager config; + +class AudioSink : SinkManager::Sink { +public: + AudioSink(SinkManager::Stream* stream, std::string streamName) { + _stream = stream; + _streamName = streamName; + + packer.init(_stream->sinkOut, 512); + + // TODO: Add choice? I don't think anyone cares on android... + sampleRate = 48000; + _stream->setSampleRate(sampleRate); + } + + ~AudioSink() { + } + + void start() { + if (running) { + return; + } + doStart(); + running = true; + } + + void stop() { + if (!running) { + return; + } + doStop(); + running = false; + } + + void menuHandler() { + // Draw menu here + } + +private: + void doStart() { + // Create stream builder + AAudioStreamBuilder *builder; + aaudio_result_t result = AAudio_createStreamBuilder(&builder); + + // Set stream options + bufferSize = round(sampleRate / 60.0); + AAudioStreamBuilder_setDirection(builder, AAUDIO_DIRECTION_OUTPUT); + AAudioStreamBuilder_setSharingMode(builder, AAUDIO_SHARING_MODE_SHARED); + AAudioStreamBuilder_setSampleRate(builder, sampleRate); + AAudioStreamBuilder_setChannelCount(builder, 2); + AAudioStreamBuilder_setFormat(builder, AAUDIO_FORMAT_PCM_FLOAT); + AAudioStreamBuilder_setBufferCapacityInFrames(builder, bufferSize); + AAudioStreamBuilder_setErrorCallback(builder, errorCallback, this); + packer.setSampleCount(bufferSize); + + // Open the stream + AAudioStreamBuilder_openStream(builder, &stream); + + // Stream stream and packer + packer.start(); + AAudioStream_requestStart(stream); + + // We no longer need the builder + AAudioStreamBuilder_delete(builder); + + // Start worker thread + workerThread = std::thread(&AudioSink::worker, this); + } + + void doStop() { + packer.stop(); + packer.out.stopReader(); + AAudioStream_requestStop(stream); + AAudioStream_close(stream); + if (workerThread.joinable()) { workerThread.join(); } + packer.out.clearReadStop(); + } + + void worker() { + while (true) { + int count = packer.out.read(); + if (count < 0) { return; } + AAudioStream_write(stream, packer.out.readBuf, count, 100000000); + packer.out.flush(); + } + } + + static void errorCallback(AAudioStream *stream, void *userData, aaudio_result_t error){ + // detect an audio device detached and restart the stream + if (error == AAUDIO_ERROR_DISCONNECTED){ + std::thread thr(&AudioSink::restart, (AudioSink*)userData); + thr.detach(); + } + } + + void restart() { + if (running) { doStop(); } + if (running) { doStart(); } + } + + std::thread workerThread; + + AAudioStream *stream = NULL; + SinkManager::Stream* _stream; + dsp::Packer packer; + + std::string _streamName; + double sampleRate; + int bufferSize; + + bool running = false; +}; + +class AudioSinkModule : public ModuleManager::Instance { +public: + AudioSinkModule(std::string name) { + this->name = name; + provider.create = create_sink; + provider.ctx = this; + + sigpath::sinkManager.registerSinkProvider("Audio", provider); + } + + ~AudioSinkModule() { + // Unregister sink, this will automatically stop and delete all instances of the audio sink + sigpath::sinkManager.unregisterSinkProvider("Audio"); + } + + void postInit() {} + + void enable() { + enabled = true; + } + + void disable() { + enabled = false; + } + + bool isEnabled() { + return enabled; + } + +private: + static SinkManager::Sink* create_sink(SinkManager::Stream* stream, std::string streamName, void* ctx) { + return (SinkManager::Sink*)(new AudioSink(stream, streamName)); + } + + std::string name; + bool enabled = true; + SinkManager::SinkProvider provider; +}; + +MOD_EXPORT void _INIT_() { + json def = json({}); + config.setPath(options::opts.root + "/audio_sink_config.json"); + config.load(def); + config.enableAutoSave(); +} + +MOD_EXPORT void* _CREATE_INSTANCE_(std::string name) { + AudioSinkModule* instance = new AudioSinkModule(name); + return instance; +} + +MOD_EXPORT void _DELETE_INSTANCE_(void* instance) { + delete (AudioSinkModule*)instance; +} + +MOD_EXPORT void _END_() { + config.disableAutoSave(); + config.save(); +} \ No newline at end of file