Changing buffer size on Android


#1

Hi,

I have been struggling for a while with a lower quality playback of the Sampler classes on Android (small clicks), and I think it comes from not being able to serve the default 2048 buffer fast enough. I have added a timer in my proc and I often go above the 46-47ms required to process a 2048 bytes buffer in a 44100 Hz stereo stream (are my maths correct?).

The process is difficult to optimize because I must serve MIDI events from collections and mix different tracks (addFrom seems to be heavier on resources, but I can’t use copyFrom).

I have tried changing the buffer size through the AudioDeviceManager::setAudioDeviceSetup() method, but the system still reports a 2048 buffer. Do you know if that can be changed?

Thanks a lot.

Mariano


#2

If you can’t keep up with a 2048 sample buffer, I don’t really think that increasing it will help much… It’s only when you have very small buffer sizes that the overhead of making each callback starts to become significant.


#3

Well, it’s a small device, so the processing takes much longer. For instance, I never had an issue with AudioSampleBuffer addFrom. But switching to copyFrom significantly reduces the clics. Though I need to mix my tracks, so that’s not a solution.

Do you know if the audio devices on Android phones can be set with a different buffer size? I have polled the current device on mine and get:

07-05 17:03:38.690: INFO/Juce(15436): **** Available audio Device Buffer : 5312
07-05 17:03:38.690: INFO/Juce(15436): **** Available audio Device Buffer : 5184
07-05 17:03:38.690: INFO/Juce(15436): **** Available audio Device Buffer : 5056
07-05 17:03:38.690: INFO/Juce(15436): **** Available audio Device Buffer : 4928
07-05 17:03:38.690: INFO/Juce(15436): **** Available audio Device Buffer : 4800
07-05 17:03:38.690: INFO/Juce(15436): **** Available audio Device Buffer : 4672
07-05 17:03:38.690: INFO/Juce(15436): **** Available audio Device Buffer : 4544
07-05 17:03:38.690: INFO/Juce(15436): **** Available audio Device Buffer : 4416
07-05 17:03:38.690: INFO/Juce(15436): **** Available audio Device Buffer : 4288
07-05 17:03:38.690: INFO/Juce(15436): **** Available audio Device Buffer : 4160

But when I try to set it to say 5312, I still get the line below as:

07-05 17:03:38.815: DEBUG/AudioHardwareALSA(2379): Buffer size: 2048

Do we need to reinit the device after changing the buffer size, or something like that?


#4

I really don’t know, but you can’t expect all devices to be able to manage big buffers like that, so you probably need a better solution. Have you considered having a background thread feeding a circular buffer, and playing the result?


#5

I was trying to avoid that, but I guess that’s inevitable. Thanks for the suggestion.


#6

Finally I realized that I could do mono sampling, because my samples were anyway mono ( :oops: ). But now the result is that I hear a perfect playback on the left, and one with the same clicks on the right.

Jules, I’ll need your help here. I do my device initialisation as follows:

	deviceManager.initialise (0, 1, 0, true, String::empty, 0);

so throughout my app I’m dealing with a single-channel output buffer. But I guess at one point that single buffer must be copied to some stereo output. Where is that done, and do you see any reason why that should be adding such clicks?

Just for info, that only happens on the actual Android device, not on emulators.

Thanks for your help!

Mariano


#7

Actually when I run it on my Desktop (dsound device), sound quality is perfect, but right ear volume is extremely low.


#8

Just because you ask it for 1 channel doesn’t mean that’s what it actually returns - if it’s not possible to open a mono device, it’s probably giving you a stereo one, and you’re not filling the second channel.


#9

well, in my AudioSource callback (getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill)), I am logging the bufferToFill.buffer->getNumChannels() and it always returns 1.


#10

Finally I could solve the problem by opening a stereo device, but only filling up the first channel (clearing both before that). Surprisingly, if only that is done, Android still plays both channels, but the right one has these cracks and pops.

