[LG V20 Themes, Apps, and Mods] thubble:
Sorry if this is the wrong thread to post this in, but I hope it will be helpful to app developers trying to use the V20's DAC.

After some investigation, it looks like the regular Android "MediaPlayer" API works perfectly on the V20: If you feed it any file, it outputs directly to the hardware (bypassing the Android mixer) at the correct sample rate.

I tested using the following code, and running "dumpsys media.audio_flinger" while it was playing:

String filePath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator +"/test1.mp3";
MediaPlayer mp = new MediaPlayer();
mp.setAudioStreamType(AudioManager.STREAM_MUSIC);
File f = new File(filePath);
FileInputStream fIS = new FileInputStream(f);
mp.setDataSource(fIS.getFD());
fIS.close();
mp.prepare();
mp.start();

For instance, here's a "dumpsys media.audio_flinger" while playing a 320kbps, 16-bit, 44.1khz MP3 (MP3 supports hardware decoding, so it's just writing the MP3 data directly to the audio driver which then sends the decoded data to the DAC):

Output thread 0xedea7000 type 4 (OFFLOAD):
Thread name: AudioOut_AD5
I/O handle: 2773
TID: 4006
Standby: no
[b]Sample rate: 44100 Hz[/b]
HAL frame count: 32768
HAL format: 0x1000000 (mp3)
HAL buffer size: 32768 bytes
Channel count: 2
Channel mask: 0x00000003 (front-left, front-right)
Processing format: 0x1000000 (mp3)
Processing frame size: 1 bytes
Pending config events: none
Output device: 0x4 (WIRED_HEADSET)
Input device: 0 (NONE)
Audio source: 0 (default)
Normal frame count: 32768
Last write occurred (msecs): 606
Total writes: 11
Delayed writes: 0
Blocked in write: no
Suspend count: 0
Sink buffer : 0xeec59000
Mixer buffer: 0xeed2d000
Effect buffer: 0xf00af000
Fast track availMask=0xfe
Standby delay ns=1000000000
AudioStreamOut: 0xf0088c08 flags 0x31 (DIRECT|COMPRESS_OFFLOAD|NON_BLOCKING)
Stream volumes in dB: 0:-10, 1:-28, 2:-28, 3:-28, 4:-28, 5:-28, 6:0, 7:-28, 8:-27, 9:-96, 10:0, 11:-28, 12:0, 13:0
Normal mixer raw underrun counters: partial=0 empty=0
1 Tracks of which 1 are active
Name Active Client Type Fmt Chn mask Session fCount S F [b]SRate[/b] L dB R dB Server Main buf Aux Buf Flags UndFrmCnt
none yes 3977 3 01000000 00000003 5921 65536 A 3 [b]44100[/b] 0 0 00040000 0xeec59000 0x0 0x000 0
0 Effect Chains

