2024/04/17

Audio library

簡介

如果是輕量的函式庫,可以分為二個部份:

  • 讀取 audio file 並且轉為 raw audio data
  • audio I/O

第一個部份,有二個函式庫可以考慮:

第二個部份,有下面的函式庫可以使用:

如果需要查詢與修改 audio 檔案的 meta-data,可以考慮 TagLib, 因為 TagLib 支援十分廣泛的音訊檔案格式。

libao

LibAO is developed under Xiph umbrella. Xiph is the organization who brought you Ogg/Vorbis, FLAC, Theora and currently they are hammering together next generation video codec Daala.

Opus-audio codec standard is also Xiph project. LibAO rised from Xiph’s need multi-platform audio output library for Vorbis-audio codec.

The libao API makes a distinction between drivers and devices. A driver is a set of functions that allow audio to be played on a particular platform (i.e. Solaris, ESD, etc.). A device is a particular output target that uses a driver. In addition, libao distinguishes between live output drivers, which write audio to playback devices (sound cards, etc.), and file output drivers, which write audio to disk in a particular format.

To use libao in your program, you need to follow these steps:

  • Include the <ao/ao.h> header into your program.
  • Call ao_initialize() to initialize the library. This loads the plugins from disk, reads the libao configuration files, and identifies an appropriate default output driver if none is specified in the configuration files.
  • Call ao_default_driver_id() to get the ID number of the default output driver. This may not be successful if no audio hardware is available, it is in use, or is not in the "standard" configuration. If you want to specify a particular output driver, you may call ao_driver_id() with a string corresponding to the short name of the device (i.e. "oss", "wav", etc.) instead.
  • If you are using the default device, no extra options are needed. However, if you wish to to pass special options to the driver, you will need to:
    • Create an option list pointer of type (ao_option *) and initialize it to NULL.
    • Through successive calls to ao_append_option(), add any driver-specific options you need. Note that the options take the form of key/value pairs where supported keys are listed in the driver documentation.
  • Call ao_open_live() and save the returned device pointer. If you are using a file output driver, you will need to call ao_open_file() instead.
  • Call ao_play() to output each block of audio.
  • Call ao_close() to close the device. Note that this will automatically free the memory that was allocated for the device. Do not attempt to free the device pointer yourself!
  • Call ao_shutdown() to close the library.
下面是一個使用 libao 與 libsndfile 的範例:
#include <ao/ao.h>
#include <signal.h>
#include <sndfile.h>

#define BUFFER_SIZE 8192

int cancel_playback;

void on_cancel_playback(int sig) {
    if (sig != SIGINT) {
        return;
    }

    cancel_playback = 1;
    exit(0);
}

static void clean(ao_device *device, SNDFILE *file) {
    ao_close(device);
    sf_close(file);
    ao_shutdown();
}

int play(const char *filename) {
    ao_device *device;
    ao_sample_format format;
    SF_INFO sfinfo;

    int default_driver;

    short *buffer;

    signal(SIGINT, on_cancel_playback);

    SNDFILE *file = sf_open(filename, SFM_READ, &sfinfo);
    if (file == NULL)
        return -1;

    printf("Samples: %d\n", sfinfo.frames);
    printf("Sample rate: %d\n", sfinfo.samplerate);
    printf("Channels: %d\n", sfinfo.channels);

    ao_initialize();

    default_driver = ao_default_driver_id();

    switch (sfinfo.format & SF_FORMAT_SUBMASK) {
    case SF_FORMAT_PCM_16:
        format.bits = 16;
        break;
    case SF_FORMAT_PCM_24:
        format.bits = 24;
        break;
    case SF_FORMAT_PCM_32:
        format.bits = 32;
        break;
    case SF_FORMAT_PCM_S8:
        format.bits = 8;
        break;
    case SF_FORMAT_PCM_U8:
        format.bits = 8;
        break;
    default:
        format.bits = 16;
        break;
    }

    format.channels = sfinfo.channels;
    format.rate = sfinfo.samplerate;
    format.byte_format = AO_FMT_NATIVE;
    format.matrix = 0;

    device = ao_open_live(default_driver, &format, NULL);

    if (device == NULL) {
        fprintf(stderr, "Error opening device.\n");
        return 1;
    }

    buffer = calloc(BUFFER_SIZE, sizeof(short));

    while (1) {
        int read = sf_read_short(file, buffer, BUFFER_SIZE);

        if (read <= 0) {
            break;
        }

        if (ao_play(device, (char *)buffer, (uint_32)(read * sizeof(short))) ==
            0) {
            printf("ao_play: failed.\n");
            clean(device, file);
            break;
        }

        if (cancel_playback) {
            clean(device, file);
            break;
        }
    }

    clean(device, file);

    return 0;
}