So I had to do a quick-and-dirty fix in juce_android_Audio.cpp to force the first channel on any output channels, as follows:

            if (outputDevice != nullptr)
            {
                if (threadShouldExit())
                    break;

                jshort* const dest = env->GetShortArrayElements (audioBuffer, 0);

                for (int chan = 0; chan < numDeviceOutputChannels; ++chan)
                {
					AudioData::Pointer <AudioData::Int16, AudioData::NativeEndian, AudioData::Interleaved, AudioData::NonConst> d (dest + chan, numDeviceOutputChannels);
					AudioData::Pointer <AudioData::Float32, AudioData::NativeEndian, AudioData::NonInterleaved, AudioData::Const> s (outputChannelBuffer.getSampleData (***0***));
					d.convertSamples (s, actualBufferSize);
                }

                env->ReleaseShortArrayElements (audioBuffer, dest, 0);
                jint numWritten = env->CallIntMethod (outputDevice, android.audioTrackWrite, audioBuffer, 0, actualBufferSize * numDeviceOutputChannels);

                if (numWritten < actualBufferSize * numDeviceOutputChannels)
                {
                    DBG ("Audio write underrun! " << numWritten);
                }
            }
        }

… and this works perfectly.

I’m still puzzled as to where the cracks/pops come from. Maybe something in the chain of callbacks. Jules does it give you any hint?


#11

Oh, it’s obviously just a blooper in that bit of code - the device has two channels, your buffer only has one, but it’s not checking that when it copies the data. It just needs to do this:

[code] for (int chan = 0; chan < numDeviceOutputChannels; ++chan)
{
AudioData::Pointer <AudioData::Int16, AudioData::NativeEndian, AudioData::Interleaved, AudioData::NonConst> d (dest + chan, numDeviceOutputChannels);

                const float* const sourceChanData = outputChannelBuffer.getSampleData (jmin (chan, outputChannelBuffer.getNumChannels() - 1));
                AudioData::Pointer <AudioData::Float32, AudioData::NativeEndian, AudioData::NonInterleaved, AudioData::Const> s (sourceChanData);
                d.convertSamples (s, actualBufferSize);
            }

[/code]

Thanks for spotting it!


#12

Well, while that correction is welcome to avoid problems when people use mono buffers, I don’t think it was the problem I faced, because I was clearly using a stereo buffer, but only “filling” the first channel and leaving the second one empty (through initial clear()).

Strangely, both channels were heard on the Android app, but the right side one had light cracks and pops. Anyway, I found that workaround (using both times the first channel) so I’m not stuck with it, but in case someone once finds an explanation, I’d be happy to hear it.

In short:

In my AudioSource callback (getNextAudioBlock) I clear the whole buffer with bufferToFill.clearActiveBufferRegion(), then fill up only the first channel with my audio data (through SamplerVoice). On desktop (DirectSound device), I only hear left ear (very slight sound on right ear, but I suspect the headphones to do that). On Android device however, I hear both ears, but left one has slight clicks/pops.

If I then go to juce_android_Audio.cpp and limit the loop above to 1 iteration, then I only hear left ear. So only solution that worked was to copy the same first channel on both output device channels; that worked perfectly, though of course it’s far from ideal, specially if you need stereo (not in this case).

Sorry for all the trouble. Again, there’s no urgency here. Just trying to understand where those mysterious cracks come from (possibly somewhere between the audio source getNextAudioBlock and then original AndroidAudioIODevice thread)


#13

Hi Swar (& Jules),

Out of curiosity, I tested what you are saying and got the same results. I did some extra tests by hard-coding a sine wave in place of the callback (to make sure the issue was not somewhere after the callback). Both channels of the buffer have the same sine data, yet I would get a constant pop on the right channel (periodic, which tells me there might be an “off by one” sample issue somewhere).
Then I proceeded to only copy (and convert) from the first channel of the buffer, as you suggested above, and the issue went away. Then I decided to “only copy from the second channel” and I got the pops on left and right channels.
I haven’t solved the problem, but we can be sure that the issue is not in the callback code, the issue is not in the env cal to write the data, and the issue is not in the conversion routines, as they are agnostic to what the channel you are giving them is, they just get a pointer to the data.
If all that is out of the question, the only thing that remains a possible problem is the buffer. I need to further explore this, but I’m posting my findings in case anyone has a thought.

Cheers.