Search code examples
c++audioplaybackwaveform

Waveform Audio - waveOutWrite gives choppy sound


I'm trying to create a C++ program with Waveform Audio library that would be playing AudioFrames (raw audio data, each frame consists of about 1920 bytes) provided by another program (right now I'm just simulating that by reading file as AudioFrames). Modifying code from this thread I was able to make SoundPlayer class that does the job, but the output I get is extremely choppy. It's gets better with bigger frame sizes, but even with frames as big as 96000 bytes the audio still glitches every second or so (and I need the frames too be much smaller than that).

How can I fix this issue?

Here is the test file I'm using. And here is the code itself:

#include <windows.h>
#include <iostream>
#pragma comment(lib, "Winmm.lib")

constexpr int FRAME_SIZE_IN_BYTES = 1920;

struct AudioFrame
{
    char *Data;
    int DataSize;
};

class SoundPlayer
{
public:

    SoundPlayer()
    {
        // Initialize the sound format we will request from sound card
        m_waveFormat.wFormatTag = WAVE_FORMAT_PCM;     // Uncompressed sound format
        m_waveFormat.nChannels = 1;                    // 1 = Mono, 2 = Stereo
        m_waveFormat.wBitsPerSample = 16;               // Bits per sample per channel
        m_waveFormat.nSamplesPerSec = 48000;           // Sample Per Second
        m_waveFormat.nBlockAlign = m_waveFormat.nChannels * m_waveFormat.wBitsPerSample / 8;
        m_waveFormat.nAvgBytesPerSec = m_waveFormat.nSamplesPerSec * m_waveFormat.nBlockAlign;
        m_waveFormat.cbSize = 0;
    }

    void Play(AudioFrame* af)
    {
        // Create our "Sound is Done" event
        m_done = CreateEvent(0, FALSE, FALSE, 0);

        // Open the audio device
        if (waveOutOpen(&m_waveOut, 0, &m_waveFormat, (DWORD)m_done, 0, CALLBACK_EVENT) != MMSYSERR_NOERROR)
        {
            std::cout << "Sound card cannot be opened." << std::endl;
            return;
        }

        // Create the wave header for our sound buffer
        m_waveHeader.lpData = af->Data;
        m_waveHeader.dwBufferLength = af->DataSize;
        m_waveHeader.dwFlags = 0;
        m_waveHeader.dwLoops = 0;

        // Prepare the header for playback on sound card
        if (waveOutPrepareHeader(m_waveOut, &m_waveHeader, sizeof(m_waveHeader)) != MMSYSERR_NOERROR)
        {
            std::cout << "Error preparing Header!" << std::endl;
            return;
        }

        ResetEvent(m_done); // Reset our Event so it is non-signaled, it will be signaled again with buffer finished

        // Play the sound!
        if (waveOutWrite(m_waveOut, &m_waveHeader, sizeof(m_waveHeader)) != MMSYSERR_NOERROR)
        {
            std::cout << "Error writing to sound card!" << std::endl;
            return;
        }

        // Wait until sound finishes playing
        if (WaitForSingleObject(m_done, INFINITE) != WAIT_OBJECT_0)
        {
            std::cout << "Error waiting for sound to finish" << std::endl;
            return;
        }

        // Unprepare our wav header
        if (waveOutUnprepareHeader(m_waveOut, &m_waveHeader, sizeof(m_waveHeader)) != MMSYSERR_NOERROR)
        {
            std::cout << "Error unpreparing header!" << std::endl;
            return;
        }

        // Close the wav device
        if (waveOutClose(m_waveOut) != MMSYSERR_NOERROR)
        {
            std::cout << "Sound card cannot be closed!" << std::endl;
            return;
        }

        // Release our event handle
        CloseHandle(m_done);
    }



private:
    HWAVEOUT m_waveOut; // Handle to sound card output
    WAVEFORMATEX m_waveFormat; // The sound format
    WAVEHDR m_waveHeader; // WAVE header for our sound data
    HANDLE m_done; // Event Handle that tells us the sound has finished being played.
                   // This is a very efficient way to put the program to sleep
                   // while the sound card is processing the sound buffer

};

int main()
{
    FILE * fileDes;
    fopen_s(&fileDes, "Ducksauce.raw", "rb");
    if (fileDes == nullptr)
        std::cout << "File opening failed.\n";

    int bufferSize = FRAME_SIZE_IN_BYTES;
    char *buffer = new char[bufferSize];

    SoundPlayer sp;

    while (fread(buffer, sizeof(char), bufferSize, fileDes) > 0)
    {
        AudioFrame af;
        af.Data = buffer;
        af.DataSize = bufferSize;
        sp.Play(&af);
    }

    fclose(fileDes);
    delete[] buffer;
    return 0;
}

Edit: Version number 2. Still doesn't work as intended.

#include <windows.h>
#include <iostream>
#pragma comment(lib, "Winmm.lib")

constexpr int FRAME_SIZE_IN_BYTES = 1920;

struct AudioFrame
{
    char *Data;
    int DataSize;
};

class SoundPlayer
{
public:

    SoundPlayer()
    {
        // Initialize the sound format we will request from sound card
        m_waveFormat.wFormatTag = WAVE_FORMAT_PCM;     // Uncompressed sound format
        m_waveFormat.nChannels = 1;                    // 1 = Mono, 2 = Stereo
        m_waveFormat.wBitsPerSample = 16;               // Bits per sample per channel
        m_waveFormat.nSamplesPerSec = 48000;           // Sample Per Second
        m_waveFormat.nBlockAlign = m_waveFormat.nChannels * m_waveFormat.wBitsPerSample / 8;
        m_waveFormat.nAvgBytesPerSec = m_waveFormat.nSamplesPerSec * m_waveFormat.nBlockAlign;
        m_waveFormat.cbSize = 0;

        // Create our "Sound is Done" event
        m_done = CreateEvent(0, FALSE, FALSE, 0);

        // Open the audio device
        if (waveOutOpen(&m_waveOut, 0, &m_waveFormat, (DWORD)m_done, 0, CALLBACK_EVENT) != MMSYSERR_NOERROR)
        {
            std::cout << "Sound card cannot be opened." << std::endl;
            return;
        }
    }

    ~SoundPlayer()
    {
        // Close the wav device
        if (waveOutClose(m_waveOut) != MMSYSERR_NOERROR)
        {
            std::cout << "Sound card cannot be closed!" << std::endl;
            return;
        }

        // Release our event handle
        CloseHandle(m_done);
    }

    void StartPlaying(AudioFrame* af)
    {
        // Create the wave header for our sound buffer
        m_waveHeader.lpData = af->Data;
        m_waveHeader.dwBufferLength = af->DataSize;
        m_waveHeader.dwFlags = 0;
        m_waveHeader.dwLoops = 0;

        // Prepare the header for playback on sound card
        if (waveOutPrepareHeader(m_waveOut, &m_waveHeader, sizeof(m_waveHeader)) != MMSYSERR_NOERROR)
        {
            std::cout << "Error preparing Header!" << std::endl;
            return;
        }

        ResetEvent(m_done); // Reset our Event so it is non-signaled, it will be signaled again with buffer finished

        // Play the sound!
        if (waveOutWrite(m_waveOut, &m_waveHeader, sizeof(m_waveHeader)) != MMSYSERR_NOERROR)
        {
            std::cout << "Error writing to sound card!" << std::endl;
            return;
        }
    }

    void WaitUntilFrameFinishes()
    {
        // Wait until sound finishes playing
        if (WaitForSingleObject(m_done, INFINITE) != WAIT_OBJECT_0)
        {
            std::cout << "Error waiting for sound to finish" << std::endl;
            return;
        }
        // Unprepare our wav header
        if (waveOutUnprepareHeader(m_waveOut, &m_waveHeader, sizeof(m_waveHeader)) != MMSYSERR_NOERROR)
        {
            std::cout << "Error unpreparing header!" << std::endl;
            return;
        }
    }

private:
    HWAVEOUT m_waveOut; // Handle to sound card output
    WAVEFORMATEX m_waveFormat; // The sound format
    WAVEHDR m_waveHeader; // WAVE header for our sound data
    HANDLE m_done; // Event Handle that tells us the sound has finished being played.
                   // This is a very efficient way to put the program to sleep
                   // while the sound card is processing the sound buffer

};

int main()
{
    FILE * fileDes;
    fopen_s(&fileDes, "Ducksauce.raw", "rb");
    if (fileDes == nullptr)
        std::cout << "File opening failed.\n";

    int bufferSize = FRAME_SIZE_IN_BYTES;
    char *buffer = new char[bufferSize];

    SoundPlayer sp;

    // Read first time
    fread(buffer, sizeof(char), bufferSize, fileDes);

    while (true)
    {
        AudioFrame af;
        af.Data = buffer;
        af.DataSize = bufferSize;
        // Start playing, but don't block
        sp.StartPlaying(&af);
        // Prepare the next chunk
        if (fread(buffer, sizeof(char), bufferSize, fileDes) <= 0)
            break;
        // Now block the code, waiting with next chunk already loaded
        // and ready to be played in the next iteration.
        sp.WaitUntilFrameFinishes();
    }

    fclose(fileDes);
    delete[] buffer;
    return 0;
}

Edit 2: It works if I add this before while:

for (int i = 0; i < 3; i++ )
{
    fread(buffer, sizeof(char), bufferSize, fileDes);
    af.Data = buffer;
    af.DataSize = bufferSize;
    sp.StartPlaying(&af);
}

Also I modified while a bit too:

while (true)
{
    // Prepare the next chunk
    if (fread(buffer, sizeof(char), bufferSize, fileDes) <= 0)
        break;
    // Now block the code, waiting with next chunk already loaded
    // and ready to be played in the next iteration.
    sp.WaitUntilFrameFinishes();

    af.Data = buffer;
    af.DataSize = bufferSize;
    sp.StartPlaying(&af);
}

Solution

  • You should read the data from disk while the sound plays, not in between buffers!

    If you can't read the whole file at once, you should change your Play function so that it doesn't just call WaitForSingleObject. Using it makes your code block and wait until the sound stops playing.

    What you need instead is to start playing, then go back to your reading loop, prepare the next buffer, and then wait for the music to end, like so (in SoundPlayer):

    void WaitUntilFrameFinishes() {
        // Wait until sound finishes playing
        if (WaitForSingleObject(m_done, INFINITE) != WAIT_OBJECT_0)
    
        // ... move all the code from Play till the end here
    }
    

    Then back in the main loop:

    // Read first frame
    fread(buffer, sizeof(char), bufferSize, fileDes);
    
    while (true)
    {
        AudioFrame af;
        af.Data = buffer;
        af.DataSize = bufferSize;
    
        // Start playing, but don't block
        sp.Play(&af);
    
        // Prepare the next chunk
        if (fread(buffer, sizeof(char), bufferSize, fileDes) <= 0) {
            break;
    
        // Now block the code, waiting with next chunk already loaded
        // and ready to be played in the next iteration.
        sp.WaitUntilFrameFinishes();
    }
    

    Ideally you'd also wrap the fread calls into something that can provide chunks in a nicer way.