本教程/案例是结合 AI 完成的,采用 FFI (Foreign Function Interface) 。里面案例都实操验证过。
AI: Claude 4 Connect
零. 什么是 BLE LC3 ?
刚接触这个,不知道这个到底是啥,有没有对应的 Flutter
库,我们可以这样问 AI
。
markdown
什么是 BLE LC3 ?有没有Flutter的库实现,如果没有有哪些方式在 Flutter 中实现这些功能
得到的回复:
markdown
BLE LC3 是指在蓝牙低功耗(Bluetooth Low Energy)环境中使用 LC3(Low Complexity Communication Codec)音频编解码器的技术组合。让我为您详细解释:
## 什么是 BLE LC3?
### LC3 编解码器
- **全称**:Low Complexity Communication Codec
- **标准**:蓝牙 SIG 在 Bluetooth LE Audio 规范中定义
- **特点**:
- 低延迟、高质量的音频编解码
- 专为蓝牙音频应用优化
- 支持立体声和单声道
- 比传统 SBC 编解码器更高效
### BLE Audio 架构
- **BAP**(Basic Audio Profile):基础音频配置文件
- **CAP**(Common Audio Profile):通用音频配置文件
- **CSIP**(Coordinated Set Identification Profile):协调集识别配置文件
- **支持功能**:助听器、音频共享、广播音频等
## Flutter 中的实现方案
### 1. 现有 Flutter 库状况
目前 **没有成熟的 Flutter 库** 直接支持 BLE LC3,主要原因:
- BLE Audio 是相对较新的技术(2020年发布)
- 需要硬件和操作系统支持
- Flutter 生态系统中此类库还在发展中
### 2. 实现方式
#### 方案一:Platform Channel + 原生实现
```dart
// Flutter 端
class BleLC3Manager {
static const MethodChannel _channel = MethodChannel('ble_lc3');
Future<bool> startLC3Streaming() async {
return await _channel.invokeMethod('startStreaming');
}
Future<void> stopLC3Streaming() async {
await _channel.invokeMethod('stopStreaming');
}
}
```
**Android 原生实现**:
- 使用 Android BluetoothLeAudio API
- 集成 LC3 编解码库
- 处理音频数据流
**iOS 原生实现**:
- 使用 Core Bluetooth 框架
- 集成相应的音频处理库
#### 方案二:使用现有蓝牙库扩展
**推荐库**:
```yaml
dependencies:
flutter_bluetooth_serial: ^0.4.0
flutter_blue_plus: ^1.12.13
just_audio: ^0.9.34
```
**基础实现框架**:
```dart
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
class BleAudioService {
BluetoothDevice? connectedDevice;
BluetoothCharacteristic? audioCharacteristic;
Future<void> connectToAudioDevice(String deviceId) async {
// 连接到 BLE 设备
// 发现音频服务
// 订阅音频特征值
}
Future<void> startAudioStream() async {
// 开始音频流传输
// 处理 LC3 编解码(需要原生实现)
}
}
```
#### 方案三:FFI (Foreign Function Interface)
```dart
import 'dart:ffi';
import 'package:ffi/ffi.dart';
// 调用 C/C++ LC3 库
class LC3Codec {
late DynamicLibrary _lib;
LC3Codec() {
_lib = DynamicLibrary.open('lc3_codec.so'); // Android
// _lib = DynamicLibrary.open('lc3_codec.dylib'); // iOS
}
// 定义 C 函数签名
late final _encode = _lib.lookupFunction<
Int32 Function(Pointer<Uint8>, Int32, Pointer<Uint8>),
int Function(Pointer<Uint8>, int, Pointer<Uint8>)
>('lc3_encode');
}
```
### 3. 推荐的开发路径
#### 第一阶段:基础 BLE 连接
```dart
// 1. 使用 flutter_blue_plus 建立 BLE 连接
// 2. 实现设备发现和配对
// 3. 建立基础的数据传输通道
```
#### 第二阶段:音频处理
```dart
// 1. 集成音频录制/播放功能
// 2. 实现基础的音频数据传输
// 3. 添加音频格式转换
```
#### 第三阶段:LC3 集成
```dart
// 1. 通过 Platform Channel 调用原生 LC3 实现
// 2. 处理编解码逻辑
// 3. 优化音频质量和延迟
```
### 4. 技术挑战和注意事项
1. **硬件支持**:需要支持 BLE Audio 的设备
2. **操作系统版本**:
- Android 13+ 对 BLE Audio 支持较好
- iOS 需要相应版本支持
3. **权限管理**:需要蓝牙和音频相关权限
4. **性能优化**:音频处理对实时性要求高
### 5. 替代方案
如果 LC3 实现复杂,可以考虑:
- 使用传统的 A2DP 蓝牙音频
- 实现自定义的音频编解码方案
- 使用 WebRTC 进行音频传输
目前来看,在 Flutter 中实现完整的 BLE LC3 功能需要大量的原生开发工作,建议根据具体需求选择合适的实现方案。
可以看到 AI 提供了三种方案,但是我看了方案二更像是完整的解决方案,因为里面还是要原生实现,所以我们可以采用方案一或方案三,下面我采用方案三的方式实现,我们基于上面的方式继续下面的操作。
我们采用方案三,但是我并不知道 lc3_codec.so 怎么获取到,于是可以接着问 AI :
markdown
我打算采用方案三 ffi 的方式实现,但是 lc3_codec.so 文件从什么地方获取
下面就是我得到的结果:
markdown
对于 LC3 编解码器的 `.so` 文件,您有几个获取途径:
## 1. 官方开源实现
### Google LC3 开源库
```bash
# 克隆官方仓库
git clone https://github.com/google/liblc3.git
cd liblc3
# 构建 Android 版本
mkdir build && cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a \
-DANDROID_PLATFORM=android-21
make
```
这会生成 `liblc3.so` 文件。
### 构建脚本示例
```bash
#!/bin/bash
# build_lc3_android.sh
export ANDROID_NDK_HOME=/path/to/android-ndk
export TOOLCHAIN=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64
# 为不同架构构建
ABIS=("arm64-v8a" "armeabi-v7a" "x86_64" "x86")
for ABI in "${ABIS[@]}"; do
mkdir -p build_$ABI && cd build_$ABI
cmake .. \
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=$ABI \
-DANDROID_PLATFORM=android-21 \
-DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
cd ..
done
```
## 2. 预编译库获取
### 从 Android 系统提取
```bash
# 如果设备支持 BLE Audio,可以从系统提取
adb shell find /system -name "*lc3*" -type f
adb shell find /vendor -name "*lc3*" -type f
# 常见位置
adb pull /system/lib64/liblc3.so
adb pull /vendor/lib64/liblc3.so
```
### 第三方预编译版本
一些开发者社区可能提供预编译版本,但建议验证其来源和完整性。
## 3. Flutter 项目集成
### 项目结构
```
your_flutter_project/
├── android/
│ └── app/
│ └── src/
│ └── main/
│ └── jniLibs/
│ ├── arm64-v8a/
│ │ └── liblc3.so
│ ├── armeabi-v7a/
│ │ └── liblc3.so
│ ├── x86_64/
│ │ └── liblc3.so
│ └── x86/
│ └── liblc3.so
├── ios/
│ └── Frameworks/
│ └── liblc3.framework/
└── lib/
└── lc3_codec.dart
```
### CMakeLists.txt 配置
```cmake
# android/app/src/main/cpp/CMakeLists.txt
cmake_minimum_required(VERSION 3.18.1)
project(lc3_flutter)
# 添加预编译库
add_library(lc3 SHARED IMPORTED)
set_target_properties(lc3 PROPERTIES
IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/liblc3.so
)
# 创建 JNI 包装器
add_library(lc3_flutter SHARED lc3_wrapper.cpp)
target_link_libraries(lc3_flutter lc3 log)
```
## 4. Flutter FFI 实现
### Dart 绑定代码
```dart
// lib/lc3_codec.dart
import 'dart:ffi';
import 'dart:io';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';
class LC3Codec {
late DynamicLibrary _lib;
// LC3 函数签名
late final Pointer<NativeFunction<Int32 Function(Int32, Int32)>> _lc3SetupEncoder;
late final Pointer<NativeFunction<Int32 Function(Pointer<Uint8>, Int32, Pointer<Uint8>, Int32)>> _lc3Encode;
late final Pointer<NativeFunction<Int32 Function(Pointer<Uint8>, Int32, Pointer<Uint8>, Int32)>> _lc3Decode;
LC3Codec() {
if (Platform.isAndroid) {
_lib = DynamicLibrary.open('liblc3.so');
} else if (Platform.isIOS) {
_lib = DynamicLibrary.open('liblc3.framework/liblc3');
} else {
throw UnsupportedError('Platform not supported');
}
_initializeFunctions();
}
void _initializeFunctions() {
_lc3SetupEncoder = _lib.lookup('lc3_setup_encoder');
_lc3Encode = _lib.lookup('lc3_encode');
_lc3Decode = _lib.lookup('lc3_decode');
}
Uint8List encode(Uint8List audioData, int sampleRate, int bitrate) {
final inputPtr = malloc<Uint8>(audioData.length);
final outputPtr = malloc<Uint8>(audioData.length * 2); // 预分配输出缓冲区
try {
// 复制输入数据
for (int i = 0; i < audioData.length; i++) {
inputPtr[i] = audioData[i];
}
// 调用编码函数
final encodedLength = _lc3Encode.asFunction<
int Function(Pointer<Uint8>, int, Pointer<Uint8>, int)
>()(inputPtr, audioData.length, outputPtr, audioData.length * 2);
// 复制输出数据
final result = Uint8List(encodedLength);
for (int i = 0; i < encodedLength; i++) {
result[i] = outputPtr[i];
}
return result;
} finally {
malloc.free(inputPtr);
malloc.free(outputPtr);
}
}
Uint8List decode(Uint8List encodedData) {
// 类似的解码实现
// ...
}
}
```
## 5. 自定义构建方案
### 创建自己的 LC3 包装库
```cpp
// lc3_wrapper.cpp
#include <jni.h>
#include "lc3.h" // 假设您有 LC3 头文件
extern "C" {
JNIEXPORT jint JNICALL
Java_com_yourpackage_LC3Native_encode(JNIEnv *env, jobject thiz,
jbyteArray input, jint inputLength,
jbyteArray output, jint outputLength) {
jbyte* inputBuffer = env->GetByteArrayElements(input, nullptr);
jbyte* outputBuffer = env->GetByteArrayElements(output, nullptr);
// 调用 LC3 编码函数
int result = lc3_encode((uint8_t*)inputBuffer, inputLength,
(uint8_t*)outputBuffer, outputLength);
env->ReleaseByteArrayElements(input, inputBuffer, 0);
env->ReleaseByteArrayElements(output, outputBuffer, 0);
return result;
}
} // extern "C"
```
## 6. 推荐的获取步骤
1. **首选**:从 Google 官方仓库构建
2. **备选**:查找设备系统中的现有库
3. **最后**:考虑第三方预编译版本(需谨慎验证)
### 构建命令总结
```bash
# 克隆并构建
git clone https://github.com/google/liblc3.git
cd liblc3
# Android 构建
./scripts/build_android.sh # 如果有提供的脚本
# 或手动 cmake 构建
# 复制到 Flutter 项目
cp build/android/arm64-v8a/liblc3.so /path/to/flutter/project/android/app/src/main/jniLibs/arm64-v8a/
```
这样您就可以获得所需的 `liblc3.so` 文件并在 Flutter 项目中使用 FFI 调用了。
现在我们就已经知道源代码的位置了。
一. LC3 代码 -> *.so
bash
git clone https://github.com/google/liblc3.git
打开 src
,这里面就全是 LC3
的源码。在你的 Flutter
项目中新建一个 native/liblc3
文件夹,然后将 src
中的代码都复制到这个目录中,通过检查代码,我们发现还有 include
文件中还有一些头文件, src
中有依赖,我们把这三个文件也复制到 native/liblc3
文件夹中。看到 lc3.c
和 common.h
有下面的代码:
c
#include <lc3.h>
我们修改成:
c
#include "lc3.h"
1.1 android so 准备
Dart
并不能直接调用 C
中的代码,即便借助 ffi 也做不到,但是能执行编译后的 *.so
文件;于是现在我们有一种直观的办法,也就是先将 LC3
代码编译成 *.so
,但是这样有点麻烦,因为我们需要编译各个架构的包;还有一种方法就是让我们编译 app
的时候自动帮我们生成对应架构的包,也就是利用 gradle 来帮我们做这件事情,首先我们编写 CMakeLists.txt
文件,内容如下:
cmake
cmake_minimum_required(VERSION 3.22.1)
project(liblc3 C)
# 收集所有 C 源文件
file(GLOB LC3_SOURCES
"${CMAKE_CURRENT_LIST_DIR}/*.c"
)
# 生成共享库而不是静态库
add_library(liblc3 SHARED ${LC3_SOURCES})
# 头文件目录
target_include_directories(liblc3 PUBLIC
"${CMAKE_CURRENT_LIST_DIR}"
)
# 设置库的输出名称(可选)
set_target_properties(liblc3 PROPERTIES
LIBRARY_OUTPUT_NAME "lc3"
)
放在这里面的 CMakeLists.txt gradle 并不能找到,同时我们需要生成 *.so ,我们能访问到,所以我们在 android/app/main/cpp 下再新建一个 CMakeLists.txt 内容如下:
cmake
cmake_minimum_required(VERSION 3.22.1)
project(flutter_native_libs C CXX)
# 添加 liblc3 子目录
add_subdirectory(../../../../../native/liblc3 ${CMAKE_BINARY_DIR}/liblc3)
# 确保 liblc3 被正确导出
# 这会确保 .so 文件被复制到正确的位置
install(TARGETS liblc3
LIBRARY DESTINATION ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}
)
然后我们再告诉 gradle
就可以搞定了,首先我们在 android/app/build.gradle.kts
下 android
下新增:
gradle
android {
// 其他配置
defaultConfig {
// 其他配置
// 添加 CMake 参数
externalNativeBuild {
cmake {
cppFlags += ""
// 可选:指定要构建的 ABI
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
}
}
}
// 添加或确保有这个 externalNativeBuild 配置
externalNativeBuild {
cmake {
// 指向我们刚刚创建的 CMakeLists.txt 文件
path = file("src/main/cpp/CMakeLists.txt")
// 指定一个 CMake 版本,通常与 Android Gradle Plugin (AGP) 兼容
// 你可以在 SDK Manager -> SDK Tools 中查看已安装的 CMake 版本
version = "3.22.1" // 或者 "3.22.1" 等,根据你的环境调整
}
}
}
这就完成了 android 。
1.2 ios
基于 xcode Version 16.2 (16C5032a)
编写。

