Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for LHDC V3 A2DP source and sink #672

Merged
merged 2 commits into from
Nov 14, 2024
Merged

Conversation

anonymix007
Copy link
Contributor

@anonymix007 anonymix007 commented Nov 12, 2023

Closes #588

Source does not work, only some buzzing noise is produced instead of music. I've tried saving pcm.data as raw pcm and it seems to be incorrect already.
Library expects interleaved 24 or 16 bit data depending on selected bit depth, so probably related to the use of BA_TRANSPORT_PCM_FORMAT_S24_4LE.
It seems to be somewhat working now, but not yet perfect.

@anonymix007 anonymix007 force-pushed the lhdc branch 3 times, most recently from 971397e to b8598eb Compare November 12, 2023 20:32
Copy link

codecov bot commented Nov 13, 2023

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 70.49%. Comparing base (59c953a) to head (1002906).
Report is 2 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #672      +/-   ##
==========================================
+ Coverage   70.46%   70.49%   +0.02%     
==========================================
  Files          96       96              
  Lines       15997    15999       +2     
  Branches     2509     2510       +1     
==========================================
+ Hits        11272    11278       +6     
+ Misses       4608     4604       -4     
  Partials      117      117              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

src/audio.c Fixed Show fixed Hide fixed
@anonymix007 anonymix007 force-pushed the lhdc branch 4 times, most recently from 854ed5a to 568280b Compare November 15, 2023 14:03
@anonymix007
Copy link
Contributor Author

I still have no idea why this happens even with BA_TRANSPORT_PCM_FORMAT_S32_4LE. Apparently, some variables in a2dp_lhdc_enc_thread aren't what I thought. liblhdcBT_enc.so built from same sources works fine in Android. PCM dump appears to have same issues as sound in headphones. liblhdcBT_enc.so built from same sources works fine in Android, so I can't see what's wrong.

@arkq
Copy link
Owner

arkq commented Nov 19, 2023

I'd like to test it locally. From where I can get the ldhcBT-dec/enc libraries? If this PR were to go to master I need to have a possibility to test the code for potential regressions.

@anonymix007
Copy link
Contributor Author

anonymix007 commented Nov 19, 2023

UPDATED Nov 13, 2024
libs.zip
headers.zip

Source code for libraries will be available later.

LHDC_INCLUDE_DIR=/path/to/headers

export LHDC_DEC_CFLAGS="-I$LHDC_INCLUDE_DIR/liblhdcdec/include -I$LHDC_INCLUDE_DIR/liblhdcdec/inc -L$LHDC_INCLUDE_DIR"
export LHDC_DEC_LIBS="-llhdcBT_dec"
export LHDC_ENC_CFLAGS="-I$LHDC_INCLUDE_DIR/liblhdc/include -I$LHDC_INCLUDE_DIR/liblhdc/inc -L$LHDC_INCLUDE_DIR"
export LHDC_ENC_LIBS="-llhdcBT_enc"

autoreconf --install
mkdir build && cd build
../configure --enable-aac --enable-lhdc --enable-debug
make
sudo make install

arkq
arkq previously approved these changes Nov 19, 2023
src/a2dp-lhdc.c Outdated Show resolved Hide resolved
@anonymix007 anonymix007 changed the title Add support for LHDC V3 A2DP sink Add support for LHDC V3 A2DP source and sink Nov 26, 2023
src/shared/a2dp-codecs.h Outdated Show resolved Hide resolved
@arkq
Copy link
Owner

arkq commented Jan 9, 2024

In the last few days I've done long overdue refactoring around A2DP codecs. Now, the logic is kept mostly in one file, which should simplify maintenance and addition of new codecs. Due to extensive changes, I've rebased your work on top of current master. The code seems to compile, but I've got no time yet to test whether it works :D

Also, I've left some commented code (I'm not sure whether it will be required or not). In the A2DP configuration for LHDC you've added #define LHDC_CHANNEL_MODE_STEREO 0x03 which suggests that the channel mode can be selected, is that true? If yes, the channel mode selection will need to be done right in a2dp_lhdc_configuration_select() and a2dp_lhdc_configuration_check() functions, otherwise, maybe the define should be removed?

@anonymix007
Copy link
Contributor Author

Most constants were directly copied from AOSP patches. This must be a remnant of that. Seems to be unused, so might be removed probably.

The commented out code seems to be for channel mode, so could be removed as well.

