AudioTrack samplerate inconsistencies
Asked Answered
C

1

30

Using AudioTrack for playback, I sometimes need to resample audio that do not conform to the sample rates supported by AudioTrack. In doing so, I need to determine the maximum sample rate supported by AudioTrack, under the current device, under the current audio configuration.

Due to the allowable sample rates for AudioTrack being poorly documented, I decided to snoop around the source code for AudioTrack and found this staggering line:

private static final int SAMPLE_RATE_HZ_MAX = 96000;

it would seem that the AudioTrack instance is applying a hard limit of 96 KHz regardless of the actual playback capabilities of the device.

More confusing is in the AudioFormat class, in which I pass to the constructor (API 21) of AudioTrack, which contains this line:

if ((sampleRate <= 0) || (sampleRate > 192000)) {

in it's setSampleRate() method. Now that's a hard limit of 192 KHz. So, passing > 192 KHz into AudioFormat (or it's builder) will result in IllegalArgumentException from AudioFormat and passing a configured 192 KHz < x < 96 KHz sample rate AudioFormat into AudioTrack will also throw an IllegalArgumentException.


What I found, by far, most confusing is the method getNativeOutputSampleRate() in AudioTrack which actually does return the correct output sampling rate (well, not much surprise given it's ran directly from the native layer, but so inconsistent).

And just to top it off, the method setPlaybackRate() which claims:

The valid sample rate range is from 1 Hz to twice the value returned by getNativeOutputSampleRate(int).

And indeed, I did try it, and it works? Consider the following snippet:

int nativeRate = AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC);

android.util.Log.i("UI", "Native stream rate: " + nativeRate + " Hz");

// Build audio attributes

AudioAttributes.Builder attribBuilder = new AudioAttributes.Builder();

attribBuilder.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC);
attribBuilder.setUsage(AudioAttributes.USAGE_MEDIA);

AudioAttributes attrib = attribBuilder.build();

// Build audio format

AudioFormat.Builder afBuilder = new AudioFormat.Builder();

afBuilder.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO);
afBuilder.setEncoding(AudioFormat.ENCODING_PCM_16BIT);
afBuilder.setSampleRate(nativeRate);

try{
    AudioTrack trackTest = new AudioTrack(attrib, afBuilder.build(), nativeRate, AudioTrack.MODE_STREAM, 0);

    android.util.Log.i("UI", "Track created successfully (direct)");
}catch(Exception ex){
    android.util.Log.w("UI", "Failed to create AudioTrack at native rate!");

    // Use a random supported samplerate to get pass constructor
    afBuilder.setSampleRate(48000);

    try{
        AudioTrack trackTest = new AudioTrack(attrib, afBuilder.build(), nativeRate, AudioTrack.MODE_STREAM, 0);

        trackTest.setPlaybackRate(nativeRate);

        android.util.Log.i("UI", "Track created successfully (indirect)");
    }catch(Exception e){
        android.util.Log.w("UI", "Failed to create AudioTrack at 48 KHz");
    }
}

following the program flow, when the native sampling rate is < 96 KHz, the code prints out:

Native stream rate: 48000 Hz
Track created successfully (direct)

but, when I hook up an external DAC with playback capabilities up to 192 KHz, I get:

Native stream rate: 192000 Hz
Failed to create AudioTrack at native rate!
Track created successfully (indirect)

What is with these inconsistencies? And is, setPlaybackRate() identical to the sample rate passed into the constructor?

Coati answered 1/5, 2015 at 7:34 Comment(2)
You might consider submitting an issue to code.google.com/p/android/issues/list about these inconsistencies (which, I can confirm, are also present in the source of API 22). Concerning setPlaybackRate(), it clearly bypasses the (inconsistent) checks you mentioned and directly set the playback in the native side. Also, it might be interesting to look at the history of [core/jni/android_media_AudioTrack.cpp][1] [1]: android.googlesource.com/platform/frameworks/base/+/88e209d%5E!Inexpressive
@AladinQ Thank you for the link to the source, I didn't know they housed native layer sources, that should help debug the problem. I'll hold off listing it as an issue given that there may be nothing wrong (simply documentation errors or undocumented behavior). It's happened before... I'll see what I can derive from the native source.Coati
U
2

Currently most Android phones on the market support only one sampling rate. I believe certain Samsungs play at 48kHz and almost all others play at 44.1kHz. These values are dictated by the hardware, and although there is a facility to change the native rate, it's function is to secondarily to future proof, but primarily to 2. resample ALL audio at runtime in SOFTWARE. This is an expensive task, and also somewhat destructive. The reason why there is a hard limit at 192kHz (= 2 * 96kHz) is likely because sending more than twice the maximum frequency (96kHz) is a massive waste of resources as you may as well just throw away every second sample to effectively downsample by a factor of 2 until you are in that range.

Specifying a non-native sampling rate is best avoided. It will be resampled in software and is at best a waste of resources and at worst a source of lag.

Or hear it from a Google engineer

Usher answered 20/7, 2015 at 18:6 Comment(2)
Appreciate the link, I'll watch it very soon. However, as I've mentioned, Android devices are capable of projecting audio to an external source such as an USB external DAC. I own such a DAC and when I hook it up, Android negotiates a native rate of 192 KHz (as indicative through system logs as well as an sample rate indicator on my DAC). The effort of determining native rate is such to minimize the performance cost of resampling when not necessary. Source files at 96 KHz+ should be kept untouched (i.e. straight through without resample) if at all possible, to maximize battery life.Coati
Ah, I did think I might have missed something when you were talking about the USB. gitlab.com/SaberMod/pa-android-frameworks-base/commit/… Haha, the 48kHz limit was "fixed" last year, fixed to 96kHz I suppose Aladin Q's comment is the correct answer! Because of this: if (sampleRateInHz < SAMPLE_RATE_HZ_MIN || sampleRateInHz > SAMPLE_RATE_HZ_MAX) { throw new IllegalArgumentException(sampleRateInHz + "Hz is not a supported sample rate."); }Usher

© 2022 - 2024 — McMap. All rights reserved.