0
votes

TL/DR: A simple echo program that records and plays back audio immediately is showing higher than expected latency.

I am working on a real-time audio broadcasting application. I have decided to use OpenAL to both capture and playback audio samples. I am planning on sending UDP packets with raw PCM data across a LAN network. My ideal latency between recording on one machine and playing back on another machine is 30ms. (A lofty goal).

As a test, I made a small program that records samples from a microphone and immediately plays them back to the host speakers. I did this to test the baseline latency. However, I'm seeing an inherent latency of about 65 - 70 ms from simply recording the audio and playing it back. I have reduced the buffer size that openAL uses to 100 samples at 44100 samples per second. Ideally, this would yield a latency of 2 - 3 ms.

I have yet to try this on another platform (MacOS / Linux) to determine if this is an OpenAL issue or a Windows issue.

Here's the code:

using std::list;

#define FREQ 44100   // Sample rate
#define CAP_SIZE 100 // How much to capture at a time (affects latency)
#define NUM_BUFFERS 10

int main(int argC, char* argV[])
{
    list<ALuint> bufferQueue; // A quick and dirty queue of buffer objects
    ALuint helloBuffer[NUM_BUFFERS], helloSource;
    ALCdevice* audioDevice = alcOpenDevice(NULL); // Request default audio device

    ALCcontext* audioContext = alcCreateContext(audioDevice, NULL); // Create the audio context
    alcMakeContextCurrent(audioContext);

    // Request the default capture device with a ~2ms buffer
    ALCdevice* inputDevice = alcCaptureOpenDevice(NULL, FREQ, AL_FORMAT_MONO16, CAP_SIZE);

    alGenBuffers(NUM_BUFFERS, &helloBuffer[0]); // Create some buffer-objects

    // Queue our buffers onto an STL list
    for (int ii = 0; ii < NUM_BUFFERS; ++ii) {
        bufferQueue.push_back(helloBuffer[ii]);
    }

    alGenSources(1, &helloSource); // Create a sound source

    short* buffer = new short[CAP_SIZE]; // A buffer to hold captured audio
    ALCint samplesIn = 0;  // How many samples are captured
    ALint availBuffers = 0; // Buffers to be recovered
    ALuint myBuff; // The buffer we're using
    ALuint buffHolder[NUM_BUFFERS]; // An array to hold catch the unqueued buffers

    alcCaptureStart(inputDevice); // Begin capturing

    bool done = false;
    while (!done) { // Main loop
        // Poll for recoverable buffers
        alGetSourcei(helloSource, AL_BUFFERS_PROCESSED, &availBuffers);
        if (availBuffers > 0) {
            alSourceUnqueueBuffers(helloSource, availBuffers, buffHolder);
            for (int ii = 0; ii < availBuffers; ++ii) {
                // Push the recovered buffers back on the queue
                bufferQueue.push_back(buffHolder[ii]);
            }
        }
        // Poll for captured audio
        alcGetIntegerv(inputDevice, ALC_CAPTURE_SAMPLES, 1, &samplesIn);
        if (samplesIn > CAP_SIZE) {
            // Grab the sound
            alcCaptureSamples(inputDevice, buffer, samplesIn);

            // Stuff the captured data in a buffer-object
            if (!bufferQueue.empty()) { // We just drop the data if no buffers are available
                myBuff = bufferQueue.front(); bufferQueue.pop_front();
                alBufferData(myBuff, AL_FORMAT_MONO16, buffer, samplesIn * sizeof(short), FREQ);

                // Queue the buffer
                alSourceQueueBuffers(helloSource, 1, &myBuff);

                // Restart the source if needed
                // (if we take too long and the queue dries up,
                //  the source stops playing).
                ALint sState = 0;
                alGetSourcei(helloSource, AL_SOURCE_STATE, &sState);
                if (sState != AL_PLAYING) {
                    alSourcePlay(helloSource);
                }
            }
        }
    }
    // Stop capture
    alcCaptureStop(inputDevice);
    alcCaptureCloseDevice(inputDevice);

    // Stop the sources
    alSourceStopv(1, &helloSource);

    alSourcei(helloSource, AL_BUFFER, 0);

    // Clean-up 
    alDeleteSources(1, &helloSource);
    alDeleteBuffers(NUM_BUFFERS, &helloBuffer[0]);
    alcMakeContextCurrent(NULL);
    alcDestroyContext(audioContext);
    alcCloseDevice(audioDevice);

    return 0;
}

Here is an image of the waveform showing the delay in input sound and the resulting echo. This example is shows a latency of about 70ms.

Waveform with 70ms echo delay

System specs:

Intel Core i7-9750H 24 GB Ram Windows 10 Home: V 2004 - Build 19041.508 Sound driver: Realtek Audio (Driver version 10.0.19041.1) Input device: Logitech G330 Headset

Issue is reproducible on other Windows Systems.

EDIT:

I tried to use PortAudio to do a similar thing and achieved a similar result. I've determined that this is due to Windows audio drivers. I rebuilt PortAudio with ASIO audio only and installed ASIO4ALL audio driver. This has achieved an acceptable latency of <10ms.

1
openal might be not the fastest, perhaps, though I'm unaware how good it performs with no filters.bipll
@bipll I would agree, I think the latency is coming from the pass through of OpenAL from Capture Device to Output Device, though I'm not entirely sure how to confirm this. Native APIs would probably be the best way to go, or Juce if it is intended to be cross-platformfdcpp
It is also worth noting the buffer size may be different than the one requested Note that the implementation may use a larger buffer than requested if it needs to, but the implementation will set up a buffer of at least the requested size. From the Open AL Specification and Referencefdcpp
For holistics, original code here. Buffer size is typically in powers of 2, do you get any change in latency when changing the buffer size? say to 64 or 256?fdcpp
Building this on macOS is a little trickier than windows. On windows 10 it does appear to be behaving as expected. There may be an issue with the way you are measuring the latency here. You want to be careful that there is no feedback from your output back into your input.fdcpp

1 Answers

0
votes

I ultimately resolved this issue by ditching OpenAL in favor of PortAudio and Steinberg ASIO. I installed ASIO4ALL and rebuilt PortAudio to accept only ASIO device drivers. I needed to use the ASIO SDK from Steinberg to do this. (Followed guide here). This has allowed me to achieve a latency of between 5 and 10 ms.

This post helped a lot: input delay with PortAudio callback and ASIO sdk