AAudio

AAudio es una API de Android C que se introdujo en la versión Android O. Está diseñada para aplicaciones de audio de alto rendimiento que requieren baja latencia. Las apps se comunican con AAudio mediante la lectura y escritura de datos en transmisiones.

La API de AAudio tiene un diseño mínimo, por lo que no realiza las siguientes funciones:

  • Enumeración de dispositivos de audio
  • Enrutamiento automatizado entre extremos de audio
  • E/S de archivos
  • Decodificación de audio comprimido
  • Presentación automática de todas las entradas/transmisiones en una sola devolución de llamada.

Introducción

Puedes llamar a AAudio desde código C++. Para agregar el conjunto de atributos de AAudio a tu app, incluye el archivo de encabezado de AAudio.h:

#include <aaudio/AAudio.h>

Reproducciones de audio

AAudio transfiere los datos de audio entre tu app y las entradas y salidas de audio en el dispositivo Android. Tu app recibe y envía datos leyendo transmisiones de audio y escribiendo en ellas, y estas se representan con la estructura AAudioStream. Estas llamadas de lectura/escritura pueden incluir bloqueo o no.

Una transmisión se define conforme a lo siguiente:

  • El dispositivo de audio, que es la fuente o el receptor de los datos de la transmisión
  • El modo de uso compartido, que determina si una transmisión tiene acceso exclusivo a un dispositivo de audio que, de lo contrario, podría compartirse entre varias transmisiones
  • El formato de los datos de audio en la transmisión

Dispositivo de audio

Cada transmisión se encuentra vinculada a un único dispositivo de audio.

Un dispositivo de audio es una interfaz de hardware o un extremo virtual que actúa como fuente o receptor de una transmisión continua de datos de audio digital. No confundas un dispositivo de audio (un micrófono integrado o auriculares Bluetooth) con el dispositivo Android (el teléfono o reloj) que ejecuta tu app.

Puedes utilizar el método getDevices() de AudioManager para descubrir los dispositivos de audio disponibles en tu dispositivo Android. El método muestra información sobre el type de cada dispositivo.

Cada dispositivo de audio tiene un ID único en el dispositivo Android. Puedes utilizar el ID para vincular una transmisión de audio a un dispositivo de audio específico. Sin embargo, puedes permitir que AAudio elija el dispositivo principal predeterminado en la mayoría de los casos, en lugar de especificarlo tú.

El dispositivo de audio vinculado a una transmisión determina si la transmisión es de entrada o salida. Una transmisión puede transferir datos en una sola dirección. Cuando defines una transmisión, también estableces su dirección. Cuando abres una transmisión, Android comprueba que la dirección del dispositivo de audio y de la transmisión concuerden.

Modo de uso compartido

Una transmisión tiene un modo de uso compartido:

  • AAUDIO_SHARING_MODE_EXCLUSIVE significa que la transmisión tiene acceso exclusivo al dispositivo de audio; ninguna otra transmisión de audio puede utilizar el dispositivo. Si el dispositivo de audio ya está en uso, quizás no sea posible que la transmisión tenga acceso exclusivo. Es probable que las transmisiones exclusivas tengan una menor latencia, pero también es más probable que se desconecten. Debes cerrar las transmisiones exclusivas tan pronto dejes de necesitarlas, de modo que otras apps puedan acceder al dispositivo. Las transmisiones exclusivas proporcionan la menor latencia posible.
  • AAUDIO_SHARING_MODE_SHARED permite que AAudio combine audio. AAudio combina todas las transmisiones compartidas asignadas al mismo dispositivo.

Puedes configurar el modo de uso compartido de forma explícita cuando creas una transmisión. De forma predeterminada, el modo de uso compartido es SHARED.

Formato de audio

Los datos que se transfieren por medio de una transmisión tienen los atributos de audio digital habituales. Son los siguientes:

  • Formato de datos de muestra
  • Recuento de canales (muestras por fotograma)
  • Tasa de muestreo

AAudio permite los siguientes formatos de muestra:

