Browse Source

暂存

weicu
fdp 1 year ago
parent
commit
06effa8702
  1. 2
      local_plugins/jieli/android/build.gradle.kts
  2. 53
      local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt
  3. 2
      local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/JieliPlugin.kt
  4. 98
      local_plugins/jl_opus/android/src/main/kotlin/com/yunqiinnovation/jl_opus/JlOpusPlugin.kt
  5. 65
      local_plugins/jl_opus/lib/jl_opus.dart

2
local_plugins/jieli/android/build.gradle.kts

@ -53,4 +53,6 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-service:2.4.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")
implementation(project(":azure_speech"))
implementation(project(":jl_opus"))
implementation(project(":jl_opus"))
}

53
local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt

@ -16,6 +16,8 @@ import java.text.SimpleDateFormat
import java.util.*
import com.yunqiinnovation.azure_speech.AzureAsrHelper
import com.yunqiinnovation.azure_speech.AzureTtsHelper
import com.yunqiinnovation.jl_opus.JlOpusPlugin
/**
* BLE服务类,提供核心蓝牙功能
*/
@ -65,6 +67,9 @@ class BleService(private val context: Context) {
// Azure服务
private var azureAsrHelper: AzureAsrHelper? = null
private var azureTtsHelper: AzureTtsHelper? = null
// 获取插件实例
private val opusPlugin = JlOpusPlugin()
// 蓝牙管理器和适配器
private val bluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
@ -158,10 +163,10 @@ class BleService(private val context: Context) {
val deviceAddress = device.address
val rssi = result.rssi
val scanRecord = result.scanRecord
// if (!deviceName.contains("sanag") && !deviceName.contains("Ola Friend")) {
// return
// }
//Log.d(TAG, "============发现设备: $deviceName [$deviceAddress], RSSI: $rssi")
if (!deviceName.contains("zxd")) {
return
}
Log.d(TAG, "============发现设备: $deviceName [$deviceAddress], RSSI: $rssi")
// 打印所有发现的UUID以便调试
val serviceUuids = scanRecord?.serviceUuids
@ -175,20 +180,20 @@ class BleService(private val context: Context) {
for (i in 0 until 65535) {
val manufacturerData = scanRecord?.getManufacturerSpecificData(i)
if (manufacturerData != null) {
// Log.d(TAG, "发现厂商ID: $i, 数据长度: ${manufacturerData.size}")
// Log.d(TAG, "发现厂商ID: $i, 数据长度: ${manufacturerData.size}")
// 这里可以根据已知的厂商ID和格式解析数据
}
}
// 避免重复添加
val existingIndex = scanResults.indexOfFirst { it.device.address == deviceAddress }
if (existingIndex >= 0) {
scanResults[existingIndex] = result
} else {
Log.d(TAG, "扫描回调A, scanResults[existingIndex]ADD")
Log.d(TAG, "扫描回调A, scanResults[existingIndex]ADD")
scanResults.add(result)
}
@ -333,6 +338,7 @@ class BleService(private val context: Context) {
updateConnectionState(STATE_ERROR)
}
}
// 添加扩展函数(在文件顶部声明)
fun List<Byte>.toByteArray(): ByteArray {
return ByteArray(this.size) { this[it] }
@ -345,6 +351,12 @@ class BleService(private val context: Context) {
val characteristicUuid = characteristic.uuid.toString()
Log.d(TAG, "收到特征变化通知: $characteristicUuid")
opusPlugin?.startOpusDecodeStream(false, 1, 16000, 40, object :
JlOpusPlugin.Callback1 {
override fun onAudioDataReceived1(pcmData: ByteArray) {
Log.i(TAG, "收到解码控制通知: ${pcmData.contentToString()}")
// 处理解码后的 PCM 数据(如播放或写入文件)
}})
when (characteristicUuid) {
// NOTIFY_CHAR_UUID -> {
// // 收到音频数据
@ -362,11 +374,12 @@ class BleService(private val context: Context) {
if (value.isNotEmpty() && (value[0].toInt() and 0xFF == 0x4b || value[0].toInt() and 0xFF == 0x48)) {
// 处理设备返回的协议数据
fos?.write(value)
// 方法1:手动转换
val byteArray = ByteArray(value.size) { index -> value[index] }
// 或者直接使用现有
azureAsrHelper?.pushAudioData(byteArray)
opusPlugin?.handleDecodeRequest(value)
//callback?.onAudioDataReceived(value)
}
// 判断是否是协议格式响应
if (value.isNotEmpty() && value[0].toInt() and 0xFF == 0xA5) {
@ -508,7 +521,8 @@ val byteArray = ByteArray(value.size) { index -> value[index] }
writeChar?.value = commandData
// 获取外部存储目录路径
// 生成带时间戳的文件名(格式:yyyyMMdd_HHmmss_audio.opus)
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val timeStamp =
SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val fileName = "${timeStamp}_audio.opus"
val externalDir = context.getExternalFilesDir(null)
outputFile = File(externalDir, fileName)
@ -519,10 +533,10 @@ val byteArray = ByteArray(value.size) { index -> value[index] }
// val subscriptionKey = call.argument<String>("subscriptionKey") ?: ""
// val region = call.argument<String>("region") ?: ""
// val supportedLanguages = call.argument<List<String>>("supportedLanguages") ?: listOf("zh-CN")
// val success = azureAsrHelper.initialize(subscriptionKey, region, supportedLanguages.toTypedArray())
// result.success(success)
val success = bluetoothGatt?.writeCharacteristic(writeChar) ?: false
@ -599,7 +613,8 @@ val byteArray = ByteArray(value.size) { index -> value[index] }
(volumeData.toInt() and 0xFF)) and 0xFF
// 创建完整命令
val commandData = byteArrayOf(header, fieldType, dataLen, volumeData, checksum.toByte())
val commandData =
byteArrayOf(header, fieldType, dataLen, volumeData, checksum.toByte())
// 写入特征
writeChar?.value = commandData
@ -646,4 +661,4 @@ val byteArray = ByteArray(value.size) { index -> value[index] }
notifyChar = null
writeChar = null
}
}
}

2
local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/JieliPlugin.kt

@ -6,6 +6,7 @@ import android.bluetooth.BluetoothManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.util.Log
import androidx.annotation.NonNull
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
@ -91,6 +92,7 @@ class JieliPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, EventChannel
override fun onAudioDataReceived(data: ByteArray) {
pluginScope.launch(Dispatchers.Main) {
Log.d("","数据流onAudioDataReceived${data}")
sendEvent(EVENT_TYPE_AUDIO_DATA, data)
}
}

98
local_plugins/jl_opus/android/src/main/kotlin/com/yunqiinnovation/jl_opus/JlOpusPlugin.kt

@ -25,22 +25,35 @@ class JlOpusPlugin: FlutterPlugin, MethodCallHandler {
private lateinit var context: Context
private var opusManager: OpusManager? = null
private var streamSink: EventChannel.EventSink? = null
// 修正1:正确的主构造函数写法
constructor() {
opusManager = OpusManager()
Log.d(TAG, "OpusManager初始化成功")
}
// 添加主线程Handler用于确保在主线程回调
private val mainHandler = Handler(Looper.getMainLooper())
interface Callback1 {
fun onAudioDataReceived1(data: ByteArray)
}
companion object {
private const val TAG = "JlOpusPlugin"
private const val CHANNEL_NAME = "com.yunqiinnovation.jl_opus"
private const val EVENT_CHANNEL_NAME = "com.yunqiinnovation.jl_opus/events"
}
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME)
channel.setMethodCallHandler(this)
eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, EVENT_CHANNEL_NAME)
eventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
streamSink = events
@ -177,7 +190,7 @@ class JlOpusPlugin: FlutterPlugin, MethodCallHandler {
val sampleRate = call.argument<Int>("sampleRate") ?: 16000
val packetSize = call.argument<Int>("packetSize") ?: 40
// 如果已经在解码流,先停止
// 如果已经在解码流,先停止
if (opusManager?.isDecodeStream == true) {
opusManager?.stopDecodeStream()
}
@ -270,8 +283,87 @@ class JlOpusPlugin: FlutterPlugin, MethodCallHandler {
}
}
}
fun handleDecodeRequest(opusdata: ByteArray) {
Log.d(TAG, "handleDecodeRequest${opusdata}")
if (opusdata == null) {
return
}
if (opusManager?.isDecodeStream == true) {
opusManager?.writeAudioStream(opusdata)
} else {
}
}
fun startOpusDecodeStream(
hasHeader: Boolean,
channel: Int,
sampleRate: Int,
packetSize: Int,
callback: Callback1
) {
Log.d(TAG, "开始解码数据流,startOpusDecodeStream: $opusManager")
// 如果已经在解码流,先停止
if (opusManager?.isDecodeStream == true) {
opusManager?.stopDecodeStream()
}
// 创建解码参数
val option = OpusOption()
.setHasHead(hasHeader)
.setChannel(channel)
.setSampleRate(sampleRate)
.setPacketSize(packetSize)
Log.d(TAG, "开始解码数据流, 参数: $option")
opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback {
override fun onDecodeStream(data: ByteArray?) {
if (data != null) {
Log.d(TAG, "onDecodeStream${data}")
callback.onAudioDataReceived1(data)
sendEventOnMainThread(mapOf(
"event" to "onDecodeStream",
"data" to data
))
}
}
override fun onStart() {
Log.d(TAG, "数据流解码开始")
sendEventOnMainThread(mapOf(
"event" to "onStart",
"type" to "stream"
))
}
override fun onComplete(outPath: String?) {
Log.d(TAG, "数据流解码完成: $outPath")
sendEventOnMainThread(mapOf(
"event" to "onComplete",
"type" to "stream"
))
}
override fun onError(code: Int, message: String?) {
Log.e(TAG, "数据流解码错误: [$code] $message")
sendEventOnMainThread(mapOf(
"event" to "onError",
"type" to "stream",
"code" to code,
"message" to message
))
}
})
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
eventChannel.setStreamHandler(null)
opusManager?.let {

65
local_plugins/jl_opus/lib/jl_opus.dart

@ -6,13 +6,13 @@ import 'package:flutter/services.dart';
class OpusOption {
/// 是否具有协议头
final bool hasHeader;
/// 通道数。取值范围:[1, 2]
final int channel;
/// 采样率。参考值:{8000, 16000, 24000, 32000, 44100, 48000}
final int sampleRate;
/// 数据包长度。仅hasHeader = false时生效。
final int packetSize;
@ -39,19 +39,19 @@ class OpusOption {
class OpusEvent {
/// 事件类型
final String event;
/// 数据类型 (file或stream)
final String? type;
/// 解码后的数据 (仅在流解码时有效)
final Uint8List? data;
/// 输出文件路径 (仅在文件解码完成时有效)
final String? filePath;
/// 错误码 (仅在发生错误时有效)
final int? errorCode;
/// 错误信息 (仅在发生错误时有效)
final String? errorMessage;
@ -80,12 +80,16 @@ class OpusEvent {
/// 杰理OPUS编解码插件
class JlOpus {
static const MethodChannel _methodChannel = MethodChannel('com.yunqiinnovation.jl_opus');
static const EventChannel _eventChannel = EventChannel('com.yunqiinnovation.jl_opus/events');
static const MethodChannel _methodChannel =
MethodChannel('com.yunqiinnovation.jl_opus');
static const EventChannel _eventChannel =
EventChannel('com.yunqiinnovation.jl_opus/events');
static const EventChannel _eventChannel1 =
EventChannel('com.yunqiinnovation.jl_opus/events1');
/// 事件流
Stream<OpusEvent>? _eventStream;
/// 获取事件流
Stream<OpusEvent> get eventStream {
_eventStream ??= _eventChannel.receiveBroadcastStream().map((event) {
@ -99,18 +103,19 @@ class JlOpus {
});
return _eventStream!;
}
/// 解码后的数据流
final StreamController<Uint8List> _decodedDataController = StreamController<Uint8List>.broadcast();
final StreamController<Uint8List> _decodedDataController =
StreamController<Uint8List>.broadcast();
/// 获取解码数据流
Stream<Uint8List> get decodedDataStream => _decodedDataController.stream;
/// 构造函数,设置事件监听
JlOpus() {
_setupEventListener();
}
/// 设置事件监听
void _setupEventListener() {
eventStream.listen((event) {
@ -119,7 +124,7 @@ class JlOpus {
}
});
}
/// 初始化OPUS解码器
Future<bool> initOpusDecoder() async {
try {
@ -129,13 +134,14 @@ class JlOpus {
return false;
}
}
/// 解码OPUS文件
///
///
/// [inPath] 输入文件路径
/// [outPath] 输出文件路径
/// [option] 解码参数
Future<String?> decodeOpusFile(String inPath, String outPath, OpusOption option) async {
Future<String?> decodeOpusFile(
String inPath, String outPath, OpusOption option) async {
try {
final result = await _methodChannel.invokeMethod<String>(
'decodeOpusFile',
@ -150,9 +156,9 @@ class JlOpus {
rethrow;
}
}
/// 开始解码OPUS数据流
///
///
/// [option] 解码参数
Future<bool> startDecodeStream(OpusOption option) async {
try {
@ -165,19 +171,20 @@ class JlOpus {
rethrow;
}
}
/// 停止解码OPUS数据流
Future<bool> stopDecodeStream() async {
try {
final result = await _methodChannel.invokeMethod<bool>('stopDecodeStream');
final result =
await _methodChannel.invokeMethod<bool>('stopDecodeStream');
return result ?? false;
} catch (e) {
return false;
}
}
/// 写入OPUS音频数据进行解码
///
///
/// [data] OPUS音频数据
Future<bool> writeAudioStream(Uint8List data) async {
try {
@ -190,7 +197,7 @@ class JlOpus {
rethrow;
}
}
/// 判断是否正在解码
Future<bool> isDecoding() async {
try {
@ -200,7 +207,7 @@ class JlOpus {
return false;
}
}
/// 释放资源
Future<bool> dispose() async {
try {

Loading…
Cancel
Save