And a 24-bit, 96khz FLAC (it's decoding in software, and outputing 24/96 PCM directly, again bypassing the mixer):

Output thread 0xee84a000 type 1 (DIRECT):
Thread name: AudioOut_ADD
I/O handle: 2781
TID: 5001
Standby: no
[b]Sample rate: 96000 Hz[/b]
HAL frame count: 3840
[b]HAL format: 0x6 (pcm24)[/b]
HAL buffer size: 23040 bytes
Channel count: 2
Channel mask: 0x00000003 (front-left, front-right)
[b]Processing format: 0x6 (pcm24)[/b]
Processing frame size: 6 bytes
Pending config events: none
Output device: 0x4 (WIRED_HEADSET)
Input device: 0 (NONE)
Audio source: 0 (default)
Normal frame count: 3840
Last write occurred (msecs): 22
Total writes: 44
Delayed writes: 0
Blocked in write: yes
Suspend count: 0
Sink buffer : 0xede8c000
Mixer buffer: 0xee924000
Effect buffer: 0xee988000
Fast track availMask=0xfe
Standby delay ns=1000000000
AudioStreamOut: 0xf0088a48 flags 0x2001 (DIRECT|0x2000)
Stream volumes in dB: 0:-10, 1:-28, 2:-28, 3:-28, 4:-28, 5:-28, 6:0, 7:-28, 8:-27, 9:-96, 10:0, 11:-28, 12:0, 13:0
Normal mixer raw underrun counters: partial=0 empty=0
1 Tracks of which 1 are active
Name Active Client Type Fmt Chn mask Session fCount S F [b]SRate[/b] L dB R dB Server Main buf Aux Buf Flags UndFrmCnt
none yes 4953 3 00000006 00000003 5929 49920 A 3 [b]96000[/b] 0 0 0002A300 0xede8c000 0x0 0x000 0
0 Effect Chains

So, that's the good news: Players that just use the android MediaPlayer to decode and play audio will likely work fine with the V20's DAC. I took a look at LG's default music player (which is confirmed working) in a Java decompiler, and it's not doing anything other than that (other than adding its built-in equalizer, effects, etc).

The bad news is that it seems MediaPlayer is the ONLY accessible API which works properly. Everything else (AudioTrack and OpenSL) will resample everything to 48khz/16-bit and pass it through the Android mixer - even CD-quality audio which is 44.1khz! So, while it's good that MediaPlayer works OK, there's no way to use it to output raw data and handle decoding separately. This limits apps using it to the built-in Android effects and precludes things like better equalizers.

Looking at the actual firmware code, it's likely that the reason MediaPlayer works is because it uses the low-level AudioTrack C++ libraries, which can accept ALL output PCM formats (including 24-bit integer), and LG's drivers correctly detect the hi-res 24-bit formats and route them appropriately. Android's public APIs for AudioTrack and OpenSL can only accept 16-bit integer PCM or 32-bit floating point - and the 32-bit floating point apparently doesn't trigger the right route, and puts everything through the mixer resampled to 48khz.

While this AudioTrack C++ interface isn't publicly documented or accessible, it is possible to use it via dlopen(). After about a week of experimenting to find the right parameters to pass, I got it working! It can directly output 24-bit stereo PCM data in any of the supported sampling rates up to 192khz, in the same way MediaPlayer does. I suspect Poweramp's "hi-res" output driver uses a similar method, and works with regular Snapdragon phones, so hopefully it would just take some small tweaks to get the V20 working.

Here's some example JNI code. It won't work as-is, and has some pseudocode. Any app using it needs a target API version <= 22, as Google purposely broke the dlopen() method in later versions, as they don't want apps doing this (I don't blame them, but there seems to be no other way to use the V20 DAC; hopefully an official version will be provided at some point). It also needs "audio.h" from the Android framework.

#include "audio.h"

/* How data is transferred to AudioTrack
*/
enum transfer_type {
TRANSFER_DEFAULT, // not specified explicitly; determine from the other parameters
TRANSFER_CALLBACK, // callback EVENT_MORE_DATA
TRANSFER_OBTAIN, // call obtainBuffer() and releaseBuffer()
TRANSFER_SYNC, // synchronous write()
TRANSFER_SHARED, // shared memory
};

struct AudioTrack
{
char padding[0x400];
int32_t guard;
};

typedef void (*AT_constructor1)(AudioTrack*);
typedef void (*AT_destructor)(AudioTrack*);
typedef void (*AT_set)(AudioTrack*,
audio_stream_type_t streamType,
uint32_t sampleRate,
audio_format_t format,
audio_channel_mask_t channelMask,
size_t frameCount, // = 0,
audio_output_flags_t flags, // = AUDIO_OUTPUT_FLAG_NONE,
callback_t cbf, // = NULL,
void* user, // = NULL,
int32_t notificationFrames, // = 0,
void* UNUSED,
//const android::sp<IMemory>& sharedBuffer, // = 0,
bool threadCanCallJava, // = false,
audio_session_t sessionId, // = AUDIO_SESSION_ALLOCATE,
transfer_type transferType, // = TRANSFER_DEFAULT,
const audio_offload_info_t *offloadInfo, // = NULL,
uid_t uid, // = -1,
pid_t pid, // = -1,
const audio_attributes_t* pAttributes, // = NULL,
bool doNotReconnect, // = false,
float maxRequiredSpeed); // = 1.0f);
typedef int32_t (*AT_write)(AudioTrack*,
const void*,
int32_t,
bool);
typedef int32_t (*AT_start)(AudioTrack*);
typedef int32_t (*AT_stop)(AudioTrack*);
typedef int32_t (*AT_pause)(AudioTrack*);
typedef int32_t (*AT_flush)(AudioTrack*);

typedef int (*RB_incStrong)(void*, const void*);
typedef int (*RB_decStrong)(void*, const void*);

