thestk / rtaudio

A set of C++ classes that provide a common API for realtime audio input/output across Linux (native ALSA, JACK, PulseAudio and OSS), Macintosh OS X (CoreAudio and JACK), and Windows (DirectSound, ASIO, and WASAPI) operating systems.
Other
1.49k stars 317 forks source link

Stream recording on PulseAudio starts before startStream being called #401

Closed dagargo closed 1 year ago

dagargo commented 1 year ago

I've noticed that when using PulseAudio as the backend, an "internal" recording buffer is filled up before startStream is being called.

Audio playback seems to work as expected. Other backends seem to record as expected too (tested on LINUX_PULSE and WINDOWS_WASAPI). This happens at least since 5.2.0 and the behavior is present in the master branch too.

This minimal patch over the included test record.cpp provides the proof. Notice the 5 s sleep.

$ git diff record.cpp
diff --git a/tests/record.cpp b/tests/record.cpp
index c5f9cb5..2587247 100644
--- a/tests/record.cpp
+++ b/tests/record.cpp
@@ -111,7 +111,7 @@ int main( int argc, char *argv[] )
   // minimal command-line checking
   if ( argc < 3 || argc > 6 ) usage();

-  RtAudio adc;
+  RtAudio adc = RtAudio(RtAudio::LINUX_PULSE);
   std::vector<unsigned int> deviceIds = adc.getDeviceIds();
   if ( deviceIds.size() < 1 ) {
     std::cout << "\nNo audio devices found!\n";
@@ -165,6 +165,9 @@ int main( int argc, char *argv[] )
     goto cleanup;
   }

+  sleep (5);
+  std::cout << "Start recording...";
+
   if ( adc.startStream() ) goto cleanup;

   std::cout << "\nRecording for " << time << " seconds ... writing file 'record.raw' (buffer frames = " << bufferFrames << ")." << std::endl;

When asking the included script record for a 5 s sample, this is the result.

tests$ ./record 2 48000 5
Start recording...
Recording for 5 seconds ... writing file 'record.raw' (buffer frames = 512).

Obviously, there is a pause of 5 s before the first line is printed. Then, there is no pause between the first message and the second one and the execution ends. The callback is invoked at the moment of calling startStream as many times as needed to provide the client the with the 5 s of recorded audio since openStream.

Can anyone replicate this or am I missing something?

Before giving it a try, I need to be sure the error is not on my side.

garyscavone commented 1 year ago

I don't have easy access to a pulse audio system to test but one thing you could try is to change "buffer_attr.maxlength = -1; " on line 9524 of the latest master branch version of RtAudio.cpp to something like "buffer_attr.maxlength = 4 * bufferBytes;"

garyscavone commented 1 year ago

Or change it to the setting for playback on line 9539.

dagargo commented 1 year ago

You were right.

The issue was in the initialization of maxlength. As per the docs, it can be initialized to (uint32_t) -1 but that doesn't seem to work.

Setting attr_ptr to nullptr like in the OUTPUT mode like doesn't work either.

https://github.com/thestk/rtaudio/blob/13a583912155f58a1289175a12f02d8fb9d930dc/RtAudio.cpp#L9545

This patches fixes it. As options might be not initialized, a check is needed to use numberOfBuffers.

$ git diff
diff --git a/RtAudio.cpp b/RtAudio.cpp
index b13f04e..5e05e03 100644
--- a/RtAudio.cpp
+++ b/RtAudio.cpp
@@ -9521,7 +9521,11 @@ bool RtApiPulse::probeDeviceOpen( unsigned int deviceId, StreamMode mode,
     pa_buffer_attr buffer_attr;
   case INPUT:
     buffer_attr.fragsize = bufferBytes;
-    buffer_attr.maxlength = -1;
+    if ( options && options->numberOfBuffers > 0 ) {
+      buffer_attr.maxlength = bufferBytes * options->numberOfBuffers;
+    } else {
+      buffer_attr.maxlength = bufferBytes;
+    }

     pah->s_rec = pa_simple_new( NULL, streamName.c_str(), PA_STREAM_RECORD,
                                 dev_input, "Record", &ss, NULL, &buffer_attr, &error );

Should I create a PR?

garyscavone commented 1 year ago

I just tested this. I verified the behavior you described initially. But when I made the suggested change, I found that the record.cpp test program never finishes if maxlength is set to bufferBytes. I had to make maxlength at least equal to 2 bufferBytes to work. I am committing a change that fixes this but I will use (options->numberOfBuffers+1) if options are provided and bufferBytes 4 otherwise. I just pushed the change to the repo.