int main(int argc, char *argv[]) {
    if (argc > 1) {
        play(argv[1]);
    }
}
編譯方式:
gcc output.c -lsndfile -lao -o output

再來是 libao 與 libmpg123 的例子:
#include <ao/ao.h>
#include <mpg123.h>

#define BITS 8

int main(int argc, char *argv[]) {
    mpg123_handle *mh;
    unsigned char *buffer;
    size_t buffer_size;
    size_t done;
    int err;

    int driver;
    ao_device *dev;

    ao_sample_format format;
    int channels, encoding;
    long rate;

    if (argc < 2)
        exit(0);

    /* initializations */
    ao_initialize();
    driver = ao_default_driver_id();
    mpg123_init();
    mh = mpg123_new(NULL, &err);
    buffer_size = mpg123_outblock(mh);
    buffer = (unsigned char *)malloc(buffer_size * sizeof(unsigned char));

    /* open the file and get the decoding format */
    mpg123_open(mh, argv[1]);
    mpg123_getformat(mh, &rate, &channels, &encoding);

    /* set the output format and open the output device */
    format.bits = mpg123_encsize(encoding) * BITS;
    format.rate = rate;
    format.channels = channels;
    format.byte_format = AO_FMT_NATIVE;
    format.matrix = 0;
    dev = ao_open_live(driver, &format, NULL);

    /* decode and play */
    while (mpg123_read(mh, buffer, buffer_size, &done) == MPG123_OK)
        ao_play(dev, buffer, done);

    /* clean up */
    free(buffer);
    ao_close(dev);
    mpg123_close(mh);
    mpg123_delete(mh);
    mpg123_exit();
    ao_shutdown();

    return 0;
}

OpenAL

OpenAL (Open Audio Library) is a cross-platform audio application programming interface (API). It is designed for efficient rendering of multichannel three-dimensional positional audio, for creation of a virtual 3D world of sound. Its API style and conventions deliberately resemble those of OpenGL.

The application programmer can specify the location, the speed and the direction of the sources of sounds and of the listener.

Objects

Since OpenAL is about audio, it also introduces new concepts, in particular:
  • the listener object
  • the source object
  • the buffer object

Each of these different objects have properties which can be set with their respective API (al*Object*).

  • context: 要播放聲音的地方,可以想成OpenGL裡面的Window
  • listener: OpenAL 支援3D音效,所以在這裡設定聽者的資料
  • sources: 聲源的資訊
  • buffer: 負責聲源的內容

The very first thing to do is to open a handle to a device. This is done like this:

ALCdevice *device;

device = alcOpenDevice(NULL);
if (!device)
        // handle errors

Prior to attempting an enumeration, Open AL provides an extension querying mechanism which allows you to know whether the runtime Open AL implementation supports a specific extension. In our case, we want to check whether Open AL supports enumerating devices:

ALboolean enumeration;

enumeration = alcIsExtensionPresent(NULL, "ALC_ENUMERATION_EXT");
if (enumeration == AL_FALSE)
        // enumeration not supported
else
        // enumeration supported

If the enumeration extension is supported, we can procede with listing the audio devices. If the enumeration is not supported listing audio devices only returns the default device, which is expected in order not to break any application.

static void list_audio_devices(const ALCchar *devices)
{
        const ALCchar *device = devices, *next = devices + 1;
        size_t len = 0;

        fprintf(stdout, "Devices list:\n");
        fprintf(stdout, "----------\n");
        while (device && *device != '\0' && next && *next != '\0') {
                fprintf(stdout, "%s\n", device);
                len = strlen(device);
                device += (len + 1);
                next += (len + 2);
        }
        fprintf(stdout, "----------\n");
}