aaudio_format_t Tipo de datos C Notas
AAUDIO_FORMAT_PCM_I16 int16_t muestras comunes de 16 bits, formato Q0.15
AAUDIO_FORMAT_PCM_FLOAT float -1.0 a +1.0
AAUDIO_FORMAT_PCM_I24_EMPAQUETADO uint8_t en grupos de 3 muestras empaquetadas de 24 bits, formato Q0.23
AAUDIO_FORMAT_PCM_I32 int32_t muestras comunes de 32 bits, formato Q0.31
AAUDIO_FORMAT_IEC61937 Uint8_t audio comprimido unido a IEC61937 para transferencia HDMI o S/PDIF

Si solicitas un formato de muestra específico, la transmisión usará ese formato, incluso si no es óptimo para el dispositivo. Si no especificas un formato de muestra, AAudio elegirá uno óptimo. Después de abrir la transmisión, debes consultar el formato de datos de muestra y, luego, convertir los datos si es necesario, como en este ejemplo:

aaudio_format_t dataFormat = AAudioStream_getDataFormat(stream);
//... later
if (dataFormat == AAUDIO_FORMAT_PCM_I16) {
     convertFloatToPcm16(...)
}

Cómo crear una transmisión de audio

La biblioteca de AAudio respeta un patrón de diseño compilador y proporciona AAudioStreamBuilder.

  1. Crea un objeto AAudioStreamBuilder:

    AAudioStreamBuilder *builder;
    aaudio_result_t result = AAudio_createStreamBuilder(&builder);
    

  2. Establece la configuración de la transmisión de audio en el compilador mediante las funciones del compilador que corresponden a los parámetros de la transmisión. Se encuentran disponibles las siguientes funciones opcionales de configuración:

    AAudioStreamBuilder_setDeviceId(builder, deviceId);
    AAudioStreamBuilder_setDirection(builder, direction);
    AAudioStreamBuilder_setSharingMode(builder, mode);
    AAudioStreamBuilder_setSampleRate(builder, sampleRate);
    AAudioStreamBuilder_setChannelCount(builder, channelCount);
    AAudioStreamBuilder_setFormat(builder, format);
    AAudioStreamBuilder_setBufferCapacityInFrames(builder, frames);
    

    Ten en cuenta que estos métodos no informan errores, como una constante sin definir o un valor fuera de rango.

    Si no especificas el deviceId, el valor predeterminado será el dispositivo de salida principal. Si no especificas la dirección de transmisión, el valor predeterminado será una transmisión de salida. En el caso de todos los demás parámetros, puedes configurar explícitamente un valor o dejar que el sistema asigne el valor óptimo si no especificas ningún parámetro o lo configuras como AAUDIO_UNSPECIFIED.

    Para estar seguro, comprueba el estado de la transmisión de audio luego de crearla, como se explica en el paso 4 a continuación.

  3. Cuando AAudioStreamBuilder esté configurado, úsalo para crear una transmisión:

    AAudioStream *stream;
    result = AAudioStreamBuilder_openStream(builder, &stream);
    

  4. Luego de crear la transmisión, verifica la configuración. Si especificaste el formato de la muestra, la tasa de muestreo o las muestras por trama, no se modificarán. Si especificaste el modo de uso compartido o la capacidad de búfer, es posible que cambien según las capacidades del dispositivo de audio de la transmisión y del dispositivo Android en el que se ejecuta. Por una cuestión de buena programación defensiva, debes comprobar la configuración de la transmisión antes de usarla. Existen funciones para recuperar el parámetro de configuración de la transmisión que corresponde a cada parámetro del compilador:

    AAudioStreamBuilder_setDeviceId() AAudioStream_getDeviceId()
    AAudioStreamBuilder_setDirection() AAudioStream_getDirection()
    AAudioStreamBuilder_setSharingMode() AAudioStream_getSharingMode()
    AAudioStreamBuilder_setSampleRate() AAudioStream_getSampleRate()
    AAudioStreamBuilder_setChannelCount() AAudioStream_getChannelCount()
    AAudioStreamBuilder_setFormat() AAudioStream_getFormat()
    AAudioStreamBuilder_setBufferCapacityInFrames() AAudioStream_getBufferCapacityInFrames()

  5. Puedes guardar el compilador y volver a utilizarlo en el futuro para crear más transmisiones. Pero si no tienes planes de volver a utilizarlo, deberías borrarlo.

    AAudioStreamBuilder_delete(builder);
    

Cómo usar una transmisión de audio

Transiciones de estado

