Beep, Beep, soy una oveja

En un nuevo artículo traducido, discutimos cómo crear un beeper en diferentes plataformas.


La E / S de audio es un tema complicado que asusta a muchos músicos que son programadores y programadores que están interesados ​​en la música. ¡Intentemos resolver esto! En este artículo, analizaremos cómo funciona el sonido en cada sistema operativo moderno (versión de escritorio).



Nuestro caso de hoy se considerará usando un simple beeper como ejemplo. ¿Recuerda esa cosa molesta dentro de la caja de su PC que hace un zumbido desagradable? ¡Ahora se ha convertido en solo un recuerdo! Sugiero crear una biblioteca que reproduzca sonidos similares en todos los sistemas operativos.



El resultado final está disponible en este enlace .



VENTANAS



Estamos de suerte con Windows: ya existe una función Beep (frecuencia, duración) en <utilapiset.h> . Podemos usarlo.



Esta característica tiene una historia muy larga y compleja . Se introdujo para reproducir señales de audio a través de un zumbador de hardware utilizando el temporizador programable 8245. A medida que se lanzaban más y más computadoras sin zumbador, esta función se volvió obsoleta con el tiempo. Sin embargo, en Windows 7 se reescribió para reproducir señales de audio utilizando la API de la tarjeta de sonido.



Sin embargo, la aparente simplicidad de esta función oculta la complejidad de todas las API de sonido de Windows. MME fue lanzado en 1991 ... Se usa por defecto para audio ya que tiene un buen soporte.



Se sabe que MME tiene una alta latencia de reproducción y probablemente no sea adecuado para la mayoría de las aplicaciones de audio. También en 2007, se lanzó WASAPI . Tiene una latencia más baja, especialmente cuando se usa en modo exclusivo (un modo en el que el usuario no puede escuchar Spotify o cualquier otra aplicación mientras su aplicación se está ejecutando). WASAPI es una buena opción para aplicaciones de audio, sin embargo, busque DirectSound , que es un contenedor WASAPI para interactuar con DirectX.



Si no está seguro, utilice WASAPI.



LINUX



El audio es una de las pocas áreas donde la API de Linux es tan buena como otras plataformas. En primer lugar, hay que decirlo de ALSA, que forma parte del propio core.



ALSA se comunica directamente con el hardware, y si desea que su aplicación funcione exclusivamente con sonido, ALSA puede ser un buen compromiso entre complejidad y rendimiento. Si está construyendo un sintetizador o muestreador para Raspberry Pi, ALSA es una buena opción.



Además, está PulseAudio, una capa de abstracción de audio construida sobre ALSA. Enruta el audio de varias aplicaciones e intenta mezclar secuencias de audio para que las aplicaciones críticas no sufran problemas de latencia. Aunque PulseAudio ofrece muchas funciones que no serían posibles con ALSA (como enrutar audio a través de Internet), la mayoría de las aplicaciones de música no lo utilizan.



Mucha gente usa el kit de conexión de audio JACK... JACK fue creado para músicos profesionales. Se encarga de la reproducción en tiempo real, mientras que PulseAudio se creó para usuarios ocasionales que pueden experimentar cierto retraso al reproducir videos de YouTube. JACK conecta aplicaciones de audio con una latencia mínima, pero tenga en cuenta que todavía se ejecuta sobre ALSA, por lo que si su aplicación va a ser la única aplicación de audio en ejecución (por ejemplo, si está construyendo una caja de ritmos a partir de una vieja Raspberry Pi), entonces ALSA es mucho más fácil de usar y mejor rendimiento también.



Hacer que una señal acústica funcione con ALSA en realidad no es tan difícil. Necesitamos abrir el dispositivo de audio predeterminado, configurarlo para que use una frecuencia de muestreo y un formato de muestra compatibles, y comenzar a escribir datos en él. Los datos de audio pueden ser una onda de diente de sierra, como se describe en mi artículo anterior .



int beep(int freq, int ms) {
  static void *pcm = NULL;
  if (pcm == NULL) {
    if (snd_pcm_open(&pcm, "default", 0, 0)) {
      return -1;
    }
    snd_pcm_set_params(pcm, 1, 3, 1, 8000, 1, 20000);
  }
  unsigned char buf[2400];
  long frames;
  long phase;
  for (int i = 0; i < ms / 50; i++) {
    snd_pcm_prepare(pcm);
    for (int j = 0; j < sizeof(buf); j++) {
      buf[j] = freq > 0 ? (255 * j * freq / 8000) : 0;
    }
    int r = snd_pcm_writei(pcm, buf, sizeof(buf));
    if (r < 0) {
      snd_pcm_recover(pcm, r, 0);
    }
  }
  return 0;
}
      
      