extern "C"
jstring
Java_com_example_nativehiresoutputtest_MainActivity_audioTrackOut(
JNIEnv *env,
jobject, /* this */ ) {

// re-load all DLLs, including "libmedia", as global instead of local, so we can access them. Doesn't work on API 23+.
void* hdr = dlopen(NULL, RTLD_GLOBAL | RTLD_NOW | RTLD_NOLOAD);

// load c++ functions from loaded DLLs
AT_constructor1 cnstFcn = reinterpret_cast<AT_constructor1>(dlsym(hdr, "_ZN7android10AudioTrackC2Ev"));
AT_destructor dstrFcn = reinterpret_cast<AT_destructor>(dlsym(hdr, "_ZN7android10AudioTrackD0Ev"));
AT_set setFcn = reinterpret_cast<AT_set>(
dlsym(hdr,
"_ZN7android10AudioTrack3setE19audio_stream_type_tj14audio_format_tjj20audio_output_flags_tPFviPvS"
"4_ES4_iRKNS_2spINS_7IMemoryEEEb15audio_session_tNS0_13transfer_typeEPK20audio_offload_info_tiiPK1"
"8audio_attributes_tbf"));
AT_write writeFcn = reinterpret_cast<AT_write>(dlsym(hdr, "_ZN7android10AudioTrack5writeEPKvjb"));
AT_start startFcn = reinterpret_cast<AT_start>(dlsym(hdr, "_ZN7android10AudioTrack5startEv"));
AT_stop stopFcn = reinterpret_cast<AT_stop>(dlsym(hdr, "_ZN7android10AudioTrack4stopEv"));
AT_pause pauseFcn = reinterpret_cast<AT_pause>(dlsym(hdr, "_ZN7android10AudioTrack5pauseEv"));
AT_flush flushFcn = reinterpret_cast<AT_flush>(dlsym(hdr, "_ZN7android10AudioTrack5flushEv"));
RB_incStrong rbisFcn = reinterpret_cast<RB_incStrong>(dlsym(hdr, "_ZNK7android7RefBase9incStrongEPKv"));
RB_decStrong rbdsFcn = reinterpret_cast<RB_decStrong>(dlsym(hdr, "_ZNK7android7RefBase9decStrongEPKv"));

AudioTrack* at = reinterpret_cast<AudioTrack*>(calloc(sizeof(AudioTrack), 1));
at->guard = 0xbaadbaad;
cnstFcn(at);
if (at->guard != 0xbaadbaad)
return env->NewStringUTF("ERROR: constructor overwrote data!");

// mark the AudioTrack (RefBase) strong pointer as referenced.
rbisFcn(at, at);

audio_attributes_t* attributes = reinterpret_cast<audio_attributes_t*>(calloc(sizeof(audio_attributes_t), 1));
attributes->content_type = AUDIO_CONTENT_TYPE_MUSIC;
attributes->source = AUDIO_SOURCE_DEFAULT;
attributes->usage = AUDIO_USAGE_MEDIA;
attributes->flags = 0x0;

void* nullSharedBufferPtr = 0;

// set parameters for the AudioTrack.
setFcn(at,
AUDIO_STREAM_MUSIC,
192000, //sampleRate,
AUDIO_FORMAT_PCM_24_BIT_PACKED,
AUDIO_CHANNEL_OUT_FRONT_LEFT | AUDIO_CHANNEL_OUT_FRONT_RIGHT,
0,
AUDIO_OUTPUT_FLAG_NONE,
NULL, //audio_callback,
NULL, //userData,
0,
&nullSharedBufferPtr,
true,
AUDIO_SESSION_ALLOCATE,
TRANSFER_DEFAULT,
NULL,
getuid(),
getpid(),
attributes,
false,
1.0f
);

free(attributes);

// start the AudioTrack playing.
startFcn(at);

// Write PCM (24-bit stereo packed) data. (PSEUDOCODE, assumes sampleData exists)
while (sampleRemaining > 0)
{
// synchronous write.
int bytesWritten = writeFcn(at, &sampleData[idx], sampleRemaining, true);
if (bytesWritten < 0) ERROR();
sampleRemaining -= bytesWritten;
idx += bytesWritten;
}

// stop the AudioTrack playing, and flush the remaining data.
stopFcn(at);
flushFcn(at);

// mark the AudioTrack (RefBase) strong pointer as de-referenced.
rbdsFcn(at, at);
//dstrFcn(at); // destructor seems to hang
free(at);
}

If anyone is working on a media player app and wants to use the above code in any form, for any reason, please feel free. I definitely don't have time to write one myself (and I'm not really an Android developer) - the important thing is to get the DAC working properly!

I've only tested on my phone (H990ds, update v10d), so I'm not sure if firmware/hardware differences would affect this.