Por lo general, una transmisión de AAudio se encuentra en uno de cinco estados estables (el estado de error, Disconnected, se describe al final de esta sección):

  • Abrir
  • Iniciado
  • Pausada
  • Cara muy sonrojada
  • Detenido

Los datos solo fluyen a través de una transmisión cuando esta se encuentra en estado Started. Para cambiar el estado de una transmisión, usa una de las funciones que solicitan una transición de estado:

aaudio_result_t result;
result = AAudioStream_requestStart(stream);
result = AAudioStream_requestStop(stream);
result = AAudioStream_requestPause(stream);
result = AAudioStream_requestFlush(stream);

Ten en cuenta que solo puedes solicitar una pausa o un vaciamiento en una transmisión de salida:

Estas funciones son asíncronas, y el cambio de estado no se produce de inmediato. Cuando solicitas un cambio de estado, la transmisión transfiere uno de los estados transitorios correspondientes:

  • Inicio
  • Pausando
  • Rubor
  • Por detenerse
  • Closing

El diagrama de estados a continuación muestra los estados estables como rectángulos redondeados y los estados transitorios como rectángulos en línea de puntos. Si bien no se muestra, puedes llamar a close() desde cualquier estado.

Ciclo de vida de AAudio

AAudio no proporciona devoluciones de llamada para alertarte sobre los cambios de estado. Se puede usar una función especial, AAudioStream_waitForStateChange(stream, inputState, nextState, timeout), para esperar un cambio de estado.

La función no detecta un cambio de estado por su cuenta ni espera un estado específico. Espera hasta que el estado actual sea diferente de inputState, que debes especificar.

Por ejemplo, después de solicitar una pausa, una transmisión debe ingresar inmediatamente en el estado transitorio Pause y llegar más tarde al estado Pausado, aunque no hay garantía de que lo hará. Como no puedes esperar el estado Pausado, usa waitForStateChange() para esperar cualquier estado que no sea Pausado. Aquí te mostramos cómo hacerlo:

aaudio_stream_state_t inputState = AAUDIO_STREAM_STATE_PAUSING;
aaudio_stream_state_t nextState = AAUDIO_STREAM_STATE_UNINITIALIZED;
int64_t timeoutNanos = 100 * AAUDIO_NANOS_PER_MILLISECOND;
result = AAudioStream_requestPause(stream);
result = AAudioStream_waitForStateChange(stream, inputState, &nextState, timeoutNanos);

Si el estado de la transmisión no es coloridos (el inputState, que asumimos que era el estado actual en el momento de la llamada), la función muestra un resultado de inmediato. De lo contrario, se bloquea hasta que el estado ya no sea Pause o se agote el tiempo de espera. Cuando la función muestra un resultado, el parámetro nextState muestra el estado actual de la transmisión.

Puedes usar esta misma técnica después de llamar a la operación de inicio, detención o vaciamiento de una solicitud, con el estado transitorio correspondiente inputState. No llames a waitForStateChange() después de llamar a AAudioStream_close(), ya que se borrará la transmisión en cuanto se cierre. No llames a AAudioStream_close() mientras waitForStateChange() se ejecuta en otro subproceso.

Cómo leer una transmisión de audio y escribir en ella

Hay dos maneras de procesar los datos en una transmisión una vez que se inicia:

Para un bloqueo de lectura o escritura que transfiera la cantidad de tramas especificada, configura timeoutNanos con un valor superior a cero. Para una llamada sin bloqueo, configura timeoutNanos en cero. En este caso, el resultado es el número real de tramas transferidas.

Cuando lees datos de entrada, debes verificar que se haya leído el número correcto de marcos. De lo contrario, el búfer puede contener datos desconocidos que podrían causar una falla de audio. Puedes rellenar el búfer con ceros para crear un abandono silencioso:

aaudio_result_t result =
    AAudioStream_read(stream, audioData, numFrames, timeout);
if (result < 0) {
  // Error!
}
if (result != numFrames) {
  // pad the buffer with zeros
  memset(static_cast<sample_type*>(audioData) + result * samplesPerFrame, 0,
      sizeof(sample_type) * (numFrames - result) * samplesPerFrame);
}

Puedes preparar el búfer de la transmisión antes de comenzar con la transmisión escribiendo datos o silencios en ella. Esto debe realizarse en una llamada sin bloqueo con timeoutNanos configurado en cero.

