Grabación de GoPro controlada por voz con audio externo en Android
Introducción
Recientemente empecé a grabar videos en moto con mi GoPro Hero 12. Me encontré con un problema: al parecer, para grabar con un micrófono con cable en GoPro es necesario comprar accesorios adicionales, y para grabar con un micrófono Bluetooth es necesario mantener la cámara encendida todo el tiempo, ya que no se puede iniciar la grabación desde el modo standby usando un micrófono inalámbrico.
Para solucionar este problema sin tener que gastar más dinero en accesorios para la cámara y poder grabar desde standby, decidí usar mis conocimientos de programación. Por ello, en este post detallaré el proceso que seguí para llegar a una solución.
Propuesta
Mi idea es grabar el audio del video en el teléfono y el video directamente desde la GoPro. Para lograr esto, necesito reconocer un comando de voz que permita al teléfono encender la GoPro e iniciar la grabación, además de comenzar a guardar el audio en el mismo instante.

Requerimientos
- Dispositivo Android 14
- Cámara GoPro Hero 12 (probablemente funcione con casi cualquier versión)
- Micrófono DJI Mic Mini (podría funcionar cualquier micrófono inalámbrico)
- Android Studio
Desarrollo
En esta sección hablaré de forma superficial y mencionaré únicamente los puntos más importantes del proyecto. En la sección de adjuntos se encuentra el enlace al repositorio.
Desarrollé el proyecto en Android Studio Narwhal, usando Kotlin con el SDK 36, ya que el ejemplo de BLE que GoPro proporciona está escrito en Kotlin. Las librerías utilizadas fueron:
implementation(libs.androidx.core.ktx)
implementation("com.juul.kable:core:0.28.0")
implementation ("com.alphacephei:vosk-android:0.3.47@aar")
implementation (project(":models"))
implementation(libs.androidx.appcompat)
implementation("net.java.dev.jna:jna:5.13.0@aar")
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidthings)
testImplementation(libs.junit)
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
implementation("io.ktor:ktor-client-core:2.2.3")
implementation("io.ktor:ktor-client-cio:2.2.3")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
implementation ("org.jetbrains.kotlin:kotlin-reflect:1.7.20")
implementation("io.coil-kt:coil-compose:2.2.2")
Debo destacar la dependencia implementation(project(":models")). Esta corresponde a un módulo interno del proyecto que almacena el modelo de IA utilizado para el reconocimiento de voz. Para esta tarea utilicé Vosk mediante la dependencia com.alphacephei:vosk-android.
La idea general del proyecto es la siguiente:

Al abrir la aplicación, se despliega un botón que inicia un servicio en segundo plano encargado de todo el proceso. Este servicio inicializa Vosk, comienza a capturar audio y se conecta a la cámara GoPro. Cuando el teléfono detecta el comando de voz "start", inicia la secuencia para detectar la cámara, encenderla y comenzar a grabar tanto el audio como el video. Al detectar el comando "stop", se detiene la grabación, se apaga la cámara y se guarda el archivo de audio en el teléfono. En esencia, es una aplicación bastante sencilla.
MainActivity.kt es el punto de entrada de la aplicación. El método onStartServiceClick se ejecuta al presionar el botón Start Service, y se encarga de iniciar o detener el servicio.
SyncService.kt es el servicio principal y donde ocurre toda la lógica. Aquí se inicializa el modelo, se establece la conexión con la cámara y se gestiona la grabación del audio. Para facilitar la lectura del código, separé la lógica en GoProRecorder.kt y MicRecorder.kt, los cuales encapsulan las tareas específicas de cada componente.
Cuando MicRecorder.kt detecta un comando de voz, llama a startCallback o stopCallback en SyncService.kt, según corresponda, para iniciar o detener la secuencia asociada a cada comando.
MicRecorder.kt da la lógica relacionada con el micrófono. Esta clase crea tres threads:
-
startListeningThread: se encarga de leer el audio desde el micrófono y almacenarlo en una cola compartida. -
startRecognitionThread: procesa el audio para reconocer los comandos de voz y ejecutar los callbacks. -
startRecording: se inicia al comenzar la grabación y guarda el audio en un archivo dentro del teléfono.
Los threads están separados porque el proceso de reconocimiento de voz es relativamente pesado y podría introducir latencias. De esta forma, un thread se dedica únicamente a recolectar el audio, mientras que los demás lo procesan de manera independiente.
GoProRecorder.kt encapsula la lógica relacionada con la cámara GoPro. En este punto también es importante mencionar el archivo Ble.kt, el cual fue extraído íntegramente del ejemplo de BLE proporcionado por GoPro, ya que facilita considerablemente la interacción con la cámara. Entre las funciones implementadas se encuentran: conectar, desconectar, iniciar grabación, detener grabación y detección de la cámara. Todas estas funciones son invocadas desde SyncService.kt.
Después de este recorrido general, ya deberías tener una idea clara de cómo funciona la aplicación. El código no es ideal, ya que se trata de un proyecto de uso personal. Además, no tengo mucha experiencia en Kotlin ni en el desarrollo móvil en general.
Conclusiones
Con este proyecto se logró desarrollar una aplicación sencilla en Kotlin que permite grabar audio y video de manera simultánea mediante comandos de voz. Además, después de implementar y probar esta solución, identifiqué un posible enfoque alternativo que vale la pena mencionar.
Este enfoque evitaría el uso de IA para el reconocimiento de voz, ya que noté que, debido al ruido en carretera (viento, motor, etc.), a veces es necesario repetir el comando varias veces antes de que sea reconocido. La idea sería que el teléfono detecte cuándo la cámara sale del modo standby y cuándo inicia o detiene la grabación, para así comenzar o finalizar la grabación del audio automáticamente.
De esta manera se eliminaría el problema del reconocimiento de comandos de voz. Sin embargo, sería necesario investigar más a fondo si este enfoque es realmente factible. Durante algunas pruebas rápidas realizadas mientras desarrollaba el proyecto, tuve la impresión de que esta solución podría no ser posible, pero fueron pruebas muy preliminares y nada concluyente. En caso de desarrollarse, esta alternativa se incluiría en el mismo repositorio del proyecto.