list_audio_devices(alcGetString(NULL, ALC_DEVICE_SPECIFIER));

Passsing NULL to alcGetString() indicates that we do not want the device specifier of a particular device, but all of them.


In order to render an audio scene, we need to create and initialize a context for this. We do this by the following calls:

ALCcontext *context;

context = alcCreateContext(device, NULL);
if (!alcMakeContextCurrent(context))
        // failed to make context current
// test for errors here using alGetError();

There is nothing specific for our context, so NULL is specified as argument.

Since there is a default listener, we do not need to explicitely create one because it is already present in our scene. If we want to define some of our listener properties however, we can proceed like this:

ALfloat listenerOri[] = { 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f };

alListener3f(AL_POSITION, 0, 0, 1.0f);
// check for errors
alListener3f(AL_VELOCITY, 0, 0, 0);
// check for errors
alListenerfv(AL_ORIENTATION, listenerOri);
// check for errors

In order to playback audio, we must create an audio source objet, this source is actually the "origin" of the audio sound. And as such must be defined in the audio scene. If you combine audio with graphics, most likely quite a lot of your graphics objects will also include an audio source object.

Note that you hold a reference (id) to a source object, you don’t manipulate the source object directly.

ALuint source;


alGenSources((ALuint)1, &source);
// check for errors

alSourcef(source, AL_PITCH, 1);
// check for errors
alSourcef(source, AL_GAIN, 1);
// check for errors
alSource3f(source, AL_POSITION, 0, 0, 0);
// check for errors
alSource3f(source, AL_VELOCITY, 0, 0, 0);
// check for errors
alSourcei(source, AL_LOOPING, AL_FALSE);
// check for errros

The buffer object is the object actually holding the raw audio stream, alone a buffer does not do much but occupying memory, so we will see later on what to do with it. Just like sources, we hold a reference to the buffer object.

ALuint buffer;

alGenBuffers((ALuint)1, &buffer);
// check for errors

下面就是載入 Audio raw data 以後的使用範例:

static inline ALenum to_al_format(short channels, short samples)
{
        bool stereo = (channels > 1);

        switch (samples) {
        case 16:
                if (stereo)
                        return AL_FORMAT_STEREO16;
                else
                        return AL_FORMAT_MONO16;
        case 8:
                if (stereo)
                        return AL_FORMAT_STEREO8;
                else
                        return AL_FORMAT_MONO8;
        default:
                return -1;
        }
}

alBufferData(buffer, to_al_format(wave->channels, wave->bitsPerSample),
                bufferData, wave->dataSize, wave->sampleRate);
// check for errors

In order to actually output something to the playback device, we need to bind the source with its buffer. Obviously you can bind the same buffer to several sources and mix different buffers to the same source. Binding is done like this:

alSourcei(source, AL_BUFFER, buffer);
// check for errors

We now have everything ready to start playing our source.

alSourcePlay(source);
// check for errors

alGetSourcei(source, AL_SOURCE_STATE, &source_state);
// check for errors
while (source_state == AL_PLAYING) {
        alGetSourcei(source, AL_SOURCE_STATE, &source_state);
        // check for errors
}

Obviously each and every single object "generated" must be freed, the following does this for us:

alDeleteSources(1, &source);
alDeleteBuffers(1, &buffer);
device = alcGetContextsDevice(context);
alcMakeContextCurrent(NULL);
alcDestroyContext(context);
alcCloseDevice(device);

問題與解法

Fix jack server is not running or cannot be started message (for openal-soft)

如果你在 OpenSUSE Leap 15.0 使用 OpenAL 時發現會出現 jack server is not running or cannot be started 的訊息, 這是因為 openal-soft 支援 jack,所以會嘗試進行初始化的動作。

解決的方法是在 /etc/openal 下新增加 alsoft.conf,並且加入下面的內容:
[general]
drivers = -jack,

Libao debug message

在 OpenSUSE Tumbleweed 中預設的 driver 是 PulseAudio,但是仍然會嘗試載入 ALSA driver 並且印出錯誤訊息。 如果遇到這個情況想要停止印出錯誤訊息,修改 /etc/libao.conf,加入一行 quiet

default_driver=pulse
quiet

參考連結

沒有留言:

張貼留言

注意:只有此網誌的成員可以留言。