Skip to content

Repository files navigation

liblhdc-collections

Collected lhdc libs from any platform.

1. Savitech LHDC Codec for AOSP

LHDC(包含LHDCV5)是Savitech LHDC发行的编解码技术,其最新开源内容可访问 Savitech LHDC Codec for AOSP获取。LHDC是用于最高质量无线音频的下一代技术。 LHDC 能大幅降低无线与有线音频设备之间的音频质量差异,提供最逼真的超高音质,让用户能尽情享受蓝牙无线音频带来的便利性和高质量。适用于视频、音乐和游戏等所有应用。以漫步者花再 Halo Space 体验为例,最新的LHDCV5(192kHz & 24bit)在实际体验上已经优于LDAC(96kHz & 32bit)。

目前,大多数优于SBC的A2DP codec算法,比如LDAC、AAC、OPUS、LC3、aptX[L/HD]、LHDC V5,都均已开源;
其中关于LHDC V5, WillyBilly06 开源了 LHDC V5 的解码器 (WillyBilly06/LHDC-V5-Decoder),与编码器 (WillyBilly06/LHDC-V5-Encoder (基于 Google 开源的 rust版 lhdcv5 编码器 (lhdcv5)。

要实现LHDC编解码,在没有源码的情况下,需要使用独立于项目的第三方库;大多数耳机用的是Savitech LHDC的".a"格式的静态库,而Android用的是Savitech LHDC的".so"格式的动态库;“.a”静态库通常伴随着BES等项目的SDK提供,“.so”动态库在支持LHDC的手机中可轻易找到。

1.1 Test of LHDC-V5-Decoder on ESP32

想要快速入门?可以使用下列仓库快速构建测试项目:


  • sprlightning's esp-idf: embeded esp-idf with multiple A2DP Codecs. Use CMD:
    git clone -b a2dp-codecs/v5.1.4 https://github.com/sprlightning/esp-idf.git
    cd esp-idf
    git submodule init
    git submodule update --init --recursive
    

其中对 LHDC-V5-Decoder 的测试效果如下(使用native方式实现),SEP注册、CAP协商、解码都没有问题:

I (1650) BTDM_INIT: BT controller compile version [1175e0a]
I (1650) BTDM_INIT: Bluetooth MAC: 00:70:07:e1:cb:de
I (1650) phy_init: phy_version 4791,2c4672b,Dec 20 2023,16:06:06
I (2070) main_task: Returned from app_main()
W (2080) BT_BTC: A2DP Enable with AVRC
I (2090) BT_AV: event: 10
I (2090) BT_AV: event: 10
I (2090) BT_AV: event: 10
I (2090) BT_AV: A2DP PROF STATE: Init Complete
I (2090) BT_AV: Get delay report value: delay_value: 1200 * 1/10 ms
I (2100) BT_AV: Set delay report value: success, delay_value: 1250 * 1/10 ms
W (17010) BT_HCI: hcif conn complete: hdl 0x80, st 0x0
I (17010) BT_AV: ESP_BT_GAP_ACL_CONN_CMPL_STAT_EVT Connected to [3c:38:24:5c:4a:cf], status: 0x100
W (17050) BT_HCI: hcif link supv_to changed: hdl 0x80, supv_to 8000
I (17470) BT_AV: A2DP connection state: Connecting, [3c:38:24:5c:4a:cf]
I (17470) AUDIO_OUTPUT_I2S: I2S output started
I (17760) LHDCV5_DEC: configure: workspace 32544 bytes (max-sized for 192k; rate=48000)
I (17760) LHDCV5_DEC: IMDCT self-test: fast=ENABLED maxref=4166.6 maxdiff=0.241
I (17760) LHDCV5_DEC: decoder configured: sr=48000 depth=24 ch=2 dur=5
I (17770) BT_AV: A2DP audio stream configuration, codec type: 255
I (17780) CODEC_CONFIG: a2dp audio_cfg_cb , codec type 255
I (17780) CODEC_CONFIG: get_codec_config: Configure audio player 3a-5-0-0-35-4c
I (17790) CODEC_CONFIG: get_codec_config: configure LHDCV5 codec
I (17800) BT_AV: A2DP audio stream configuration, codec name: LHDC V5
I (17810) BT_AV: protocol service apabilities configured: 0x1 
I (17810) BT_AV: Apply audio cfg: 48000Hz | 32bit | 2ch
I (17810) BT_AV: Peer device support delay reporting
I (17820) AUDIO_OUTPUT_I2S: I2S output stopped
I (17830) AUDIO_OUTPUT_I2S: Configure I2S: 48000Hz | 2ch | 32bit | I2S_STD_FORMAT
I (17840) AUDIO_OUTPUT_I2S: I2S configured: 48000Hz | 32bit | 2ch | I2S_STD_FORMAT
I (17850) cs43131: Updating dual CS43131: 48000Hz | 32bit | Slave
W (17850) BT_APPL: new conn_srvc id:19, app_id:0
I (17860) BT_AV: A2DP connection state: Connected, [3c:38:24:5c:4a:cf]
I (17860) cs43131: Dual CS43131 update success
I (17870) BT_AV: DAC updated: 48000Hz | 32bit
W (17870) AUDIO_OUTPUT_I2S: I2S output already started
W (17880) AUDIO_OUTPUT_I2S: I2S output already started
I (17970) RC_CT: AVRC conn_state event: state 1, [3c:38:24:5c:4a:cf]
I (17970) RC_TG: AVRC conn_state evt: state 1, [3c:38:24:5c:4a:cf]
I (17970) RC_CT: AVRC remote features 24b, TG features 11
I (17980) RC_TG: AVRC remote features: 24b, CT features: 2
I (17980) RC_CT: remote rn_cap count 8, bitmask 0x1f26
I (18050) RC_CT: AVRC metadata rsp: attribute id 0x1, We've got to take our sweet sweet time
I (18050) RC_CT: AVRC metadata rsp: attribute id 0x2, Lenka-Sweet Time
I (18050) RC_CT: AVRC metadata rsp: attribute id 0x4, Discover
I (18060) RC_CT: AVRC metadata rsp: attribute id 0x20, Unavailable
W (21060) BT_HCI: hci cmd send: sniff: hdl 0x80, intv(400 800)
W (21120) BT_HCI: hcif mode change: hdl 0x80, mode 2, intv 768, status 0x0
I (21120) BT_AV: ESP_BT_GAP_MODE_CHG_EVT mode: 2
W (24500) BT_HCI: hcif mode change: hdl 0x80, mode 0, intv 0, status 0x0
I (24500) BT_AV: ESP_BT_GAP_MODE_CHG_EVT mode: 0
I (24510) RC_CT: AVRC event notification: 2
I (24510) BT_LOG: bta_av_link_role_ok hndl:x41 role:1 conn_audio:x1 bits:1 features:x864b

W (24520) BT_APPL: new conn_srvc id:19, app_id:1
I (24530) RC_CT: AVRC event notification: 1
I (24530) BT_AV: Playback status changed: 0x1
I (24530) BT_AV: A2DP audio state: Started
I (24540) RC_CT: AVRC event notification: 5
W (24540) AUDIO_OUTPUT_I2S: I2S output already started
I (24540) BT_AV: Play position changed: 15488-ms
I (24560) RC_CT: AVRC metadata rsp: attribute id 0x1, It feels so real to just let go
I (24560) RC_CT: AVRC metadata rsp: attribute id 0x2, Lenka-Sweet Time
I (24570) RC_CT: AVRC metadata rsp: attribute id 0x4, Discover
I (24580) RC_CT: AVRC metadata rsp: attribute id 0x20, Unavailable
I (24680) AUDIO_OUTPUT_I2S: ringbuffer data increased! mode changed: RINGBUFFER_MODE_PROCESSING
I (25100) BT_AV: Audio packet count: 100
I (25600) BT_AV: Audio packet count: 200
I (25700) RC_CT: AVRC event notification: 5
I (25700) BT_AV: Play position changed: 16998-ms
I (26100) BT_AV: Audio packet count: 300
I (26600) BT_AV: Audio packet count: 400
I (26770) RC_CT: AVRC event notification: 5
I (26770) BT_AV: Play position changed: 18079-ms
I (27100) BT_AV: Audio packet count: 500
I (27600) BT_AV: Audio packet count: 600
I (27850) RC_CT: AVRC event notification: 5
I (27850) BT_AV: Play position changed: 19160-ms
I (28100) BT_AV: Audio packet count: 700
I (28600) BT_AV: Audio packet count: 800
I (28620) RC_CT: AVRC event notification: 5
I (28620) BT_AV: Play position changed: 19938-ms
I (28680) RC_CT: AVRC event notification: 2
I (28720) RC_CT: AVRC metadata rsp: attribute id 0x1, And make each moment slide and take my sweet time
I (28720) RC_CT: AVRC metadata rsp: attribute id 0x2, Lenka-Sweet Time
I (28730) RC_CT: AVRC metadata rsp: attribute id 0x4, Discover
I (28740) RC_CT: AVRC metadata rsp: attribute id 0x20, Unavailable
I (29100) BT_AV: Audio packet count: 900
I (29620) BT_AV: Audio packet count: 1000
I (29730) RC_CT: AVRC event notification: 5
I (29730) BT_AV: Play position changed: 21029-ms
I (30100) BT_AV: Audio packet count: 1100
I (30600) BT_AV: Audio packet count: 1200
I (30800) RC_CT: AVRC event notification: 5
I (30800) BT_AV: Play position changed: 22101-ms
I (31100) BT_AV: Audio packet count: 1300
I (31600) BT_AV: Audio packet count: 1400
I (31860) RC_CT: AVRC event notification: 5
I (31860) BT_AV: Play position changed: 23178-ms
I (32100) BT_AV: Audio packet count: 1500
I (32600) BT_AV: Audio packet count: 1600
I (32930) RC_CT: AVRC event notification: 5
I (32930) BT_AV: Play position changed: 24242-ms
I (33100) BT_AV: Audio packet count: 1700
I (33220) RC_CT: AVRC event notification: 1
I (33220) BT_AV: Playback status changed: 0x1
I (33240) RC_CT: AVRC event notification: 5
I (33240) BT_AV: Play position changed: 24122-ms
I (33560) RC_CT: AVRC event notification: 1
I (33560) BT_AV: Playback status changed: 0x2
I (33600) BT_AV: Audio packet count: 1800
I (34100) BT_AV: Audio packet count: 1900
I (34600) BT_AV: Audio packet count: 2000
I (35100) BT_AV: Audio packet count: 2100
I (35610) BT_AV: Audio packet count: 2200
I (36100) BT_AV: Audio packet count: 2300
I (36260) BT_AV: A2DP audio state: Suspended
W (39460) BT_HCI: hci cmd send: sniff: hdl 0x80, intv(30 54)
W (39470) BT_HCI: hcif mode change: hdl 0x80, mode 2, intv 48, status 0x0
I (39470) BT_AV: ESP_BT_GAP_MODE_CHG_EVT mode: 2
W (41550) BT_HCI: hcif ssr evt: st 0x0, hdl 0x80, tx_lat 48 rx_lat 1200

test_of_LHDC-V5-Decoder

1.2 测试遇到的问题(非解码器问题):

Problem Status Solution
解码器初始化失败 需要足够的PSRAM,需要设置 CONFIG_SPIRAM_USE_MALLOC=y,问题可解决
间歇性的 buffer overflow 设置 components/bt/host/bluedroid/btc/profile/std/a2dp/btc_a2dp_sink.c 里的 BT_A2DP_SINK_BUF_SIZE 不低于 16384 ,问题可解决
播放音乐期间会短时失速并加速 检查应用层I2S缓存区配置,包括容量、阈值

假如用的是ESP-IDF的a2dp_sink_demo且未使用第三方库 ESP32_A2DP/arduino_audio_tools ,就需要注意下面的内容:

Problem Status Solution
频繁出现bt_app_send_msg xQueue send failed 检查应用层队列超时时间是否过短,检查应用层BtAppTask是否在reconnection期间错误 self suicide
Source端切换配置(比如 采样率/位深 ),会丢失AVRCP元数据 这仍然是应用层的问题,Source调整蓝牙参数会导致reconnection,Sink应抢在Source前完成reconnection
Source端切换配置 ,会没声音 Source端切换参数,Sink端可捕获到 hcif mode chang ,分析 audio_cfg_cb 确实是Source采用的参数,继续播放音乐,能看见不断增加的 Audio packet count ,这说明解码器本身没问题,还是应用层有问题,应用层需要再重新配置I2S后再Start I2Ss数据流

1.3 测试遇到的解码器问题

发现44.1kHz/48kHz音量明显比96kHz小,这是旧版decoder电平校准参数不对,@WillyBilly06 已在新版deocder处理了包括此问题在内的若干问题。新版目前暂未发现问题。


2. 本仓库有什么?

Project 属性 简介
LHDC-V5-Decoder LHDC V5 Decoder WillyBilly06 从零开始构建的LHDC V5解码器源码,我已在 ESP32 验证其真实有效且足够稳定。并且 WillyBilly06 基于.so编码器库/rust版编码器源码 与解码器的 rountrip,在不断完善解码器参数。据观测,这是开源的第一份功能有效的LHDC V5解码器。最新内容请访问:WillyBilly06/LHDC-V5-Decoder
LHDC-V5-Encoder LHDC V5 Encoder WillyBilly06 基于 Google的Rust版lhdcv5编码器 转换成C语言版的LHDC V5编码器源码,最新内容请访问:WillyBilly06/LHDC-V5-Encoder
lhdcv5 LHDC V5 Encoder 由Google开源的LHDC V5 编码器,由Rust编写,适用于Android 17,从内容来看其是编码器的完整实现,不依赖任何外部库,已在rust环境验证可行,参数反馈到解码器表现优秀。据观测,这是开源的第一份内容功能完整的LHDC V5编码器。最新内容请访问:lhdcv5
BES-IHC 包含.a格式静态库 适用于BES的LHDC[V5]库,包含.a静态库和头文件,完整SDK详见audio_prj_collections
AOSP 包含.so动态库 适用于AOSP的的LHDC[V5]库,就decoder而言,除了无解码算法源文件(lhdcv5_util_dec.c),其余源文件及头文件是完整的;此外也包含提取自XIAOMI HyperOS 2.0.211.0的 .so动态库 和一些其他版本的 .so动态库/.a静态库
ESP-IDF ESP-IDF Integration 包含适用于ESP-IDF的移植版liblhdcv5dec和lhdcv5 decoder,均具备完整的源文件和头文件,均移植于AOSP,使用LHDCV5协商后,可听到正弦波生成的标准音;其中lhdcv5_util_dec.c仅具备解码函数占位的作用,仅供参考(其中我这版Integration是从AOSP移植过来的,同时支持Sink和Source框架,此外注意由于解码器只支持16bit和24bit,所以注册的32bit没有意义;总之能用,日志有些啰嗦,建议优先用WillyBilly0提供的Integration)

3. LHDCV5解码原理

如前面所说, WillyBilly06 开源了 LHDC V5 解码器(WillyBilly06/LHDC-V5-Decoder),同时 Google 开源了 lhdcv5 编码器(lhdcv5)。

这里就不废话了,分析 WillyBilly06LHDC-V5-Decoder可知LHDC V5解码原理如下:

3.1 整体流程概览

LHDC V5 的解码流程可划分为以下阶段:

A2DP 包 -> 解包/帧重组 -> [u16 LE 帧头 + payload] -> 每声道独立解码
                                                    -> 解扰 (descramble)
                                                    -> 读取前端参数 (gain/SNS/nzc/flag2/resid)
                                                    -> FAC 熵解码 (stream1 + stream2)
                                                    -> Rice 商解码
                                                    -> 尾数 + 符号平面解码
                                                    -> 系数反序 (dereversal)
                                                    -> 反量化 (inverse quantize)
                                                    -> SNS 合成 (spectral noise shaping)
                                                    -> IMDCT
                                                    -> 加窗重叠相加 (overlap-add)
                                                    -> 输出电平归一化与裁剪
                                                    -> 交织输出 PCM

每帧由两个字节的小端长度头开始,payload_len = (hdr & 0x3FF) * 2,帧内两个声道分别占用等长字节切片,独立进行熵解码与 IMDCT。


3.2 帧格式与解扰

3.2.1 帧头

uint16_t fhdr = in_data[0] | (in_data[1] << 8);   // LE
payload_len = (fhdr & 0x3FF) * 2;
flags       = fhdr >> 10;

流参数(采样率、位深、声道数、帧时长)来自 A2DP CIE 配置,而非帧头本身。

3.2.2 声道切片

ch_bytes = frame_bytes / channels;
ch_start = channel * ch_bytes;

每个声道在 payload 中占固定等长切片,第二声道从 ch_start 开始。

3.2.3 八字节解扰

每个声道的前 8 字节被独立扰码,扰码选择由该声道第 9 字节 (buf[8] & 1) 决定:

static const uint8_t LHDC_PERM[2][8] = {
    { 4, 0, 1, 5, 7, 3, 2, 6 },
    { 6, 3, 7, 0, 2, 1, 4, 5 },
};
static const uint8_t LHDC_XOR_MASK[8] = {
    0xFF, 0xE7, 0x7A, 0xB3, 0xDA, 0xE5, 0xCD, 0x73
};

解扰后每个声道初始化独立的位读取器。

3.2.4 自动选择器纠错

buf[8]&1 的选择器在少数帧上是错误的,会导致 FAC 解码出巨大的谱系数。实现采用双峰检测:先用自动选择器解码,若输出峰值超过阈值,则恢复上一帧 overlap 后用 flipped 选择器重试,保留峰值较小的结果。


3.3 前端参数解析

每声道 leading section 的比特顺序如下:

字段 位数 说明
flag 1 必须为 0
global_gain 9 全局增益索引
sns_mode 4 SNS 自适应步长初始状态(读值 +8)
SNS direction bits num_sfb-1 每频带 1 bit
nzc calc_bits(spf)-1 非零系数计数的一半减一
flag2 (pred_mode) 1 尾数移位预测器模式
resid 4 仅当 enc_frame_len >= 320 时存在,尾数平面初始 shift

nzc = (nzc_raw + 1) * 2,上限为 num_coeffs = mdct_size / 2


3.4 SNS(Spectral Noise Shaping)合成

3.4.1 参数重建

SNS 通过 num_sfb-1 个 1-bit 侧信息重建每个频带的 scalefactor。

  • sf[0] = 0
  • 初始 state = sns_modesns_mode = 读值 + 8
  • i >= 1
    • 若当前 bit 与上一位相同:run++state = min(state + run, 63)
    • 否则:state = max((3*state + 2) >> 2, 0)run = 1
    • sf[i] = sf[i-1] + sns_step(bit, state)

3.4.2 后处理

重建后的 sf 必须经过:

  1. 去均值
  2. 4-tap 滑动平均平滑
  3. 取反
  4. 裁剪到 [-2560, 1024]

否则残留 DC 偏移会导致谱形状错误。

3.4.3 频带增益应用

gain = POW2_MANT[idx] * (sf >= 0 ? 2^-20 : 2^-30)
idx  = (sf >= 0) ? sf >> 4 : (sf + 0xa0f) >> 4

默认对谱做除法( inverse noise shaping )。sns_mode == 23 时为 flat SNS,所有 sf = 0


3.5 FAC 熵解码与 Rice 商解码

3.5.1 整体结构

频谱系数通过两个三元(3-symbol)流编码:

  • stream1(s1):长度 count = nzc,初始频率 {54, 12, 1},滑动窗口 ma_win1
  • stream2(s2):剩余商码,初始频率 {90, 16, 1},滑动窗口 ma_win2 = ma_win1 * 8

两个流共享同一个字节 range coder 流(初始 code 为前 4 字节,总频率 2^15)。

3.5.2 FAC-MA 自适应模型

#define FAC_TOTAL_BITS 15
#define FAC_TOTAL      (1 << FAC_TOTAL_BITS)

模型维护 symbol 频率、阈值和 15-bit 归一化累积频率 cum[]。解码符号时:

r = range >> 15;
sym = max s.t. r * cum[sym+1] <= code;
code  -= r * cum[sym];
range  = r * (cum[sym+1] - cum[sym]);
while ((range >> 24) == 0) {
    code = (code << 8) | next_byte;
    range <<= 8;
}
update_model(sym);

3.5.3 Rice 商逆变换

split = num_coeffs / 3pivot = max(count - split, 0)run = 最后一个 s1==2 的索引pivot2 = (run < pivot) ? count : run

对每个系数:

  • i <= pivot2
    • s1[i] == 2coeff[i] = rice_quotient(s2) + 2
    • 否则:coeff[i] = s1[i]
  • i > pivot2coeff[i] = (rice_quotient(s2) << 1) | s1[i]

3.5.4 尾数 + 符号平面

FAC 只传输了系数的高位。完整幅度通过尾数平面恢复:

shift[k] = calc_bits(|M[k-1]|)   // 因果 IIR 预测器
M[k]     = (q[k] << shift[k]) | mantissa
if M[k] != 0:  1 符号 bit1 为负

预测器两种模式:

  • flag2 = 0A[k] = (5*x + 3*A[k-1]) >> 3
  • flag2 = 1B[k] = (7*B[k-1] + x) >> 3

x = calc_bits(|M[k]|) << 7

3.5.5 尾数平面起始位置

FAC 流结束后留下 2 或 3 字节 lookahead gap,由最终 range 决定:

best_delta = (g_fac_final_range <= 0x2000000u) ? 2 : 3;

若解码出的峰值超过 24-bit 满幅(8388607),则尝试另一 gap。


3.6 系数反序与反量化

3.6.1 系数反序

编码器按逆序量化谱系数,因此解码后需将 qs[0..nzc-1] 反序:

for (a = 0, b = nzc - 1; a < b; a++, b--) swap(qs[a], qs[b]);

3.6.2 反量化

step = 2^e(global_gain)
spectrum[k] = q[k] * step

e(gain_idx) 是分段函数:

  • idx <= splite = idx
  • split < idx <= 503e = split + (idx - split) * slope
  • idx > 503e = e(503) + (idx - 504),上限 30

16-bit 与 24-bit 的 split 公式不同;24-bit 使用固定 slope 0.0378 并将 split 上限设为 5。


3.7 IMDCT 与时域重建

3.7.1 IMDCT 定义

x[n] = (2/N) * sum_{k=0}^{N/2-1} X[k] * cos(pi/(2N)*(2n+1+N/2)*(2k+1))

3.7.2 快速路径

实现针对三种长度提供 FFT 快速路径:

采样率 MDCT 长度 N FFT 结构
44.1k/48k 480 120 = 8 x 15
96k 960 240 = 16 x 15
192k 1920 480 = 32 x 15

所有快速路径在初始化时都会用参考实现做 self-test,失败则回退到 O(N^2) 参考实现。

3.7.3 加窗与重叠相加

使用低重叠窗(low-overlap window),满足 Princen-Bradley 功率互补:

w[n]^2 + w[n + N/2]^2 = 1

窗结构:前沿零填充、正弦过渡、平顶 1.0、正弦下降、后沿零填充。

重叠相加:

pcm_out[n] = window[n] * mdct_out[n] + overlap_buf[n];
overlap_buf[n] = window[overlap + n] * mdct_out[overlap + n];

每帧只输出 N/2 个样本。


3.8 输出处理

3.8.1 电平归一化

sc = 2^(out_bit_depth - 23) * rate_norm(mdct_size) * level_cal(sr, bit_depth)
rate_norm = (mdct_size / 480)^2

24-bit 输出采用 32-bit 容器(左对齐)。

3.8.2 错误隐藏

若解码后峰值超过 2 * clip_lim,认为熵/尾数解码失步,当前帧输出静音并清空 overlap buffer,防止 glitch 扩散。


3.9 关键数据结构

3.9.1 频段配置

typedef struct {
    int cfg_idx;
    int num_sfb;
    int mdct_size;
    const uint16_t *band_off;
    const uint16_t *band_scale;
    int ma_win1;
    int ma_win2;
} lhdc_band_cfg_desc_t;

主要配置:

配置 num_sfb mdct_size ma_win1 ma_win2
480_HR 32 480 57 63
960 32 960 96 64
1920 32 1920 96 64

3.9.2 解码器状态

typedef struct {
    lhdc_dec_config_t config;
    lhdc_dec_frame_header_t header;
    lhdc_dec_sns_params_t sns_params;
    const lhdc_band_cfg_desc_t *band_cfg;
    float *mdct_in;          // 也用作 quant_spectrum / ch_pcm 别名
    float *mdct_out;
    float *overlap_buf[2];
    float *pcm_mid;
    float *ov_save;
    uint8_t *ent_s1;
    uint8_t *ent_s2;
    uint8_t payload_buf[...];
    uint8_t fac_buf[...];
} lhdc_decoder_t;

3.10 与 A2DP 集成的要点

  1. 包格式:A2DP 媒体包头部后紧跟 LHDC payload,payload 首字节为 seq/marker,随后是 [u16 LE len][payload] 帧序列。
  2. 分片:payload 可能分片到达,需要在 a2dp_vendor_lhdcv5_decoder.c 中重组。
  3. 采样率切换:工作区按 192k 最大尺寸一次性分配,避免运行时因堆碎片导致切换失败。
  4. 声道格式:立体声为 L/R 直接传输,非 M/S。
  5. 24-bit 输出:实际以 32-bit 容器输出,便于 ESP32 I2S 路径。

3.11 逆向实现中的关键假设与验证点

项目 实现假设 验证建议
解扰选择器 buf[8]&1 决定,少数帧错误并自动纠错 与官方库对比选择器分布
FAC ma_win1/ma_win2 48k:57/63;96k/192k:96/64 不同码率/采样率下测试
尾数 gap 规则 range <= 0x2000000 => 2 字节,否则 3 字节 构造高密度帧验证
24-bit slope 固定 0.0378 用 24-bit 码流比对
输出电平 rate_norm=(mdct/480)^2 多采样率下测响度一致性
IMDCT 快速路径 480/960/1920 均通过 self-test 足够多样化的测试向量

4. 本仓库目录详细介绍

  • 目录 LHDC-V5-Decoder,由 WillyBilly06 从零开始构建的LHDC V5解码器源码,我已在 ESP32 验证其真实有效且足够稳定。据观测,这是开源的第一份功能有效的LHDC V5解码器。

    这里以submodule形式添加的只是当前的哈希值版本,最新内容请访问 WillyBilly06/LHDC-V5-Decoder .

  • 目录 LHDC-V5-Encoder,由 WillyBilly06 基于Google的Rust版lhdcv5编码器转换成C语言版的LHDC V5编码器源码。

    这里以submodule形式添加的只是当前的哈希值版本,最新内容请访问:WillyBilly06/LHDC-V5-Encoder .

  • 目录 lhdcv5,由Google开源的LHDC V5 编码器,由Rust编写,适用于Android 17,从内容来看其是编码器的完整实现,不依赖任何外部库,已在rust环境验证可行,参数反馈到解码器表现优秀。据观测,这是开源的第一份内容功能完整的LHDC V5编码器。

    这里收录的是编码器的备份,最新内容请访问 lhdcv5

     lhdcv5
     │  Android.bp
     │  Bluetooth-refs_heads_android17-release-system-audio-codecs-lhdcv5.tar.gz
     │  Cargo.toml
     │  generate.bash
     │
     ├─include
     │      lhdcv5BT.h
     │      lhdcv5_api.h
     │
     └─src
     	│  arith.rs
     	│  ffi.rs
     	│  kiss_fft.rs
     	│  lhdcv5BT_enc.c
     	│  lhdc_api.rs
     	│  lib.rs
     	│  math.rs
     	│
     	├─arith
     	│      ring_buf.rs
     	│
     	├─bin
     	│  └─simulator
     	│          lhdc_abr.rs
     	│          main.rs
     	│          wave_file.rs
     	│
     	├─bits
     	│      mod.rs
     	│      output.rs
     	│
     	├─common
     	│      lhdc_level.rs
     	│
     	├─enc
     	│  │  context.rs
     	│  │  mod.rs
     	│  │  process.rs
     	│  │
     	│  └─process
     	│          tables.rs
     	│
     	├─lhdc_api
     	│      cirbuf.rs
     	│      lhdc_api_internal.rs
     	│
     	└─lhdc_enc
     			lhdc_enc_freq_process.rs
     			lhdc_enc_header.rs
     			lhdc_enc_workspace.rs
    
  • 目录 BES-IHC,适用于BES的LHDC[V5]库,包含.a静态库和头文件,完整SDK详见audio_prj_collections:

     ├─a2dp_decodera2dp_decoder_lhdc.cppa2dp_decoder_lhdcv5.cpp
     │
     ├─audio_codec
     │  ├─liblhdc-dec
     │  │      BEST2500P_libLHDC_V2_V3_V4_4_0_13_SAVI_KEYPRO_UUID.a
     │  │
     │  └─liblhdcv5-decBEST2500P_libLHDC_V5_5_2_0_SAVI_KEYPRO_UUID.a
     │
     └─audio_codec_lib
     	├─liblhdc-dec
     	│  │  Makefile
     	│  │
     	│  └─inclhdcUtil.h
     	│
     	├─liblhdc-enc
     	│  │  Makefile
     	│  │
     	│  └─inclhdc_cfg.hlhdc_enc_api.h
     	│
     	└─liblhdcv5-decMakefile
     		│
     		└─inc
     				lhdcv5_util_dec.h
  • 目录 AOSP,适用于AOSP的的LHDC[V5]库,就decoder而言,除了无解码算法源文件(lhdcv5_util_dec.c),其余源文件及头文件是完整的;此外也包含提取自XIAOMI HyperOS 2.0.211.0的 .so动态库 和其他版本的 .so动态库/.a静态库

     AOSP
     ├─liblhdc
     │  │  Android.bp
     │  │  release_note
     │  │
     │  ├─inc
     │  │      lhdcBT.h
     │  │
     │  ├─include
     │  │      cirbuf.h
     │  │      lhdcv2_process.h
     │  │      lhdcv3_process.h
     │  │      lhdc_api.h
     │  │      lhdc_cfg.h
     │  │      lhdc_enc_config.h
     │  │      lhdc_process.h
     │  │      llac_enc_api.h
     │  │
     │  └─src
     │          lhdcBT_enc.c
     │
     ├─liblhdc-hyperos2_0_211_0
     │  └─system
     │      └─lib64
     │              liblhdc.so
     │              liblhdcv5.so
     │
     ├─liblhdcdec
     │  │  Android.bp
     │  │  release_note
     │  │
     │  ├─inc
     │  │      lhdcBT_dec.h
     │  │
     │  ├─include
     │  │      lhdcUtil.h
     │  │
     │  └─src
     │          lhdcBT_dec.c
     │
     ├─liblhdcv5
     │  │  Android.bp
     │  │  release_note
     │  │
     │  ├─inc
     │  │      lhdcv5BT.h
     │  │
     │  ├─include
     │  │      lhdcv5BT_ext_func.h
     │  │      lhdcv5_api.h
     │  │
     │  └─src
     │          lhdcv5BT_enc.c
     │
     ├─liblhdcv5dec
     │  │  Android.bp
     │  │  release_note
     │  │
     │  ├─inc
     │  │      lhdcv5BT_dec.h
     │  │
     │  ├─include
     │  │      lhdcv5_util_dec.h
     │  │
     │  └─src
     │          lhdcv5BT_dec.c
     │
     └─other_so_files
     	├─dec
     	│  ├─lhdc
     	│  │  ├─aarch64
     	│  │  │      liblhdcBT_dec.so
     	│  │  │      liblhdcBT_enc.so
     	│  │  │
     	│  │  └─x86_64
     	│  │          liblhdcBT_dec.so
     	│  │          liblhdcBT_enc.so
     	│  │
     	│  └─lhdcv5
     	│          BEST2500P_libLHDC_V5_5_2_0_SAVI_KEYPRO_UUID.a
     	│
     	└─enc
     		│  liblhdcv5.so
     		│
     		└─LHDC-magisk-11_arm64
     			│  customize.sh
     			│  module.prop
     			│  README.md
     			│
     			├─common
     			│      post-fs-data.sh
     			│      service.sh
     			│      system.prop
     			│
     			├─META-INF
     			│  └─com
     			│      └─google
     			│          └─android
     			│                  update-binary
     			│                  updater-script
     			│
     			└─system
     				├─lib
     				│      liblhdc.so
     				│      liblhdcBT_enc.so
     				│      liblhdcv5.so
     				│      liblhdcv5BT_enc.so
     				│
     				└─lib64
     						liblhdc.so
     						liblhdcBT_enc.so
     						liblhdcv5.so
     						liblhdcv5BT_enc.so
    
  • 目录 ESP-IDF,包含适用于ESP-IDF的移植版liblhdcv5dec和lhdcv5 decoder,均具备完整的源文件和头文件,均移植于AOSP,使用LHDCV5协商后,可听到正弦波生成的标准音;其中lhdcv5_util_dec.c仅具备模拟解码的能力,仅供参考,用真正的LHDCV5解码算法替换其中的正弦波(模拟解码)部分可实现完整的LHDCV5音频Sink。下面是目录结构:

     └─bluedroid
     	├─api
     	│  └─includeesp_a2dp_api.h
     	│
     	├─external
     	│  └─liblhdcv5dec
     	│      │  CMakeLists.txt
     	│      │  release_note
     	│      │
     	│      ├─inc
     	│      │      lhdcv5BT_dec.h
     	│      │
     	│      ├─include
     	│      │      lhdcv5_util_dec.h
     	│      │
     	│      └─srclhdcv5BT_dec.clhdcv5_util_dec.c
     	│
     	└─stack
     		├─a2dpa2dp_vendor.ca2dp_vendor_lhdcv5.ca2dp_vendor_lhdcv5_decoder.c
     		│
     		└─include
     			└─stack
     					a2dp_vendor.h
     					a2dp_vendor_lhdcv5.h
     					a2dp_vendor_lhdcv5_constants.h
     					a2dp_vendor_lhdcv5_decoder.h
     					a2dp_vendor_lhdc_constants.h

5. LHDCV5移植

我对BES的项目不是很了解,因为这方面资料不完整;而AOSP方面资料倒是挺多的。

早段时间我从AOSP移植了LHDCV5到ESP-IDF,因为不知道LHDCV5解码算法,所以只是用正弦波替换了解码函数,ESP32作为A2DP Sink连接手机且使用LHDCV5协商后,手机播放音乐时Sink端听到的是固定的标准音,证明移植成功了。ESP-IDF默认用的是SBC,其实它还额外支持AAC(M12、M24),忘了是5.1.6还是哪个版本的ESP-IDF,其已经内置了A2DP拓展逻辑,能加入其它codecs,不过一直没人做这方面的具体拓展。我注意到cfint对ESP-IDF 5.1.4写了一套较为成熟的A2DP拓展逻辑(5.1.4本身不具备拓展能力),能依据CIE结构体在协商时动态调用对应的解码器来解码A2DP数据包,所以我就顺着他的路走下去了。大致就是:

  • Step 1: 改CIE结构体;
  • Step 2: 在拓展的A2DP函数中增加 LHDCV5 a2dp vendor 函数,这方面依葫芦画瓢模仿LDAC可实现。
  • Step 3: LHDCV5第三方库的移植与实现。得益于 WillyBilly06 的 LHDC V5 解码器和 Google 的 lhdcv5 编码器,这一步非常顺利。否则将要亲自使用IDA Pro分析so和a文件,这将是浩大的工程。

5.1 CIE结构体

ESP-IDF的bluedroid-stack有一个存储codec能力的CIE结构体(位于esp_a2dp_api.h),这里可加入LHDCV5的CIE_LEN,可与其他设备进行A2DP协商;因为大部分CIE_LEN = CODEC_LEN - 2,而LHDCV5的CODEC_LEN查询a2dp_vendor_lhdc_constants.h可知是13,所以LHDCV5的CIE_LEN其长度是11,这已经验证过了是对的;

5.2 ESP-IDF的LHDCV5函数文件移植

为ESP-IDF添加LHDCV5的decoder函数,这方面参考AOSP,移植内容包括4个头文件(a2dp_vendor_lhdc_constants.h、a2dp_vendor_lhdcv5_constants.h、a2dp_vendor_lhdcv5_decoder.h、a2dp_vendor_lhdcv5.h)和个源文件(a2dp_vendor_lhdcv5_decoder.c、a2dp_vendor_lhdcv5.c),移植的内容无非是更改日志打印、内存函数、变量定义,其它大体上差不多不用动;

函数依赖方面是这样:a2d_sbc.c(修改版) --调用--> a2d_sbc_decoder.c --调用--> a2dp_vendor.c --调用--> a2dp_vendor_lhdcv5.c --调用--> a2dp_vendor_lhdcv5_decoder.c --调用--> lhdcv5BT_dec.c(外部) --调用--> lhdcv5_util_dec.c(外部)。

5.3 LHDCV5外部库

LHDCV5的外部库一直是闭源以so或者a文件流传,即使是高度开源的AOSP,也只得到了lhdcv5BT_dec.c/.h + lhdcv5_util_dec.h这3个文件,缺乏包含LHDCV5解码算法的lhdcv5_util_dec.c;可以依据lhdcv5_util_dec.h声明函数的参数逆推出lhdcv5_util_dec.c,但是如前面所说,因为不知道LHDCV5解码算法,所以我只是用正弦波替换了解码函数,当然连接后听到的也只是正弦波生成的标准音。

常规思路是用(IDA Pro)逆向Android LHDCV5动态库(liblhdcv5.so)或BES的静态库来推导出LHDCV5的解码算法(我不会操作);还有就是等待大佬开源,很幸运等到了: WillyBilly06 的 LHDC V5 解码器和 Google 的 lhdcv5 编码器就是这一环节至关重要的内容,尤其是 Google 的 lhdcv5 编码器,它比前者更完整。

About

Collected lhdc libs from any platform.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages