Reputation: 391
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);
}
Upvotes: 1
Views: 1382
Reputation: 391
After day of trying to figure out how to go about audio playback based on the documentation alone I found this excellent tutorial. If anyone found this thread while trying to create audio playback using Waveform audio it's a very good point of reference (surely much better than my buggy code above).
About my code I suspect it doesn't work correctly because one is supposed to keep the AudioFrames queue using waveOutWrite() with at least several frames at all time to prevent situation where sound card would have to wait for another AudioFrame.
Upvotes: 1
Reputation: 39390
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.
Upvotes: 2