Another issue would be to implement LLAC/V3/V4 selection logic, which is basically the following:

  1. If LLAC is supported and LLAC-friendly bitrate is selected (ABR or < 500 kbps) then default to 48 kHz and enable LLAC (V4 flag needs to be set to 0).
  2. If LLAC is not supported then enable V4 if it's supported, otherwise go for V3.

At least sink seems to still work.

@anonymix007
Copy link
Contributor Author

anonymix007 commented Mar 1, 2024

I've tried to run it once again, but now it doesn't seem to work (even AAC doesn't work).

$ bluealsa-aplay
bluealsa-aplay: [8934] W: ../../../utils/aplay/aplay.c:1278: Couldn't get BlueALSA PCM list: Sender is not authorized to send message

or from root:

bluealsa-aplay: [9413] D: ../../../utils/aplay/aplay.c:876: Creating IO worker XX:XX:XX:XX:XX:XX
bluealsa-aplay: [9413] D: ../../../utils/aplay/aplay.c:1287: Starting main loop
bluealsa-aplay: [9414] D: ../../../utils/aplay/aplay.c:542: Opening BlueALSA source PCM: /org/bluealsa/hci0/dev_XX_XX_XX_XX_XX_XX/a2dpsnk/source
bluealsa-aplay: [9414] D: ../../../utils/aplay/aplay.c:568: Starting IO loop
bluealsa-aplay: [9414] D: ../../../utils/aplay/aplay.c:692: Opening ALSA playback PCM: name=default channels=2 rate=44100
bluealsa-aplay: [9414] W: ../../../utils/aplay/aplay.c:696: Couldn't open ALSA playback PCM: Open PCM: Host is down

Probably related to #681, though I haven't recompiled PipeWire, just renamed /usr/lib/spa-0.2/bluez5. My phone also says "Problem connecting. Turn device off & back on", but I definitely can see audio packets in WireShark.

UPD
This works for AAC:

sudo bluealsa-cli open /org/bluealsa/hci0/dev_XX_XX_XX_XX_XX_XX/a2dpsnk/source | play -e signed-integer -b 16 -c 2 -r 44100 -t raw -

But it segfaults with LHDC:

#0  0x00007ffff768632c in ??? () at /usr/lib/libc.so.6
#1  0x00007ffff76356c8 in raise () at /usr/lib/libc.so.6
#2  0x00007ffff761d4b8 in abort () at /usr/lib/libc.so.6
#3  0x00007ffff761e395 in ??? () at /usr/lib/libc.so.6
#4  0x00007ffff76902a7 in ??? () at /usr/lib/libc.so.6
#5  0x00007ffff7690de4 in ??? () at /usr/lib/libc.so.6
#6  0x00007ffff7690f2b in ??? () at /usr/lib/libc.so.6
#7  0x00007ffff7691fdd in ??? () at /usr/lib/libc.so.6
#8  0x00007ffff7692669 in ??? () at /usr/lib/libc.so.6
#9  0x00007ffff7694e93 in free () at /usr/lib/libc.so.6
#10 0x000055555555ff81 in ffb_free (ffb=0x7ffff4bffc70) at ../../src/shared/ffb.c:46
#11 0x000055555557b1d7 in a2dp_lhdc_dec_thread (t_pcm=0x5555555c08f0) at ../../src/a2dp-lhdc.c:347
#12 0x00007ffff768455a in ??? () at /usr/lib/libc.so.6
#13 0x00007ffff7701a3c in ??? () at /usr/lib/libc.so.6

@anonymix007 anonymix007 force-pushed the lhdc branch 2 times, most recently from ce802b3 to 12725d6 Compare March 1, 2024 15:30
@arkq
Copy link
Owner

arkq commented Apr 4, 2024

But it segfaults with LHDC

It seems that the problem is somewhere within encoder code. The reason of abort is heap corruption due to memmove() beyond allocated memory in ffb_t bt buffer. I've tested with new library and the old one, but the result is all the same:

==1263640== Invalid write of size 2
==1263640==    at 0x48529E3: memmove (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==1263640==    by 0x4F1C7E4: ??? (in /mnt/storage1t/Downloads/balsa/build/lhdc/liblhdcBT_enc.new.so)
==1263640==    by 0x125B5E: a2dp_lhdc_enc_thread (a2dp-lhdc.c:179) 
==1263640==    by 0x5309AC2: start_thread (pthread_create.c:442)
==1263640==    by 0x539AA03: clone (clone.S:100)
==1263640==  Address 0x5af146a is 1,002 bytes inside a block of size 1,003 alloc'd
==1263640==    at 0x48487A9: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==1263640==    by 0x111C9C: ffb_init (ffb.c:26) 
==1263640==    by 0x125801: a2dp_lhdc_enc_thread (a2dp-lhdc.c:125) 
==1263640==    by 0x5309AC2: start_thread (pthread_create.c:442) 
==1263640==    by 0x539AA03: clone (clone.S:100)
==1263640==
==1263640== Invalid write of size 1
==1263640==    at 0x4852A13: memmove (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==1263640==    by 0x4F1C7E4: ??? (in /mnt/storage1t/Downloads/balsa/build/lhdc/liblhdcBT_enc.new.so) 
==1263640==    by 0x125B5E: a2dp_lhdc_enc_thread (a2dp-lhdc.c:179) 
==1263640==    by 0x5309AC2: start_thread (pthread_create.c:442)
==1263640==    by 0x539AA03: clone (clone.S:100) 
==1263640==  Address 0x5af1478 is 13 bytes after a block of size 1,003 alloc'd
==1263640==    at 0x48487A9: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==1263640==    by 0x111C9C: ffb_init (ffb.c:26) 
==1263640==    by 0x125801: a2dp_lhdc_enc_thread (a2dp-lhdc.c:125) 
==1263640==    by 0x5309AC2: start_thread (pthread_create.c:442)
==1263640==    by 0x539AA03: clone (clone.S:100) 

@anonymix007
Copy link
Contributor Author

I was testing the decoder, but encoder having the same issue is really strange. None of them should call memmove directly (except third-party submodules). I'll look into that.

Do you have any suggestions for coexisting with pipewire?

@arkq
Copy link
Owner

arkq commented Apr 4, 2024

I was testing the decoder, but encoder having the same issue is really strange.

I've added a test code which encodes PCM and then decodes it, so due to encoder failure I was not able to check decoder, not yet :D

Do you have any suggestions for coexisting with pipewire?

What do you mean "coexisting"? If you have BlueALSA and PipeWire running on the same host at the same time, then you should disable (somehow) Bluetooth support in PipeWire (I'm not sure whether renaming BlueZ plugin is sufficient, it might be though). Then, remove paired device and pair it again, so BlueZ will not reuse cached SDP information.

EDIT:
In order to test encoder/decoder with IO test you can run it with valgrind like this:

make check TESTS=
CK_FORK=no valgrind ./test/test-io lhdc-v3

@anonymix007
Copy link
Contributor Author

What do you mean "coexisting"?

bluealsa-aplay doesn't play any sound: Couldn't open ALSA playback PCM: Open PCM: Host is down

Though, I guess, for debugging this issue I might just run that test.

@arkq
Copy link
Owner

arkq commented Apr 4, 2024

bluealsa-aplay doesn't play any sound: Couldn't open ALSA playback PCM: Open PCM: Host is down

I guess that you have to use PCM playback which is not grabbed by PipeWire or use some playback PCM exposed by PipeWire itself (if there is any). However, I'm not PipeWire user (not yet), so I've got zero knowledge about PipeWire configuration. Anyway, the error code "Host is down" is kinda strange...

@anonymix007
Copy link
Contributor Author

Apparently, it doesn't like the following line:

memcpy(out_put, enc, handle->host_mtu_size);

And this is exactly what original library does.

So this, per say, is not a library error. Should be fixed now.

src/a2dp-lhdc.c Show resolved Hide resolved
@arkq
Copy link
Owner

arkq commented Apr 5, 2024

So this, per say, is not a library error. Should be fixed now.

OK, now the test passes but only with the old version of the library. With the new one (UPDATED Mar 1, 2024) encoder produces only 78 bytes per packet, where the old one produces 270 bytes. It looks like the output is truncated because these 78 bytes matches with the first 78 bytes in the packet from the old encoder.

@arkq
Copy link
Owner

arkq commented Nov 13, 2024

@anonymix007 Do you have any plans of opensourcing the library? I'm OK with the library being closed-sorce and even commercial, but the case is that I'd like to merge this PR sooner or later because syncing it is a burden I'd like to avoid. However, I need some way of verifying whether the codec code compiles and the unit test passes. I'm mostly developing on arm64, so it would be more convenient for me to have arm64 libraries instead of x64 (sometimes I'm doing some checks on x64, so such version is also required). If you don't want to opensource the lib and you don't want to release arm64 builds to the public, could you at least send them to me (on my gmail address)?

If there will be no way for me to verify the code during further development of bluez-alsa I'm not going to merge it, because it would be impossible for me to maintain it... I hope that you understand that :)

@anonymix007
Copy link
Contributor Author

anonymix007 commented Nov 13, 2024

Yes, I'm going to publish all the sources once V5 encoding library starts working. Maybe I should publish V2/V3/V4/LLAC already even in the current state, but I haven't decided yet. I kinda want to make them at ABI-compatible with latest official libraries (4.1.0). Encoder is more or less at parity with 4.0.6p1, while decoder is in a worse state being only a bit better than 4.0.2, but the latest official LHDC libraries aren't available anywhere.

Sure, uploaded aarch64 builds along with the same x86-64 versions to #672 (comment). Please let me know if they work, these were compiled on aarch64 (X Elite) without any special flags.

I wasn't able to make bluez-alsa work at all last time I checked, so I shifted efforts to pipewire and ExtA2DP implementations and both worked fine, so you may use them for tests.

@arkq
Copy link
Owner

arkq commented Nov 13, 2024

Please let me know if they work

I was able to compile LHDC support and run tests, but it seems that something is not right with the encoder/decoder itself. When running IO test, the encoder produced payloads with lengths: 266, 262, 250, 230, 226, 222, 222, 214 So, it's strange that payload length is not constant. After decoding it I have almost all silence...

I can see one warning reported by the library:

WARNING: cal_frame_size_and_frames_in_packet: max_mtu_limit(989)

And yes, I've used MTU greater than that in the IO test, so maybe that's the case...

Making MTU smaller yields different warning:

WARNING: cal_frame_size_and_frames_in_packet: max_mtu_limit(789)

I will have to verify with old x64 libraries to see whether the problem is on bluez-alsa side or maybe with the arm64 version, or in general with your new libraries...

EDIT:
It seems that I've broken something when syncing with master, because commit 6487760 seems to work.... or maybe not...

@arkq
Copy link
Owner

arkq commented Nov 13, 2024

@anonymix007 I've been trying to figure out what is wrong with the encoder/decoder, so I've created a simple recoder which feeds audio through lhdc_encode and lhdc_decode. Unfortunately, the code does not work as expected (logic is copied from the src/a2dp-lhdc.c). If you have some time, could you have a look at it? Maybe you will find out what is wrong with the usage of LHDC API.

You can compile the code from my https://github.com/arkq/bluez-alsa/tree/lhdc branch as follows:

  1. Make sure that you have libsndfile installed: pkg-config sndfile --cflags
  2. Setup env vars required for your libs
  3. Configure bluez-alsa with: ../configure --enable-test --enable-lhdc
  4. Compile tests: make check TESTS=
  5. Run ./test/lhdcrecode some-2-channel-audio.ogg (audio is read via libsndfile, so mp3, wav and other formats are supported)
  6. This program will produce recoded.wav (which should be the same as original, but unfortunately is not....)

I've check with valgrind and it reports lots of Conditional jump or move depends on uninitialised value warnings in your lib. But it might be due to invalid API usage.... For the record, I'm testing it on arm64.

@arkq
Copy link
Owner

arkq commented Nov 13, 2024

Also, I have another question. Is the decoder thread safe? I see that there is int lhdcBT_dec_deinit_decoder(void); which looks not-thread-safe (if it uses some static internal storage instead of e.g. thread_local).

@anonymix007
Copy link
Contributor Author

I'll take a look at these issues.

Is the decoder thread safe

I'm quite sure it is not. However, there's not much I can do about it without changing ABI. This is what the original library does. I guess I can provide separate thread-safe API version if it's really needed.

@anonymix007
Copy link
Contributor Author

I wrote a few simple utils for testing my library, you may refer to them for the library usage.
These were verified to be working.
lhdcenc.c, lhdcenc_stereo.c are used for encoding (difference is that the latter uses lhdcBT_encode_stereo API) specially named .wav files.
lhdcdec.c is used for decoding of whatever is produced by lhdcenc*.c.
lhdc-parser.c is a simple yet functional LHDC V2/V3/V4 frame header parser.
lhdc-utils.zip

@anonymix007
Copy link
Contributor Author

Your test program is wrong in many ways, in fact, it doesn't even compile (though I tried compiling it alongside liblhdc, not in bluez-alsa):

lhdcrecode.c: In functionmain’:
lhdcrecode.c:186:50: error: passing argument 2 oflhdcBT_encode_stereofrom incompatible pointer type [-Wincompatible-pointer-types]
  186 |                 if (lhdcBT_encode_stereo(handle, pcm_ch1, pcm_ch2, encoded_data.data, &encoded, &frames) < 0) {
      |                                                  ^~~~~~~
      |                                                  |
      |                                                  int16_t * {aka short int *}
In file included from lhdcrecode.c:10:
liblhdc/inc/lhdcBT.h:359:58: note: expectedint *but argument is of typeint16_t *’ {aka ‘short int *’}
  359 | int lhdcBT_encode_stereo(HANDLE_LHDC_BT hLhdcParam, int *left_pcm, int *right_pcm, unsigned char *out_put, uint32_t *written, uint32_t *out_frames);
      |                                                     ~~~~~^~~~~~~~
lhdcrecode.c:186:59: error: passing argument 3 oflhdcBT_encode_stereofrom incompatible pointer type [-Wincompatible-pointer-types]
  186 |                 if (lhdcBT_encode_stereo(handle, pcm_ch1, pcm_ch2, encoded_data.data, &encoded, &frames) < 0) {
      |                                                           ^~~~~~~
      |                                                           |
      |                                                           int16_t * {aka short int *}
liblhdc/inc/lhdcBT.h:359:73: note: expectedint *but argument is of typeint16_t *’ {aka ‘short int *’}
  359 | ncode_stereo(HANDLE_LHDC_BT hLhdcParam, int *left_pcm, int *right_pcm, unsigned char *out_put, uint32_t *written, uint32_t *out_frames);
      |                                                        ~~~~~^~~~~~~~~

lhdcrecode.c:209:35: error: passing argument 1 oflhdcBT_dec_decodefrom incompatible pointer type [-Wincompatible-pointer-types]
  209 |                 lhdcBT_dec_decode(&encoded_data, encoded_data_len, decoded_pcm, &decoded_len, bit_depth);
      |                                   ^~~~~~~~~~~~~
      |                                   |
      |                                   struct <anonymous> *
In file included from lhdcrecode.c:11:
liblhdcdec/inc/lhdcBT_dec.h:24:38: note: expectedconst uint8_t *’ {akaconst unsigned char *’} but argument is of typestruct <anonymous> *24 | int lhdcBT_dec_decode(const uint8_t *frameData, uint32_t frameBytes, uint8_t* pcmData, uint32_t* pcmBytes, uint32_t bits_depth);
      |                       ~~~~~~~~~~~~~~~^~~~~~~~~
lhdcrecode.c:209:68: error: passing argument 3 oflhdcBT_dec_decodefrom incompatible pointer type [-Wincompatible-pointer-types]
  209 |          lhdcBT_dec_decode(&encoded_data, encoded_data_len, decoded_pcm, &decoded_len, bit_depth);
      |                                                             ^~~~~~~~~~~
      |                                                             |
      |                                                             int16_t * {aka short int *}

liblhdcdec/inc/lhdcBT_dec.h:24:79: note: expecteduint8_t *’ {aka ‘unsigned char *’} but argument is of typeint16_t *’ {aka ‘short int *’}
   24 | ode(const uint8_t *frameData, uint32_t frameBytes, uint8_t* pcmData, uint32_t* pcmBytes, uint32_t bits_depth);
      |                                                    ~~~~~~~~~^~~~~~~

With these warnings and a few other logic errors fixed, it appears to be working.
0001-Fix-LHDC-recode.zip
Screenshot from 2024-11-14 02-42-27
Screenshot from 2024-11-14 02-42-10

When running IO test, the encoder produced payloads with lengths: 266, 262, 250, 230, 226, 222, 222, 214 So, it's strange that payload length is not constant.

It is not. LHDC V2/V3/V4 are quite similar to LossyFLAC/LossyWavpack, all are by default VBR. Bitrate only defines the upper limit, actual bitrate might be lower. For example, it'll always encode "constant frames" into 12 bytes or something about it. I think you may set need_padding in lhdcBT_init_encoder to make it zero-pad the frames, but there isn't much sense in doing that for real use and I don't think this was extensively tested.

I can see one warning reported by the library:
WARNING: cal_frame_size_and_frames_in_packet: max_mtu_limit(989)

I guess I shouldn't have made this a warning... This is normal, library just reports the actual MTU used internally.

For the record, I'm testing it on arm64.

And so did I. Fortunately, I now have a good arm64 machine so it's quite easy for me to test it.

This program will produce recoded.wav (which should be the same as original, but unfortunately is not....)

I'm quite sure it won't be in most cases, though it should sound the same. LHDC is a lossy codec.

@arkq
Copy link
Owner

arkq commented Nov 14, 2024

Your test program is wrong in many ways, in fact, it doesn't even compile

It might be, however, it should compile without -Werror. Anyway, thanks for sending your test encoder and decoder. I was able to compile it and they recode audio correctly (the implementation in the lhdcrecode.c still has some bug...). I will try to compare a2d-lhdc.c implementation with your apps to see what is not right.

I have more questions, though (in order to better understand the API). What is the purpose of the bitPerSample in lhdcBT_init_encoder() and bits_depth in lhdcBT_dec_decode(). I thought that it controls the bit-depth of PCM input/ouput samples (i.e. 16 := s16_le, 24 := s24_le (or s24_4le). But apparently I was wrong :D So, regardless of the bit-depth, the lhdcBT_encode_stereo() takes s32_le samples, and the lhdcBT_dec_decode() produces s24_4le. Is that right? I've been trying to change the bits_depth in lhdcBT_dec_decode(), but it seems it does nothing at all...

@arkq
Copy link
Owner

arkq commented Nov 14, 2024

Is the decoder thread safe

I'm quite sure it is not. However, there's not much I can do about it without changing ABI. This is what the original library does. I guess I can provide separate thread-safe API version if it's really needed.

The are some implication of that, though. BlueALSA runs a dedicated thread for encoder/decoder for every connected transport. So, in case when two A2DP source devices will try to use LHDC, bad things will happen... The encoder looks thread-safe, but it would be nice to know it for sure. As for decoder, I will have to add some restrictions, or maybe you can mark decoder global state as thread_local (C11), so at least for cases where there is a single decoder instance per-thread (like in bluealsa), there will be no side effects (with your copy of the library). I can also add some warning message which will warn user about the thread-safety concerns regarding the LHDC decoder library.

@anonymix007
Copy link
Contributor Author

What is the purpose of the bitPerSample in lhdcBT_init_encoder() and bits_depth in lhdcBT_dec_decode()

bitPerSample in lhdcBT_init_encoder() actually controls bit depth of the encoded stream, but it's done internally (32-bit samples from lhdcBT_encode_stereo() are right-shifted and saved into internal buffers). As for bits_depth in lhdcBT_dec_decode(), it only controls output bit depth (so samples are left shifted) and there's no point in setting it to anything except 24 (though maybe I should add support for 32).
640 KiB is enough for everyone s24_4le is enough for everyone.

The encoder looks thread-safe, but it would be nice to know it for sure

Well, it probably isn't either. Worked fine in AOSP which also creates a separate thread for encoding though.

As for decoder, I will have to add some restrictions, or maybe you can mark decoder global state as thread_local (C11)

There is more than variable for the global state and I don't really want to fix theoretical issue while there are more practical ones.

I might do something about this after the public release though, but for now let's assume that only 1 instance of LHDC encoder/decoder could be running in the same process and maybe add some runtime checks for that.

@arkq
Copy link
Owner

arkq commented Nov 14, 2024

it only controls output bit depth (so samples are left shifted) and there's no point in setting it to anything except 24 (though maybe I should add support for 32)

Thanks for the explanation. As for support for 32, I think that I will produce s32_le for both encoder and decoder anyway. It will allow easier internal testing (my entire testing infrastructure relies on decoder and encoder be able to consume/produce the same PCM sample format). So, for now I will add right-shift for produces 24-bit output.

Worked fine in AOSP which also creates a separate thread for encoding though.

As far as I know Android is not able to stream audio to more than one remote device at a time. So, for Android that's not a problem.

@arkq arkq merged commit 1002906 into arkq:master Nov 14, 2024
21 checks passed
@arkq
Copy link
Owner

arkq commented Nov 14, 2024

I was able to test the codec on my side. It seems that everything works as expected (within tested scope). The a2dp-lhdc.c code was OK after all, but the issue was with my test harness. However, after understanding how the codec API works I was able to fix that. Many thank for contribution!

Also as a side note. I've seen that the lhdcBT.h file contains commented out lhdcBT_get_error_code(HANDLE_LHDC_BT handle) function. It would be nice to have a way of retrieving any errors and print them to the user by the application instead of the library.

@arkq
Copy link
Owner

arkq commented Nov 16, 2024

@anonymix007 I have another question :D Is the decoder capable of decoding data encoded by devices with support for LHDC-v2 A2DP capabilities type? I have a Huawei phone which seems to support that. So, I've made an adaptation layer for LHDC-v2: https://github.com/arkq/bluez-alsa/tree/lhdc-v2 However, it seems that the codec payload is bit different than in case of v3. There is some header though, because second byte is a counter.

Here is an example of captured payload when playing some sound followed by a silence (without RTP header), maybe you will spot some syncword or something in these bytes :D :

10e0564c414c0402010160feffffccd98100cea82870b6b9320cc53fac61ff5451c50500aefb00e6019049821a1192ea4304d7621e1020501f1553f6f1c7d05b2f8f068181a91e8e3b42d3c8983870a92b57300032224dd734213a39149e11554cc0cebe0521b2b9302de01c708039651f64058588084731b195be945809a09c24107c0d436db77b58ed5fe326cf9ef3ddeca2e0e8db5f6c3a1543187d4b14c73402eb4a9055bc90127822219950fdec22115fd73187de3ea5412b6a4960b032d38f8f4f0424b25b216669b6c2a24e0aa2a2ba7fd75ad5871c9b4a777452d64897189aefe43e99bb20a5bf76525219c5a7797069bbf4534d73840b659e15a7ad704285b6ea2b74d21ba1922c959d4992244924499224b4e9a48010427b2b420821840821841021841042499200082449922492244992218424498410420810420821420821840821841000001042000c004c0000010100000000000c004c0000010100000000000c004c0000010100000000
10e1000c004c0000010100000000000c004c0000010100000000000c004c0000010100000000000c004c0000010100000000
0ce2000c004c0000010100000000000c004c0000010100000000000c004c0000010100000000
10e3000c004c0000010100000000000c004c0000010100000000000c004c0000010100000000000c004c0000010100000000
10e4000c004c0000010100000000000c004c0000010100000000000c004c0000010100000000000c004c0000010100000000

EDIT:
I think that the header is 3 bytes wide. After stripping 3 bytes I was able to parse the payload with your lhdc header parser.

EDIT2:
Or maybe the header is 2 bytes with swapped byte order (in comparison to v3), or maybe the rtp_lhdc_media_header_t is incorrect and the header is the same for v2 and v3.... I do not have a device which supports v3 to verify that... I was able to decode "something" but unfortunately it's not the source audio :/

@anonymix007
Copy link
Contributor Author

Is the decoder capable of decoding data encoded by devices with support for LHDC-v2 A2DP capabilities type

It should be capable, V3 is V2+lpc+different frame size and V4 is basically V3+LLAC. I definitely tested that once though, but something might've broke since then.

There is some header though, because second byte is a counter.

There is more than one header: first is 2 bytes (rtp_lhdc_media_header_t) and the second one is 5..7 bytes. So to "decode" the frame with lhdc_parser.c, you would need to strip first 2 bytes.

syncword

0x4C, but it's usually not the first byte because frames are byte-swapped in groups of 4 after encoding. You may use lhdc_parser.c from lhdc-utils.zip to learn more about these frames.

@arkq
Copy link
Owner

arkq commented Nov 16, 2024

It should be capable, V3 is V2+lpc+different frame size and V4 is basically V3+LLAC. I definitely tested that once though, but something might've broke since then.

OK, so something is not right, because only silence is decoded (and some random other frames).

Do you have some real data with encoded v3 (from some real retail device)? I'd like to see a single frame with RTP header to double check that lhdc media header is correct in bluez-alsa code base.

@anonymix007
Copy link
Contributor Author

Funnily enough, I don't have real retail device with LHDC. My phone doesn't support it out of the box (though is seems that LHDC is somewhat added in a newer version), so I'm using ExtA2DP.

You may refer to the official AOSP integration to verify that payload header is correct. Looks good to me though.

Will capture a few frames a bit later.

@arkq
Copy link
Owner

arkq commented Nov 17, 2024

You may refer to the official AOSP integration to verify that payload header is correct. Looks good to me though.

But looking here it seems that the LHDC 2-bytes header is in fact a byte header (number of frames + latency) followed by a byte sequence counter, which matches the header that I've got from my phone (which is a real retail device with LHDC v2 support). So this structure seems to be incorrect after all.

@xabolcs
Copy link

xabolcs commented Nov 17, 2024

I do not have a device which supports v3 to verify that...

I do have a phone and a headset.
How could I help?

Screenshot_20241117-163220
Screenshot_20241117-163233

I wasn't able to make bluez-alsa work at all last time I checked, so I shifted efforts to pipewire and ExtA2DP implementations and both worked fine, so you may use them for tests.

Offtopic here, but as a pipewire user, I can't wait for introducing LHDC support in pipewire! Thanks all your effort! 🙏

@arkq
Copy link
Owner

arkq commented Nov 17, 2024

@xabolcs If you have a device which supports that I'd be glad if you could dump Bluetooth traffic from your phone when streaming short (1-2 seconds) audio from your phone to headset which supports LHDC. You can use this https://www.wireshark.org/docs/man-pages/androiddump.html or any manufacturer specicif way to obtain HCI log from android. If you have a device with bluealsa, you can use this patch (applied on top of https://github.com/arkq/bluez-alsa/tree/lhdc-v2 branch) to capture incoming packets:

diff --git a/src/a2dp-lhdc.c b/src/a2dp-lhdc.c
index da394c4..5c67989 100644
--- a/src/a2dp-lhdc.c
+++ b/src/a2dp-lhdc.c
@@ -422,6 +422,8 @@ void *a2dp_lhdc_dec_thread(struct ba_transport_pcm *t_pcm) {
                if ((rtp_lhdc_media_header = rtp_a2dp_get_payload(rtp_header)) == NULL)
                        continue;

+               hexdump("PAYLOAD", rtp_header, len);
+
                int missing_rtp_frames = 0;
                rtp_state_sync_stream(&rtp, rtp_header, &missing_rtp_frames, NULL);

diff --git a/src/bluez.c b/src/bluez.c
index d4a0d69..d17332a 100644
--- a/src/bluez.c
+++ b/src/bluez.c
@@ -1367,6 +1367,7 @@ static void bluez_signal_interfaces_added(GDBusConnection *conn, const char *sen
                debug("Adding new Stream End-Point: %s: %s: %s",
                                batostr_(&addr), sep_cfg.type == A2DP_SOURCE ? "SRC" : "SNK",
                                a2dp_codecs_codec_id_to_string(sep_cfg.codec_id));
+               hexdump("SEP capabilities", &sep_cfg.capabilities, sep_cfg.caps_size);

                GArray *sep_cfgs = bluez_adapter_get_device_sep_configs(&bluez_adapters[dev_id], &addr);
                g_array_append_val(sep_cfgs, sep_cfg);

Compile bluez-alsa with debug enabled: ../configure --enable-debug --enable-lhdc then run bluealsad with -p a2dp-sink -c lhdc-v2 -c lhdc-v3 arguments. And finally post logs here. With bluealsa setup try to play audio when using LHDC v2 and LHDC v3.

@O2C14
Copy link

O2C14 commented Nov 17, 2024

hci_dump_intel.zip
Source: Redmi Note12 Turbo
Sink:be200
Capture using btstack

@arkq
Copy link
Owner

arkq commented Nov 17, 2024

@O2C14 thanks for the dump! So, the header for v3 is the same as for v2, where the first byte is a "header" and the second one is a sequence.

@arkq
Copy link
Owner

arkq commented Nov 17, 2024

@O2C14 I've just updated the https://github.com/arkq/bluez-alsa/tree/lhdc-v2 branch, if you have a system with bluez-alsa on it could you verify that LHDC v3 works as expected? Use setup from #672 (comment) to compile bluez-alsa with LHDC. Then run it as bluealsad -p a2dp-source -c lhdc-v3. Also, you can verify decoder part with bluealsad -p a2dp-sink -c lhdc-v2 -c lhdc-v3 and bluealsa-aplay -vv -D <PCM-for-audio-output>.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

LHDC support
4 participants