Los datos del búfer deben coincidir con el formato de datos que muestra AAudioStream_getDataFormat().

Cómo cerrar una transmisión de audio

Cuando hayas terminado de usar una transmisión, ciérrala:

AAudioStream_close(stream);

Después de cerrar una transmisión, no podrás usarla con ninguna función basada en transmisión de AAudio.

Transmisión de audio desconectada

Una transmisión de audio puede desconectarse en cualquier momento si ocurre alguno de los eventos siguientes:

  • El dispositivo de audio asociado ya no está conectado (por ejemplo, cuando se desconectan los auriculares).
  • Se produce un error interno.
  • Un dispositivo de audio ya no es el dispositivo de audio principal.

Cuando se desconecta una transmisión, su estado es "Desconectado" y cualquier intento por ejecutar AAudioStream_write() o alguna otra función mostrará un error. Siempre debes detener y cerrar una transmisión desconectada, independientemente del código de error.

Si usas una devolución de llamada de datos (en lugar de uno de los métodos directos de lectura o escritura), no recibirás ningún código de retorno cuando se desconecte la transmisión. Para que se te notifique cuando esto suceda, escribe una función AAudioStream_errorCallback y regístrala con AAudioStreamBuilder_setErrorCallback().

Si recibes una notificación sobre la desconexión en un subproceso de devolución de llamada de error, la detención y el cierre de la transmisión deben realizarse desde otro subproceso. De lo contrario, podrías generar un interbloqueo.

Ten en cuenta que, si abres una transmisión nueva, puede tener una configuración diferente a la de la original (por ejemplo, marcosPerBurst):

void errorCallback(AAudioStream *stream,
                   void *userData,
                   aaudio_result_t error) {
    // Launch a new thread to handle the disconnect.
    std::thread myThread(my_error_thread_proc, stream, userData);
    myThread.detach(); // Don't wait for the thread to finish.
}

Optimización del rendimiento

Si deseas optimizar el rendimiento de una aplicación de audio, puedes ajustar los búferes internos y utilizar subprocesos de prioridad alta especiales.

Cómo ajustar los búferes para minimizar la latencia

AAudio envía y recibe datos en los búferes internos que mantiene, uno para cada dispositivo de audio.

La capacidad del búfer es la cantidad total de datos que puede retener. Puedes llamar a AAudioStreamBuilder_setBufferCapacityInFrames() para configurar la capacidad. El método limita la capacidad que puedes asignar al valor máximo que permite el dispositivo. Usa AAudioStream_getBufferCapacityInFrames() para verificar la capacidad real del búfer.

Una app no debe utilizar la capacidad total de un búfer. AAudio llenará un búfer hasta un tamaño que puedes configurar. El tamaño de un búfer no puede superar su capacidad y, por lo general, es más pequeño. Con el control del tamaño del búfer, determinas la cantidad necesaria de picos de actividad para llenarlo y, por lo tanto, controlas la latencia. Usa los métodos AAudioStreamBuilder_setBufferSizeInFrames() y AAudioStreamBuilder_getBufferSizeInFrames() para trabajar con el tamaño del búfer.

Cuando una aplicación reproduce audio, escribe en un búfer y lo bloquea hasta completar la escritura. AAudio lee del búfer en picos de actividad discretos. Cada pico de actividad contiene varias tramas de audio y, por lo general, su tamaño es inferior al del búfer que se lee. El sistema controla el tamaño y la velocidad del pico de actividad, y el circuito del dispositivo de audio suele determinar estas propiedades. Si bien no puedes cambiar el tamaño ni la frecuencia de los aumentos de actividad, puedes configurar el tamaño del búfer interno según la cantidad de picos de actividad que contenga. Por lo general, se obtiene la latencia más baja si el tamaño del búfer de AAudioStream es múltiplo del tamaño del aumento de actividad informado.

      Almacenamiento en búfer de AAudio

Una manera de optimizar el tamaño del búfer es comenzar con un búfer grande y reducirlo gradualmente hasta llegar al agotamiento y luego volver a incrementarlo para corregirlo. Como alternativa, puedes comenzar con un tamaño de búfer pequeño y, si se agota, aumentar el tamaño del búfer hasta que los datos de salida fluyan sin problema nuevamente.

