4

I am trying to write a buffer for an audio system with Oboe, the do and don'ts are

Callback do's and don'ts You should never perform an operation which could block inside onAudioReady. Examples of blocking operations include:

allocate memory using, for example, malloc() or new;

file operations such as opening, closing, reading or writing;

network operations such as streaming;

use mutexes or other synchronization primitives sleep

stop or close the stream

Call read() or write() on the stream which invoked it

Audio thread reads from my buffer and decoder thread writes to it and as you can imagine its all good until threading issues kicks in. My main problem is I can just use a mutex to overcome this issue but if i do so I will be blocking one of the threads and if audio thread is blocked then the sound basically not played resulting in "popcorn" sound. (A Sound that is so disturbing to listen to)

I play the sound through a callback where I feed the data to it.

DataCallbackResult
OboeStreamCallback::onAudioReady(AudioStream *audioStream, void *audioData, int32_t numFrames) {
    // fill data here
    return DataCallbackResult::Continue;
}

So my main issue is how may I solve read data from audio thread also not block audio thread so it can still play audio?

This sounds impossible to me. How may one can assure thread safety without a mutex? How does oboe expect you to not use mutex for dynamic audio decoding?

Wasif
  • 14,755
  • 3
  • 14
  • 34
cs guy
  • 926
  • 2
  • 13
  • 33
  • 1
    https://github.com/google/oboe/tree/master/samples/RhythmGame#sharing-objects-with-the-audio-thread – Asteroids With Wings Nov 22 '20 at 18:38
  • @AsteroidsWithWings I looked at this but the problem is If I use a `LockFreeQueue`; `pop()` call uses two atomic variables, consider the following: in a single read Oboe asks me to fill 350 samples. This means 350 `pop()` calls. It will use atomic variables 350 times. This would make it slow since atomic variables are slow, I dont think I can use this class as a buffer. In the example they used it with size of only 4 too. – cs guy Nov 22 '20 at 19:02
  • _"This would make it slow since atomic variables are slow"_ ??? – Asteroids With Wings Nov 22 '20 at 19:24
  • Perhaps try it and measure to find out whether you really have a performance problem there. I bet you don't. – Asteroids With Wings Nov 22 '20 at 19:24
  • @AsteroidsWithWings well I tried and no issues. Thank you. I am amazed by the speed of this although my phone is Samsung Note 10. I am also decoding and writing to the same queue while all of reading is going on inside the oboe audio callback. Another point is im using FFmpeg and decoding is way faster than reading so do you think that it would be a good idea to only start decoding after queue is emptied to certain threshold? Do you reckon this is a good design to implement a buffer? – cs guy Nov 22 '20 at 20:15
  • No idea but if you have a new question please post it anew! – Asteroids With Wings Nov 22 '20 at 20:52
  • What is in your buffer that needs mutex-like protection, bulk audio data, (bad), or pointers to audio data, (much better)? – Martin James Nov 23 '20 at 05:21
  • I mean, bulk data is much more easily, and quickly, handled by pools and queues of pointers/references/instances than trying to lock up large buffers that require lengthy handling while the lock is kept:( – Martin James Nov 23 '20 at 05:24
  • @MartinJames My buffer is PCM floats between [-1,+1], decoder thread writes to it, oboe reads from it making this the classic write, read from a queue with threads problem. I used LockFreeQueue to handle this without using Mutexes but with Atomic variables, this worked with one reader, one writer. However, im curios if i can do this without atomic variables although it seems like its not possible since this is a race condition and any kind of race condition would require a lock? – cs guy Nov 23 '20 at 12:59
  • 2
    `LockFreeQueue` contains atomics for the read and write counters. This ensures that the reader and writer threads can safely access those values. Atomics represent the most performant way of achieving thread-safety in this case. Why do you want to remove them - are you running into performance issues? – donturner Nov 23 '20 at 13:38
  • @donturner How my system works is I use a mixer on oboe callback and I sum the PCM floats of the "playing" sounds from their own buffer(LockFreeQueue). This works fine for 4 sounds (I tested this), but my app will be hosting up to 20 sounds at the same time, so I thought this would eventually get me performance issues since 20 sounds means oboe callback has to sum values from 20 buffers which means 20 read calls to 20 different LockFreeQueue, this seems scary because those Queue's have also write operations by their FFmpeg decoder thread. Atomic read/writes felt like it could hit the performan – cs guy Nov 23 '20 at 14:36
  • 2
    The real lesson here is that talking about performance without measuring it is just contradicting yourself. Our everyday experience can't inform us much about performance of modern, highly complex CPU cores with extensive and proprietary mechanisms that aim to extract maximum performance out of common code. I've been working on and off on improving performance in an open source project and the only viable way to make sure I'm not full of nonsense is to run the build and verify that it's faster in common use patterns. – Kuba hasn't forgotten Monica Nov 23 '20 at 14:47
  • @UnslanderMonica I agree with `talking about performance without measuring it is just contradicting yourself.` My issue is the only phone I have is a very good phone so testing this kinda pointless. I need to test it on Amazons device lab but thats just not possible at the moment. So I wanted to discover the best way to this rather than optimizing later since its a very core part of my audio engine. – cs guy Nov 23 '20 at 14:55
  • 1
    For your use case using `std::atomic` is unlikely to be a performance bottleneck. It's *far* more likely to be FFmpeg or some other component in the decoding pipeline. As @UnslanderMonica said, you're almost certainly prematurely optimizing. – donturner Nov 23 '20 at 17:56

1 Answers1

3

You can use a thread-safe lock-free queue for reading/writing data. This will avoid the need for mutexes, will be much faster and should fix your "popcorn" issues.

An example implementation can be found here: https://github.com/google/oboe/blob/master/samples/RhythmGame/src/main/cpp/utils/LockFreeQueue.h

donturner
  • 17,867
  • 8
  • 59
  • 81