Hace varios años que trabajo en un equipo de Telefónica donde prototipamos con frecuencia con la voz, audio y sonido en general. Esto abre un gran abanico de experiencias nuevas que puedes crear o enriquecer. Pero para hacerlo, muchas veces es necesario poder manipular, transformar y experimentar con las señales que grabamos, procesamos o reproducimos. Este tratamiento avanzado de audio implica tener que partir de una señal sin compresión ni modificaciones para mantener la máxima fidelidad en los datos capturados.
En este artículo se muestra cómo hacer una captura básica de audio sin procesar en Android con Kotlin. El recorrido trata de explicar de forma simple las variables, términos y conceptos más importantes implicados en el proceso de manera general, para poder comprender en definitiva las bases.
Repositorio y estructura del proyecto
Se explicarán ejemplos en base a un repositorio adjunto que puedes encontrar al final del artículo. Los principales archivos que debes conocer son:
- AudioRecorder: Se encarga de la captura del audio.
- RecorderViewModel: Intermediario entre la lógica y la interfaz.
- RecorderScreen: Interfaz visual de la aplicación

Recuerda añadir permisos de grabación en tu AndroidManifest.xml y pedírselos al usuario o activarlos manualmente desde los ajustes de tu aplicación.
<uses-permission android:name="android.permission.RECORD_AUDIO" />Configuración y características del audio sin procesar
El ejemplo se basa en la captura de datos de audio de una fuente de voz. Veremos cómo los datos se capturan correctamente y después generaremos un archivo que los guarde. Todo ello manteniendo el formato sin procesar de la señal.
Primero es esencial configurar unos parámetros de grabación en base a nuestro caso de uso:
- Frecuencia de muestreo: (Hz) Define la cantidad de muestras tomadas por segundo. Se usará una frecuencia de 16000 Hz adecuada para la voz humana.
- Canales: Indica cuántas muestras se registran en cada instante de tiempo. Utilizaremos un solo canal (Mono), lo que implica una sola muestra.
- Formato de codificación: Formato PCM, sin compresión, con una resolución de 16 bits por muestra (Cada muestra ocupa 2 bytes en memoria).
Como has podido observar, la elección de los parámetros debe adaptarse a la fuente que queremos estudiar. Puedes ver más información al respecto en este artículo de Hugging Face.
Crearemos un objeto para facilitar la reutilización de estos parámetros en distintas partes del proyecto.
object AudioConfig {
const val SAMPLE_RATE_HZ = 16000
const val CHANNELS = AudioFormat.CHANNEL_IN_MONO
const val AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT
}Inicialización de AudioRecord para captura de audio raw
En Android, AudioRecord es la mejor opción para trabajar con audio sin procesar, en lugar de MediaRecord. Aunque existe una mayor complejidad al tener que encargarnos manualmente de controlar los parámetros y el comportamiento, permite un acceso y manipulación a bajo nivel de los datos de audio.
Además de los parámetros anteriormente descritos, para crear la instancia de AudioRecord se ha añadido la definición de la fuente de audio y un tamaño de buffer.
audioRecord = AudioRecord(
MediaRecorder.AudioSource.DEFAULT,
AudioConfig.SAMPLE_RATE_HZ,
AudioConfig.CHANNELS,
AudioConfig.AUDIO_ENCODING,
bufferSize
)Gestión del buffer de audio y latencia
El buffer, en este caso, funciona como un contenedor en el que se irán almacenando temporalmente muestras de audio para ser leídas. Estás serán sustituidas por otras muestras, y leídas de nuevo progresivamente. Como has podido comprobar necesitamos el dato del tamaño de buffer para inicializar la grabadora.
Trabajando con audio a tiempo real, un buffer pequeño, permitirá una menor latencia y procesado rápido. Pero configurar un buffer más pequeño del soportado por el dispositivo, puede derivar en problemas de pérdida de información, al no haber tiempo suficiente para procesar y leer las muestras de audio.
Podemos calcular la capacidad en bytes del buffer mínimo para la correcta creación del AudioRecorder, para cada dispositivo concreto, usando getMinBufferSize(). Debemos calcularlo antes de crear la instancia de AudioRecord, para poder configurarla correctamente.
getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)A continuación, puedes ver los valores obtenidos para la capacidad del buffer en dos dispositivos distintos.
- Píxel 6: Con una capacidad de 640 bytes, es decir 320 muestras de 2 bytes.
- Samsung S23: 1280 bytes que equivalen a 640 muestras de 2 bytes.
Esto demuestra que, para dos dispositivos distintos, con los mismos parámetros, obtenemos un valor de capacidad de buffer mínima distinta. Más adelante, al obtener muestras de audio, puedes probar a configurar un buffer mucho menor y observar cómo se empiezan a producir pérdidas de información y cortes.
Hilo de ejecución y prioridad para audio en tiempo real
Debemos tener en cuenta que la operación de escritura y lectura de datos no puede bloquear la interfaz de la aplicación, por lo que asignamos un hilo para ejecutar las operaciones, al que también dotamos de una prioridad especial, frente a operaciones que puedan ocurrir de forma simultánea.
Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO)Ciclo de vida y funcionamiento global de la grabadora
Vistos estos elementos, ahora podemos hacer un recorrido por las funciones disponibles en la grabadora:
- create(): Incluye el cálculo del buffer mínimo e inicialización del AudioRecord.
- play(): Comenzamos a grabar en el hilo reservado para ello y almacenamos y leemos en el buffer la información capturada por la grabadora
- stop(): detenemos la grabación y liberamos recursos
IsRecording.value = false
audioRecord?.stop()
audioRecord?.release()
recordingThread = null
audioRecord = null
Lectura y validación de los datos de audio
En primer lugar, haremos una rápida comprobación de que los datos llegan correctamente al buffer y se reciclan. Imprimiremos un log de los bytes presentes en el buffer en forma de string.

