# sbc 2.2 — el códec de audio OBLIGATORIO de A2DP. Sin esto, un auricular bluetooth no suena. # # ══ POR QUÉ ENTRA: EL BLUETOOTH EMPAREJABA Y NO SONABA ═════════════════════════════════════════ # Medido el 2026-10-21. El estado era este, y es la figura que este catálogo persigue repartida # entre dos paquetes SANOS: # # · emparejar: FUNCIONA. `bluez` trae `bluetoothctl` 5.87 (corre, estático) y su daemon # `bluetoothd`, y los CUATRO escritorios que lo instalan también lo HABILITAN en `servicios`. # · oír algo: IMPOSIBLE. `pipewire` iba con `-Dbluez5=disabled`, así que `/usr/lib/spa-0.2/` # traía alsa, audioconvert, aec… y CERO bluez ⇒ PipeWire no ve el auricular como salida. # # Ninguno de los dos paquetes estaba roto, y la capacidad no existía. # # ══ POR QUÉ SÓLO SBC, Y NO LA «CADENA ENORME» QUE LA RECETA DE PIPEWIRE TEMÍA ══════════════════ # La nota de `pipewire` justificaba apagar bluez5 por «cadena de códecs aptx/ldac/aac/opus enorme». # Es cierto que esa cadena existe — y es OPCIONAL. Leyendo `spa/meson.build:49-53`, las deps DURAS # de `bluez5=enabled` son exactamente cinco: # # bluez >= 4.101 · gio-2.0 · gio-unix-2.0 · glib-2.0 · sbc # # Las cuatro primeras ya estaban (verificado: `bluez.pc` dice 5.87, y el artefacto de `glib` # publica los nueve `.pc` incluidos `gio-2.0` y `gio-unix-2.0`). **Faltaba una sola receta.** # # Y es la correcta de elegir: **SBC es el único códec que A2DP exige**. Todo auricular del mercado # lo habla; aptX, LDAC y AAC son mejoras de calidad que el perfil puede añadir después sin tocar # esto. Así que el salto de «no suena» a «suena» cuesta un tarball de 277 KB, no una campaña. # # ══ ⚠ PIC NO ES OPCIONAL ACÁ ══════════════════════════════════════════════════════════════════ # El consumidor es `libspa-bluez5.so`, un plugin que PipeWire carga con `dlopen`. Una `.a` sin PIC # dentro de una `.so` falla al enlazar («relocation R_X86_64_32S against ... can not be used when # making a shared object») o, peor, enlaza y revienta. El corpus ya pagó esto en el trío de fuentes # que pasó a `--with-pic` el 2026-10-19. ⇒ `--with-pic` explícito, y el guardián lo COMPRUEBA. name = "sbc" version = "2.2" license = "LGPL-2.1-or-later AND GPL-2.0-or-later" [source] tarball = "https://www.kernel.org/pub/linux/bluetooth/sbc-2.2.tar.xz" sha256 = "a1ada76ef35e5af9c2fbd063754dc9e37a8d989417c6eb1ecebb089b1383ae9e" [build] compiler = "zig-cc" target = "x86_64-linux-musl" link = "static" flags = [] [build.phases] # `--disable-tools` apaga `sbcenc`/`sbcdec`/`sbcinfo`, utilidades de línea de comandos para # convertir ficheros: PipeWire no las usa (enlaza la librería) y arrastran `libsndfile`. # # ⚠ **Y HACE FALTA TAMBIÉN `--disable-tester`, que es quien exige sndfile de verdad.** Con sólo # `--disable-tools` el configure muere así, y el mensaje no nombra la opción que lo arregla: # checking for sndfile... no # configure: error: sndfile library is required # Leyendo `configure.ac:60-72`: `--disable-tools` gobierna el condicional TOOLS, pero el # `PKG_CHECK_MODULES(SNDFILE, …, AC_MSG_ERROR(...))` está dentro del bloque de **`enable_tester`**, # que es otra opción. Son dos interruptores distintos para lo que parece una sola cosa, y ninguno # de los dos sale en `./configure --help`. Comprobado en seco antes de volver a construir: con las # dos el configure sale 0 y genera `sbc/sbc.pc`. # # ⚠ **Y `--with-pic` NO BASTA: hace falta `CFLAGS=-fPIC` explícito.** Medido acá — el guardián de # PIC de abajo rechazó el primer build con `sbc.o trae 600 reubicaciones absolutas` teniendo # `--with-pic` puesto. La razón es libtool: con `--disable-shared` sólo construye la variante # estática y **para esa variante ignora `--with-pic`** (ese flag gobierna los objetos de la `.so`). # `--with-pic` se deja igual porque documenta la intención y no estorba, pero quien lo consigue es # el `CFLAGS`. Mismo patrón que `argp-standalone`, que lo dice en su cabecera: «Fix = --with-pic + # CFLAGS=-fPIC». Se APPENDEA a `$CFLAGS` en vez de reemplazarlo, para no perder lo que pone el lab # (ésa es la familia de error de `cgo` perdiendo `-mcpu=baseline` al redefinir `CC`). configure = ''' # ⚠ **LOS PRIMITIVOS MMX/SSE NO COMPILAN CON CLANG + PIC, y hay que apagarlos.** Con `-fPIC` el # build muere así: # sbc/sbc_primitives_mmx.c:300:5: error: invalid operand for inline asm constraint 'i' # sbc/sbc_primitives_sse.c:268:5: error: invalid operand for inline asm constraint 'i' # Su asm en línea pasa la dirección de una tabla con la restricción `"i"` (inmediato). En código # PIC esa dirección deja de ser un inmediato, y clang lo rechaza donde GCC lo tolera. O sea que el # conflicto es real: **PIC y ese asm no pueden coexistir con este compilador**, y PIC no es # negociable porque el consumidor es una `.so` cargada con `dlopen`. # # La guarda que los activa es `defined(__GNUC__) && (__i386__ || __amd64__) && …`, y **zig/clang # define `__GNUC__` por compatibilidad**: de ahí que se enciendan solos. El parche dice la verdad # en vez de esquivarla — ese asm es de GCC, así que se exige que NO sea clang: sed -i 's/^#if defined(__GNUC__) \&\& (defined(__i386__) || defined(__amd64__)) \&\& \\/#if defined(__GNUC__) \&\& !defined(__clang__) \&\& (defined(__i386__) || defined(__amd64__)) \&\& \\/' \ sbc/sbc_primitives_mmx.h sbc/sbc_primitives_sse.h # Comprobación de que el parche MORDIÓ: un `sed` que no casa no falla, y sin esto el error de arriba # volvería disfrazado. Es la familia de error que este repo ya documentó con los parches de fcitx5. for h in mmx sse; do grep -q '!defined(__clang__)' "sbc/sbc_primitives_$h.h" \ || { echo "sbc: el parche de $h NO se aplicó — upstream cambió la guarda" >&2; sed -n '14,20p' "sbc/sbc_primitives_$h.h" >&2; exit 1; } done # ⚠ **SEGUNDO PARCHE, y hace falta porque el primero no alcanza.** Apagadas las dos rutas SIMD, el # enlace seguía muriendo: # ld.lld: error: undefined symbol: __cpu_indicator_init # >>> referenced by sbc_primitives.c:581 (sbc_init_primitives_x86) # `__cpu_indicator_init` es el soporte de `__builtin_cpu_supports()` que vive en **libgcc**, y el # compiler-rt de zig no lo trae. La llamada culpable es `__builtin_cpu_init()`, que # `sbc_primitives.c:581` hace **FUERA** de los `#ifdef` de MMX/SSE — o sea que queda aunque no haya # ni una ruta SIMD que seleccionar. Se guarda con los mismos macros, que es lo semánticamente # correcto: sin implementaciones SIMD compiladas, detectar la CPU no sirve para nada. # # (La alternativa era declarar `gcc-libs` y enlazar `-lgcc` para conseguir el símbolo. Se descarta: # traer libgcc entera para una detección que no se va a usar es pagar una dep por nada.) sed -i 's/^\t__builtin_cpu_init();$/#if defined(SBC_BUILD_WITH_MMX_SUPPORT) || defined(SBC_BUILD_WITH_SSE_SUPPORT)\n\t__builtin_cpu_init();\n#endif/' sbc/sbc_primitives.c grep -q 'defined(SBC_BUILD_WITH_MMX_SUPPORT) || defined(SBC_BUILD_WITH_SSE_SUPPORT)' sbc/sbc_primitives.c \ || { echo "sbc: el parche de __builtin_cpu_init NO se aplicó — mirá sbc_primitives.c cerca de sbc_init_primitives_x86" >&2; grep -n '__builtin_cpu_init' sbc/sbc_primitives.c >&2; exit 1; } # ⇒ Se pierde la ruta SIMD del codificador y queda la de C (`sbc_primitives.c`), que es la de # referencia y siempre se construye; la selección era por CPUID en tiempo de ejecución. El coste es # despreciable para lo que hace falta: SBC sobre un flujo de audio de unos cientos de kbps. ./configure --build=$CBUILD --host=$CHOST --prefix=/usr --libdir=/usr/lib --disable-shared --enable-static --with-pic --disable-tools --disable-tester CFLAGS="$CFLAGS -fPIC" ''' compile = 'make -j"$(nproc)"' install = ''' set -e make DESTDIR=/out install [ -f /out/usr/lib/libsbc.a ] || { echo "sbc: no quedó libsbc.a — NO sello" >&2; exit 1; } # El `.pc` es lo que meson busca con `dependency('sbc')`: sin él, `bluez5=enabled` de pipewire falla # con «Dependency sbc not found» aunque la librería esté. [ -f /out/usr/lib/pkgconfig/sbc.pc ] || { echo "sbc: falta lib/pkgconfig/sbc.pc — es lo que busca el dependency('sbc') de pipewire" >&2; find /out -name '*.pc' >&2; exit 1; } [ -f /out/usr/include/sbc/sbc.h ] || { echo "sbc: falta include/sbc/sbc.h" >&2; exit 1; } # ⚠ GUARDIÁN DE PIC — **LA PRIMERA VERSIÓN ESTABA MAL Y RECHAZÓ UN BUILD CORRECTO.** # # Contaba reubicaciones `R_X86_64_32/32S` en el `.o` y abortaba con «321 reubicaciones absolutas # ⇒ la .a NO es PIC». Falso: al mirar en QUÉ SECCIÓN están, todas eran de `.debug_abbrev` y # `.debug_str` —información DWARF—, que siempre lleva reubicaciones absolutas y **no se carga en # ejecución**. O sea que el control medía algo real y concluía algo falso, y de paso me hizo # perseguir un `-fPIC` que ya estaba surtiendo efecto. # # ⇒ Se reemplaza por la prueba AUTORITATIVA: **enlazar la `.a` dentro de una `.so`**, que es # literalmente lo que hace el consumidor (`libspa-bluez5.so`). Si el objeto no fuera PIC, el # enlazador lo diría con «relocation R_X86_64_32S against ... can not be used when making a shared # object». No hay forma de que esto pase y el consumidor falle. printf 'extern int sbc_init(void*,unsigned long);\nint envoltura(void*p){return sbc_init(p,0);}\n' > /tmp/so.c /opt/zig/zig cc -mcpu=baseline -shared -fPIC -o /tmp/libprueba.so /tmp/so.c /out/usr/lib/libsbc.a 2>/tmp/so.err \ || { echo "sbc: libsbc.a NO entra en una .so ⇒ no es PIC y libspa-bluez5.so no podría enlazarla" >&2; tail -8 /tmp/so.err >&2; exit 1; } [ -f /tmp/libprueba.so ] || { echo "sbc: el enlace de la .so dijo OK y no dejó fichero" >&2; exit 1; } # ⚠ GUARDIÁN 2: que la librería SIRVA, compilando y EJECUTANDO contra ella. # «Existe libsbc.a» no prueba nada: una .a se crea igual con objetos inútiles. Acá se inicializa un # codificador A2DP de verdad y se piden sus tamaños de trama, que es lo primero que hace el plugin # de PipeWire. Si `sbc_init` o `sbc_get_frame_length` no funcionan, no se sella. cat > /tmp/t.c <<'EOF' #include #include int main(void) { sbc_t s; if (sbc_init(&s, 0) < 0) { printf("sbc_init FALLO\n"); return 1; } s.frequency = SBC_FREQ_44100; s.blocks = SBC_BLK_16; s.subbands = SBC_SB_8; s.mode = SBC_MODE_STEREO; s.allocation = SBC_AM_LOUDNESS; s.bitpool = 53; size_t fl = sbc_get_frame_length(&s), cs = sbc_get_codesize(&s); sbc_finish(&s); if (fl == 0 || cs == 0) { printf("tamanos en cero: fl=%zu cs=%zu\n", fl, cs); return 1; } printf("OK frame=%zu codesize=%zu\n", fl, cs); return 0; } EOF /opt/zig/zig cc -mcpu=baseline -I/out/usr/include -o /tmp/t /tmp/t.c /out/usr/lib/libsbc.a 2>/tmp/cc.err \ || { echo "sbc: el programa de prueba NO ENLAZA contra libsbc.a" >&2; tail -12 /tmp/cc.err >&2; exit 1; } sal=$(/tmp/t) || { echo "sbc: el programa de prueba enlaza y FALLA al correr: $sal" >&2; exit 1; } case "$sal" in OK*) : ;; *) echo "sbc: salida inesperada del programa de prueba: $sal" >&2; exit 1 ;; esac echo "sbc: libsbc.a PIC + sbc.pc + cabecera · codificador A2DP probado ($sal)" ''' [deps] # `binutils` por `readelf`/`ar` del guardián de PIC: sin ellos el control se caería por falta de # herramienta en vez de por el defecto que busca. build = ["make", "pkgconf", "binutils"]