然后把名称命名为: liblc3
。

然后选中 liblc3
鼠标右键:

然后选择刚才我们的 native/liblc3
下所有的 *.c
和 *.h
的文件。

这里我们选择引用,而不是复制文件,这样的目的是为了只保留一份就可以了,即便我们修改了文件的内容也会同步修改。
然后选择:

在搜索框中搜索 Header Search Paths
,然后:

自此 android 和 ios 的 C 代码的准备工作已经完成。
二. ffi 封装 C 暴露的函数
我对 LC3 不怎么了解,也不知道哪些方法可以使用,但是幸好, liblc3 有 python 基于 c 的封装,代码位置在 github.com/google/libl... 。我把这些代码全部复制出来,然后通过 AI ,让 AI 帮我把这些代码转化成 Dart 的实现:
markdown
帮我把上面的 Python 代码转换成 Dart 的方式实现,采用 ffi 的方式暴露给 Flutter 端使用。
然后就得到了下面的代码:
dart
import 'dart:ffi';
import 'dart:io';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';
/// Base error raised by liblc3
class BaseError implements Exception {
final String message;
BaseError(this.message);
@override
String toString() => 'BaseError: $message';
}
/// Error raised when liblc3 cannot be initialized
class InitializationError extends BaseError {
InitializationError(String message) : super(message);
@override
String toString() => 'InitializationError: $message';
}
/// Error raised when a bad argument is given
class InvalidArgumentError extends BaseError {
InvalidArgumentError(String message) : super(message);
@override
String toString() => 'InvalidArgumentError: $message';
}
/// PCM format enum
enum PcmFormat {
s16(0),
s24(1),
s24_3le(2),
float(3);
final int value;
const PcmFormat(this.value);
}
/// Native function signatures
typedef Lc3HrFrameSamplesNative =
Int32 Function(Bool hrmode, Int32 dt_us, Int32 sr_hz);
typedef Lc3HrFrameSamples = int Function(bool hrmode, int dt_us, int sr_hz);
typedef Lc3HrFrameBlockBytesNative =
Int32 Function(
Bool hrmode,
Int32 dt_us,
Int32 sr_hz,
Int32 num_channels,
Int32 bitrate,
);
typedef Lc3HrFrameBlockBytes =
int Function(
bool hrmode,
int dt_us,
int sr_hz,
int num_channels,
int bitrate,
);
typedef Lc3HrResolveBitrateNative =
Int32 Function(Bool hrmode, Int32 dt_us, Int32 sr_hz, Int32 nbytes);
typedef Lc3HrResolveBitrate =
int Function(bool hrmode, int dt_us, int sr_hz, int nbytes);
typedef Lc3HrDelaySamplesNative =
Int32 Function(Bool hrmode, Int32 dt_us, Int32 sr_hz);
typedef Lc3HrDelaySamples = int Function(bool hrmode, int dt_us, int sr_hz);
typedef Lc3HrEncoderSizeNative =
Uint32 Function(Bool hrmode, Int32 dt_us, Int32 sr_hz);
typedef Lc3HrEncoderSize = int Function(bool hrmode, int dt_us, int sr_hz);
typedef Lc3HrSetupEncoderNative =
Pointer Function(
Bool hrmode,
Int32 dt_us,
Int32 sr_hz,
Int32 sr_pcm_hz,
Pointer mem,
);
typedef Lc3HrSetupEncoder =
Pointer Function(
bool hrmode,
int dt_us,
int sr_hz,
int sr_pcm_hz,
Pointer mem,
);
typedef Lc3EncodeNative =
Int32 Function(
Pointer encoder,
Int32 pcm_fmt,
Pointer pcm,
Int32 stride,
Int32 nbytes,
Pointer data,
);
typedef Lc3Encode =
int Function(
Pointer encoder,
int pcm_fmt,
Pointer pcm,
int stride,
int nbytes,
Pointer data,
);
typedef Lc3HrDecoderSizeNative =
Uint32 Function(Bool hrmode, Int32 dt_us, Int32 sr_hz);
typedef Lc3HrDecoderSize = int Function(bool hrmode, int dt_us, int sr_hz);
typedef Lc3HrSetupDecoderNative =
Pointer Function(
Bool hrmode,
Int32 dt_us,
Int32 sr_hz,
Int32 sr_pcm_hz,
Pointer mem,
);
typedef Lc3HrSetupDecoder =
Pointer Function(
bool hrmode,
int dt_us,
int sr_hz,
int sr_pcm_hz,
Pointer mem,
);
typedef Lc3DecodeNative =
Int32 Function(
Pointer decoder,
Pointer data,
Int32 nbytes,
Int32 pcm_fmt,
Pointer pcm,
Int32 stride,
);
typedef Lc3Decode =
int Function(
Pointer decoder,
Pointer data,
int nbytes,
int pcm_fmt,
Pointer pcm,
int stride,
);
/// Base class for LC3 encoder and decoder
abstract class LC3Base {
late final DynamicLibrary _lib;
late final bool hrmode;
late final int frameDurationUs;
late final int sampleRateHz;
late final int pcmSampleRateHz;
late final int numChannels;
// Function pointers
late final Lc3HrFrameSamples _lc3HrFrameSamples;
late final Lc3HrFrameBlockBytes _lc3HrFrameBlockBytes;
late final Lc3HrResolveBitrate _lc3HrResolveBitrate;
late final Lc3HrDelaySamples _lc3HrDelaySamples;
LC3Base({
required this.frameDurationUs,
required this.sampleRateHz,
required this.numChannels,
this.hrmode = false,
int? pcmSampleRateHz,
String? libPath,
}) : pcmSampleRateHz = pcmSampleRateHz ?? sampleRateHz {
// Validate frame duration
if (![2500, 5000, 7500, 10000].contains(frameDurationUs)) {
throw InvalidArgumentError(
'Invalid frame duration: $frameDurationUs us (${frameDurationUs / 1000} ms)',
);
}
// Validate sample rate
final allowedSampleRates = hrmode
? [48000, 96000]
: [8000, 16000, 24000, 32000, 48000];
if (!allowedSampleRates.contains(sampleRateHz)) {
throw InvalidArgumentError('Invalid sample rate: $sampleRateHz Hz');
}
// Load library
_loadLibrary();
_initializeFunctions();
}
void _loadLibrary() {
if (Platform.isAndroid) {
// Android 上加载我们编译的共享库
_lib = DynamicLibrary.open('liblc3.so');
} else if (Platform.isIOS) {
// iOS 上使用 process() 因为库会被静态链接
_lib = DynamicLibrary.process();
} else {
throw UnsupportedError('Unsupported platform');
}
}
void _initializeFunctions() {
// Check if HR functions are available
try {
_lc3HrFrameSamples = _lib
.lookup<NativeFunction<Lc3HrFrameSamplesNative>>(
'lc3_hr_frame_samples',
)
.asFunction();
_lc3HrFrameBlockBytes = _lib
.lookup<NativeFunction<Lc3HrFrameBlockBytesNative>>(
'lc3_hr_frame_block_bytes',
)
.asFunction();
_lc3HrResolveBitrate = _lib
.lookup<NativeFunction<Lc3HrResolveBitrateNative>>(
'lc3_hr_resolve_bitrate',
)
.asFunction();
_lc3HrDelaySamples = _lib
.lookup<NativeFunction<Lc3HrDelaySamplesNative>>(
'lc3_hr_delay_samples',
)
.asFunction();
} catch (e) {
if (hrmode) {
throw InitializationError('High-Resolution interface not available');
}
// Fallback to standard functions
_initializeFallbackFunctions();
}
}
void _initializeFallbackFunctions() {
// Create wrapper functions for non-HR mode
final frameSamples = _lib
.lookup<NativeFunction<Int32 Function(Int32, Int32)>>(
'lc3_frame_samples',
)
.asFunction<int Function(int, int)>();
final frameBytes = _lib
.lookup<NativeFunction<Int32 Function(Int32, Int32)>>('lc3_frame_bytes')
.asFunction<int Function(int, int)>();
final resolveBitrate = _lib
.lookup<NativeFunction<Int32 Function(Int32, Int32)>>(
'lc3_resolve_bitrate',
)
.asFunction<int Function(int, int)>();
final delaySamples = _lib
.lookup<NativeFunction<Int32 Function(Int32, Int32)>>(
'lc3_delay_samples',
)
.asFunction<int Function(int, int)>();
_lc3HrFrameSamples = (hrmode, dtUs, srHz) => frameSamples(dtUs, srHz);
_lc3HrFrameBlockBytes = (hrmode, dtUs, srHz, numChannels, bitrate) =>
numChannels * frameBytes(dtUs, bitrate ~/ 2);
_lc3HrResolveBitrate = (hrmode, dtUs, srHz, nbytes) =>
resolveBitrate(dtUs, nbytes);
_lc3HrDelaySamples = (hrmode, dtUs, srHz) => delaySamples(dtUs, srHz);
}
/// Returns the number of PCM samples in an LC3 frame
int getFrameSamples() {
final ret = _lc3HrFrameSamples(hrmode, frameDurationUs, pcmSampleRateHz);
if (ret < 0) {
throw InvalidArgumentError('Bad parameters');
}
return ret;
}
/// Returns the size of LC3 frame blocks, from bitrate in bits per second
int getFrameBytes(int bitrate) {
final ret = _lc3HrFrameBlockBytes(
hrmode,
frameDurationUs,
sampleRateHz,
numChannels,
bitrate,
);
if (ret < 0) {
throw InvalidArgumentError('Bad parameters');
}
return ret;
}
/// Returns the bitrate in bits per second, from the size of LC3 frames
int resolveBitrate(int numBytes) {
final ret = _lc3HrResolveBitrate(
hrmode,
frameDurationUs,
sampleRateHz,
numBytes,
);
if (ret < 0) {
throw InvalidArgumentError('Bad parameters');
}
return ret;
}
/// Returns the algorithmic delay, as a number of samples
int getDelaySamples() {
final ret = _lc3HrDelaySamples(hrmode, frameDurationUs, pcmSampleRateHz);
if (ret < 0) {
throw InvalidArgumentError('Bad parameters');
}
return ret;
}
static (PcmFormat, int) resolvePcmFormat(int? bitDepth) {
switch (bitDepth) {
case 16:
return (PcmFormat.s16, 2);
case 24:
return (PcmFormat.s24_3le, 3);
case null:
return (PcmFormat.float, 4);
default:
throw InvalidArgumentError('Could not interpret PCM bit_depth');
}
}
}
/// LC3 Encoder wrapper
class LC3Encoder extends LC3Base {
final List<Pointer> _encoders = [];
late final Lc3HrEncoderSize _lc3HrEncoderSize;
late final Lc3HrSetupEncoder _lc3HrSetupEncoder;
late final Lc3Encode _lc3Encode;
LC3Encoder({
required int frameDurationUs,
required int sampleRateHz,
int numChannels = 1,
bool hrmode = false,
int? inputSampleRateHz,
String? libPath,
}) : super(
frameDurationUs: frameDurationUs,
sampleRateHz: sampleRateHz,
numChannels: numChannels,
hrmode: hrmode,
pcmSampleRateHz: inputSampleRateHz,
libPath: libPath,
) {
_initializeEncoderFunctions();
_createEncoders();
}
void _initializeEncoderFunctions() {
try {
_lc3HrEncoderSize = _lib
.lookup<NativeFunction<Lc3HrEncoderSizeNative>>('lc3_hr_encoder_size')
.asFunction();
_lc3HrSetupEncoder = _lib
.lookup<NativeFunction<Lc3HrSetupEncoderNative>>(
'lc3_hr_setup_encoder',
)
.asFunction();
} catch (e) {
if (hrmode) {
throw InitializationError('High-Resolution interface not available');
}
_initializeEncoderFallbackFunctions();
}
_lc3Encode = _lib
.lookup<NativeFunction<Lc3EncodeNative>>('lc3_encode')
.asFunction();
}
void _initializeEncoderFallbackFunctions() {
final encoderSize = _lib
.lookup<NativeFunction<Uint32 Function(Int32, Int32)>>(
'lc3_encoder_size',
)
.asFunction<int Function(int, int)>();
final setupEncoder = _lib
.lookup<NativeFunction<Pointer Function(Int32, Int32, Int32, Pointer)>>(
'lc3_setup_encoder',
)
.asFunction<Pointer Function(int, int, int, Pointer)>();
_lc3HrEncoderSize = (hrmode, dtUs, srHz) => encoderSize(dtUs, srHz);
_lc3HrSetupEncoder = (hrmode, dtUs, srHz, srPcmHz, mem) =>
setupEncoder(dtUs, srHz, srPcmHz, mem);
}
void _createEncoders() {
for (int i = 0; i < numChannels; i++) {
final size = _lc3HrEncoderSize(hrmode, frameDurationUs, pcmSampleRateHz);
final mem = malloc.allocate(size);
final encoder = _lc3HrSetupEncoder(
hrmode,
frameDurationUs,
sampleRateHz,
pcmSampleRateHz,
mem,
);
_encoders.add(encoder);
}
}
/// Encodes LC3 frame(s), for each channel
Uint8List encode(dynamic pcm, int numBytes, {int? bitDepth}) {
final frameSamples = getFrameSamples();
final (pcmFormat, sampleSize) = LC3Base.resolvePcmFormat(bitDepth);
final pcmLen = numChannels * frameSamples;
Pointer<Uint8> pcmBuffer;
if (bitDepth == null) {
// Float input
if (pcm is! List<double>) {
throw InvalidArgumentError('Expected List<double> for float PCM input');
}
// Validate range
final sum = pcm.fold<double>(0, (sum, val) => sum + val.abs());
if (sum / frameSamples >= 2) {
throw InvalidArgumentError('Out of range PCM input');
}
// Create buffer with padding
final floatBuffer = malloc<Float>(pcmLen);
for (int i = 0; i < pcm.length && i < pcmLen; i++) {
floatBuffer[i] = pcm[i];
}
for (int i = pcm.length; i < pcmLen; i++) {
floatBuffer[i] = 0.0;
}
pcmBuffer = floatBuffer.cast<Uint8>();
} else {
// Byte input
if (pcm is! Uint8List && pcm is! List<int>) {
throw InvalidArgumentError('Expected byte data for PCM input');
}
final bytes = pcm is Uint8List ? pcm : Uint8List.fromList(pcm);
final requiredBytes = pcmLen * sampleSize;
final paddedBytes = Uint8List(requiredBytes);
paddedBytes.setRange(0, bytes.length.clamp(0, requiredBytes), bytes);
pcmBuffer = malloc<Uint8>(requiredBytes);
for (int i = 0; i < requiredBytes; i++) {
pcmBuffer[i] = paddedBytes[i];
}
}
// Allocate output buffer
final dataBuffer = malloc<Uint8>(numBytes);
int dataOffset = 0;
try {
for (int ich = 0; ich < numChannels; ich++) {
final pcmOffset = ich * sampleSize;
final pcmPtr = pcmBuffer.elementAt(pcmOffset);
final dataSize =
numBytes ~/ numChannels + (ich < numBytes % numChannels ? 1 : 0);
final dataPtr = dataBuffer.elementAt(dataOffset);
dataOffset += dataSize;
final ret = _lc3Encode(
_encoders[ich],
pcmFormat.value,
pcmPtr,
numChannels,
dataSize,
dataPtr,
);
if (ret < 0) {
throw InvalidArgumentError('Bad parameters');
}
}
// Copy to Dart Uint8List
final result = Uint8List(numBytes);
for (int i = 0; i < numBytes; i++) {
result[i] = dataBuffer[i];
}
return result;
} finally {
malloc.free(pcmBuffer);
malloc.free(dataBuffer);
}
}
void dispose() {
for (final encoder in _encoders) {
malloc.free(encoder);
}
_encoders.clear();
}
}
/// LC3 Decoder wrapper
class LC3Decoder extends LC3Base {
final List<Pointer> _decoders = [];
late final Lc3HrDecoderSize _lc3HrDecoderSize;
late final Lc3HrSetupDecoder _lc3HrSetupDecoder;
late final Lc3Decode _lc3Decode;
LC3Decoder({
required int frameDurationUs,
required int sampleRateHz,
int numChannels = 1,
bool hrmode = false,
int? outputSampleRateHz,
String? libPath,
}) : super(
frameDurationUs: frameDurationUs,
sampleRateHz: sampleRateHz,
numChannels: numChannels,
hrmode: hrmode,
pcmSampleRateHz: outputSampleRateHz,
libPath: libPath,
) {
_initializeDecoderFunctions();
_createDecoders();
}
void _initializeDecoderFunctions() {
try {
_lc3HrDecoderSize = _lib
.lookup<NativeFunction<Lc3HrDecoderSizeNative>>('lc3_hr_decoder_size')
.asFunction();
_lc3HrSetupDecoder = _lib
.lookup<NativeFunction<Lc3HrSetupDecoderNative>>(
'lc3_hr_setup_decoder',
)
.asFunction();
} catch (e) {
if (hrmode) {
throw InitializationError('High-Resolution interface not available');
}
_initializeDecoderFallbackFunctions();
}
_lc3Decode = _lib
.lookup<NativeFunction<Lc3DecodeNative>>('lc3_decode')
.asFunction();
}
void _initializeDecoderFallbackFunctions() {
final decoderSize = _lib
.lookup<NativeFunction<Uint32 Function(Int32, Int32)>>(
'lc3_decoder_size',
)
.asFunction<int Function(int, int)>();
final setupDecoder = _lib
.lookup<NativeFunction<Pointer Function(Int32, Int32, Int32, Pointer)>>(
'lc3_setup_decoder',
)
.asFunction<Pointer Function(int, int, int, Pointer)>();
_lc3HrDecoderSize = (hrmode, dtUs, srHz) => decoderSize(dtUs, srHz);
_lc3HrSetupDecoder = (hrmode, dtUs, srHz, srPcmHz, mem) =>
setupDecoder(dtUs, srHz, srPcmHz, mem);
}
void _createDecoders() {
for (int i = 0; i < numChannels; i++) {
final size = _lc3HrDecoderSize(hrmode, frameDurationUs, pcmSampleRateHz);
final mem = malloc.allocate(size);
final decoder = _lc3HrSetupDecoder(
hrmode,
frameDurationUs,
sampleRateHz,
pcmSampleRateHz,
mem,
);
_decoders.add(decoder);
}
}
/// Decodes an LC3 frame
dynamic decode(Uint8List? data, {int? bitDepth}) {
final (pcmFormat, sampleSize) = LC3Base.resolvePcmFormat(bitDepth);
final pcmLen = numChannels * getFrameSamples();
final pcmBuffer = malloc<Uint8>(pcmLen * sampleSize);
Pointer<Uint8>? dataBuffer;
if (data != null) {
dataBuffer = malloc<Uint8>(data.length);
for (int i = 0; i < data.length; i++) {
dataBuffer[i] = data[i];
}
}
try {
int dataOffset = 0;
for (int ich = 0; ich < numChannels; ich++) {
final pcmOffset = ich * sampleSize;
final pcmPtr = pcmBuffer.elementAt(pcmOffset);
int ret;
if (data == null || dataBuffer == null) {
// PLC mode
ret = _lc3Decode(
_decoders[ich],
nullptr,
0,
pcmFormat.value,
pcmPtr,
numChannels,
);
} else {
final dataSize =
data.length ~/ numChannels +
(ich < data.length % numChannels ? 1 : 0);
final dataPtr = dataBuffer.elementAt(dataOffset);
dataOffset += dataSize;
ret = _lc3Decode(
_decoders[ich],
dataPtr,
dataSize,
pcmFormat.value,
pcmPtr,
numChannels,
);
}
if (ret < 0) {
throw InvalidArgumentError('Bad parameters');
}
}
// Convert output based on bit depth
if (bitDepth == null) {
// Return float array
final floatPtr = pcmBuffer.cast<Float>();
final result = List<double>.generate(pcmLen, (i) => floatPtr[i]);
return result;
} else {
// Return byte array
final result = Uint8List(pcmLen * sampleSize);
for (int i = 0; i < result.length; i++) {
result[i] = pcmBuffer[i];
}
return result;
}
} finally {
malloc.free(pcmBuffer);
if (dataBuffer != null) {
malloc.free(dataBuffer);
}
}
}
void dispose() {
for (final decoder in _decoders) {
malloc.free(decoder);
}
_decoders.clear();
}
}
我把这个文件放在了 lib/native/liblc3/lc3.dart
中。自此就已经可以使用了。如果你对这里面的代码不怎么看懂,你可以采用以下方式进行理解和排查问题,首先这里面的代码都是 Dart ,那么语法应该不是问题,如果有看不懂的,可以去 Dart 官方看对应的语法。如果不了解 ffi 相关的 API ,可以点击我上面提供的链接去学习一波。如果你并不希望慢慢的学习这些,那么可以让 AI 帮忙解释,如果你跟我一样,比较喜欢采用大局观到小局观的方式理解,可以让 AI 在编写这些代码之前先输出整个 ffi 加载 C 库的整体逻辑,也就是下面的提示词:
markdown
帮我把上面的 Python 代码转换成 Dart 的方式实现,采用 ffi 的方式暴露给 Flutter 端使用。先帮我讲解其原理然后再帮我转换,生成完成后再结合原理讲解一下其对应的实现。
通过上面的提示词就可以得到下面的结果(Claude 4 Sonnet):
markdown
我来帮您分析这个Python代码的原理,然后转换为Dart FFI实现。
## 原理分析
这个Python代码是LC3音频编解码器的包装器,主要原理如下:
### 1. 核心组件
- **LC3 (Low Complexity Communications Codec)**: 一种低延迟音频编解码器
- **FFI (Foreign Function Interface)**: 通过ctypes调用C库函数
- **编码器/解码器**: 分别处理PCM音频的压缩和解压缩
### 2. 主要功能
- **动态库加载**: 自动查找并加载LC3库
- **参数验证**: 验证帧时长、采样率等参数
- **内存管理**: 通过malloc/free管理C库内存
- **多声道支持**: 支持多声道音频处理
- **高分辨率模式**: 支持HR模式的更高采样率
### 3. 工作流程
1. 初始化编码器/解码器
2. 分配内存空间
3. 调用C库函数进行编解码
4. 返回处理后的音频数据
## Dart FFI 实现
```dart
// dart 代码
```
## 实现原理对应关系
### 1. **库加载机制**
- **Python**: 使用 `ctypes.cdll.LoadLibrary()` 和 `find_library()`
- **Dart**: 使用 `DynamicLibrary.open()` 和平台特定的库名
### 2. **函数绑定**
- **Python**: 通过 `lib.function_name` 直接访问
- **Dart**: 使用 `lookupFunction<NativeType, DartType>()` 显式绑定
### 3. **内存管理**
- **Python**: 通过 libc 的 malloc/free
- **Dart**: 使用 `package:ffi` 的 `malloc`/`free` 或直接调用 libc
### 4. **数据类型转换**
- **Python**: ctypes 自动处理类型转换
- **Dart**: 需要显式的指针操作和类型转换
### 5. **错误处理**
- **Python**: 基于异常的错误处理
- **Dart**: 保持相同的异常层次结构
### 6. **多态支持**
- **Python**: 使用 `@typing.overload` 装饰器
- **Dart**: 使用可选参数和动态返回类型
### 7. **资源管理**
- **Python**: `__del__` 方法自动清理
- **Dart**: 显式的 `dispose()` 方法
这个Dart实现保持了原Python代码的核心功能和API设计,同时利用了Dart的类型安全特性和FFI机制,为Flutter应用提供了高效的LC3音频编解码能力。
三. 流程理解

ffi
调用编译工具编译完的结果,然后我们在 Dart
代码中就能直接使用了。建议先对 ffi
层封装一下,而不是在我们业务代码中直接使用 ffi
对 c
代码进行加载。