while (isRecording.value) {
val numberRead = audioRecord!!.read(audioBuffer, 0, audioBuffer.size)
if (numberRead > 0) {
val dataLog = audioBuffer.take(numberRead).joinToString()
Log.d(LOGTAG, "Audio data (bytes): $dataLog")
}
}
En la terminal podrás ver cómo se imprimen valores entre -128 a 127. Cada número corresponde a una muestra de audio = 2 bytes = 16 bits (Ej. 0010100010101011), como habíamos indicado en la configuración inicial.
Guardado del audio PCM en memoria interna
Ahora podemos dar un paso más y guardar los datos de audio de manera progresiva en un archivo de audio .pcm. Así comprobaremos de forma rápida, que efectivamente es el audio que estamos grabando. Una forma fácil de hacerlo es guardar este archivo en la memoria interna del dispositivo.
private var audioFile: File? = null
private var fileOutputStream: FileOutputStream? = null
Antes de iniciar la grabación definiremos el archivo en el guardaremos progresivamente los bytes de audio.
audioFile = File(context.filesDir, “recording_test.pcm”)
FileOutputStream = FileOutputStream(audioFile)
Para escribir en el archivo, usaremos la clase FileOutputStream que permite escribir de forma incremental en el archivo a medida que recibimos datos nuevos.
fileOutputStream?.write(audioBuffer, 0, numberRead)Recuerda que también debes cerrar correctamente la escritura del archivo una vez finalizada la grabación.
fileOutputStream?.close()
fileOutputStream = null
Puedes encontrar el archivo guardado en la memoria interna del dispositivo, en Android Studio desde la vista de Device Explorer.

Los archivos .pcm contienen audio sin tratar y por tanto no podrás leerlos con cualquier reproductor. Para ello puedes emplear un programa de edición avanzada de audio como Audacity o un conversor a formatos como .mp3 o .wav.
Ahora que ya conoces las bases puedes empezar a explorar infinidad de aplicaciones con análisis o transformación de audio en crudo. Marcará la diferencia para que puedas ser un verdadero artesano, que manipule, transforme y adapte el sonido para crear experiencias únicas. Consulta el repositorio adjunto para ver el código referenciado en los ejemplos.







