I'm trying to implement a very simple API for audio playback using portaudio
. I have minimal amount of code needed to play the audio file but I am not getting any errors and/or audio output.
Here is the code,
AudioStream::AudioStream()
{
PaError err = Pa_Initialize();
if (err != paNoError)
SH_LOG_ERROR("PAError: {}", Pa_GetErrorText(err));
}
AudioStream::~AudioStream()
{
PaError err = Pa_Terminate();
if (err != paNoError)
SH_LOG_ERROR("PAError: {}", Pa_GetErrorText(err));
}
int AudioStream::Callback(const void* inputBuffer, void* outputBuffer,
unsigned long framesPerBuffer,
const PaStreamCallbackTimeInfo* timeInfo,
PaStreamCallbackFlags statusFlags,
void* userData)
{
// Prevent warnings
(void)inputBuffer;
(void)timeInfo;
(void)statusFlags;
// an AudioFile gets passed as userData
AudioFile* file = (AudioFile*)userData;
float* out = (float*)outputBuffer;
sf_seek(file->file, file->readHead, SF_SEEK_SET);
auto data = std::make_unique<float[]>(framesPerBuffer * file->info.channels);
file->count = sf_read_float(file->file, data.get(), framesPerBuffer * file->info.channels);
for (int i = 0; i < framesPerBuffer * file->info.channels; i++)
*out++ = data[i];
file->readHead += file->buffer_size;
if (file->count > 0)
return paContinue;
else
return paComplete;
}
AudioFile AudioStream::Load(const char* path)
{
AudioFile file;
std::memset(&file.info, 0, sizeof(file.info));
file.file = sf_open(path, SFM_READ, &file.info);
return file;
}
bool AudioStream::Play(AudioFile* file)
{
m_OutputParameters.device = Pa_GetDefaultOutputDevice();
m_OutputParameters.channelCount = file->info.channels;
m_OutputParameters.sampleFormat = paFloat32;
m_OutputParameters.suggestedLatency = Pa_GetDeviceInfo(m_OutputParameters.device)->defaultLowOutputLatency;
m_OutputParameters.hostApiSpecificStreamInfo = nullptr;
// Check if m_OutputParameters work
PaError err = Pa_IsFormatSupported(nullptr, &m_OutputParameters, file->info.samplerate);
if (err != paFormatIsSupported)
{
SH_LOG_ERROR("PAError: {}", Pa_GetErrorText(err));
return false;
}
err = Pa_OpenStream(&m_pStream,
nullptr,
&m_OutputParameters,
file->info.samplerate,
file->buffer_size,
paClipOff,
&AudioStream::Callback,
file);
if (err != paNoError)
{
SH_LOG_ERROR("PAError: {}", Pa_GetErrorText(err));
return false;
}
if (m_pStream)
{
err = Pa_StartStream(m_pStream);
if (err != paNoError)
{
SH_LOG_ERROR("PAError: {}", Pa_GetErrorText(err));
return false;
}
SH_LOG_DEBUG("Waiting for playback to finish..");
while (IsStreamActive()) // <----- this works, but the application freezes until this ends
Pa_Sleep(500);
Stop();
if (IsStreamStopped())
{
SH_LOG_DEBUG("Stream stopped..");
err = Pa_CloseStream(m_pStream);
if (err != paNoError)
{
SH_LOG_ERROR("PAError: {}", Pa_GetErrorText(err));
return false;
}
}
SH_LOG_DEBUG("Done..");
}
return true;
}
bool AudioStream::Stop()
{
PaError err = Pa_StopStream(m_pStream);
if (err != paNoError)
{
SH_LOG_ERROR("PAError: {}", Pa_GetErrorText(err));
return false;
}
else
return true;
}
bool AudioStream::IsStreamStopped()
{
if (Pa_IsStreamStopped(m_pStream))
return true;
else
return false;
}
I noticed that if I add a print statement in the while
loop I do get audio output.
// Wait until file finishes playing
while (file->count > 0) { SH_LOG_DEBUG(""); }
Why doesn't it play with a print statement or anything in while loop? Do I have to have something in the while loop?
You've got two flaws that I can see.
One is using what looks like a class method as the callback to PulseAudio. Since PA is a C API, it expects a C function pointer here. It won't call that function with the this
pointer set, so you can't have a class method here. But maybe AudioStream::Callback
is static
? That will work.
Two is that you need to consider that the callback is called in another thread. The compiler does not take that into account when it optimizes the code. As far as it knows, there is nothing in your empty while loop that could possibly change the value of file->count
.
Once you call the debug function, it brings in enough code, some probably in libraries that are already compiled, that the compiler can't be sure nothing has modified file->count
. Maybe SH_LOG_DEBUG()
calls prinf()
and then printf()
calls AudioStream::Load()
? Of course it doesn't, but if the compiler doesn't see the code of printf()
because it's already in a library, and your AudioStream
object is global, then it's possible. So it actually works like you want.
But even when it works, it's really bad. Because the thread is sitting there busy waiting for the count to stop, hogging the/a CPU. It should block and sleep, then get notified when the playback finishes.
If you want to pass information between threads in a way that works, and also block instead of busy waiting too, look into the C++ concurrency support library. The C++20 std::latch
and std::barrier
would work well here. Or use a C++11 std::condition_variable
.