Este proceso puede ser muy rápido, incluso puede completarse antes de que el usuario reproduzca el primer sonido. Es posible que desees realizar primero el tamaño del búfer inicial, con silencio, de modo que el usuario no escuche fallas de audio. Es posible que el rendimiento del sistema cambie con el tiempo (por ejemplo, el usuario puede desactivar el modo de avión). Dado que el ajuste de búfer suma muy poca sobrecarga, tu app puede hacerlo continuamente mientras lee o escribe datos en una transmisión.

A continuación, te mostramos un ejemplo de un bucle de optimización del búfer:

int32_t previousUnderrunCount = 0;
int32_t framesPerBurst = AAudioStream_getFramesPerBurst(stream);
int32_t bufferSize = AAudioStream_getBufferSizeInFrames(stream);

int32_t bufferCapacity = AAudioStream_getBufferCapacityInFrames(stream);

while (go) {
    result = writeSomeData();
    if (result < 0) break;

    // Are we getting underruns?
    if (bufferSize < bufferCapacity) {
        int32_t underrunCount = AAudioStream_getXRunCount(stream);
        if (underrunCount > previousUnderrunCount) {
            previousUnderrunCount = underrunCount;
            // Try increasing the buffer size by one burst
            bufferSize += framesPerBurst;
            bufferSize = AAudioStream_setBufferSize(stream, bufferSize);
        }
    }
}

Utilizar esta técnica para optimizar el tamaño del búfer para una transmisión de entrada no conlleva ninguna ventaja. Las transmisiones de entrada se ejecutan lo más rápido posible y tratan de minimizar la cantidad de datos almacenados en el búfer y, luego, se completan cuando se interrumpe la app.

Cómo usar una devolución de llamada de alta prioridad

Si tu app lee o escribe datos de audio de un subproceso ordinario, es posible que se la evite o experimente fluctuación en el tiempo. Esto puede causar fallas de audio. Los búferes más grandes pueden servir como protección contra esas fallas, pero también traen aparejada una mayor latencia de audio. En el caso de las aplicaciones que requieren baja latencia, una transmisión de audio puede usar una función de devolución de llamada asíncrona para transferir datos hacia y desde tu app. AAudio ejecuta la devolución de llamada en un subproceso de mayor prioridad que tiene mejor rendimiento.

La función de devolución de llamada tiene el siguiente prototipo:

typedef aaudio_data_callback_result_t (*AAudioStream_dataCallback)(
        AAudioStream *stream,
        void *userData,
        void *audioData,
        int32_t numFrames);

Usa la generación de transmisiones para registrar la devolución de llamada:

AAudioStreamBuilder_setDataCallback(builder, myCallback, myUserData);

En el caso más simple, la transmisión ejecuta periódicamente la función de devolución de llamada para adquirir los datos del próximo aumento de actividad.

La función de devolución de llamada no debe realizar tareas de lectura ni escritura en la transmisión que la invocó. Si la devolución de llamada pertenece a una transmisión de entrada, el código debe procesar los datos que se suministran en el búfer audioData (especificado como el tercer argumento). Si la devolución de llamada pertenece a una transmisión de salida, el código debe colocar los datos en el búfer.

Por ejemplo, podrías usar una devolución de llamada para generar continuamente una salida de onda sinusoidal como la siguiente:

aaudio_data_callback_result_t myCallback(
        AAudioStream *stream,
        void *userData,
        void *audioData,
        int32_t numFrames) {
    int64_t timeout = 0;

    // Write samples directly into the audioData array.
    generateSineWave(static_cast<float *>(audioData), numFrames);
    return AAUDIO_CALLABCK_RESULT_CONTINUE;
}

Es posible procesar más de una transmisión con AAudio. Puedes usar una transmisión como principal y pasar punteros a otras transmisiones en los datos del usuario. Registra una devolución de llamada para la transmisión principal. Luego, utiliza E/S sin bloqueo en las demás transmisiones. A continuación, te mostramos un ejemplo de una devolución de llamada de ida y vuelta que transfiere una transmisión de entrada a una de salida. La transmisión de llamada principal es la de salida. La de entrada está incluida en los datos del usuario.

La devolución de llamada realiza una lectura sin bloqueo de la transmisión de entrada colocando los datos en el búfer de la transmisión de salida:

