Forgive me if my question title is terrible. My wife is always telling me I'm not good at phrasing things.
I've written some code that reads a buffer that's filled by another thread. The buffer is filled with audio data encoded by the opus codec. VoIP data is received from the remote side 20ms at a time. In an attempt to play audio as quickly as possible, in a loop, I take 20ms of data at a time out of the buffer, and then decode it, then send it straight to play on snd_pcm_writei.
I've looked around on Google for some examples on using snd_pcm_writei with previously encoded audio to see how others are doing it. I haven't had much luck.
My thought is, if I'm waiting on a mutex and waiting for encoding I can't logically see the audio being "smooth." I'd imagine that between each 20ms frame there be gaps of time where no audio is being sent to the speakers. Are my suspicions correct that this will likely create imperfect audio?
My code relating to this:
while( true )
{
// We need a positive lock
if( !buffer_lock )
buffer_lock.lock();
LOG_DEBUG( *logger_ ) << "After the mutex lock.";
LOG_DEBUG( *logger_ ) << "Buffer size: " << current_audio->buffer_size_;
LOG_DEBUG( *logger_ ) << "Read pointer: " << current_audio->read_pointer_;
opus_int32 payload_size;
LOG_DEBUG( *logger_ ) << "calling audioCanDecodeChunk()";
// Now fisticuffs do we have enouffs?
if( audioCanDecodeChunk( current_audio, payload_size ) )
{
LOG_DEBUG( *logger_ ) << "We have enough current_audio buffer.";
// Are we dank?
if( payload_size<0 or payload_size>MAX_PACKET )
{
LOG_ERROR( *logger_ ) << "Decoding error, payload size (" << payload_size << ") is outsize range.";
break; // Terminal
}
// We have enough!
// Advance the read pointer
current_audio->read_pointer_+= 4;
// Copy it out
memcpy( payload_buffer, current_audio->buffer_+current_audio->read_pointer_, payload_size );
// Release it
buffer_lock.unlock();
// Now thingify it
int samples_decoded = opus_decode( opus_decoder_,
(const unsigned char *)payload_buffer,
payload_size,
(opus_int16 *)pcm_buffer,
MAX_FRAME_SIZE,
0 );
// How did we do?
if( samples_decoded<0 )
{
// What hap?
LOG_ERROR( *logger_ ) << "Error decoding samples: " << opus_strerror( samples_decoded );
break;
}
else
{
// Now we have our PCM!
int bytes_decoded = current_audio->recording_.channels*sizeof( opus_int16 )*samples_decoded;
LOG_DEBUG( *logger_ ) << "We have decoded " << bytes_decoded << " bytes payload: " << payload_size;
// Now write
if( (error = snd_pcm_writei( playback_handle_, pcm_buffer, samples_decoded ))!=samples_decoded )
{
LOG_ERROR( *logger_ ) << "snd_pcm_writei error: " << snd_strerror( error );
}
}
// Advance pointer
current_audio->read_pointer_+= payload_size;
} // If we don't have enough let it slide and unlock
else if( current_audio->done_ ) // Were we issued a flush?
{
LOG_DEBUG( *logger_ ) << "We are done.";
// We are done with this loop
break;
}
else
{
// Wait for it (an update)
LOG_DEBUG( *logger_ ) << "Before wait_buffer wait. Done: " << ( current_audio->done_ ? "true" : "false" ) <<
"Size: " << current_audio->buffer_size_
<< ", Read: " << current_audio->read_pointer_;
current_audio->wait_buffer_.wait( buffer_lock );
LOG_DEBUG( *logger_ ) << "After wait_buffer wait";
}
} // End while( true )
If the time between writing 20-ms chunks is exactly 20 ms, then the device's buffer will be empty when you are writing a new chunk. Even the smallest delay will then result in an underrun.
To prevent underrungs, you must keep the buffer as full as possible. This means that at the start, you must fill it without waiting between chunks.
When the sender's clock runs faster than the device's clock, the stream will underrun eventually.This can be avoided by measuring the clock difference, and either changing the sender's transmit rate, or resampling the data dynamically.