Aquí usamos una API síncrona y no buscamos errores para mantener la función breve y simple. El bloqueo de E / S sincrónico probablemente no sea la mejor opción para aplicaciones de audio serias y, afortunadamente, ALSA viene con diferentes métodos de transferencia y modos de operación: enlace . Pero para nuestro simple experimento, esto es suficiente. En caso de duda, utilice ALSA. Si tiene que interactuar con otras aplicaciones de audio, use JACK.



MAC OS



En el caso de MacOS, todo es bastante simple, pero no del todo elemental.



MacOS tiene un marco CoreAudio para funciones de audio en el escritorio e iOS. CoreAudio en sí es una API de bajo nivel estrechamente integrada con el sistema operativo para optimizar la latencia y el rendimiento. Para reproducir audio usando CoreAudio, necesita crear una AudioUnit (complemento de audio). La API AudioUnit es un poco larga, pero fácil de entender. A continuación, se explica cómo crear una nueva AudioUnit:



AudioComponent output;
AudioUnit unit;
AudioComponentDescription descr;
AURenderCallbackStruct cb;
AudioStreamBasicDescription stream;

descr.componentType = kAudioUnitType_Output,
descr.componentSubType = kAudioUnitSubType_DefaultOutput,
descr.componentManufacturer = kAudioUnitManufacturer_Apple,

// Actual sound will be generated asynchronously in the callback tone_cb
cb.inputProc = tone_cb;

stream.mFormatID = kAudioFormatLinearPCM;
stream.mFormatFlags = 0;
stream.mSampleRate = 8000;
stream.mBitsPerChannel = 8;
stream.mChannelsPerFrame = 1;
stream.mFramesPerPacket = 1;
stream.mBytesPerFrame = 1;
stream.mBytesPerPacket = 1;

output = AudioComponentFindNext(NULL, &descr);
AudioComponentInstanceNew(output, &unit);
AudioUnitSetProperty(unit, kAudioUnitProperty_SetRenderCallback,
										 kAudioUnitScope_Input, 0, &cb, sizeof(cb));
AudioUnitSetProperty(unit, kAudioUnitProperty_StreamFormat,
										 kAudioUnitScope_Input, 0, &stream, sizeof(stream));
AudioUnitInitialize(unit);
AudioOutputUnitStart(unit);
      
      





Este código solo crea e inicia una nueva AudioUnit, la generación de sonido real ocurrirá de forma asincrónica en la devolución de llamada:



static OSStatus tone_cb(void *inRefCon,
                        AudioUnitRenderActionFlags *ioActionFlags,
                        const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber,
                        UInt32 inNumberFrames, AudioBufferList *ioData) {
  unsigned int frame;
  unsigned char *buf = ioData->mBuffers[0].mData;
  unsigned long i = 0;
  for (i = 0; i < inNumberFrames; i++) {
    buf[i] = beep_freq > 0 ? (255 * theta * beep_freq / 8000) : 0;
    theta++;
    counter--;
  }
  return 0;
}
      
      





Esta devolución de llamada genera audio de la misma manera que lo hicimos con ALSA, pero se llama de forma asincrónica cuando CoreAudio cree que el búfer de audio está casi vacío y debe llenarse con nuevas muestras de audio.



Este enfoque asincrónico de la generación de sonido es muy común y casi todas las bibliotecas de audio modernas lo admiten. Si desea crear una aplicación de música, debe diseñarla teniendo en cuenta la reproducción asincrónica.



En caso de duda, utilice CoreAudio.



Suena complicado, ¿verdad?



Si está creando una aplicación de música, puede seguir el mismo camino implementando un backend de audio para WASAPI, ALSA y CoreAudio. De hecho, no es tan difícil. Puede ver el código fuente completo del beeper , esto es aproximadamente 100 líneas de código para las tres plataformas.



Sin embargo, hay una serie de buenas bibliotecas multiplataforma como:



  • RtAudio + RtMidi (muy fácil de usar, un archivo .cpp y .h);
  • PortAudio + PortMidi (escrito en C y un poco más grande), tiene muchos backends diferentes;
  • SoundIO es una pequeña biblioteca maravillosa del creador de Zig.


Algunas personas prefieren usar JUCE para aplicaciones de audio multiplataforma, pero tiene sus limitaciones.



Todo lo anterior puede parecer una tarea abrumadora, pero hay muchas opciones de implementación y la mayoría de ellas son buenas. ¡Así que sigue intentándolo!



Espero que hayas disfrutado de este artículo. Puede seguir noticias y proyectos en Github , Twitter o suscribirse a través de RSS .



All Articles