aaudio_data_callback_result_t myCallback(
        AAudioStream *stream,
        void *userData,
        void *audioData,
        int32_t numFrames) {
    AAudioStream *inputStream = (AAudioStream *) userData;
    int64_t timeout = 0;
    aaudio_result_t result =
        AAudioStream_read(inputStream, audioData, numFrames, timeout);

  if (result == numFrames)
      return AAUDIO_CALLABCK_RESULT_CONTINUE;
  if (result >= 0) {
      memset(static_cast<sample_type*>(audioData) + result * samplesPerFrame, 0,
          sizeof(sample_type) * (numFrames - result) * samplesPerFrame);
      return AAUDIO_CALLBACK_RESULT_CONTINUE;
  }
  return AAUDIO_CALLBACK_RESULT_STOP;
}

Ten en cuenta que, en este ejemplo, se asume que las transmisiones de entrada y salida poseen igual cantidad de canales, formato y tasa de muestreo. El formato de las transmisiones puede no coincidir; siempre y cuando el código realice las traducciones correctamente.

Cómo configurar el modo de rendimiento

Cada AAudioStream tiene un modo de rendimiento que tiene un gran efecto en el comportamiento de tu app. Existen tres modos:

  • AAUDIO_PERFORMANCE_MODE_NONE es el modo predeterminado. Emite una transmisión básica que equilibra el ahorro de latencia y energía.
  • AAUDIO_PERFORMANCE_MODE_LOW_LATENCY usa búferes más pequeños y una ruta de acceso de datos optimizada para reducir la latencia.
  • AAUDIO_PERFORMANCE_MODE_POWER_SAVING utiliza búferes internos más grandes y una ruta de acceso de datos que intercambia latencia por menor energía.

Puedes llamar a setPerformanceMode() para seleccionar el modo de rendimiento y llamar a getPerformanceMode() a fin de descubrir el modo actual.

Si la latencia baja tiene prioridad por sobre el ahorro de energía en tu aplicación, utiliza AAUDIO_PERFORMANCE_MODE_LOW_LATENCY. Este modo es útil para las apps muy interactivas, como juegos o sintetizadores con teclado.

Si el ahorro de energía tiene prioridad por sobre la latencia baja en tu aplicación, usa AAUDIO_PERFORMANCE_MODE_POWER_SAVING. Este modo es el típico para apps que reproducen música ya generada, como es el caso de la transmisión de audio o los reproductores de archivos MIDI.

En la versión actual de AAudio, para lograr la latencia más baja posible, debes usar el modo de rendimiento AAUDIO_PERFORMANCE_MODE_LOW_LATENCY junto con una devolución de llamada de alta prioridad. Sigue este ejemplo:

// Create a stream builder
AAudioStreamBuilder *streamBuilder;
AAudio_createStreamBuilder(&streamBuilder);
AAudioStreamBuilder_setDataCallback(streamBuilder, dataCallback, nullptr);
AAudioStreamBuilder_setPerformanceMode(streamBuilder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);

// Use it to create the stream
AAudioStream *stream;
AAudioStreamBuilder_openStream(streamBuilder, &stream);

Seguridad del subproceso

La API de AAudio no es completamente segura para el subproceso. No puedes llamar a algunas funciones de AAudio conjuntamente desde más de un subproceso a la vez. Esto se debe a que AAudio evita utilizar exclusiones mutuas, que pueden provocar fallas en los subprocesos y evitarlos.

Como medida de seguridad, no llames a AAudioStream_waitForStateChange() ni realices tareas de lectura o escritura en la misma transmisión desde dos subprocesos diferentes. Del mismo modo, no cierres una transmisión en un subproceso mientras lees o escribes en otro subproceso.

Las llamadas que devuelven configuraciones de transmisión, como AAudioStream_getSampleRate() y AAudioStream_getChannelCount(), son seguras para el subproceso.

Estas llamadas también son seguras para el subproceso:

  • AAudio_convert*ToText()
  • AAudio_createStreamBuilder()
  • AAudioStream_get*(), excepto por AAudioStream_getTimestamp()

Errores conocidos

  • La latencia de audio es alta para bloquear la función write() debido a que la versión de Android O DP2 no utiliza una pista RÁPIDA. Utiliza una devolución de llamada para obtener menor latencia.

Recursos adicionales

Para obtener más información, utiliza los siguientes recursos:

Referencia de las APIs

Codelabs

Videos