Browse Source

Merge commit 'b7f28a6450f0caf9fd5b5583899a3a517851be89'

newdev_shunjiawei
lxm 1 year ago
parent
commit
20d5ed40ce
  1. 2
      .gitignore
  2. 11
      .mcp.json
  3. 1
      amap_flutter/android/src/main/java/com/example/amap_flutter/AmapFlutterPlugin.java
  4. 30
      android/app/src/main/AndroidManifest.xml
  5. 45
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/BleBroadcastReceiver.kt
  6. 335
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/BleWakeService.kt
  7. 115
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt
  8. 35
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionHandler.kt
  9. 106
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionService.kt
  10. 30
      lib/data/models/appconfig_model.dart
  11. 37
      lib/data/models/appconfig_model.g.dart
  12. 5
      lib/data/services/asr_service.dart
  13. 9
      lib/data/services/network/api.dart
  14. 62
      lib/data/services/open_ai_service_adapter.dart
  15. 150
      lib/data/services/speech_impl/azure_asr_service.dart
  16. 202
      lib/data/services/speech_impl/azure_tts_service.dart
  17. 239
      lib/data/services/speech_impl/flutter_asr_service.dart
  18. 5
      lib/data/services/speech_impl/volcano_asr_api_service.dart
  19. 6
      lib/data/services/speech_impl/xunfei_asr_service.dart
  20. 208
      lib/data/services/spotify_service.dart
  21. 184
      lib/data/services/voice_interaction_service.dart
  22. 133
      lib/data/services/volcano_translation_service.dart
  23. 208
      lib/modules/chat/controllers/chat_controller.dart
  24. 12
      lib/modules/chat/models/message_model.dart
  25. 69
      lib/modules/chat/views/animated_gradient_background.dart
  26. 36
      lib/modules/chat/views/chat_music_card.dart
  27. 60
      lib/modules/chat/views/chat_navigation_card.dart
  28. 590
      lib/modules/chat/views/chat_view.dart
  29. 16
      lib/modules/chat/views/weather_day_card.dart
  30. 33
      lib/modules/login/controllers/login_controller.dart
  31. 9
      lib/modules/opus_test/bindings/opus_test_binding.dart
  32. 404
      lib/modules/opus_test/controllers/opus_test_controller.dart
  33. 445
      lib/modules/opus_test/views/opus_test_view.dart
  34. 23
      lib/modules/settings/views/settings_view.dart
  35. 23
      lib/modules/splash/controllers/splash_controller.dart
  36. 310
      lib/modules/translation/controllers/translation_controller.dart
  37. 7
      lib/routes/app_pages.dart
  38. 1
      lib/routes/app_routes.dart
  39. 36
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  40. 16
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  41. 5
      local_plugins/chat_storage/lib/chat_storage.dart
  42. 3
      local_plugins/jieli/android/build.gradle.kts
  43. 326
      local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/BleService.kt
  44. 2
      local_plugins/jieli/android/src/main/kotlin/com/yunqiinnovation/jieli/JieliPlugin.kt
  45. 81
      local_plugins/jieli/lib/jieli.dart
  46. 126
      local_plugins/jl_opus/README.md
  47. 62
      local_plugins/jl_opus/android/build.gradle.kts
  48. 1
      local_plugins/jl_opus/android/settings.gradle.kts
  49. 9
      local_plugins/jl_opus/android/src/main/AndroidManifest.xml
  50. 377
      local_plugins/jl_opus/android/src/main/kotlin/com/yunqiinnovation/jl_opus/JlOpusPlugin.kt
  51. BIN
      local_plugins/jl_opus/android/src/main/libs/jl_audio_decode_V2.0.0_20005.aar
  52. 221
      local_plugins/jl_opus/lib/jl_opus.dart
  53. 25
      local_plugins/jl_opus/pubspec.yaml
  54. 3
      local_plugins/open_ai_service/android/build.gradle.kts
  55. 304
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt
  56. 38
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt
  57. 9
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt
  58. 7
      local_plugins/open_ai_service/lib/open_ai_service.dart
  59. BIN
      local_plugins/opus/.gradle/8.10/checksums/checksums.lock
  60. 0
      local_plugins/opus/.gradle/8.10/dependencies-accessors/gc.properties
  61. BIN
      local_plugins/opus/.gradle/8.10/fileChanges/last-build.bin
  62. BIN
      local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.bin
  63. BIN
      local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock
  64. 0
      local_plugins/opus/.gradle/8.10/gc.properties
  65. BIN
      local_plugins/opus/.gradle/8.9/checksums/checksums.lock
  66. 0
      local_plugins/opus/.gradle/8.9/dependencies-accessors/gc.properties
  67. BIN
      local_plugins/opus/.gradle/8.9/fileChanges/last-build.bin
  68. BIN
      local_plugins/opus/.gradle/8.9/fileHashes/fileHashes.lock
  69. 0
      local_plugins/opus/.gradle/8.9/gc.properties
  70. BIN
      local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock
  71. 2
      local_plugins/opus/.gradle/buildOutputCleanup/cache.properties
  72. 1
      local_plugins/opus/.gradle/nb-cache/trust/404236D79074CDA6A1B2372C665DCA9E587766B94C3493E9904CD4249D51B6BF
  73. 0
      local_plugins/opus/.gradle/vcs-1/gc.properties
  74. 92
      local_plugins/opus/README.md
  75. 21
      local_plugins/opus/android/CMakeLists.txt
  76. 67
      local_plugins/opus/android/build.gradle.kts
  77. 6
      local_plugins/opus/android/libs/opus/AUTHORS
  78. 744
      local_plugins/opus/android/libs/opus/CMakeLists.txt
  79. 44
      local_plugins/opus/android/libs/opus/COPYING
  80. 0
      local_plugins/opus/android/libs/opus/ChangeLog
  81. 368
      local_plugins/opus/android/libs/opus/INSTALL
  82. 489
      local_plugins/opus/android/libs/opus/Makefile.am
  83. 4853
      local_plugins/opus/android/libs/opus/Makefile.in
  84. 169
      local_plugins/opus/android/libs/opus/Makefile.mips
  85. 167
      local_plugins/opus/android/libs/opus/Makefile.unix
  86. 0
      local_plugins/opus/android/libs/opus/NEWS
  87. 189
      local_plugins/opus/android/libs/opus/README
  88. 1214
      local_plugins/opus/android/libs/opus/aclocal.m4
  89. 182
      local_plugins/opus/android/libs/opus/celt/_kiss_fft_guts.h
  90. 291
      local_plugins/opus/android/libs/opus/celt/arch.h
  91. 353
      local_plugins/opus/android/libs/opus/celt/arm/arm2gnu.pl
  92. 171
      local_plugins/opus/android/libs/opus/celt/arm/arm_celt_map.c
  93. 291
      local_plugins/opus/android/libs/opus/celt/arm/armcpu.c
  94. 90
      local_plugins/opus/android/libs/opus/celt/arm/armcpu.h
  95. 37
      local_plugins/opus/android/libs/opus/celt/arm/armopts.s.in
  96. 173
      local_plugins/opus/android/libs/opus/celt/arm/celt_fft_ne10.c
  97. 258
      local_plugins/opus/android/libs/opus/celt/arm/celt_mdct_ne10.c
  98. 272
      local_plugins/opus/android/libs/opus/celt/arm/celt_neon_intr.c
  99. 555
      local_plugins/opus/android/libs/opus/celt/arm/celt_pitch_xcorr_arm-gnu.S
  100. 551
      local_plugins/opus/android/libs/opus/celt/arm/celt_pitch_xcorr_arm.s

2
.gitignore

@ -48,6 +48,8 @@ pubspec.lock
.pub/ .pub/
/build/ /build/
.cxx/
# Android related # Android related
**/android/**/gradle-wrapper.jar **/android/**/gradle-wrapper.jar
**/android/.gradle **/android/.gradle

11
.mcp.json

@ -1,12 +1,13 @@
{ {
"mcpServers": { "mcpServers": {
"deepsound-mcp": {
"amap-amap-sse": { "url": "https://api.deapsound.com/sse"
"url": "https://mcp.amap.com/sse?key=e5fdc9605eabdeb5626f18f5721f343d" },
"voitrans-mcp": {
"url": "https://www.voitrans.net/sse"
}, },
"web-search": { "web-search": {
"url": "http://mcp.ideapsound.com:8000/sse" "url": "http://mcp.ideapsound.com:8000/sse"
} }
} }
} }

1
amap_flutter/android/src/main/java/com/example/amap_flutter/AmapFlutterPlugin.java

@ -152,6 +152,7 @@ public class AmapFlutterPlugin implements FlutterPlugin, MethodCallHandler {
Map<String, Object> locationData = new HashMap<>(); Map<String, Object> locationData = new HashMap<>();
locationData.put("latitude", amapLocation.getLatitude()); locationData.put("latitude", amapLocation.getLatitude());
locationData.put("longitude", amapLocation.getLongitude()); locationData.put("longitude", amapLocation.getLongitude());
locationData.put("city", amapLocation.getCity());
locationData.put("address", amapLocation.getAddress()); locationData.put("address", amapLocation.getAddress());
eventSink.success(locationData); eventSink.success(locationData);
} }

30
android/app/src/main/AndroidManifest.xml

@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- Android 13+ 前台服务特定类型权限 --> <!-- Android 13+ 前台服务特定类型权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH"/>
@ -48,6 +49,7 @@
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" /> <uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
<!--允许写设备缓存,用于问题排查--> <!--允许写设备缓存,用于问题排查-->
<uses-permission android:name="android.permission.WRITE_SETTINGS" /> <uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.VIBRATE"/>
<application <application
android:label="DeapSound" android:label="DeapSound"
@ -70,7 +72,7 @@
android:enabled="true"> android:enabled="true">
<intent-filter android:priority="1000"> <intent-filter android:priority="1000">
<action android:name="android.intent.action.MEDIA_BUTTON" /> <action android:name="android.intent.action.MEDIA_BUTTON" />
<action android:name="android.media.browse.MediaBrowserService" /> <!-- <action android:name="android.media.browse.MediaBrowserService" /> -->
</intent-filter> </intent-filter>
</service> </service>
@ -84,6 +86,32 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<!-- 添加BLE唤醒服务 -->
<service
android:name=".BleWakeService"
android:foregroundServiceType="location"
android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="android.bluetooth.adapter.action.REQUEST_ENABLE" />
</intent-filter>
</service>
<!-- 添加蓝牙广播接收器 -->
<receiver
android:name=".BleBroadcastReceiver"
android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="com.yunqiinnovation.deepsound.ACTION_BLE_SCAN_RESULT" />
<action android:name="android.bluetooth.adapter.action.STATE_CHANGED" />
<action android:name="android.bluetooth.adapter.action.DISCOVERY_FINISHED" />
<action android:name="android.bluetooth.adapter.action.DISCOVERY_STARTED" />
</intent-filter>
</receiver>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"

45
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/BleBroadcastReceiver.kt

@ -0,0 +1,45 @@
package com.yunqiinnovation.deepsound
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import com.yunqiinnovation.deepsound.core.utils.FileLogger
/**
* 蓝牙BLE广播接收器
* 用于接收系统通过PendingIntent触发的蓝牙扫描结果
* 即使应用进程已被杀死,系统也能通过此接收器唤醒应用
*/
class BleBroadcastReceiver : BroadcastReceiver() {
companion object {
private const val TAG = "BleBroadcastReceiver"
// 蓝牙扫描结果Action
const val ACTION_BLE_SCAN_RESULT = "com.yunqiinnovation.deepsound.ACTION_BLE_SCAN_RESULT"
}
override fun onReceive(context: Context, intent: Intent) {
FileLogger.init(context)
//FileLogger.d(TAG, "接收到广播: ${intent.action}")
if (intent.action == ACTION_BLE_SCAN_RESULT) {
//FileLogger.d(TAG, "收到系统BLE扫描唤醒事件")
// 打印日志,表明唤醒成功
//FileLogger.d(TAG, "应用被蓝牙BLE广播唤醒成功")
// 启动BLE唤醒服务,该服务将执行后续操作
val serviceIntent = Intent(context, BleWakeService::class.java)
serviceIntent.action = "android.bluetooth.adapter.action.REQUEST_ENABLE"
// 对于Android 8.0及以上版本,必须使用startForegroundService启动前台服务
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent)
} else {
context.startService(serviceIntent)
}
}
}
}

335
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/BleWakeService.kt

@ -0,0 +1,335 @@
package com.yunqiinnovation.deepsound
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.bluetooth.le.BluetoothLeScanner
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Context
import android.content.Intent
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.os.ParcelUuid
import androidx.core.app.NotificationCompat
import com.yunqiinnovation.deepsound.core.utils.FileLogger
import java.util.UUID
/**
* BLE唤醒服务,用于接收蓝牙BLE广播唤醒应用
* 可以作为前台服务运行,以提高可靠性
*/
class BleWakeService : Service() {
companion object {
private const val TAG = "BleWakeService"
private const val NOTIFICATION_ID = 1002
private const val CHANNEL_ID = "ble_wake_channel"
// 定义唤醒用的特定UUID,应与耳机约定一致
// 此UUID仅作示例,实际使用时应根据耳机厂商提供的UUID进行修改
val OLA_FRIEND_UUID = UUID.fromString("0000fd7b-0000-1000-8000-00805f9b34fb")
// 服务启动标志
private var isRunning = false
// 获取服务运行状态
fun isServiceRunning(): Boolean = isRunning
}
private val binder = LocalBinder()
private var bluetoothAdapter: BluetoothAdapter? = null
private var bluetoothLeScanner: BluetoothLeScanner? = null
private var isScanning = false
/**
* 本地绑定器
*/
inner class LocalBinder : Binder() {
fun getService(): BleWakeService = this@BleWakeService
}
override fun onCreate() {
super.onCreate()
FileLogger.d(TAG, "BLE唤醒服务正在创建")
// 初始化蓝牙
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothAdapter = bluetoothManager.adapter
bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner
// 标记服务状态
isRunning = true
// 启动前台服务
startForeground(NOTIFICATION_ID, createNotification())
// 开始扫描
// startScanning()
FileLogger.d(TAG, "BLE唤醒服务已启动")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// FileLogger.d(TAG, "收到onStartCommand: intent=$intent")
// 如果是PendingIntent触发(BLE广播唤醒),打印日志
if (intent?.action == "android.bluetooth.adapter.action.REQUEST_ENABLE") {
//FileLogger.d(TAG, "收到BLE广播唤醒事件,应用被唤醒")
// 在这里可以执行唤醒后的动作,如启动语音交互服务
val serviceIntent = Intent(this, VoiceInteractionService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
} else {
startService(serviceIntent)
}
}
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder {
return binder
}
override fun onDestroy() {
super.onDestroy()
FileLogger.d(TAG, "BLE唤醒服务即将销毁")
// 停止扫描
stopScanning()
// 标记服务状态
isRunning = false
}
/**
* 开始扫描BLE广播
*/
fun startScanning() {
FileLogger.d(TAG, "开始扫描BLE广播")
if (bluetoothLeScanner == null) {
FileLogger.e(TAG, "蓝牙扫描器不可用")
return
}
if (isScanning) {
FileLogger.d(TAG, "已在扫描中,忽略")
return
}
// 构建扫描过滤器,匹配特定的UUID
val scanFilter = ScanFilter.Builder()
.setServiceUuid(ParcelUuid(OLA_FRIEND_UUID))
.build()
// 设置扫描参数
val scanSettings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) // 低功耗模式
.build()
try {
FileLogger.d(TAG, "开始BLE扫描, 扫描过滤器: $OLA_FRIEND_UUID")
// 开始扫描
bluetoothLeScanner?.startScan(listOf(scanFilter), scanSettings, scanCallback)
isScanning = true
FileLogger.d(TAG, "BLE扫描已启动")
} catch (e: Exception) {
FileLogger.e(TAG, "开始BLE扫描失败: ${e.message}")
}
}
/**
* 停止扫描BLE广播
*/
fun stopScanning() {
FileLogger.d(TAG, "停止扫描BLE广播")
if (bluetoothLeScanner == null || !isScanning) {
FileLogger.d(TAG, "没有正在进行的扫描")
return
}
try {
// 停止扫描
bluetoothLeScanner?.stopScan(scanCallback)
isScanning = false
FileLogger.d(TAG, "BLE扫描已停止")
} catch (e: Exception) {
FileLogger.e(TAG, "停止BLE扫描失败: ${e.message}")
}
}
/**
* 扫描回调
*/
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
FileLogger.d(TAG, "收到扫描结果: ${result.device.address}")
// 检查是否是目标设备和唤醒广播
val serviceUuids = result.scanRecord?.serviceUuids
if (serviceUuids != null && serviceUuids.any { it.uuid == OLA_FRIEND_UUID }) {
FileLogger.d(TAG, "检测到Ola Friend耳机唤醒广播")
// 从广播包中获取额外数据
val manufacturerData = result.scanRecord?.getManufacturerSpecificData(0xFFFF)
if (manufacturerData != null && manufacturerData.size > 1) {
// 假设第一个字节为命令类型,如0x01表示语音唤醒
val commandType = manufacturerData[0].toInt() and 0xFF
FileLogger.d(TAG, "收到命令类型: $commandType")
if (commandType == 0x01) { // 语音唤醒命令
FileLogger.d(TAG, "耳机语音唤醒命令,启动语音交互")
// 启动语音交互服务
val serviceIntent = Intent(applicationContext, VoiceInteractionService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
} else {
startService(serviceIntent)
}
}
}
}
}
override fun onScanFailed(errorCode: Int) {
FileLogger.e(TAG, "BLE扫描失败,错误代码: $errorCode")
isScanning = false
}
}
/**
* 创建通知
*/
private fun createNotification(): Notification {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Android 8.0+ 需要创建通知渠道
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"蓝牙唤醒服务",
NotificationManager.IMPORTANCE_LOW
)
manager.createNotificationChannel(channel)
}
// 点击通知打开MainActivity
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("蓝牙唤醒服务")
.setContentText("正在监听耳机唤醒信号")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(pendingIntent)
.build()
}
/**
* 为后台持久扫描注册PendingIntent
* 这种方式即使应用被销毁后仍然可被唤醒
*/
fun registerBackgroundScan() {
FileLogger.d(TAG, "注册后台持久扫描")
if (bluetoothLeScanner == null) {
FileLogger.e(TAG, "蓝牙扫描器不可用")
return
}
// 构建扫描过滤器,匹配特定的UUID
val scanFilter = ScanFilter.Builder()
// .setServiceUuid(ParcelUuid(OLA_FRIEND_UUID))
.build()
// 设置扫描参数
val scanSettings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) // 低功耗模式
.build()
// 创建PendingIntent
val intent = Intent(this, BleBroadcastReceiver::class.java)
intent.action = BleBroadcastReceiver.ACTION_BLE_SCAN_RESULT
val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getBroadcast(
this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
} else {
PendingIntent.getBroadcast(
this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
try {
// 启动带PendingIntent的扫描
bluetoothLeScanner?.startScan(
listOf(scanFilter),
scanSettings,
pendingIntent
)
FileLogger.d(TAG, "后台持久扫描已注册")
} catch (e: Exception) {
FileLogger.e(TAG, "注册后台持久扫描失败: ${e.message}")
}
}
/**
* 注销后台持久扫描
*/
fun unregisterBackgroundScan() {
FileLogger.d(TAG, "注销后台持久扫描")
if (bluetoothLeScanner == null) {
FileLogger.e(TAG, "蓝牙扫描器不可用")
return
}
// 创建与注册时相同的PendingIntent
val intent = Intent(this, BleBroadcastReceiver::class.java)
intent.action = BleBroadcastReceiver.ACTION_BLE_SCAN_RESULT
val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getBroadcast(
this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
} else {
PendingIntent.getBroadcast(
this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
}
try {
// 停止带PendingIntent的扫描
bluetoothLeScanner?.stopScan(pendingIntent)
FileLogger.d(TAG, "后台持久扫描已注销")
} catch (e: Exception) {
FileLogger.e(TAG, "注销后台持久扫描失败: ${e.message}")
}
}
}

115
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt

@ -24,6 +24,9 @@ import com.yunqiinnovation.deepsound.core.utils.FileLogger
import android.content.pm.PackageManager import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import android.content.ServiceConnection
import android.content.ComponentName
import android.os.IBinder
class MainActivity: FlutterActivity() { class MainActivity: FlutterActivity() {
@ -47,7 +50,8 @@ class MainActivity: FlutterActivity() {
Manifest.permission.SEND_SMS, Manifest.permission.SEND_SMS,
Manifest.permission.READ_CONTACTS, Manifest.permission.READ_CONTACTS,
Manifest.permission.CALL_PHONE, Manifest.permission.CALL_PHONE,
Manifest.permission.POST_NOTIFICATIONS Manifest.permission.POST_NOTIFICATIONS,
Manifest.permission.FOREGROUND_SERVICE_LOCATION
) )
} else { } else {
arrayOf( arrayOf(
@ -173,7 +177,9 @@ class MainActivity: FlutterActivity() {
var volcanoSpeechAppId: String = "" var volcanoSpeechAppId: String = ""
var volcanoSpeechAppToken: String = "" var volcanoSpeechAppToken: String = ""
var mcpServer: String = "" var mcpServer: String = ""
var spotifyClientId: String = ""
var spotifyRedirectUrl: String = ""
// 安全存储相关常量 // 安全存储相关常量
private const val SECURE_PREFS_FILENAME = "deep_voice_secure_prefs" private const val SECURE_PREFS_FILENAME = "deep_voice_secure_prefs"
private const val KEY_AZURE_SPEECH_KEY = "azure_speech_key" private const val KEY_AZURE_SPEECH_KEY = "azure_speech_key"
@ -186,6 +192,8 @@ class MainActivity: FlutterActivity() {
private const val KEY_VOLCANO_SPEECH_APP_ID = "volcano_speech_app_id" private const val KEY_VOLCANO_SPEECH_APP_ID = "volcano_speech_app_id"
private const val KEY_VOLCANO_SPEECH_APP_TOKEN = "volcano_speech_app_token" private const val KEY_VOLCANO_SPEECH_APP_TOKEN = "volcano_speech_app_token"
private const val KEY_MCP_SERVER = "mcp_server" private const val KEY_MCP_SERVER = "mcp_server"
private const val KEY_SPOTIFY_CLIENT_ID = "spotify_client_id"
private const val KEY_SPOTIFY_REDIRECT_URL = "spotify_redirect_url"
// 会话管理 // 会话管理
private const val KEY_SESSION_ID = "session_id" private const val KEY_SESSION_ID = "session_id"
@ -227,6 +235,8 @@ class MainActivity: FlutterActivity() {
.putString(KEY_VOLCANO_SPEECH_APP_ID, volcanoSpeechAppId) .putString(KEY_VOLCANO_SPEECH_APP_ID, volcanoSpeechAppId)
.putString(KEY_VOLCANO_SPEECH_APP_TOKEN, volcanoSpeechAppToken) .putString(KEY_VOLCANO_SPEECH_APP_TOKEN, volcanoSpeechAppToken)
.putString(KEY_MCP_SERVER, mcpServer) .putString(KEY_MCP_SERVER, mcpServer)
.putString(KEY_SPOTIFY_CLIENT_ID, spotifyClientId)
.putString(KEY_SPOTIFY_REDIRECT_URL, spotifyRedirectUrl)
.apply() .apply()
FileLogger.d("MainActivity", "密钥已安全保存到加密存储中") FileLogger.d("MainActivity", "密钥已安全保存到加密存储中")
@ -287,6 +297,9 @@ class MainActivity: FlutterActivity() {
return false return false
} }
} }
// 请求码
private const val REQUEST_BLUETOOTH_PERMISSIONS = 1003
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -309,6 +322,9 @@ class MainActivity: FlutterActivity() {
Log.d(TAG, "Flutter 引擎已存在,立即设置事件通道") Log.d(TAG, "Flutter 引擎已存在,立即设置事件通道")
setupEventChannels(engine) setupEventChannels(engine)
} }
// 初始化BLE唤醒服务
initBleWakeService()
} }
private fun registerReceivers() { private fun registerReceivers() {
@ -700,6 +716,101 @@ class MainActivity: FlutterActivity() {
FileLogger.d(TAG, "所有必要权限已授权") FileLogger.d(TAG, "所有必要权限已授权")
} }
} }
when (requestCode) {
REQUEST_BLUETOOTH_PERMISSIONS -> {
if (grantResults.isNotEmpty() && grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
FileLogger.d(TAG, "蓝牙权限已授予,初始化BLE唤醒服务")
initBleWakeService()
} else {
FileLogger.e(TAG, "蓝牙权限被拒绝")
}
}
// 处理其他权限请求...
}
}
/**
* 初始化BLE唤醒服务
*/
private fun initBleWakeService() {
FileLogger.d(TAG, "初始化BLE唤醒服务")
// 检查蓝牙权限
if (hasBluetoothPermissions()) {
// 启动BLE唤醒服务
val serviceIntent = Intent(this, BleWakeService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
} else {
startService(serviceIntent)
}
FileLogger.d(TAG, "BLE唤醒服务已启动")
// 绑定服务以访问其方法
bindService(serviceIntent, object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
FileLogger.d(TAG, "已连接到BLE唤醒服务")
val binder = service as BleWakeService.LocalBinder
val bleWakeService = binder.getService()
// 注册后台持久扫描,用于应用被杀死后的唤醒
bleWakeService.registerBackgroundScan()
}
override fun onServiceDisconnected(name: ComponentName?) {
FileLogger.d(TAG, "与BLE唤醒服务的连接已断开")
}
}, Context.BIND_AUTO_CREATE)
} else {
FileLogger.e(TAG, "缺少蓝牙权限,无法启动BLE唤醒服务")
// 可以在这里请求权限
requestBluetoothPermissions()
}
}
/**
* 检查是否有蓝牙权限
*/
private fun hasBluetoothPermissions(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return hasPermission(Manifest.permission.BLUETOOTH_SCAN) &&
hasPermission(Manifest.permission.BLUETOOTH_CONNECT)
} else {
return hasPermission(Manifest.permission.BLUETOOTH) &&
hasPermission(Manifest.permission.BLUETOOTH_ADMIN)
}
}
/**
* 请求蓝牙权限
*/
private fun requestBluetoothPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
requestPermissions(
arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
),
REQUEST_BLUETOOTH_PERMISSIONS
)
} else {
requestPermissions(
arrayOf(
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN
),
REQUEST_BLUETOOTH_PERMISSIONS
)
}
}
/**
* 检查权限是否已授予
*/
private fun hasPermission(permission: String): Boolean {
return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
} }
} }

35
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionHandler.kt

@ -12,6 +12,8 @@ import com.yunqiinnovation.azure_speech.AzureAsrHelper
import com.yunqiinnovation.azure_speech.AzureTtsHelper import com.yunqiinnovation.azure_speech.AzureTtsHelper
import com.yunqiinnovation.open_ai_service.OpenAIService import com.yunqiinnovation.open_ai_service.OpenAIService
import com.yunqiinnovation.open_ai_service.SystemFunctionHandler import com.yunqiinnovation.open_ai_service.SystemFunctionHandler
import de.minimalme.spotify_sdk.SpotifySdkHelper
import kotlinx.serialization.json.JsonObject
/** /**
@ -26,7 +28,9 @@ class VoiceInteractionHandler(
private val openaiModel: String = "", private val openaiModel: String = "",
private val volcanoSpeechAppId: String, private val volcanoSpeechAppId: String,
private val volcanoSpeechAppToken: String, private val volcanoSpeechAppToken: String,
private val mcpServer: String private val mcpServer: String,
private val spotifyclientid: String,
private val spotifyRedirectUrl: String
) { ) {
private val TAG = "VoiceInteractionHandler" private val TAG = "VoiceInteractionHandler"
@ -36,7 +40,9 @@ class VoiceInteractionHandler(
// OpenAI服务 // OpenAI服务
private val openAIService = OpenAIService(context.applicationContext) private val openAIService = OpenAIService(context.applicationContext)
private var spotifyHelper: SpotifySdkHelper? = null
// 当前用户输入 // 当前用户输入
private var currentUserInput = "" private var currentUserInput = ""
@ -269,6 +275,17 @@ class VoiceInteractionHandler(
override fun onFunctionCall(call: JSONObject) { override fun onFunctionCall(call: JSONObject) {
FileLogger.d(TAG, "processWithOpenAI 收到函数调用请求: ${call.getString("name")}") FileLogger.d(TAG, "processWithOpenAI 收到函数调用请求: ${call.getString("name")}")
// Log.e("OpenAIServicePlugin", "context:${call.toString()}")
// val context = call["context"]
// if (call.has("meta")) {
// val meta = call.get("meta")
// val metaJsonObject: JsonObject = meta as JsonObject
// Log.e("OpenAIServicePlugin", "工具调用结果 context:${context} meta:${meta.toString()}")
// if (metaJsonObject.containsKey("card_spotify")) {
// val card_spotify:JsonObject = metaJsonObject.get("card_spotify") as JsonObject
// playSpotifyMusic(card_spotify.get("name") as String)
// }
// }
} }
} }
) )
@ -312,7 +329,19 @@ class VoiceInteractionHandler(
notifyStateChanged() notifyStateChanged()
} }
} }
/*
* 播放音乐。Spotify
* */
fun playSpotifyMusic(url:String){
if (spotifyHelper == null){
spotifyHelper = SpotifySdkHelper(context).apply {
initialize(spotifyclientid,spotifyRedirectUrl)
}
}
spotifyHelper?.play(url)
}
/** /**
* 播放AI回复 * 播放AI回复
*/ */

106
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionService.kt

@ -30,6 +30,7 @@ import com.yunqiinnovation.deepsound.core.utils.FileLogger
import com.yunqiinnovation.azure_speech.AzureAsrHelper import com.yunqiinnovation.azure_speech.AzureAsrHelper
import com.yunqiinnovation.volcano_speech.VolcanoTtsHelper import com.yunqiinnovation.volcano_speech.VolcanoTtsHelper
import com.yunqiinnovation.open_ai_service.OpenAIService import com.yunqiinnovation.open_ai_service.OpenAIService
import android.os.Bundle
/** /**
@ -148,6 +149,8 @@ class VoiceInteractionService : Service() {
var volcanoSpeechAppId = MainActivity.volcanoSpeechAppId ?: "" var volcanoSpeechAppId = MainActivity.volcanoSpeechAppId ?: ""
var volcanoSpeechAppToken = MainActivity.volcanoSpeechAppToken ?: "" var volcanoSpeechAppToken = MainActivity.volcanoSpeechAppToken ?: ""
var mcpServer = MainActivity.mcpServer ?: "" var mcpServer = MainActivity.mcpServer ?: ""
var spotifyclientid = MainActivity.spotifyClientId ?: ""
var spotifyRedirectUrl = MainActivity.spotifyRedirectUrl ?: ""
// 如果静态变量中没有配置,尝试从加密存储中加载 // 如果静态变量中没有配置,尝试从加密存储中加载
if (subscriptionKey.isEmpty() || serviceRegion.isEmpty() || openaiKey.isEmpty()) { if (subscriptionKey.isEmpty() || serviceRegion.isEmpty() || openaiKey.isEmpty()) {
@ -166,6 +169,8 @@ class VoiceInteractionService : Service() {
volcanoSpeechAppId = MainActivity.volcanoSpeechAppId ?: "" volcanoSpeechAppId = MainActivity.volcanoSpeechAppId ?: ""
volcanoSpeechAppToken = MainActivity.volcanoSpeechAppToken ?: "" volcanoSpeechAppToken = MainActivity.volcanoSpeechAppToken ?: ""
mcpServer = MainActivity.mcpServer ?: "" mcpServer = MainActivity.mcpServer ?: ""
spotifyclientid = MainActivity.spotifyClientId ?: ""
spotifyRedirectUrl = MainActivity.spotifyRedirectUrl ?: ""
FileLogger.d(TAG, "已从加密存储加载配置信息") FileLogger.d(TAG, "已从加密存储加载配置信息")
} else { } else {
FileLogger.e(TAG, "无法从加密存储加载配置信息") FileLogger.e(TAG, "无法从加密存储加载配置信息")
@ -177,7 +182,7 @@ class VoiceInteractionService : Service() {
subscriptionKey, serviceRegion, subscriptionKey, serviceRegion,
openaiKey, openaiBaseUrl, openaiModel, openaiKey, openaiBaseUrl, openaiModel,
volcanoSpeechAppId, volcanoSpeechAppToken, volcanoSpeechAppId, volcanoSpeechAppToken,
mcpServer) mcpServer,spotifyclientid,spotifyRedirectUrl)
// 初始化回调 // 初始化回调
voiceInteractionHandler.setCallback(object : VoiceInteractionHandler.InteractionCallback { voiceInteractionHandler.setCallback(object : VoiceInteractionHandler.InteractionCallback {
@ -220,6 +225,11 @@ class VoiceInteractionService : Service() {
// 设置回调 // 设置回调
mediaSession.setCallback(object : MediaSessionCompat.Callback() { mediaSession.setCallback(object : MediaSessionCompat.Callback() {
override fun onCustomAction(action: String, extras: Bundle?) {
FileLogger.d(TAG, "收到自定义动作: action=${action}, extras=${extras}")
}
// 处理媒体按钮事件
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean { override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java) mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java)
@ -246,7 +256,7 @@ class VoiceInteractionService : Service() {
return false return false
} }
FileLogger.d(TAG, "收到无按键的媒体按钮事件") FileLogger.d(TAG, "收到无按键的媒体按钮事件: mediaButtonEvent=${mediaButtonEvent}")
return false return false
} }
@ -475,88 +485,30 @@ class VoiceInteractionService : Service() {
* 当 Service 被启动时调用 * 当 Service 被启动时调用
*/ */
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
FileLogger.d(TAG, "onStartCommand: intent=$intent, action=${intent?.action}") //FileLogger.d(TAG, "onStartCommand: intent=$intent, action=${intent?.action}")
// 处理媒体按键 Intent intent?.let {receivedIntent ->
if (intent?.action == Intent.ACTION_MEDIA_BUTTON) { when(receivedIntent.action) {
FileLogger.d(TAG, "收到媒体按钮Intent: ${intent.extras}") Intent.ACTION_MEDIA_BUTTON -> {
FileLogger.d(TAG, "onStartCommand中收到媒体按钮事件")
val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { MediaButtonReceiver.handleIntent(mediaSession, receivedIntent)
intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) as? KeyEvent
}
if (keyEvent != null) {
FileLogger.d(TAG, "从Intent中解析到按键事件: action=${keyEvent.action}, keyCode=${keyEvent.keyCode}")
// 使用统一的按键处理逻辑
handleKeyEvent(keyEvent)
}
} else if (intent?.action == ACTION_PAUSE_VOICE_INTERACTION) {
// 处理来自 Flutter 的暂停请求
FileLogger.d(TAG, "收到暂停请求")
pauseVoiceInteraction()
} else {
FileLogger.d(TAG, "收到其他Intent: action=${intent?.action}")
}
// 让 MediaSession 处理 Intent,MediaSession将通过callback处理事件
try {
MediaButtonReceiver.handleIntent(mediaSession, intent)
FileLogger.d(TAG, "MediaButtonReceiver处理Intent完成")
} catch (e: Exception) {
FileLogger.e(TAG, "MediaButtonReceiver处理Intent失败: ${e.message}", e)
}
return START_STICKY
}
/**
* 处理按键事件
*/
private fun handleKeyEvent(keyEvent: KeyEvent) {
FileLogger.d(TAG, "处理按键事件: action=${keyEvent.action}, keyCode=${keyEvent.keyCode}")
// 只处理按下操作和相关媒体按键
if (keyEvent.action == KeyEvent.ACTION_DOWN) {
when (keyEvent.keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY,
KeyEvent.KEYCODE_MEDIA_PAUSE,
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
FileLogger.d(TAG, "处理标准媒体按键: ${keyEvent.keyCode}")
handleDoubleClick()
} }
KeyEvent.KEYCODE_HEADSETHOOK -> {
FileLogger.d(TAG, "处理HEADSETHOOK按键") ACTION_PAUSE_VOICE_INTERACTION -> {
// 检测双击 // 处理来自 Flutter 的暂停请求
val currentTime = System.currentTimeMillis() FileLogger.d(TAG, "onStartCommand收到暂停后台交互请求")
val timeDiff = currentTime - lastKeyEventTime pauseVoiceInteraction()
FileLogger.d(TAG, "按键时间差: $timeDiff ms, 当前计数: $keyEventCount")
if (timeDiff < 500) { // 500毫秒内的按键视为连续按键
keyEventCount++
FileLogger.d(TAG, "连续按键,计数增加到: $keyEventCount")
if (keyEventCount == 1) { // 这是第二次按键,即双击
FileLogger.d(TAG, "检测到双击,执行处理")
handleDoubleClick()
keyEventCount = 0
}
} else {
FileLogger.d(TAG, "非连续按键,重置计数")
keyEventCount = 0
}
lastKeyEventTime = currentTime
} }
else -> { else -> {
FileLogger.d(TAG, "忽略不相关的按键: ${keyEvent.keyCode}") //FileLogger.d(TAG, "onStartCommand收到未知Intent: action=${receivedIntent.action}")
} }
} }
} else {
FileLogger.d(TAG, "忽略非ACTION_DOWN事件: ${keyEvent.action}")
} }
return START_STICKY
} }
@Nullable @Nullable
override fun onBind(intent: Intent?): IBinder? = null override fun onBind(intent: Intent?): IBinder? = null

30
lib/data/models/appconfig_model.dart

@ -0,0 +1,30 @@
import 'package:json_annotation/json_annotation.dart';
part 'appconfig_model.g.dart'; // 生成的文件名
@JsonSerializable()
class UserGetAppConfigResp {
final Map<String, String> env;
final Map<String, DBMCPServer> mcps;
UserGetAppConfigResp({required this.env, required this.mcps});
factory UserGetAppConfigResp.fromJson(Map<String, dynamic> json) =>
_$UserGetAppConfigRespFromJson(json);
Map<String, dynamic> toJson() => _$UserGetAppConfigRespToJson(this);
}
@JsonSerializable()
class DBMCPServer {
final String servername;
final String url;
final List<String>? tools;
DBMCPServer({
required this.servername,
required this.url,
required this.tools,
});
factory DBMCPServer.fromJson(Map<String, dynamic> json) =>
_$DBMCPServerFromJson(json);
Map<String, dynamic> toJson() => _$DBMCPServerToJson(this);
}

37
lib/data/models/appconfig_model.g.dart

@ -0,0 +1,37 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'appconfig_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
UserGetAppConfigResp _$UserGetAppConfigRespFromJson(
Map<String, dynamic> json) =>
UserGetAppConfigResp(
env: Map<String, String>.from(json['env'] as Map),
mcps: (json['mcps'] as Map<String, dynamic>).map(
(k, e) => MapEntry(k, DBMCPServer.fromJson(e as Map<String, dynamic>)),
),
);
Map<String, dynamic> _$UserGetAppConfigRespToJson(
UserGetAppConfigResp instance) =>
<String, dynamic>{
'env': instance.env,
'mcps': instance.mcps,
};
DBMCPServer _$DBMCPServerFromJson(Map<String, dynamic> json) => DBMCPServer(
servername: json['servername'] as String,
url: json['url'] as String,
tools:
(json['tools'] as List<dynamic>?)?.map((e) => e as String).toList(),
);
Map<String, dynamic> _$DBMCPServerToJson(DBMCPServer instance) =>
<String, dynamic>{
'servername': instance.servername,
'url': instance.url,
'tools': instance.tools,
};

5
lib/data/services/asr_service.dart

@ -16,6 +16,11 @@ abstract class AsrService {
/// 返回识别的最终结果 /// 返回识别的最终结果
Future<RecognitionEvent> recognizeOnce(); Future<RecognitionEvent> recognizeOnce();
/// 开始连续语音识别
///
/// 推送音频数据到连续识别中
Future<void> pushAudioData(Uint8List data);
/// 开始连续语音识别 /// 开始连续语音识别
/// ///
/// 返回一个包含识别事件的流 /// 返回一个包含识别事件的流

9
lib/data/services/network/api.dart

@ -64,4 +64,13 @@ class Api {
params: params, params: params,
); );
} }
//app配置
static getappconfig([params]) {
return DioManager().request(
NWMethod.post,
'/api/home/user_getappconfig',
params: params,
);
}
} }

62
lib/data/services/open_ai_service_adapter.dart

@ -1,7 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:convert'; import 'dart:convert';
import 'package:deep_voice/data/models/appconfig_model.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
import 'package:open_ai_service/open_ai_service.dart'; import 'package:open_ai_service/open_ai_service.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'ai_service.dart'; import 'ai_service.dart';
@ -9,14 +11,15 @@ import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
/// 定义流事件类型,用于区分不同类型的事件 /// 定义流事件类型,用于区分不同类型的事件
enum StreamEventType { token, complete, error } enum StreamEventType { token, functionCall, complete, error }
/// 流事件包装类 /// 流事件包装类
class StreamEvent { class StreamEvent {
final StreamEventType type; final StreamEventType type;
final String? content; final String? content;
final Map? meta;
StreamEvent(this.type, {this.content}); StreamEvent(this.type, {this.content, this.meta});
} }
/// OpenAI服务适配器 - 连接AiService接口与OpenAIService插件 /// OpenAI服务适配器 - 连接AiService接口与OpenAIService插件
@ -25,6 +28,7 @@ class OpenAIServiceAdapter implements AiService {
StreamSubscription<OpenAIEvent>? _eventSubscription; StreamSubscription<OpenAIEvent>? _eventSubscription;
final StreamController<StreamEvent> _tokenStreamController = final StreamController<StreamEvent> _tokenStreamController =
StreamController<StreamEvent>.broadcast(); StreamController<StreamEvent>.broadcast();
final GetStorage _storage = GetStorage();
/// 构造函数 /// 构造函数
OpenAIServiceAdapter() { OpenAIServiceAdapter() {
@ -63,6 +67,9 @@ class OpenAIServiceAdapter implements AiService {
printInfo( printInfo(
info: info:
'接收到 functionCall content: ${event.content} meta: ${event.meta}'); '接收到 functionCall content: ${event.content} meta: ${event.meta}');
_tokenStreamController.add(StreamEvent(
StreamEventType.functionCall,
meta: event.meta as Map<String, dynamic>?));
// _tokenStreamController.add(StreamEvent( // _tokenStreamController.add(StreamEvent(
// StreamEventType.error, // StreamEventType.error,
// content: '收到函数调用,该流仅支持文本响应' // content: '收到函数调用,该流仅支持文本响应'
@ -93,9 +100,13 @@ class OpenAIServiceAdapter implements AiService {
Future<bool> initialize() async { Future<bool> initialize() async {
try { try {
// 从.env文件中读取配置 // 从.env文件中读取配置
final apiKey = dotenv.env['OPENAI_API_KEY'] ?? ''; // final apiKey = dotenv.env['OPENAI_API_KEY'] ?? '';
final baseUrl = dotenv.env['OPENAI_BASE_URL'] ?? ''; // final baseUrl = dotenv.env['OPENAI_BASE_URL'] ?? '';
final model = dotenv.env['OPENAI_MODEL'] ?? ''; // final model = dotenv.env['OPENAI_MODEL'] ?? '';
final _env = _storage.read("ENV") as Map<String, String>;
final apiKey = _env['OPENAI_API_KEY'] ?? '';
final baseUrl = _env['OPENAI_BASE_URL'] ?? '';
final model = _env['OPENAI_MODEL'] ?? '';
printInfo(info: '从.env读取OpenAI配置'); printInfo(info: '从.env读取OpenAI配置');
printInfo(info: '基础URL: $baseUrl'); printInfo(info: '基础URL: $baseUrl');
@ -105,24 +116,24 @@ class OpenAIServiceAdapter implements AiService {
printError(info: '错误: OpenAI API密钥未配置,请在.env文件中设置OPENAI_API_KEY'); printError(info: '错误: OpenAI API密钥未配置,请在.env文件中设置OPENAI_API_KEY');
return false; return false;
} }
final _mcps = _storage.read("MCPS") as Map<String, DBMCPServer>;
// 读取.mcp.json文件 // 读取.mcp.json文件
String mcpConfig = ''; String mcpConfig = jsonEncode(_mcps);
try { // try {
// 从Flutter资源包中加载.mcp.json // // 从Flutter资源包中加载.mcp.json
mcpConfig = await rootBundle.loadString('.mcp.json'); // mcpConfig = await rootBundle.loadString('.mcp.json');
printInfo(info: '成功从资源包加载.mcp.json配置文件'); // printInfo(info: '成功从资源包加载.mcp.json配置文件');
// 验证JSON格式 // // 验证JSON格式
final jsonData = jsonDecode(mcpConfig); // final jsonData = jsonDecode(mcpConfig);
if (jsonData is Map && jsonData.containsKey('mcpServers')) { // if (jsonData is Map && jsonData.containsKey('mcpServers')) {
printInfo(info: '解析到有效的mcpServers配置'); // printInfo(info: '解析到有效的mcpServers配置');
} else { // } else {
printInfo(info: '.mcp.json内容格式不正确,期望包含mcpServers字段'); // printInfo(info: '.mcp.json内容格式不正确,期望包含mcpServers字段');
} // }
} catch (e) { // } catch (e) {
printError(info: '加载.mcp.json资源文件时出错: $e'); // printError(info: '加载.mcp.json资源文件时出错: $e');
} // }
// 初始化OpenAI服务 // 初始化OpenAI服务
final result = await _openAIService.initialize( final result = await _openAIService.initialize(
@ -170,7 +181,7 @@ class OpenAIServiceAdapter implements AiService {
/// 发送消息并获取流式回复 /// 发送消息并获取流式回复
@override @override
Stream<String> sendMessageStream({ Stream sendMessageStream({
required List<Map<String, String>> messages, required List<Map<String, String>> messages,
required String systemPrompt, required String systemPrompt,
}) async* { }) async* {
@ -180,7 +191,7 @@ class OpenAIServiceAdapter implements AiService {
messages.map((m) => Map<String, dynamic>.from(m)).toList(); messages.map((m) => Map<String, dynamic>.from(m)).toList();
// 创建用于接收token的控制器 // 创建用于接收token的控制器
final localController = StreamController<String>(); final localController = StreamController();
// 添加从广播流到本地流的订阅 // 添加从广播流到本地流的订阅
final subscription = _tokenStreamController.stream.listen((streamEvent) { final subscription = _tokenStreamController.stream.listen((streamEvent) {
@ -190,6 +201,11 @@ class OpenAIServiceAdapter implements AiService {
localController.add(streamEvent.content!); localController.add(streamEvent.content!);
} }
break; break;
case StreamEventType.functionCall:
if (streamEvent.meta != null && !localController.isClosed) {
localController.add(streamEvent.meta!);
}
break;
case StreamEventType.complete: case StreamEventType.complete:
if (!localController.isClosed) { if (!localController.isClosed) {
localController.close(); localController.close();

150
lib/data/services/speech_impl/azure_asr_service.dart

@ -1,76 +1,79 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../asr_service.dart'; import '../asr_service.dart';
/// Azure 语音识别服务 /// Azure 语音识别服务
/// ///
/// 该服务提供了通过平台通道与 Android 上的 Microsoft Speech SDK 交互的接口 /// 该服务提供了通过平台通道与 Android 上的 Microsoft Speech SDK 交互的接口
class AzureAsrService extends GetxService implements AsrService { class AzureAsrService extends GetxService implements AsrService {
static final AzureAsrService to = Get.put(AzureAsrService()); static final AzureAsrService to = Get.put(AzureAsrService());
static const MethodChannel _channel = MethodChannel('azure_speech/asr'); static const MethodChannel _channel = MethodChannel('azure_speech/asr');
static const EventChannel _eventChannel = EventChannel('azure_speech/asr_events'); static const EventChannel _eventChannel =
EventChannel('azure_speech/asr_events');
final GetStorage _storage = GetStorage();
bool _isInitialized = false; bool _isInitialized = false;
late final String _subscriptionKey; late final String _subscriptionKey;
late final String _serviceRegion; late final String _serviceRegion;
final List<String> _defaultSupportedLanguages = ['zh-CN']; final List<String> _defaultSupportedLanguages = ['zh-CN'];
@override @override
List<String> get supportedLanguages => _defaultSupportedLanguages; List<String> get supportedLanguages => _defaultSupportedLanguages;
// 连续识别相关 // 连续识别相关
bool _isContinuousRecognitionActive = false; bool _isContinuousRecognitionActive = false;
StreamController<RecognitionEvent>? _eventStreamController; StreamController<RecognitionEvent>? _eventStreamController;
StreamSubscription? _eventSubscription; StreamSubscription? _eventSubscription;
// 最新的识别结果 // 最新的识别结果
String _latestRecognizedText = ''; String _latestRecognizedText = '';
String get latestRecognizedText => _latestRecognizedText; String get latestRecognizedText => _latestRecognizedText;
// 最新检测到的语言 // 最新检测到的语言
String _latestDetectedLanguage = ''; String _latestDetectedLanguage = '';
String get latestDetectedLanguage => _latestDetectedLanguage; String get latestDetectedLanguage => _latestDetectedLanguage;
AzureAsrService() { AzureAsrService() {
_loadConfig(); _loadConfig();
} }
/// 从环境变量加载配置 /// 从环境变量加载配置
void _loadConfig() { void _loadConfig() {
_subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; final _env = _storage.read("ENV") as Map<String, String>;
_serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; _subscriptionKey = _env['AZURE_SPEECH_KEY'] ?? '';
_serviceRegion = _env['AZURE_SPEECH_REGION'] ?? '';
if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) {
throw Exception('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); throw Exception(
'未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION');
} }
} }
/// 设置事件通道 /// 设置事件通道
void _setupEventChannel() { void _setupEventChannel() {
_eventSubscription = _eventChannel _eventSubscription = _eventChannel.receiveBroadcastStream().listen((event) {
.receiveBroadcastStream() if (event is Map) {
.listen((event) { _handleRecognitionEvent(event);
if (event is Map) { }
_handleRecognitionEvent(event); }, onError: _handleRecognitionError);
}
}, onError: _handleRecognitionError);
} }
@override @override
Future<bool> initialize({ Future<bool> initialize({
List<String>? supportedLanguages, List<String>? supportedLanguages,
}) async { }) async {
try { try {
final List<String> languages = supportedLanguages ?? _defaultSupportedLanguages; final List<String> languages =
supportedLanguages ?? _defaultSupportedLanguages;
// 检查是否需要重新初始化 // 检查是否需要重新初始化
if (_isInitialized) { if (_isInitialized) {
await dispose(); await dispose();
} }
final bool result = await _channel.invokeMethod('initialize', { final bool result = await _channel.invokeMethod('initialize', {
'subscriptionKey': _subscriptionKey, 'subscriptionKey': _subscriptionKey,
'region': _serviceRegion, 'region': _serviceRegion,
@ -87,22 +90,23 @@ class AzureAsrService extends GetxService implements AsrService {
rethrow; rethrow;
} }
} }
@override @override
Future<RecognitionEvent> recognizeOnce() async { Future<RecognitionEvent> recognizeOnce() async {
if (!_isInitialized) { if (!_isInitialized) {
await initialize(); await initialize();
} }
try { try {
final result = await _channel.invokeMethod('recognizeOnce'); final result = await _channel.invokeMethod('recognizeOnce');
if (result is Map) { if (result is Map) {
final String text = result['text'] as String? ?? ''; final String text = result['text'] as String? ?? '';
final String detectedLanguage = result['detectedLanguage'] as String? ?? ''; final String detectedLanguage =
result['detectedLanguage'] as String? ?? '';
_latestRecognizedText = text; _latestRecognizedText = text;
_latestDetectedLanguage = detectedLanguage; _latestDetectedLanguage = detectedLanguage;
return RecognitionEvent.finalResult( return RecognitionEvent.finalResult(
text: text, text: text,
detectedLanguage: detectedLanguage, detectedLanguage: detectedLanguage,
@ -112,35 +116,36 @@ class AzureAsrService extends GetxService implements AsrService {
_latestRecognizedText = result; _latestRecognizedText = result;
return RecognitionEvent.finalResult(text: result); return RecognitionEvent.finalResult(text: result);
} }
throw Exception('无效的识别结果格式'); throw Exception('无效的识别结果格式');
} catch (e) { } catch (e) {
Logger.error('语音识别失败: $e'); Logger.error('语音识别失败: $e');
return RecognitionEvent.error(e.toString()); return RecognitionEvent.error(e.toString());
} }
} }
@override @override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async { Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
if (!_isInitialized) { if (!_isInitialized) {
await initialize(); await initialize();
} }
if (_isContinuousRecognitionActive) { if (_isContinuousRecognitionActive) {
await stopContinuousRecognition(); await stopContinuousRecognition();
} }
try { try {
_eventStreamController = StreamController<RecognitionEvent>.broadcast(); _eventStreamController = StreamController<RecognitionEvent>.broadcast();
// 开始连续识别 // 开始连续识别
final bool result = await _channel.invokeMethod('startContinuousRecognition'); final bool result =
await _channel.invokeMethod('startContinuousRecognition');
if (!result) { if (!result) {
_cleanupEventStream(); _cleanupEventStream();
throw Exception('启动连续识别失败'); throw Exception('启动连续识别失败');
} }
_isContinuousRecognitionActive = true; _isContinuousRecognitionActive = true;
return _eventStreamController!.stream; return _eventStreamController!.stream;
} catch (e) { } catch (e) {
@ -149,15 +154,16 @@ class AzureAsrService extends GetxService implements AsrService {
rethrow; rethrow;
} }
} }
@override @override
Future<bool> stopContinuousRecognition() async { Future<bool> stopContinuousRecognition() async {
if (!_isInitialized || !_isContinuousRecognitionActive) { if (!_isInitialized || !_isContinuousRecognitionActive) {
return true; return true;
} }
try { try {
final bool result = await _channel.invokeMethod('stopContinuousRecognition'); final bool result =
await _channel.invokeMethod('stopContinuousRecognition');
_cleanupEventStream(); _cleanupEventStream();
return result; return result;
} catch (e) { } catch (e) {
@ -166,26 +172,27 @@ class AzureAsrService extends GetxService implements AsrService {
rethrow; rethrow;
} }
} }
@override @override
bool isContinuousRecognitionActive() { bool isContinuousRecognitionActive() {
return _isContinuousRecognitionActive; return _isContinuousRecognitionActive;
} }
/// 处理来自原生端的识别事件 /// 处理来自原生端的识别事件
void _handleRecognitionEvent(dynamic event) { void _handleRecognitionEvent(dynamic event) {
if (event is! Map || _eventStreamController == null) return; if (event is! Map || _eventStreamController == null) return;
final Map<dynamic, dynamic> eventMap = event; final Map<dynamic, dynamic> eventMap = event;
final String eventType = eventMap['type'] as String? ?? ''; final String eventType = eventMap['type'] as String? ?? '';
// 添加日志帮助调试 // 添加日志帮助调试
Logger.debug('收到语音识别事件: $eventType, 数据: $eventMap'); Logger.debug('收到语音识别事件: $eventType, 数据: $eventMap');
switch (eventType) { switch (eventType) {
case 'result': case 'result':
final String text = eventMap['text'] as String? ?? ''; final String text = eventMap['text'] as String? ?? '';
final String detectedLanguage = eventMap['detectedLanguage'] as String? ?? ''; final String detectedLanguage =
eventMap['detectedLanguage'] as String? ?? '';
_latestRecognizedText = text; _latestRecognizedText = text;
_latestDetectedLanguage = detectedLanguage; _latestDetectedLanguage = detectedLanguage;
_eventStreamController?.add(RecognitionEvent( _eventStreamController?.add(RecognitionEvent(
@ -194,45 +201,46 @@ class AzureAsrService extends GetxService implements AsrService {
detectedLanguage: detectedLanguage, detectedLanguage: detectedLanguage,
)); ));
break; break;
case 'recognizing': case 'recognizing':
final String text = eventMap['text'] as String? ?? ''; final String text = eventMap['text'] as String? ?? '';
final String detectedLanguage = eventMap['detectedLanguage'] as String? ?? ''; final String detectedLanguage =
eventMap['detectedLanguage'] as String? ?? '';
_eventStreamController?.add(RecognitionEvent( _eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.intermediateResult, type: RecognitionEventType.intermediateResult,
text: text, text: text,
detectedLanguage: detectedLanguage, detectedLanguage: detectedLanguage,
)); ));
break; break;
case 'sessionStarted': case 'sessionStarted':
_eventStreamController?.add(RecognitionEvent( _eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.sessionStarted, type: RecognitionEventType.sessionStarted,
)); ));
break; break;
case 'sessionStopped': case 'sessionStopped':
_isContinuousRecognitionActive = false; _isContinuousRecognitionActive = false;
_eventStreamController?.add(RecognitionEvent( _eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.sessionStopped, type: RecognitionEventType.sessionStopped,
)); ));
break; break;
case 'canceled': case 'canceled':
_isContinuousRecognitionActive = false; _isContinuousRecognitionActive = false;
final String reason = eventMap['reason'] as String? ?? ''; final String reason = eventMap['reason'] as String? ?? '';
final String errorDetails = eventMap['errorDetails'] as String? ?? ''; final String errorDetails = eventMap['errorDetails'] as String? ?? '';
if (reason.isNotEmpty || errorDetails.isNotEmpty) { if (reason.isNotEmpty || errorDetails.isNotEmpty) {
Logger.error('识别取消: $reason - $errorDetails'); Logger.error('识别取消: $reason - $errorDetails');
} }
_eventStreamController?.add(RecognitionEvent( _eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.canceled, type: RecognitionEventType.canceled,
error: '$reason: $errorDetails', error: '$reason: $errorDetails',
)); ));
break; break;
case 'error': case 'error':
final String error = eventMap['message'] as String? ?? ''; final String error = eventMap['message'] as String? ?? '';
Logger.error('识别错误: $error'); Logger.error('识别错误: $error');
@ -243,36 +251,36 @@ class AzureAsrService extends GetxService implements AsrService {
break; break;
} }
} }
/// 处理识别事件流错误 /// 处理识别事件流错误
void _handleRecognitionError(Object error) { void _handleRecognitionError(Object error) {
Logger.error('识别事件流错误: $error'); Logger.error('识别事件流错误: $error');
_eventStreamController?.addError(error); _eventStreamController?.addError(error);
_cleanupEventStream(); _cleanupEventStream();
} }
/// 清理事件流资源 /// 清理事件流资源
void _cleanupEventStream() { void _cleanupEventStream() {
_eventStreamController?.close(); _eventStreamController?.close();
_eventStreamController = null; _eventStreamController = null;
_isContinuousRecognitionActive = false; _isContinuousRecognitionActive = false;
} }
@override @override
Future<void> dispose() async { Future<void> dispose() async {
try { try {
if (_isContinuousRecognitionActive) { if (_isContinuousRecognitionActive) {
await stopContinuousRecognition(); await stopContinuousRecognition();
} }
// 取消事件订阅 // 取消事件订阅
await _eventSubscription?.cancel(); await _eventSubscription?.cancel();
_eventSubscription = null; _eventSubscription = null;
// 通知原生端释放资源 // 通知原生端释放资源
await _channel.invokeMethod('dispose'); await _channel.invokeMethod('dispose');
_isInitialized = false; _isInitialized = false;
Logger.info('Azure 语音识别资源已释放'); Logger.info('Azure 语音识别资源已释放');
} catch (e) { } catch (e) {
Logger.error('释放语音识别资源失败: $e'); Logger.error('释放语音识别资源失败: $e');
@ -280,4 +288,22 @@ class AzureAsrService extends GetxService implements AsrService {
_isInitialized = false; _isInitialized = false;
} }
} }
}
@override
Future<void> pushAudioData(Uint8List data) async {
try {
// 开始连续识别
final bool result = await _channel.invokeMethod('pushAudioData', {
'AudioData': data // Flutter 会自动将 Uint8List 转换为 ByteBuffer
});
if (!result) {
_cleanupEventStream();
throw Exception('开始pushAudioData失败');
}
} catch (e) {
Logger.error('开始pushAudioData失败: $e');
rethrow;
}
}
}

202
lib/data/services/speech_impl/azure_tts_service.dart

@ -3,27 +3,28 @@ import 'dart:io';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import '../tts_service.dart'; import '../tts_service.dart';
/// 微软 Text-to-Speech 服务 /// 微软 Text-to-Speech 服务
/// ///
/// 该服务通过平台通道与原生 Microsoft Speech SDK 交互, /// 该服务通过平台通道与原生 Microsoft Speech SDK 交互,
/// 提供文本转语音功能。 /// 提供文本转语音功能。
class AzureTtsService extends GetxService implements TtsService { class AzureTtsService extends GetxService implements TtsService {
static final AzureTtsService to = Get.put(AzureTtsService()); static final AzureTtsService to = Get.put(AzureTtsService());
static const MethodChannel _channel = MethodChannel('azure_speech/tts'); static const MethodChannel _channel = MethodChannel('azure_speech/tts');
final GetStorage _storage = GetStorage();
bool _isInitialized = false; bool _isInitialized = false;
late final String _subscriptionKey; late final String _subscriptionKey;
late final String _serviceRegion; late final String _serviceRegion;
// 当前设置 // 当前设置
String _currentVoice = 'zh-CN-XiaoxiaoNeural'; String _currentVoice = 'zh-CN-XiaoxiaoNeural';
int _currentRate = 0; int _currentRate = 0;
int _currentPitch = 0; int _currentPitch = 0;
int _currentVolume = 100; int _currentVolume = 100;
// 支持的语音列表 // 支持的语音列表
final List<String> _supportedVoices = [ final List<String> _supportedVoices = [
'zh-CN-XiaoxiaoNeural', 'zh-CN-XiaoxiaoNeural',
@ -32,103 +33,98 @@ class AzureTtsService extends GetxService implements TtsService {
'en-US-JennyNeural', 'en-US-JennyNeural',
'en-US-GuyNeural' 'en-US-GuyNeural'
]; ];
// 语音合成队列 // 语音合成队列
final List<_SpeechItem> _textQueue = []; final List<_SpeechItem> _textQueue = [];
bool _isProcessingQueue = false; bool _isProcessingQueue = false;
// 可观察状态 // 可观察状态
final isEnabled = true.obs; final isEnabled = true.obs;
// 流式文本缓冲区 // 流式文本缓冲区
String _streamBuffer = ''; String _streamBuffer = '';
/// 缓存的播放状态 /// 缓存的播放状态
bool _speakingCache = false; bool _speakingCache = false;
DateTime _lastSpeakingCheck = DateTime.now(); DateTime _lastSpeakingCheck = DateTime.now();
@override @override
String get currentVoice => _currentVoice; String get currentVoice => _currentVoice;
@override @override
bool get isSpeaking { bool get isSpeaking {
// 如果未初始化,直接返回false // 如果未初始化,直接返回false
if (!_isInitialized) return false; if (!_isInitialized) return false;
// 如果距离上次检查不到50ms,则使用缓存值 // 如果距离上次检查不到50ms,则使用缓存值
if (DateTime.now().difference(_lastSpeakingCheck).inMilliseconds < 50) { if (DateTime.now().difference(_lastSpeakingCheck).inMilliseconds < 50) {
return _speakingCache; return _speakingCache;
} }
// 否则异步更新缓存(但仍返回当前缓存值) // 否则异步更新缓存(但仍返回当前缓存值)
_checkSpeakingStatus(); _checkSpeakingStatus();
return _speakingCache; return _speakingCache;
} }
@override @override
Stream<TtsEvent> get onEvent => const Stream.empty(); Stream<TtsEvent> get onEvent => const Stream.empty();
@override @override
List<String> get supportedVoices => _supportedVoices; List<String> get supportedVoices => _supportedVoices;
AzureTtsService() { AzureTtsService() {
_loadConfig(); _loadConfig();
} }
/// 从环境变量加载配置 /// 从环境变量加载配置
void _loadConfig() { void _loadConfig() {
_subscriptionKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; final _env = _storage.read("ENV") as Map<String, String>;
_serviceRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; _subscriptionKey = _env['AZURE_SPEECH_KEY'] ?? '';
_serviceRegion = _env['AZURE_SPEECH_REGION'] ?? '';
} }
@override @override
Future<bool> initialize({ Future<bool> initialize({
List<String>? supportedLanguages, List<String>? supportedLanguages,
}) async { }) async {
// 检查是否需要重新初始化 // 检查是否需要重新初始化
if (_isInitialized) { if (_isInitialized) {
await dispose(); await dispose();
} }
try { try {
if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) { if (_subscriptionKey.isEmpty || _serviceRegion.isEmpty) {
Logger.error('未找到Azure语音服务配置'); Logger.error('未找到Azure语音服务配置');
return false; return false;
} }
// 更新支持的语音列表(如果提供) // 更新支持的语音列表(如果提供)
final String language = supportedLanguages?.first ?? 'zh-CN'; final String language = supportedLanguages?.first ?? 'zh-CN';
final result = await _channel.invokeMethod('initialize', {
final result = await _channel.invokeMethod('initialize', { 'subscriptionKey': _subscriptionKey,
'subscriptionKey': _subscriptionKey, 'region': _serviceRegion,
'region': _serviceRegion, 'language': language,
'language': language, });
});
_isInitialized = true;
return result;
_isInitialized = true;
return result;
} catch (e) { } catch (e) {
Logger.error('初始化失败: $e'); Logger.error('初始化失败: $e');
return false; return false;
} }
} }
@override @override
Future<bool> setVoice(String voiceName) async { Future<bool> setVoice(String voiceName) async {
if (!_isInitialized) await initialize(); if (!_isInitialized) await initialize();
if (voiceName == _currentVoice) return true; if (voiceName == _currentVoice) return true;
try { try {
final result = await _channel.invokeMethod('setVoice', { final result = await _channel.invokeMethod('setVoice', {
'voiceName': voiceName, 'voiceName': voiceName,
}); });
if (result) _currentVoice = voiceName; if (result) _currentVoice = voiceName;
return result; return result;
} catch (e) { } catch (e) {
@ -136,45 +132,46 @@ class AzureTtsService extends GetxService implements TtsService {
return false; return false;
} }
} }
/// 设置语音参数 /// 设置语音参数
Future<bool> setSpeechParams({int rate = 0, int pitch = 0, int volume = 100}) async { Future<bool> setSpeechParams(
{int rate = 0, int pitch = 0, int volume = 100}) async {
if (!_isInitialized) await initialize(); if (!_isInitialized) await initialize();
try { try {
final result = await _channel.invokeMethod('setSpeechParams', { final result = await _channel.invokeMethod('setSpeechParams', {
'rate': rate, 'rate': rate,
'pitch': pitch, 'pitch': pitch,
'volume': volume, 'volume': volume,
}); });
if (result) { if (result) {
_currentRate = rate; _currentRate = rate;
_currentPitch = pitch; _currentPitch = pitch;
_currentVolume = volume; _currentVolume = volume;
} }
return result; return result;
} catch (e) { } catch (e) {
Logger.error('设置语音参数失败: $e'); Logger.error('设置语音参数失败: $e');
return false; return false;
} }
} }
@override @override
Future<bool> speakOnce(String text) async { Future<bool> speakOnce(String text) async {
if (!_isInitialized) await initialize(); if (!_isInitialized) await initialize();
if (!isEnabled.value || text.isEmpty) return false; if (!isEnabled.value || text.isEmpty) return false;
try { try {
// 主动设置缓存状态为true // 主动设置缓存状态为true
_speakingCache = true; _speakingCache = true;
final result = await _channel.invokeMethod('speakText', {'text': text}); final result = await _channel.invokeMethod('speakText', {'text': text});
// 调用完成后主动检查状态 // 调用完成后主动检查状态
_checkSpeakingStatus(); _checkSpeakingStatus();
return result == "OK"; return result == "OK";
} catch (e) { } catch (e) {
// 发生错误时重置状态 // 发生错误时重置状态
@ -183,21 +180,21 @@ class AzureTtsService extends GetxService implements TtsService {
return false; return false;
} }
} }
/// 播放SSML /// 播放SSML
Future<bool> speakSsmlOnce(String ssml) async { Future<bool> speakSsmlOnce(String ssml) async {
if (!_isInitialized) await initialize(); if (!_isInitialized) await initialize();
if (!isEnabled.value || ssml.isEmpty) return false; if (!isEnabled.value || ssml.isEmpty) return false;
try { try {
// 主动设置缓存状态为true // 主动设置缓存状态为true
_speakingCache = true; _speakingCache = true;
final result = await _channel.invokeMethod('speakSsml', {'ssml': ssml}); final result = await _channel.invokeMethod('speakSsml', {'ssml': ssml});
// 调用完成后主动检查状态 // 调用完成后主动检查状态
_checkSpeakingStatus(); _checkSpeakingStatus();
return result == "OK"; return result == "OK";
} catch (e) { } catch (e) {
// 发生错误时重置状态 // 发生错误时重置状态
@ -206,15 +203,16 @@ class AzureTtsService extends GetxService implements TtsService {
return false; return false;
} }
} }
/// 等待语音播放完成 /// 等待语音播放完成
Future<void> waitForSpeechCompletion({Duration timeout = const Duration(seconds: 30)}) async { Future<void> waitForSpeechCompletion(
{Duration timeout = const Duration(seconds: 30)}) async {
// 首先检查当前状态 // 首先检查当前状态
_checkSpeakingStatus(); _checkSpeakingStatus();
if (!_speakingCache) return; if (!_speakingCache) return;
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
while (_speakingCache && stopwatch.elapsed < timeout) { while (_speakingCache && stopwatch.elapsed < timeout) {
// 短暂等待后再次检查 // 短暂等待后再次检查
await Future.delayed(const Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
@ -227,31 +225,30 @@ class AzureTtsService extends GetxService implements TtsService {
}); });
} }
} }
@override @override
Future<bool> speakStream(String text) async { Future<bool> speakStream(String text) async {
if (!isEnabled.value || text.isEmpty) return false; if (!isEnabled.value || text.isEmpty) return false;
try { try {
// 添加文本到缓冲区 // 添加文本到缓冲区
_streamBuffer += text; _streamBuffer += text;
// 如果缓冲区为空,直接返回 // 如果缓冲区为空,直接返回
if (_streamBuffer.isEmpty) return true; if (_streamBuffer.isEmpty) return true;
// 使用正则表达式匹配句子,包括结束符号 // 使用正则表达式匹配句子,包括结束符号
// 匹配任意字符,直到遇到句子结束符号 // 匹配任意字符,直到遇到句子结束符号
final sentenceRegex = RegExp( final sentenceRegex = RegExp(
r'([^。.!!??;;:\n\r]+[。.!!??;;::\n\r]|[^。.!!??;;:\n\r]+(?:\.{3,}|…)|[^。.!!??;;:\n\r]+["」』])' r'([^。.!!??;;:\n\r]+[。.!!??;;::\n\r]|[^。.!!??;;:\n\r]+(?:\.{3,}|…)|[^。.!!??;;:\n\r]+["」』])');
);
bool hasProcessed = false; bool hasProcessed = false;
// 查找所有完整句子 // 查找所有完整句子
final matches = sentenceRegex.allMatches(_streamBuffer); final matches = sentenceRegex.allMatches(_streamBuffer);
final List<String> sentences = []; final List<String> sentences = [];
int lastMatchEnd = 0; int lastMatchEnd = 0;
for (final match in matches) { for (final match in matches) {
// 提取完整句子(包含结束符号) // 提取完整句子(包含结束符号)
final sentence = match.group(1)?.trim(); final sentence = match.group(1)?.trim();
@ -260,7 +257,7 @@ class AzureTtsService extends GetxService implements TtsService {
lastMatchEnd = match.end; lastMatchEnd = match.end;
} }
} }
// 处理找到的句子 // 处理找到的句子
for (final sentence in sentences) { for (final sentence in sentences) {
_textQueue.add(_SpeechItem( _textQueue.add(_SpeechItem(
@ -271,32 +268,32 @@ class AzureTtsService extends GetxService implements TtsService {
)); ));
hasProcessed = true; hasProcessed = true;
} }
// 更新缓冲区,只保留未完成的部分 // 更新缓冲区,只保留未完成的部分
if (lastMatchEnd > 0) { if (lastMatchEnd > 0) {
_streamBuffer = _streamBuffer.substring(lastMatchEnd); _streamBuffer = _streamBuffer.substring(lastMatchEnd);
} }
// 如果处理了文本并且队列未在处理中,开始处理队列 // 如果处理了文本并且队列未在处理中,开始处理队列
if (hasProcessed && !_isProcessingQueue) { if (hasProcessed && !_isProcessingQueue) {
_processQueue(); _processQueue();
} }
return true; return true;
} catch (e) { } catch (e) {
Logger.error('处理流式文本失败: $e'); Logger.error('处理流式文本失败: $e');
return false; return false;
} }
} }
@override @override
Future<bool> flushStream() async { Future<bool> flushStream() async {
if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true; if (_textQueue.isEmpty && _streamBuffer.isEmpty) return true;
try { try {
// 处理缓冲区中可能的完整句子 // 处理缓冲区中可能的完整句子
await speakStream(''); await speakStream('');
// 如果缓冲区仍有剩余文本,将其添加到播放队列 // 如果缓冲区仍有剩余文本,将其添加到播放队列
if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) { if (_streamBuffer.isNotEmpty && _streamBuffer.trim().isNotEmpty) {
_textQueue.add(_SpeechItem( _textQueue.add(_SpeechItem(
@ -305,25 +302,25 @@ class AzureTtsService extends GetxService implements TtsService {
pitch: _currentPitch, pitch: _currentPitch,
volume: _currentVolume, volume: _currentVolume,
)); ));
// 清空缓冲区 // 清空缓冲区
_streamBuffer = ''; _streamBuffer = '';
// 如果队列未在处理中,开始处理队列 // 如果队列未在处理中,开始处理队列
if (!_isProcessingQueue) { if (!_isProcessingQueue) {
_processQueue(); _processQueue();
} }
} }
Logger.info('等待TTS队列播放完成,剩余${_textQueue.length}条'); Logger.info('等待TTS队列播放完成,剩余${_textQueue.length}条');
// 等待队列处理完成 // 等待队列处理完成
await _checkSpeakingStatus(); await _checkSpeakingStatus();
while (_isProcessingQueue || _speakingCache) { while (_isProcessingQueue || _speakingCache) {
await Future.delayed(const Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
await _checkSpeakingStatus(); await _checkSpeakingStatus();
} }
Logger.info('TTS队列播放完成'); Logger.info('TTS队列播放完成');
return true; return true;
} catch (e) { } catch (e) {
@ -331,33 +328,35 @@ class AzureTtsService extends GetxService implements TtsService {
return false; return false;
} }
} }
/// 处理语音合成队列 /// 处理语音合成队列
Future<void> _processQueue() async { Future<void> _processQueue() async {
if (_textQueue.isEmpty || _isProcessingQueue) return; if (_textQueue.isEmpty || _isProcessingQueue) return;
_isProcessingQueue = true; _isProcessingQueue = true;
try { try {
while (_textQueue.isNotEmpty) { while (_textQueue.isNotEmpty) {
if (!isEnabled.value) { if (!isEnabled.value) {
_textQueue.clear(); _textQueue.clear();
break; break;
} }
final item = _textQueue.removeAt(0); final item = _textQueue.removeAt(0);
if (item.rate != _currentRate || item.pitch != _currentPitch || item.volume != _currentVolume) { if (item.rate != _currentRate ||
item.pitch != _currentPitch ||
item.volume != _currentVolume) {
await setSpeechParams( await setSpeechParams(
rate: item.rate, rate: item.rate,
pitch: item.pitch, pitch: item.pitch,
volume: item.volume, volume: item.volume,
); );
} }
// 使用speakOnce播放当前项 // 使用speakOnce播放当前项
await speakOnce(item.text); await speakOnce(item.text);
// 等待播放完成 // 等待播放完成
await waitForSpeechCompletion(); await waitForSpeechCompletion();
} }
@ -367,26 +366,26 @@ class AzureTtsService extends GetxService implements TtsService {
_isProcessingQueue = false; _isProcessingQueue = false;
} }
} }
@override @override
Future<bool> stop() async { Future<bool> stop() async {
try { try {
_textQueue.clear(); _textQueue.clear();
if (_speakingCache) { if (_speakingCache) {
final result = await _channel.invokeMethod('stopSpeaking'); final result = await _channel.invokeMethod('stopSpeaking');
// 重置缓存状态 // 重置缓存状态
_speakingCache = false; _speakingCache = false;
return result; return result;
} }
return true; return true;
} catch (e) { } catch (e) {
Logger.error('停止语音合成出错: $e'); Logger.error('停止语音合成出错: $e');
return false; return false;
} }
} }
/// 生成SSML文本 /// 生成SSML文本
String generateSsml({ String generateSsml({
required String text, required String text,
@ -399,11 +398,15 @@ class AzureTtsService extends GetxService implements TtsService {
final rateValue = (rate ?? _currentRate).clamp(-100, 100); final rateValue = (rate ?? _currentRate).clamp(-100, 100);
final pitchValue = (pitch ?? _currentPitch).clamp(-100, 100); final pitchValue = (pitch ?? _currentPitch).clamp(-100, 100);
final volumeValue = (volume ?? _currentVolume).clamp(0, 100); final volumeValue = (volume ?? _currentVolume).clamp(0, 100);
final rateStr = rateValue == 0 ? '0%' : rateValue < 0 ? '${(rateValue * 0.9).round()}%' : '$rateValue%'; final rateStr = rateValue == 0
? '0%'
: rateValue < 0
? '${(rateValue * 0.9).round()}%'
: '$rateValue%';
final pitchStr = pitchValue == 0 ? '0%' : '${(pitchValue * 0.5).round()}%'; final pitchStr = pitchValue == 0 ? '0%' : '${(pitchValue * 0.5).round()}%';
final volumeStr = "$volumeValue%"; final volumeStr = "$volumeValue%";
return ''' return '''
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="zh-CN"> <speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="zh-CN">
<voice name="$voice"> <voice name="$voice">
@ -414,17 +417,17 @@ class AzureTtsService extends GetxService implements TtsService {
</speak> </speak>
'''; ''';
} }
/// 切换启用状态 /// 切换启用状态
void toggleEnabled() { void toggleEnabled() {
isEnabled.toggle(); isEnabled.toggle();
if (!isEnabled.value) stop(); if (!isEnabled.value) stop();
} }
@override @override
Future<void> dispose() async { Future<void> dispose() async {
if (!_isInitialized) return; if (!_isInitialized) return;
try { try {
await stop(); await stop();
await _channel.invokeMethod('dispose'); await _channel.invokeMethod('dispose');
@ -448,7 +451,6 @@ class AzureTtsService extends GetxService implements TtsService {
return _speakingCache; return _speakingCache;
} }
} }
} }
/// 语音合成项目 /// 语音合成项目
@ -457,11 +459,11 @@ class _SpeechItem {
final int rate; final int rate;
final int pitch; final int pitch;
final int volume; final int volume;
_SpeechItem({ _SpeechItem({
required this.text, required this.text,
this.rate = 0, this.rate = 0,
this.pitch = 0, this.pitch = 0,
this.volume = 100, this.volume = 100,
}); });
} }

239
lib/data/services/speech_impl/flutter_asr_service.dart

@ -11,26 +11,26 @@ import '../asr_service.dart';
enum AsrState { enum AsrState {
/// 未初始化 /// 未初始化
notInitialized, notInitialized,
/// 已初始化但未开始监听 /// 已初始化但未开始监听
initialized, initialized,
/// 正在监听 /// 正在监听
listening, listening,
/// 正在处理识别结果 /// 正在处理识别结果
processing, processing,
/// 识别完成 /// 识别完成
done, done,
/// 发生错误 /// 发生错误
error, error,
} }
/// Flutter ASR服务,负责本地语音转文本功能 /// Flutter ASR服务,负责本地语音转文本功能
/// 使用speech_to_text插件实现语音识别功能 /// 使用speech_to_text插件实现语音识别功能
class FlutterAsrService extends GetxService implements AsrService { class FlutterAsrService extends GetxService implements AsrService {
static final FlutterAsrService to = Get.put(FlutterAsrService()); static final FlutterAsrService to = Get.put(FlutterAsrService());
// speech_to_text 实例 // speech_to_text 实例
@ -40,34 +40,34 @@ class FlutterAsrService extends GetxService implements AsrService {
final List<String> _supportedLanguages = []; final List<String> _supportedLanguages = [];
@override @override
List<String> get supportedLanguages => _supportedLanguages; List<String> get supportedLanguages => _supportedLanguages;
// 当前选择的语言 // 当前选择的语言
final RxString _currentLocale = ''.obs; final RxString _currentLocale = ''.obs;
String get currentLocale => _currentLocale.value; String get currentLocale => _currentLocale.value;
// 是否可用 // 是否可用
final RxBool _isAvailable = false.obs; final RxBool _isAvailable = false.obs;
bool get isAvailable => _isAvailable.value; bool get isAvailable => _isAvailable.value;
// 是否正在监听 // 是否正在监听
bool _isListening = false; bool _isListening = false;
// 当前ASR状态 // 当前ASR状态
final Rx<AsrState> _asrState = AsrState.notInitialized.obs; final Rx<AsrState> _asrState = AsrState.notInitialized.obs;
AsrState get asrState => _asrState.value; AsrState get asrState => _asrState.value;
// 连续识别模式的结果流控制器 // 连续识别模式的结果流控制器
StreamController<RecognitionEvent>? _continuousRecognitionController; StreamController<RecognitionEvent>? _continuousRecognitionController;
FlutterAsrService() { FlutterAsrService() {
_initSpeech(); _initSpeech();
} }
/// 初始化语音识别 /// 初始化语音识别
Future<void> _initSpeech() async { Future<void> _initSpeech() async {
try { try {
_asrState.value = AsrState.notInitialized; _asrState.value = AsrState.notInitialized;
// 初始化语音识别,使用较短的最终超时时间,提高响应速度 // 初始化语音识别,使用较短的最终超时时间,提高响应速度
_isAvailable.value = await _speech.initialize( _isAvailable.value = await _speech.initialize(
onStatus: _onSpeechStatus, onStatus: _onSpeechStatus,
@ -75,11 +75,11 @@ class FlutterAsrService extends GetxService implements AsrService {
debugLogging: false, debugLogging: false,
finalTimeout: const Duration(milliseconds: 800), // 更短的最终超时时间,加速结果返回 finalTimeout: const Duration(milliseconds: 800), // 更短的最终超时时间,加速结果返回
); );
if (_isAvailable.value) { if (_isAvailable.value) {
// 获取支持的语言列表 // 获取支持的语言列表
final systemLocales = await _speech.locales(); final systemLocales = await _speech.locales();
// 清空并重新填充支持的语言列表 // 清空并重新填充支持的语言列表
_supportedLanguages.clear(); _supportedLanguages.clear();
for (var locale in systemLocales) { for (var locale in systemLocales) {
@ -87,7 +87,7 @@ class FlutterAsrService extends GetxService implements AsrService {
_supportedLanguages.add(locale.localeId); _supportedLanguages.add(locale.localeId);
} }
} }
// 设置默认语言(优先使用系统语言或中文) // 设置默认语言(优先使用系统语言或中文)
var systemLocale = await _speech.systemLocale(); var systemLocale = await _speech.systemLocale();
if (systemLocale != null) { if (systemLocale != null) {
@ -97,7 +97,7 @@ class FlutterAsrService extends GetxService implements AsrService {
} else if (_supportedLanguages.isNotEmpty) { } else if (_supportedLanguages.isNotEmpty) {
_currentLocale.value = _supportedLanguages.first; _currentLocale.value = _supportedLanguages.first;
} }
_asrState.value = AsrState.initialized; _asrState.value = AsrState.initialized;
Logger.info('语音识别初始化成功,支持 ${_supportedLanguages.length} 种语言'); Logger.info('语音识别初始化成功,支持 ${_supportedLanguages.length} 种语言');
} else { } else {
@ -110,11 +110,11 @@ class FlutterAsrService extends GetxService implements AsrService {
Logger.error('语音识别初始化失败: $e'); Logger.error('语音识别初始化失败: $e');
} }
} }
/// 处理语音识别状态变化 /// 处理语音识别状态变化
void _onSpeechStatus(String status) { void _onSpeechStatus(String status) {
Logger.info('语音识别状态: $status'); Logger.info('语音识别状态: $status');
switch (status) { switch (status) {
case 'listening': case 'listening':
_asrState.value = AsrState.listening; _asrState.value = AsrState.listening;
@ -129,31 +129,33 @@ class FlutterAsrService extends GetxService implements AsrService {
case 'done': case 'done':
_asrState.value = AsrState.done; _asrState.value = AsrState.done;
_isListening = false; _isListening = false;
// 在连续识别模式下,当一次识别完成后立即开始下一次识别 // 在连续识别模式下,当一次识别完成后立即开始下一次识别
if (_continuousRecognitionController != null && !_continuousRecognitionController!.isClosed) { if (_continuousRecognitionController != null &&
!_continuousRecognitionController!.isClosed) {
_restartContinuousRecognition(); _restartContinuousRecognition();
} }
break; break;
} }
} }
/// 处理语音识别错误 /// 处理语音识别错误
void _onSpeechError(SpeechRecognitionError error) { void _onSpeechError(SpeechRecognitionError error) {
Logger.error('语音识别错误: ${error.errorMsg} (永久性错误: ${error.permanent})'); Logger.error('语音识别错误: ${error.errorMsg} (永久性错误: ${error.permanent})');
_asrState.value = AsrState.error; _asrState.value = AsrState.error;
// 向连续识别控制器发送错误事件(如果存在) // 向连续识别控制器发送错误事件(如果存在)
_continuousRecognitionController?.add( _continuousRecognitionController
RecognitionEvent.error(error.errorMsg) ?.add(RecognitionEvent.error(error.errorMsg));
);
// 在连续识别模式下,尝试从错误中恢复(非永久性错误) // 在连续识别模式下,尝试从错误中恢复(非永久性错误)
if (!error.permanent && _continuousRecognitionController != null && !_continuousRecognitionController!.isClosed) { if (!error.permanent &&
_continuousRecognitionController != null &&
!_continuousRecognitionController!.isClosed) {
_restartContinuousRecognition(); _restartContinuousRecognition();
} }
} }
/// 启动语音识别的统一方法 /// 启动语音识别的统一方法
Future<bool> _startListening({ Future<bool> _startListening({
required Function(SpeechRecognitionResult) onResult, required Function(SpeechRecognitionResult) onResult,
@ -168,36 +170,37 @@ class FlutterAsrService extends GetxService implements AsrService {
// 延长等待时间,确保资源完全释放 // 延长等待时间,确保资源完全释放
await Future.delayed(const Duration(milliseconds: 800)); await Future.delayed(const Duration(milliseconds: 800));
} }
// 在启动前再次检查状态 // 在启动前再次检查状态
if (_speech.isListening) { if (_speech.isListening) {
Logger.warning('停止识别后,仍处于监听状态,可能有系统冲突'); Logger.warning('停止识别后,仍处于监听状态,可能有系统冲突');
await _speech.stop(); await _speech.stop();
await Future.delayed(const Duration(milliseconds: 500)); await Future.delayed(const Duration(milliseconds: 500));
} }
// 启动语音识别前确保状态正确 // 启动语音识别前确保状态正确
_asrState.value = AsrState.listening; _asrState.value = AsrState.listening;
// 启动语音识别 // 启动语音识别
final result = await _speech.listen( final result = await _speech.listen(
onResult: onResult, onResult: onResult,
localeId: _currentLocale.value, localeId: _currentLocale.value,
listenFor: listenFor, listenFor: listenFor,
pauseFor: pauseFor, pauseFor: pauseFor,
listenOptions: stt.SpeechListenOptions( listenOptions: stt.SpeechListenOptions(
partialResults: true, partialResults: true,
autoPunctuation: autoPunctuation, autoPunctuation: autoPunctuation,
// 使用独白模式,减少与系统交互冲突 // 使用独白模式,减少与系统交互冲突
listenMode: stt.ListenMode.dictation, listenMode: stt.ListenMode.dictation,
), ),
) ?? false; ) ??
false;
if (!result) { if (!result) {
Logger.error('语音识别启动失败'); Logger.error('语音识别启动失败');
_asrState.value = AsrState.error; _asrState.value = AsrState.error;
} }
return result; return result;
} catch (e) { } catch (e) {
Logger.error('启动语音识别失败: $e'); Logger.error('启动语音识别失败: $e');
@ -205,33 +208,35 @@ class FlutterAsrService extends GetxService implements AsrService {
return false; return false;
} }
} }
/// 重新启动连续识别 /// 重新启动连续识别
Future<void> _restartContinuousRecognition() async { Future<void> _restartContinuousRecognition() async {
try { try {
// 确保控制器有效 // 确保控制器有效
if (_continuousRecognitionController == null || _continuousRecognitionController!.isClosed) { if (_continuousRecognitionController == null ||
_continuousRecognitionController!.isClosed) {
Logger.info('连续识别控制器已关闭,跳过重启'); Logger.info('连续识别控制器已关闭,跳过重启');
return; return;
} }
// 检查系统状态,避免频繁重启 // 检查系统状态,避免频繁重启
if (_speech.isListening) { if (_speech.isListening) {
Logger.info('当前已在监听中,无需重启'); Logger.info('当前已在监听中,无需重启');
return; return;
} }
// 延长等待时间,避免与系统资源冲突 // 延长等待时间,避免与系统资源冲突
await Future.delayed(const Duration(milliseconds: 1000)); await Future.delayed(const Duration(milliseconds: 1000));
// 再次检查状态 // 再次检查状态
if (_continuousRecognitionController == null || _continuousRecognitionController!.isClosed) { if (_continuousRecognitionController == null ||
_continuousRecognitionController!.isClosed) {
Logger.info('延迟期间控制器已关闭,跳过重启'); Logger.info('延迟期间控制器已关闭,跳过重启');
return; return;
} }
Logger.info('尝试重新启动连续识别'); Logger.info('尝试重新启动连续识别');
// 使用统一方法启动识别 // 使用统一方法启动识别
final success = await _startListening( final success = await _startListening(
onResult: _onSpeechResult, onResult: _onSpeechResult,
@ -239,49 +244,42 @@ class FlutterAsrService extends GetxService implements AsrService {
listenFor: const Duration(seconds: 30), listenFor: const Duration(seconds: 30),
pauseFor: const Duration(seconds: 4), pauseFor: const Duration(seconds: 4),
); );
if (success) { if (success) {
Logger.info('连续识别重启成功'); Logger.info('连续识别重启成功');
} else { } else {
Logger.error('连续识别重启失败'); Logger.error('连续识别重启失败');
_continuousRecognitionController?.add( _continuousRecognitionController
RecognitionEvent.error('重启语音识别失败,可能存在系统资源冲突') ?.add(RecognitionEvent.error('重启语音识别失败,可能存在系统资源冲突'));
);
} }
} catch (e) { } catch (e) {
Logger.error('重启连续识别失败: $e'); Logger.error('重启连续识别失败: $e');
// 发送错误给控制器 // 发送错误给控制器
_continuousRecognitionController?.add( _continuousRecognitionController?.add(RecognitionEvent.error('重启失败: $e'));
RecognitionEvent.error('重启失败: $e')
);
} }
} }
/// 处理语音识别结果 /// 处理语音识别结果
void _onSpeechResult(SpeechRecognitionResult result) { void _onSpeechResult(SpeechRecognitionResult result) {
// 如果是连续识别,发送到连续识别控制器 // 如果是连续识别,发送到连续识别控制器
if (_continuousRecognitionController != null) { if (_continuousRecognitionController != null) {
if (result.finalResult) { if (result.finalResult) {
Logger.info('收到最终识别结果: "${result.recognizedWords}"'); Logger.info('收到最终识别结果: "${result.recognizedWords}"');
_continuousRecognitionController!.add( _continuousRecognitionController!.add(RecognitionEvent.finalResult(
RecognitionEvent.finalResult( text: result.recognizedWords,
text: result.recognizedWords, detectedLanguage: currentLocale,
detectedLanguage: currentLocale, ));
)
);
} else { } else {
_continuousRecognitionController!.add( _continuousRecognitionController!.add(RecognitionEvent(
RecognitionEvent( type: RecognitionEventType.intermediateResult,
type: RecognitionEventType.intermediateResult, text: result.recognizedWords,
text: result.recognizedWords, detectedLanguage: currentLocale,
detectedLanguage: currentLocale, ));
)
);
} }
} }
} }
/// 设置当前语言 /// 设置当前语言
void setLocale(String localeId) { void setLocale(String localeId) {
if (_supportedLanguages.contains(localeId)) { if (_supportedLanguages.contains(localeId)) {
@ -291,7 +289,7 @@ class FlutterAsrService extends GetxService implements AsrService {
Logger.error('不支持的语言: $localeId'); Logger.error('不支持的语言: $localeId');
} }
} }
@override @override
Future<bool> initialize({ Future<bool> initialize({
List<String>? supportedLanguages, List<String>? supportedLanguages,
@ -301,7 +299,7 @@ class FlutterAsrService extends GetxService implements AsrService {
_supportedLanguages.clear(); _supportedLanguages.clear();
_supportedLanguages.addAll(supportedLanguages); _supportedLanguages.addAll(supportedLanguages);
} }
// 初始化语音识别 // 初始化语音识别
await _initSpeech(); await _initSpeech();
return _isAvailable.value; return _isAvailable.value;
@ -310,24 +308,24 @@ class FlutterAsrService extends GetxService implements AsrService {
return false; return false;
} }
} }
@override @override
Future<RecognitionEvent> recognizeOnce() async { Future<RecognitionEvent> recognizeOnce() async {
try { try {
if (!_isAvailable.value) { if (!_isAvailable.value) {
return RecognitionEvent.error('语音识别不可用'); return RecognitionEvent.error('语音识别不可用');
} }
if (_isListening) { if (_isListening) {
await stopContinuousRecognition(); await stopContinuousRecognition();
} }
// 创建Completer来等待最终结果 // 创建Completer来等待最终结果
final completer = Completer<RecognitionEvent>(); final completer = Completer<RecognitionEvent>();
// 开始监听 // 开始监听
_asrState.value = AsrState.listening; _asrState.value = AsrState.listening;
// 定义结果处理函数 // 定义结果处理函数
void resultListener(SpeechRecognitionResult result) { void resultListener(SpeechRecognitionResult result) {
if (result.finalResult) { if (result.finalResult) {
@ -338,21 +336,21 @@ class FlutterAsrService extends GetxService implements AsrService {
)); ));
} }
} }
// 使用统一方法启动识别,单次识别使用更短的时间参数 // 使用统一方法启动识别,单次识别使用更短的时间参数
final success = await _startListening( final success = await _startListening(
onResult: resultListener, onResult: resultListener,
listenFor: const Duration(seconds: 15), listenFor: const Duration(seconds: 15),
pauseFor: const Duration(seconds: 2), pauseFor: const Duration(seconds: 2),
); );
if (!success) { if (!success) {
return RecognitionEvent.error('启动语音识别失败'); return RecognitionEvent.error('启动语音识别失败');
} }
// 设置较短的超时时间 // 设置较短的超时时间
return await completer.future.timeout( return await completer.future.timeout(
const Duration(seconds: 20), // 略长于listenFor以确保获取到结果 const Duration(seconds: 20), // 略长于listenFor以确保获取到结果
onTimeout: () { onTimeout: () {
_speech.stop(); _speech.stop();
return RecognitionEvent.error('语音识别超时'); return RecognitionEvent.error('语音识别超时');
@ -363,7 +361,7 @@ class FlutterAsrService extends GetxService implements AsrService {
return RecognitionEvent.error('$e'); return RecognitionEvent.error('$e');
} }
} }
@override @override
Future<Stream<RecognitionEvent>> startContinuousRecognition() async { Future<Stream<RecognitionEvent>> startContinuousRecognition() async {
try { try {
@ -373,43 +371,42 @@ class FlutterAsrService extends GetxService implements AsrService {
controller.close(); controller.close();
return controller.stream; return controller.stream;
} }
// 如果已经在监听,先停止 // 如果已经在监听,先停止
if (_isListening) { if (_isListening) {
await stopContinuousRecognition(); await stopContinuousRecognition();
} }
// 创建新的流控制器 // 创建新的流控制器
_continuousRecognitionController = StreamController<RecognitionEvent>.broadcast(); _continuousRecognitionController =
StreamController<RecognitionEvent>.broadcast();
// 开始会话 // 开始会话
_continuousRecognitionController!.add( _continuousRecognitionController!
RecognitionEvent(type: RecognitionEventType.sessionStarted) .add(RecognitionEvent(type: RecognitionEventType.sessionStarted));
);
// 开始连续监听 // 开始连续监听
_asrState.value = AsrState.listening; _asrState.value = AsrState.listening;
// 使用统一方法启动识别,连续识别使用较长的参数 // 使用统一方法启动识别,连续识别使用较长的参数
final success = await _startListening( final success = await _startListening(
onResult: _onSpeechResult, onResult: _onSpeechResult,
); );
if (!success) { if (!success) {
// 添加错误事件,但保留流 // 添加错误事件,但保留流
_continuousRecognitionController!.add( _continuousRecognitionController!
RecognitionEvent.error('启动连续语音识别失败') .add(RecognitionEvent.error('启动连续语音识别失败'));
);
// 返回已有的流控制器 // 返回已有的流控制器
return _continuousRecognitionController!.stream; return _continuousRecognitionController!.stream;
} }
_isListening = true; _isListening = true;
return _continuousRecognitionController!.stream; return _continuousRecognitionController!.stream;
} catch (e) { } catch (e) {
Logger.error('启动连续语音识别失败: $e'); Logger.error('启动连续语音识别失败: $e');
// 如果已经创建了控制器,使用它 // 如果已经创建了控制器,使用它
if (_continuousRecognitionController != null) { if (_continuousRecognitionController != null) {
_continuousRecognitionController!.add(RecognitionEvent.error('$e')); _continuousRecognitionController!.add(RecognitionEvent.error('$e'));
@ -422,7 +419,7 @@ class FlutterAsrService extends GetxService implements AsrService {
} }
} }
} }
@override @override
Future<bool> stopContinuousRecognition() async { Future<bool> stopContinuousRecognition() async {
try { try {
@ -430,36 +427,35 @@ class FlutterAsrService extends GetxService implements AsrService {
if (_speech.isListening) { if (_speech.isListening) {
await _speech.stop(); await _speech.stop();
} }
_isListening = false; _isListening = false;
// 发送会话结束事件 // 发送会话结束事件
_continuousRecognitionController?.add( _continuousRecognitionController
RecognitionEvent(type: RecognitionEventType.sessionStopped) ?.add(RecognitionEvent(type: RecognitionEventType.sessionStopped));
);
// 关闭流控制器 // 关闭流控制器
await _continuousRecognitionController?.close(); await _continuousRecognitionController?.close();
_continuousRecognitionController = null; _continuousRecognitionController = null;
_asrState.value = AsrState.done; _asrState.value = AsrState.done;
return true; return true;
} catch (e) { } catch (e) {
Logger.error('停止连续语音识别失败: $e'); Logger.error('停止连续语音识别失败: $e');
// 强制重置状态 // 强制重置状态
_isListening = false; _isListening = false;
_asrState.value = AsrState.error; _asrState.value = AsrState.error;
return false; return false;
} }
} }
@override @override
bool isContinuousRecognitionActive() { bool isContinuousRecognitionActive() {
return _isListening; return _isListening;
} }
@override @override
Future<void> dispose() async { Future<void> dispose() async {
try { try {
@ -467,15 +463,18 @@ class FlutterAsrService extends GetxService implements AsrService {
if (_speech.isListening) { if (_speech.isListening) {
await _speech.stop(); await _speech.stop();
} }
// 关闭控制器 // 关闭控制器
await _continuousRecognitionController?.close(); await _continuousRecognitionController?.close();
_continuousRecognitionController = null; _continuousRecognitionController = null;
_isListening = false; _isListening = false;
_asrState.value = AsrState.notInitialized; _asrState.value = AsrState.notInitialized;
} catch (e) { } catch (e) {
Logger.error('释放语音识别资源失败: $e'); Logger.error('释放语音识别资源失败: $e');
} }
} }
}
@override
Future<void> pushAudioData(Uint8List data) async {}
}

5
lib/data/services/speech_impl/volcano_asr_api_service.dart

@ -809,4 +809,9 @@ class VolcanoAsrApiService implements AsrService {
_isContinuousRecognitionActive = false; _isContinuousRecognitionActive = false;
_isRecognitionEndRequested = false; _isRecognitionEndRequested = false;
} }
@override
Future<void> pushAudioData(Uint8List data) {
throw UnimplementedError();
}
} }

6
lib/data/services/speech_impl/xunfei_asr_service.dart

@ -269,4 +269,10 @@ class XunfeiAsrService extends GetxService implements AsrService {
_isInitialized = false; _isInitialized = false;
} }
} }
@override
Future<void> pushAudioData(Uint8List data) {
// TODO: implement pushAudioData
throw UnimplementedError();
}
} }

208
lib/data/services/spotify_service.dart

@ -0,0 +1,208 @@
import 'dart:async';
import 'dart:io';
import 'dart:convert';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
import 'package:spotify_sdk/spotify_sdk.dart';
import 'package:get/get.dart';
import 'ai_service.dart';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
/// OpenAI服务适配器 - 连接AiService接口与OpenAIService插件
class SpotifService {
bool isInitialized = false;
final GetStorage _storage = GetStorage();
late String SPOTIFY_CLIENT_ID;
late String SPOTIFY_REDIRECT_URL;
/// 初始化OpenAI服务
Future<bool> initialize() async {
if (!isInitialized) {
final _env = _storage.read("ENV") as Map<String, String>;
SPOTIFY_CLIENT_ID = _env['SPOTIFY_CLIENT_ID'] ?? '';
SPOTIFY_REDIRECT_URL = _env['SPOTIFY_REDIRECT_URL'] ?? '';
printInfo(
info:
"SpotifService----------- initialize SPOTIFY_CLIENT_ID:${SPOTIFY_CLIENT_ID} SPOTIFY_REDIRECT_URL:${SPOTIFY_REDIRECT_URL}");
await getAccessToken();
await connectToSpotifyRemote();
isInitialized = true;
}
return true;
}
/// 获取Spotify SDK的版本信息
Future<void> connectToSpotifyRemote() async {
try {
var result = await SpotifySdk.connectToSpotifyRemote(
clientId: SPOTIFY_CLIENT_ID,
redirectUrl: SPOTIFY_REDIRECT_URL,
scope:
'user-read-private user-read-email streaming app-remote-control user-modify-playback-state',
);
printInfo(
info: "SpotifService----------- connectToSpotifyRemote $result");
} on PlatformException catch (e) {
printError(info: "SpotifService------------ PlatformException$e");
} on MissingPluginException {
printError(info: "SpotifService------------ MissingPluginException");
}
}
/// 获取Spotify的访问令牌
Future<String> getAccessToken() async {
try {
var authenticationToken = await SpotifySdk.getAccessToken(
clientId: SPOTIFY_CLIENT_ID,
redirectUrl: SPOTIFY_REDIRECT_URL,
scope: 'app-remote-control, '
'user-modify-playback-state, '
'playlist-read-private, '
'playlist-modify-public,user-read-currently-playing',
);
printInfo(
info: "SpotifService----------- getAccessToken $authenticationToken");
return authenticationToken;
} on PlatformException catch (e) {
printError(
info:
"SpotifService------------getAccessToken ${e.code}: ${e.message}");
return Future.error('$e.code: $e.message');
} on MissingPluginException {
printError(
info: "SpotifService------------getAccessToken not implemented");
return Future.error('not implemented');
}
}
//获取当前播放状态
Future<bool> isPaused() async {
try {
var state = await SpotifySdk.getPlayerState();
if (state != null) {
printInfo(info: "SpotifService----------- isPaused $state");
return state.isPaused;
} else {
printError(info: "SpotifService----------- isPaused null");
return true;
}
} on PlatformException catch (e) {
printError(
info:
"SpotifService------------getPlayerState ${e.code}: ${e.message}");
} on MissingPluginException {
printError(
info: "SpotifService------------getPlayerState not implemented");
}
return false;
}
//播放 音乐
Future<void> play(String url) async {
try {
await SpotifySdk.play(spotifyUri: url);
printInfo(info: "SpotifService----------- play $url");
} on PlatformException catch (e) {
printError(info: "SpotifService------------play ${e.code}: ${e.message}");
} on MissingPluginException {
printError(
info: "SpotifService------------getAccessToken not implemented");
}
}
//暂停 音乐
Future<void> pause() async {
try {
await SpotifySdk.pause();
printInfo(info: "SpotifService----------- pause");
} on PlatformException catch (e) {
printError(
info: "SpotifService------------pause ${e.code}: ${e.message}");
} on MissingPluginException {
printError(
info: "SpotifService------------getAccessToken not implemented");
}
}
//停止 音乐
Future<void> resume() async {
try {
await SpotifySdk.resume();
printInfo(info: "SpotifService----------- resume");
} on PlatformException catch (e) {
printError(
info: "SpotifService------------pause ${e.code}: ${e.message}");
} on MissingPluginException {
printError(
info: "SpotifService------------getAccessToken not implemented");
}
}
//下一首 音乐
Future<void> skipNext() async {
try {
await SpotifySdk.skipNext();
} on PlatformException catch (e) {
printError(
info: "SpotifService------------pause ${e.code}: ${e.message}");
} on MissingPluginException {
printError(
info: "SpotifService------------getAccessToken not implemented");
}
}
//跳过上一首
Future<void> skipPrevious() async {
try {
await SpotifySdk.skipPrevious();
} on PlatformException catch (e) {
printError(
info: "SpotifService------------pause ${e.code}: ${e.message}");
} on MissingPluginException {
printError(
info: "SpotifService------------getAccessToken not implemented");
}
}
//获取当前播放状态
Future<void> seekTo() async {
try {
await SpotifySdk.seekTo(positionedMilliseconds: 20000);
} on PlatformException catch (e) {
printError(
info: "SpotifService------------pause ${e.code}: ${e.message}");
} on MissingPluginException {
printError(
info: "SpotifService------------getAccessToken not implemented");
}
}
//获取当前播放状态
Future<void> seekToRelative() async {
try {
await SpotifySdk.seekToRelativePosition(relativeMilliseconds: 20000);
} on PlatformException catch (e) {
printError(
info: "SpotifService------------pause ${e.code}: ${e.message}");
} on MissingPluginException {
printError(
info: "SpotifService------------getAccessToken not implemented");
}
}
//获取当前播放状态
Future<void> switchToLocalDevice() async {
try {
await SpotifySdk.switchToLocalDevice();
} on PlatformException catch (e) {
printError(
info: "SpotifService------------pause ${e.code}: ${e.message}");
} on MissingPluginException {
printError(
info: "SpotifService------------getAccessToken not implemented");
}
}
}

184
lib/data/services/voice_interaction_service.dart

@ -1,8 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:deep_voice/data/models/appconfig_model.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:get_storage/get_storage.dart';
import '../models/events/voice_interaction_event.dart'; import '../models/events/voice_interaction_event.dart';
import '../../core/utils/logger.dart'; import '../../core/utils/logger.dart';
import '../../modules/chat/models/message_model.dart'; import '../../modules/chat/models/message_model.dart';
@ -14,29 +16,33 @@ import 'chat_history_service.dart';
/// 管理与平台原生语音交互服务的通信,提供统一的接口供应用使用 /// 管理与平台原生语音交互服务的通信,提供统一的接口供应用使用
class VoiceInteractionService extends GetxService { class VoiceInteractionService extends GetxService {
static VoiceInteractionService get to => Get.find<VoiceInteractionService>(); static VoiceInteractionService get to => Get.find<VoiceInteractionService>();
final GetStorage _storage = GetStorage();
// 方法通道 // 方法通道
static const MethodChannel _channel = MethodChannel('com.deep_voice.voice_interaction'); static const MethodChannel _channel =
MethodChannel('com.deep_voice.voice_interaction');
// 事件通道 // 事件通道
static const EventChannel _eventChannel = EventChannel('com.deep_voice.voice_interaction_events'); static const EventChannel _eventChannel =
EventChannel('com.deep_voice.voice_interaction_events');
// 服务状态 // 服务状态
final _isServiceRunning = false.obs; final _isServiceRunning = false.obs;
bool get isServiceRunning => _isServiceRunning.value; bool get isServiceRunning => _isServiceRunning.value;
// 流控制器 // 流控制器
final _eventStreamController = StreamController<VoiceInteractionEvent>.broadcast(); final _eventStreamController =
StreamController<VoiceInteractionEvent>.broadcast();
// 事件流 // 事件流
Stream<VoiceInteractionEvent> get eventStream => _eventStreamController.stream; Stream<VoiceInteractionEvent> get eventStream =>
_eventStreamController.stream;
// 事件通道订阅 // 事件通道订阅
StreamSubscription? _eventSubscription; StreamSubscription? _eventSubscription;
// 标记是否初始化 // 标记是否初始化
bool _isInitialized = false; bool _isInitialized = false;
// 配置信息 // 配置信息
late String _azureSpeechKey; late String _azureSpeechKey;
late String _azureSpeechRegion; late String _azureSpeechRegion;
@ -47,61 +53,73 @@ class VoiceInteractionService extends GetxService {
late String _volcanoSpeechAppToken; late String _volcanoSpeechAppToken;
// MCP配置信息 // MCP配置信息
String? _mcpConfig; String? _mcpConfig;
// 聊天历史服务 // 聊天历史服务
late final ChatHistoryService _chatHistoryService; late final ChatHistoryService _chatHistoryService;
/// 设置事件通道 /// 设置事件通道
void _setupEventChannel() { void _setupEventChannel() {
_eventSubscription = _eventChannel _eventSubscription = _eventChannel
.receiveBroadcastStream() .receiveBroadcastStream()
.listen(_handleVoiceInteractionEvent, onError: (error) { .listen(_handleVoiceInteractionEvent, onError: (error) {
Logger.error('语音交互事件通道错误: $error'); Logger.error('语音交互事件通道错误: $error');
}); });
} }
/// 从环境变量加载配置 /// 从环境变量加载配置
void _loadConfig() { void _loadConfig() {
_azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? ''; // _azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? '';
_azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? ''; // _azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? '';
_openaiApiKey = dotenv.env['OPENAI_API_KEY'] ?? ''; // _openaiApiKey = dotenv.env['OPENAI_API_KEY'] ?? '';
_openaiBaseUrl = dotenv.env['OPENAI_BASE_URL'] ?? ''; // _openaiBaseUrl = dotenv.env['OPENAI_BASE_URL'] ?? '';
_openaiModel = dotenv.env['OPENAI_MODEL'] ?? ''; // _openaiModel = dotenv.env['OPENAI_MODEL'] ?? '';
_volcanoSpeechAppId = dotenv.env['VOLCANO_SPEECH_APP_ID'] ?? ''; // _volcanoSpeechAppId = dotenv.env['VOLCANO_SPEECH_APP_ID'] ?? '';
_volcanoSpeechAppToken = dotenv.env['VOLCANO_SPEECH_APP_TOKEN'] ?? ''; // _volcanoSpeechAppToken = dotenv.env['VOLCANO_SPEECH_APP_TOKEN'] ?? '';
final _env = _storage.read("ENV") as Map<String, String>;
_azureSpeechKey = _env['AZURE_SPEECH_KEY'] ?? '';
_azureSpeechRegion = _env['AZURE_SPEECH_REGION'] ?? '';
_openaiApiKey = _env['OPENAI_API_KEY'] ?? '';
_openaiBaseUrl = _env['OPENAI_BASE_URL'] ?? '';
_openaiModel = _env['OPENAI_MODEL'] ?? '';
_volcanoSpeechAppId = _env['VOLCANO_SPEECH_APP_ID'] ?? '';
_volcanoSpeechAppToken = _env['VOLCANO_SPEECH_APP_TOKEN'] ?? '';
if (_azureSpeechKey.isEmpty || _azureSpeechRegion.isEmpty) { if (_azureSpeechKey.isEmpty || _azureSpeechRegion.isEmpty) {
Logger.warning('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION'); Logger.warning(
'未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION');
} }
// 加载MCP配置 // 加载MCP配置
_loadMcpConfig(); _loadMcpConfig();
} }
/// 加载MCP配置 /// 加载MCP配置
void _loadMcpConfig() { void _loadMcpConfig() {
try { try {
rootBundle.loadString('.mcp.json').then((config) { final mcps = _storage.read("MCPS") as Map<String, DBMCPServer>;
_mcpConfig = config; String config = jsonEncode(mcps);
Logger.info('成功加载MCP配置, 配置内容: $_mcpConfig'); _mcpConfig = config;
}); Logger.info('成功加载MCP配置, 配置内容: $_mcpConfig');
// rootBundle.loadString('.mcp.json').then((config) {
// _mcpConfig = config;
// Logger.info('成功加载MCP配置, 配置内容: $_mcpConfig');
// });
} catch (e) { } catch (e) {
Logger.warning('加载MCP配置失败: $e'); Logger.warning('加载MCP配置失败: $e');
_mcpConfig = null; _mcpConfig = null;
} }
} }
/// 处理来自原生层的事件 /// 处理来自原生层的事件
void _handleVoiceInteractionEvent(dynamic event) { void _handleVoiceInteractionEvent(dynamic event) {
if (event is! Map) return; if (event is! Map) return;
final eventMap = event as Map<dynamic, dynamic>; final eventMap = event as Map<dynamic, dynamic>;
final String eventType = eventMap['type'] as String? ?? ''; final String eventType = eventMap['type'] as String? ?? '';
// final int timestamp = eventMap['timestamp'] as int? ?? DateTime.now().millisecondsSinceEpoch; // final int timestamp = eventMap['timestamp'] as int? ?? DateTime.now().millisecondsSinceEpoch;
Logger.info('收到语音交互事件: $eventType'); Logger.info('收到语音交互事件: $eventType');
switch (eventType) { switch (eventType) {
case 'recognition_started': case 'recognition_started':
// 语音识别开始事件 // 语音识别开始事件
@ -110,13 +128,14 @@ class VoiceInteractionService extends GetxService {
); );
_eventStreamController.add(recognitionEvent); _eventStreamController.add(recognitionEvent);
break; break;
case 'chat_history_updated': case 'chat_history_updated':
// 聊天历史更新事件 // 聊天历史更新事件
final String agentId = eventMap['agentId'] as String? ?? ''; final String agentId = eventMap['agentId'] as String? ?? '';
final String userMessage = eventMap['userMessage'] as String? ?? ''; final String userMessage = eventMap['userMessage'] as String? ?? '';
final String assistantMessage = eventMap['assistantMessage'] as String? ?? ''; final String assistantMessage =
eventMap['assistantMessage'] as String? ?? '';
final chatHistoryEvent = ChatHistoryEvent( final chatHistoryEvent = ChatHistoryEvent(
agentId: agentId, agentId: agentId,
userMessage: userMessage, userMessage: userMessage,
@ -124,72 +143,74 @@ class VoiceInteractionService extends GetxService {
timestamp: DateTime.now().millisecondsSinceEpoch, timestamp: DateTime.now().millisecondsSinceEpoch,
); );
_eventStreamController.add(chatHistoryEvent); _eventStreamController.add(chatHistoryEvent);
// 保存聊天历史到ChatHistoryService // 保存聊天历史到ChatHistoryService
_saveChatHistory(agentId, userMessage, assistantMessage); _saveChatHistory(agentId, userMessage, assistantMessage);
break; break;
case 'enter_translation_mode': case 'enter_translation_mode':
// 进入翻译模式事件 // 进入翻译模式事件
Logger.info('收到进入翻译模式事件,正在导航到翻译界面'); Logger.info('收到进入翻译模式事件,正在导航到翻译界面');
_navigateToTranslation(); _navigateToTranslation();
break; break;
} }
} }
/// 导航到翻译界面 /// 导航到翻译界面
void _navigateToTranslation() { void _navigateToTranslation() {
try { try {
// 使用GetX导航到翻译页面 // 使用GetX导航到翻译页面
Get.offNamedUntil(Routes.translation, (route) => route.settings.name == Routes.home); Get.offNamedUntil(
Routes.translation, (route) => route.settings.name == Routes.home);
Logger.info('已导航到翻译界面'); Logger.info('已导航到翻译界面');
} catch (e) { } catch (e) {
Logger.error('导航到翻译界面失败: $e'); Logger.error('导航到翻译界面失败: $e');
} }
} }
/// 保存聊天历史 /// 保存聊天历史
Future<void> _saveChatHistory(String agentId, String userMessage, String assistantMessage) async { Future<void> _saveChatHistory(
String agentId, String userMessage, String assistantMessage) async {
try { try {
final timestamp = DateTime.now().millisecondsSinceEpoch; final timestamp = DateTime.now().millisecondsSinceEpoch;
// 创建用户消息和助手消息 // 创建用户消息和助手消息
final userMsg = Message( final userMsg = Message(
role: 'user', role: 'user',
content: userMessage, content: userMessage,
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp), timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp),
); );
final assistantMsg = Message( final assistantMsg = Message(
role: 'assistant', role: 'assistant',
content: assistantMessage, content: assistantMessage,
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp + 1), // 确保助手消息时间戳晚于用户消息 timestamp: DateTime.fromMillisecondsSinceEpoch(
timestamp + 1), // 确保助手消息时间戳晚于用户消息
); );
// 加载现有历史记录 // 加载现有历史记录
final existingMessages = await _chatHistoryService.loadHistory(agentId); final existingMessages = await _chatHistoryService.loadHistory(agentId);
// 添加新消息 // 添加新消息
existingMessages.addAll([userMsg, assistantMsg]); existingMessages.addAll([userMsg, assistantMsg]);
// 保存更新后的历史记录 // 保存更新后的历史记录
await _chatHistoryService.saveHistory(agentId, existingMessages); await _chatHistoryService.saveHistory(agentId, existingMessages);
Logger.info('已保存聊天历史: agentId=$agentId'); Logger.info('已保存聊天历史: agentId=$agentId');
} catch (e) { } catch (e) {
Logger.error('保存聊天历史失败: $e'); Logger.error('保存聊天历史失败: $e');
} }
} }
/// 初始化服务 /// 初始化服务
Future<bool> initialize() async { Future<bool> initialize() async {
if (_isInitialized) return true; if (_isInitialized) return true;
try { try {
Logger.info('正在初始化语音交互服务...'); Logger.info('正在初始化语音交互服务...');
// 获取聊天历史服务 // 获取聊天历史服务
try { try {
_chatHistoryService = Get.find<ChatHistoryService>(); _chatHistoryService = Get.find<ChatHistoryService>();
@ -197,16 +218,16 @@ class VoiceInteractionService extends GetxService {
Logger.warning('获取ChatHistoryService失败,将创建新实例'); Logger.warning('获取ChatHistoryService失败,将创建新实例');
_chatHistoryService = Get.put(ChatHistoryService()); _chatHistoryService = Get.put(ChatHistoryService());
} }
// 设置事件通道 // 设置事件通道
_setupEventChannel(); _setupEventChannel();
// 加载配置 // 加载配置
_loadConfig(); _loadConfig();
// 检查服务是否已在运行 // 检查服务是否已在运行
final bool running = await checkServiceStatus(); final bool running = await checkServiceStatus();
// 如果服务未运行,启动服务 // 如果服务未运行,启动服务
if (!running) { if (!running) {
// 启动语音交互服务 // 启动语音交互服务
@ -216,7 +237,7 @@ class VoiceInteractionService extends GetxService {
return false; return false;
} }
} }
_isInitialized = true; _isInitialized = true;
Logger.info('语音交互服务初始化完成'); Logger.info('语音交互服务初始化完成');
return true; return true;
@ -225,12 +246,12 @@ class VoiceInteractionService extends GetxService {
return false; return false;
} }
} }
/// 启动语音交互服务 /// 启动语音交互服务
Future<bool> startService() async { Future<bool> startService() async {
try { try {
Logger.info('启动语音交互服务...'); Logger.info('启动语音交互服务...');
// 构建参数 // 构建参数
final Map<String, dynamic> params = { final Map<String, dynamic> params = {
'azure_speech_key': _azureSpeechKey, 'azure_speech_key': _azureSpeechKey,
@ -241,53 +262,55 @@ class VoiceInteractionService extends GetxService {
'volcano_speech_app_id': _volcanoSpeechAppId, 'volcano_speech_app_id': _volcanoSpeechAppId,
'volcano_speech_app_token': _volcanoSpeechAppToken, 'volcano_speech_app_token': _volcanoSpeechAppToken,
}; };
// 如果有MCP配置,添加到参数中 // 如果有MCP配置,添加到参数中
if (_mcpConfig != null) { if (_mcpConfig != null) {
params['mcp_config'] = _mcpConfig; params['mcp_config'] = _mcpConfig;
} }
final result = await _channel.invokeMethod<bool>('startService', params) ?? false; final result =
await _channel.invokeMethod<bool>('startService', params) ?? false;
if (result) { if (result) {
_isServiceRunning.value = true; _isServiceRunning.value = true;
Logger.info('语音交互服务已启动'); Logger.info('语音交互服务已启动');
} else { } else {
Logger.error('启动语音交互服务失败'); Logger.error('启动语音交互服务失败');
} }
return result; return result;
} catch (e) { } catch (e) {
Logger.error('启动语音交互服务时发生错误: $e'); Logger.error('启动语音交互服务时发生错误: $e');
return false; return false;
} }
} }
/// 停止语音交互服务 /// 停止语音交互服务
Future<bool> stopService() async { Future<bool> stopService() async {
try { try {
Logger.info('停止语音交互服务...'); Logger.info('停止语音交互服务...');
final result = await _channel.invokeMethod<bool>('stopService') ?? false; final result = await _channel.invokeMethod<bool>('stopService') ?? false;
if (result) { if (result) {
_isServiceRunning.value = false; _isServiceRunning.value = false;
Logger.info('语音交互服务已停止'); Logger.info('语音交互服务已停止');
} else { } else {
Logger.error('停止语音交互服务失败'); Logger.error('停止语音交互服务失败');
} }
return result; return result;
} catch (e) { } catch (e) {
Logger.error('停止语音交互服务时发生错误: $e'); Logger.error('停止语音交互服务时发生错误: $e');
return false; return false;
} }
} }
/// 检查服务是否运行 /// 检查服务是否运行
Future<bool> checkServiceStatus() async { Future<bool> checkServiceStatus() async {
try { try {
final bool result = await _channel.invokeMethod<bool>('isServiceRunning') ?? false; final bool result =
await _channel.invokeMethod<bool>('isServiceRunning') ?? false;
_isServiceRunning.value = result; _isServiceRunning.value = result;
return result; return result;
} catch (e) { } catch (e) {
@ -295,27 +318,28 @@ class VoiceInteractionService extends GetxService {
return false; return false;
} }
} }
/// 暂停语音交互 /// 暂停语音交互
Future<bool> pauseVoiceInteraction() async { Future<bool> pauseVoiceInteraction() async {
try { try {
Logger.info('暂停后台语音交互...'); Logger.info('暂停后台语音交互...');
final result = await _channel.invokeMethod<bool>('pauseVoiceInteraction') ?? false; final result =
await _channel.invokeMethod<bool>('pauseVoiceInteraction') ?? false;
if (result) { if (result) {
Logger.info('语音交互已暂停'); Logger.info('语音交互已暂停');
} else { } else {
Logger.error('暂停语音交互失败'); Logger.error('暂停语音交互失败');
} }
return result; return result;
} catch (e) { } catch (e) {
Logger.error('暂停语音交互时发生错误: $e'); Logger.error('暂停语音交互时发生错误: $e');
return false; return false;
} }
} }
/// 资源释放 /// 资源释放
@override @override
void onClose() { void onClose() {
@ -323,4 +347,4 @@ class VoiceInteractionService extends GetxService {
_eventStreamController.close(); _eventStreamController.close();
super.onClose(); super.onClose();
} }
} }

133
lib/data/services/volcano_translation_service.dart

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
@ -10,7 +11,7 @@ import 'language_manager.dart';
class VolcanoTranslationService extends GetxService { class VolcanoTranslationService extends GetxService {
// API 配置 // API 配置
late String _baseUrl; late String _baseUrl;
final String _endpoint = '/'; // 修改为根路径 final String _endpoint = '/'; // 修改为根路径
late final String _accessKey; late final String _accessKey;
late final String _secretKey; late final String _secretKey;
late final String _region; late final String _region;
@ -21,7 +22,7 @@ class VolcanoTranslationService extends GetxService {
// 语言管理器 // 语言管理器
final LanguageManager _languageManager = Get.find<LanguageManager>(); final LanguageManager _languageManager = Get.find<LanguageManager>();
final GetStorage _storage = GetStorage();
// 构造函数 // 构造函数
VolcanoTranslationService(); VolcanoTranslationService();
@ -32,9 +33,10 @@ class VolcanoTranslationService extends GetxService {
String? region, String? region,
}) async { }) async {
try { try {
_accessKey = accessKey ?? dotenv.env['VOLCANO_TRANSLATION_ACCESS_KEY'] ?? ''; final _env = _storage.read("ENV") as Map<String, String>;
_secretKey = secretKey ?? dotenv.env['VOLCANO_TRANSLATION_SECRET_KEY'] ?? ''; _accessKey = accessKey ?? _env['VOLCANO_TRANSLATION_ACCESS_KEY'] ?? '';
_region = region ?? dotenv.env['VOLCANO_TRANSLATION_REGION'] ?? 'cn-north-1'; _secretKey = secretKey ?? _env['VOLCANO_TRANSLATION_SECRET_KEY'] ?? '';
_region = region ?? _env['VOLCANO_TRANSLATION_REGION'] ?? 'cn-north-1';
_service = 'translate'; _service = 'translate';
_baseUrl = 'https://translate.volcengineapi.com'; _baseUrl = 'https://translate.volcengineapi.com';
@ -50,14 +52,21 @@ class VolcanoTranslationService extends GetxService {
} }
// 生成签名 // 生成签名
Map<String, String> _generateSignature(String method, Map<String, dynamic> requestBody, Map<String, String> queryParams) { Map<String, String> _generateSignature(String method,
Map<String, dynamic> requestBody, Map<String, String> queryParams) {
// 1. 准备时间相关参数 // 1. 准备时间相关参数
final now = DateTime.now().toUtc(); final now = DateTime.now().toUtc();
final date = now.toIso8601String().split('T')[0].replaceAll('-', ''); final date = now.toIso8601String().split('T')[0].replaceAll('-', '');
final timestamp = now.toIso8601String().replaceAll(':', '').replaceAll('-', '').split('.')[0] + 'Z'; final timestamp = now
.toIso8601String()
.replaceAll(':', '')
.replaceAll('-', '')
.split('.')[0] +
'Z';
// 2. 构建规范查询字符串 - 按照参数名ASCII排序 // 2. 构建规范查询字符串 - 按照参数名ASCII排序
final sortedParams = Map.fromEntries(queryParams.entries.toList()..sort((a, b) => a.key.compareTo(b.key))); final sortedParams = Map.fromEntries(
queryParams.entries.toList()..sort((a, b) => a.key.compareTo(b.key)));
final List<String> queryParts = []; final List<String> queryParts = [];
sortedParams.forEach((key, value) { sortedParams.forEach((key, value) {
// 使用RFC 3986编码,确保特殊字符被正确处理 // 使用RFC 3986编码,确保特殊字符被正确处理
@ -66,48 +75,53 @@ class VolcanoTranslationService extends GetxService {
queryParts.add('$encodedKey=$encodedValue'); queryParts.add('$encodedKey=$encodedValue');
}); });
final canonicalQueryString = queryParts.join('&'); final canonicalQueryString = queryParts.join('&');
// 3. 创建规范请求 // 3. 创建规范请求
final contentType = 'application/json'; final contentType = 'application/json';
final payloadHash = sha256.convert(utf8.encode(json.encode(requestBody))).toString(); final payloadHash =
sha256.convert(utf8.encode(json.encode(requestBody))).toString();
final host = 'translate.volcengineapi.com'; final host = 'translate.volcengineapi.com';
// 确保头部按照字典序排序,且键名小写 // 确保头部按照字典序排序,且键名小写
// 注意:只包含host和x-date头部,与示例保持一致 // 注意:只包含host和x-date头部,与示例保持一致
final canonicalHeaders = 'host:$host\nx-date:$timestamp\n'; final canonicalHeaders = 'host:$host\nx-date:$timestamp\n';
final signedHeaders = 'host;x-date'; final signedHeaders = 'host;x-date';
// 构建规范请求,格式必须与示例完全一致 // 构建规范请求,格式必须与示例完全一致
final canonicalRequest = '$method\n$_endpoint\n$canonicalQueryString\n$canonicalHeaders\n$signedHeaders\n$payloadHash'; final canonicalRequest =
'$method\n$_endpoint\n$canonicalQueryString\n$canonicalHeaders\n$signedHeaders\n$payloadHash';
// 4. 创建待签字符串 // 4. 创建待签字符串
final algorithm = 'HMAC-SHA256'; final algorithm = 'HMAC-SHA256';
final credentialScope = '$date/$_region/$_service/request'; final credentialScope = '$date/$_region/$_service/request';
final stringToSign = '$algorithm\n$timestamp\n$credentialScope\n${sha256.convert(utf8.encode(canonicalRequest)).toString()}'; final stringToSign =
'$algorithm\n$timestamp\n$credentialScope\n${sha256.convert(utf8.encode(canonicalRequest)).toString()}';
// 5. 计算签名 - 按照文档正确派生密钥 // 5. 计算签名 - 按照文档正确派生密钥
// 注意:这里是关键修复点,确保密钥派生过程完全符合文档 // 注意:这里是关键修复点,确保密钥派生过程完全符合文档
final kSecret = utf8.encode(_secretKey); // 直接使用原始密钥,不添加前缀 final kSecret = utf8.encode(_secretKey); // 直接使用原始密钥,不添加前缀
// kDate = HMAC(kSecret, Date) // kDate = HMAC(kSecret, Date)
var kDate = Hmac(sha256, kSecret).convert(utf8.encode(date)).bytes; var kDate = Hmac(sha256, kSecret).convert(utf8.encode(date)).bytes;
// kRegion = HMAC(kDate, Region) // kRegion = HMAC(kDate, Region)
var kRegion = Hmac(sha256, kDate).convert(utf8.encode(_region)).bytes; var kRegion = Hmac(sha256, kDate).convert(utf8.encode(_region)).bytes;
// kService = HMAC(kRegion, Service) // kService = HMAC(kRegion, Service)
var kService = Hmac(sha256, kRegion).convert(utf8.encode(_service)).bytes; var kService = Hmac(sha256, kRegion).convert(utf8.encode(_service)).bytes;
// kSigning = HMAC(kService, "request") // kSigning = HMAC(kService, "request")
var kSigning = Hmac(sha256, kService).convert(utf8.encode('request')).bytes; var kSigning = Hmac(sha256, kService).convert(utf8.encode('request')).bytes;
// 使用派生的签名密钥计算最终签名 // 使用派生的签名密钥计算最终签名
final signature = Hmac(sha256, kSigning).convert(utf8.encode(stringToSign)).toString(); final signature =
Hmac(sha256, kSigning).convert(utf8.encode(stringToSign)).toString();
// 6. 添加签名到请求头 - 格式必须与示例完全一致 // 6. 添加签名到请求头 - 格式必须与示例完全一致
final authorization = '$algorithm Credential=$_accessKey/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature'; final authorization =
'$algorithm Credential=$_accessKey/$credentialScope, SignedHeaders=$signedHeaders, Signature=$signature';
// 只返回必要的头部,与示例保持一致 // 只返回必要的头部,与示例保持一致
return { return {
'Content-Type': contentType, 'Content-Type': contentType,
@ -131,27 +145,29 @@ class VolcanoTranslationService extends GetxService {
// 获取源语言和目标语言的短代码 // 获取源语言和目标语言的短代码
String sourceCode = ''; String sourceCode = '';
String targetCode = 'zh'; String targetCode = 'zh';
// 从 ASR 代码获取短代码 // 从 ASR 代码获取短代码
if (sourceLanguageCode.isNotEmpty) { if (sourceLanguageCode.isNotEmpty) {
final sourceShortCode = _languageManager.getShortCodeByAsrCode(sourceLanguageCode); final sourceShortCode =
_languageManager.getShortCodeByAsrCode(sourceLanguageCode);
if (sourceShortCode != null) { if (sourceShortCode != null) {
sourceCode = sourceShortCode; sourceCode = sourceShortCode;
} }
} }
final targetShortCode = _languageManager.getShortCodeByAsrCode(targetLanguageCode); final targetShortCode =
_languageManager.getShortCodeByAsrCode(targetLanguageCode);
if (targetShortCode != null) { if (targetShortCode != null) {
targetCode = targetShortCode; targetCode = targetShortCode;
} }
// 构建请求体 // 构建请求体
final requestBody = { final requestBody = {
'SourceLanguage': sourceCode, 'SourceLanguage': sourceCode,
'TargetLanguage': targetCode, 'TargetLanguage': targetCode,
'TextList': [text], 'TextList': [text],
}; };
// 添加必要的查询参数 - 确保包含所有必需参数 // 添加必要的查询参数 - 确保包含所有必需参数
final queryParams = { final queryParams = {
'Action': 'TranslateText', 'Action': 'TranslateText',
@ -159,60 +175,61 @@ class VolcanoTranslationService extends GetxService {
'Region': _region, 'Region': _region,
'Service': _service, 'Service': _service,
}; };
// 生成包含查询参数的签名 // 生成包含查询参数的签名
final headers = _generateSignature('POST', requestBody, queryParams); final headers = _generateSignature('POST', requestBody, queryParams);
// 构建带有查询参数的URL // 构建带有查询参数的URL
final queryString = Uri(queryParameters: queryParams).query; final queryString = Uri(queryParameters: queryParams).query;
final url = '$_baseUrl/?$queryString'; final url = '$_baseUrl/?$queryString';
final response = await http.post( final response = await http.post(
Uri.parse(url), Uri.parse(url),
headers: headers, headers: headers,
body: json.encode(requestBody), body: json.encode(requestBody),
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
final responseData = json.decode(response.body); final responseData = json.decode(response.body);
// 尝试从不同的响应结构中提取翻译结果 // 尝试从不同的响应结构中提取翻译结果
String? translation; String? translation;
// 1. 标准响应结构 // 1. 标准响应结构
if (responseData['TranslationList'] != null && if (responseData['TranslationList'] != null &&
responseData['TranslationList'] is List && responseData['TranslationList'] is List &&
responseData['TranslationList'].isNotEmpty) { responseData['TranslationList'].isNotEmpty) {
final translationItem = responseData['TranslationList'][0]; final translationItem = responseData['TranslationList'][0];
if (translationItem is Map && translationItem.containsKey('Translation')) { if (translationItem is Map &&
translationItem.containsKey('Translation')) {
translation = translationItem['Translation']; translation = translationItem['Translation'];
} }
} }
// 2. 检查是否有错误响应 // 2. 检查是否有错误响应
if (translation == null && if (translation == null &&
responseData['ResponseMetadata'] != null && responseData['ResponseMetadata'] != null &&
responseData['ResponseMetadata'] is Map && responseData['ResponseMetadata'] is Map &&
responseData['ResponseMetadata']['Error'] != null) { responseData['ResponseMetadata']['Error'] != null) {
return null; return null;
} }
// 3. 尝试其他可能的响应结构 // 3. 尝试其他可能的响应结构
if (translation == null) { if (translation == null) {
if (responseData.containsKey('Translation')) { if (responseData.containsKey('Translation')) {
translation = responseData['Translation']; translation = responseData['Translation'];
} else if (responseData.containsKey('Result') && } else if (responseData.containsKey('Result') &&
responseData['Result'] is Map && responseData['Result'] is Map &&
responseData['Result'].containsKey('Translation')) { responseData['Result'].containsKey('Translation')) {
translation = responseData['Result']['Translation']; translation = responseData['Result']['Translation'];
} else if (responseData.containsKey('Data') && } else if (responseData.containsKey('Data') &&
responseData['Data'] is Map) { responseData['Data'] is Map) {
final data = responseData['Data']; final data = responseData['Data'];
if (data.containsKey('Translation')) { if (data.containsKey('Translation')) {
translation = data['Translation']; translation = data['Translation'];
} else if (data.containsKey('TranslationList') && } else if (data.containsKey('TranslationList') &&
data['TranslationList'] is List && data['TranslationList'] is List &&
data['TranslationList'].isNotEmpty) { data['TranslationList'].isNotEmpty) {
final item = data['TranslationList'][0]; final item = data['TranslationList'][0];
if (item is Map && item.containsKey('Translation')) { if (item is Map && item.containsKey('Translation')) {
translation = item['Translation']; translation = item['Translation'];
@ -220,14 +237,14 @@ class VolcanoTranslationService extends GetxService {
} }
} }
} }
// 返回提取到的翻译结果 // 返回提取到的翻译结果
return translation; return translation;
} }
return null; return null;
} catch (e) { } catch (e) {
return null; return null;
} }
} }
} }

208
lib/modules/chat/controllers/chat_controller.dart

@ -1,9 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:amap_flutter/amap_flutter.dart';
import 'package:deep_voice/data/services/spotify_service.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:haptic_feedback/haptic_feedback.dart';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
import 'package:chat_storage/chat_storage.dart'; import 'package:chat_storage/chat_storage.dart';
import 'package:path/path.dart';
import '../../../data/services/ai_service.dart'; import '../../../data/services/ai_service.dart';
import '../../../data/services/deapsound_ai_service.dart'; import '../../../data/services/deapsound_ai_service.dart';
import '../../../data/services/kouzi_ai_service.dart'; import '../../../data/services/kouzi_ai_service.dart';
@ -25,10 +30,16 @@ class ChatController extends GetxController {
final AsrService _asrService = Get.find<AsrService>(); final AsrService _asrService = Get.find<AsrService>();
final VoiceInteractionService _voiceInteractionService = final VoiceInteractionService _voiceInteractionService =
Get.find<VoiceInteractionService>(); Get.find<VoiceInteractionService>();
late SpotifService _spotifService;
// 添加ScrollController用于控制列表滚动 // 添加ScrollController用于控制列表滚动
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
final _amapFlutter = AmapFlutter();
StreamSubscription? _locationStream;
String _latLng = '';
String _city = '深圳市';
// Agent 信息 // Agent 信息
late final Agent agent; late final Agent agent;
@ -76,10 +87,19 @@ class ChatController extends GetxController {
// 记录最后一次活动时间 // 记录最后一次活动时间
DateTime _lastActivityTime = DateTime.now(); DateTime _lastActivityTime = DateTime.now();
// 音乐播放
final player = AudioPlayer(); final player = AudioPlayer();
final RxString audiourl = ''.obs; final RxString audiourl = ''.obs;
final RxBool isPlay = false.obs; final RxBool isPlay = false.obs;
// 按住说话、文字输入切换
final RxBool isTextInput = false.obs;
final RxBool isHoldSpeak = false.obs;
String finalResult = '';
// 按住说话防抖
Timer? _talkDebounceTimer;
@override @override
Future<void> onInit() async { Future<void> onInit() async {
super.onInit(); super.onInit();
@ -93,8 +113,9 @@ class ChatController extends GetxController {
// 获取Agent // 获取Agent
final Agent? foundAgent = AgentProvider.getAgentById(agentId); final Agent? foundAgent = AgentProvider.getAgentById(agentId);
if (foundAgent == null) { if (foundAgent == null) {
Logger.error('无法找到Agent: $agentId');
Get.back(); Get.back();
Logger.error('无法找到Agent: $agentId');
return; return;
} }
@ -117,7 +138,8 @@ class ChatController extends GetxController {
_aiService = Get.find<OpenAIServiceAdapter>(); _aiService = Get.find<OpenAIServiceAdapter>();
Logger.info('使用OpenAIServiceAdapter'); Logger.info('使用OpenAIServiceAdapter');
} }
Get.put(SpotifService());
_spotifService = Get.find<SpotifService>();
// 使用克隆音色语音合成 // 使用克隆音色语音合成
final GetStorage storage = GetStorage(); final GetStorage storage = GetStorage();
String timbre = storage.read('timbre') ?? '1'; String timbre = storage.read('timbre') ?? '1';
@ -166,6 +188,8 @@ class ChatController extends GetxController {
// 订阅VoiceInteractionService事件 // 订阅VoiceInteractionService事件
_subscribeToVoiceInteractionEvents(); _subscribeToVoiceInteractionEvents();
_getLocation();
player.playerStateStream.listen((state) async { player.playerStateStream.listen((state) async {
if (state.processingState == ProcessingState.completed) { if (state.processingState == ProcessingState.completed) {
await player.seek(Duration.zero); await player.seek(Duration.zero);
@ -180,6 +204,16 @@ class ChatController extends GetxController {
}); });
} }
void _getLocation() async {
await _amapFlutter.initLocation();
_locationStream = _amapFlutter.locationStream().listen((data) async {
_latLng = '${data['longitude']},${data['latitude']}}';
_city = data['city'];
_amapFlutter.stopLocation();
});
_amapFlutter.startLocation();
}
/// 订阅VoiceInteractionService事件 /// 订阅VoiceInteractionService事件
void _subscribeToVoiceInteractionEvents() { void _subscribeToVoiceInteractionEvents() {
if (_voiceInteractionService.eventStream != null) { if (_voiceInteractionService.eventStream != null) {
@ -260,6 +294,10 @@ class ChatController extends GetxController {
_voiceInteractionSubscription?.cancel(); _voiceInteractionSubscription?.cancel();
_autoStopTimer?.cancel(); _autoStopTimer?.cancel();
_scrollDebounceTimer?.cancel(); // 清理防抖计时器 _scrollDebounceTimer?.cancel(); // 清理防抖计时器
_talkDebounceTimer?.cancel();
_amapFlutter.dispose();
_locationStream?.cancel();
// 停止TTS // 停止TTS
_ttsService.stop(); _ttsService.stop();
@ -337,6 +375,7 @@ class ChatController extends GetxController {
timestamp: DateTime.now(), timestamp: DateTime.now(),
); );
messages.add(userMessage); messages.add(userMessage);
_saveMessageToDb(userMessage);
// 清空输入框 // 清空输入框
inputText.value = ''; inputText.value = '';
@ -365,18 +404,23 @@ class ChatController extends GetxController {
messages: messageHistory, messages: messageHistory,
systemPrompt: agent.systemPrompt, systemPrompt: agent.systemPrompt,
); );
Logger.info(
'liwei----------- 发送用户提问 ${text}',
);
// 订阅响应流 // 订阅响应流
_aiResponseSubscription = responseStream.listen( _aiResponseSubscription = responseStream.listen(
(chunk) { (chunk) {
if (_shouldCancelAiResponse) return; if (_shouldCancelAiResponse) return;
Logger.info('chat ${chunk}');
if (chunk is Map) { if (chunk is Map) {
_updateIntermediateMessage(data: chunk); Logger.info(
if (chunk['musicaudio'] != null) { 'liwei-----------收到卡片消息 ${chunk}',
play(chunk['musicaudio']); );
_updateIntermediateMessage(meta: chunk);
if (chunk['card_spotify'] != null) {
play(chunk['card_spotify']);
} }
} else { } else if (chunk is String) {
fullResponse += chunk; fullResponse += chunk;
// 更新中间状态的消息内容 // 更新中间状态的消息内容
@ -384,11 +428,15 @@ class ChatController extends GetxController {
// 如果TTS启用,使用流式处理发送到TTS // 如果TTS启用,使用流式处理发送到TTS
if (isTtsEnabled.value) { if (isTtsEnabled.value) {
_ttsService.speakStream(chunk); String str = chunk.replaceAll('*', '');
_ttsService.speakStream(str);
} }
} }
}, },
onDone: () { onDone: () {
Logger.info(
'liwei-----------结束当前问题 ${text}',
);
if (_shouldCancelAiResponse) return; if (_shouldCancelAiResponse) return;
// 完成响应,将isIntermediate设为false // 完成响应,将isIntermediate设为false
@ -421,6 +469,10 @@ class ChatController extends GetxController {
final recentMessages = messages.where((m) => !m.isIntermediate).toList(); final recentMessages = messages.where((m) => !m.isIntermediate).toList();
final startIndex = final startIndex =
recentMessages.length > 10 ? recentMessages.length - 10 : 0; recentMessages.length > 10 ? recentMessages.length - 10 : 0;
messageHistory.add({
'role': 'user',
'content': '我当前所在城市是$_city,我的经纬度是$_latLng',
});
for (int i = startIndex; i < recentMessages.length; i++) { for (int i = startIndex; i < recentMessages.length; i++) {
final m = recentMessages[i]; final m = recentMessages[i];
@ -434,14 +486,14 @@ class ChatController extends GetxController {
} }
// 更新中间状态的消息 // 更新中间状态的消息
void _updateIntermediateMessage({Map? data, String? content}) { void _updateIntermediateMessage({Map? meta, String? content}) {
final index = final index =
messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant'); messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
if (index != -1) { if (index != -1) {
if (data != null) { if (meta != null) {
Map messagesData = messages[index].data ?? {}; Map messagesData = messages[index].meta ?? {};
messages[index] = messages[index].copyWith( messages[index] = messages[index].copyWith(
data: {...messagesData, ...data}, meta: {...messagesData, ...meta},
); );
} }
messages[index] = messages[index].copyWith( messages[index] = messages[index].copyWith(
@ -453,18 +505,12 @@ class ChatController extends GetxController {
} }
// 播放音乐 // 播放音乐
void play(Map musicaudio) async { void play(Map card) async {
try { try {
isPlay.value = false; await _spotifService.initialize();
if (audiourl.value != musicaudio['audiourl']) { _spotifService.play(card['url'] as String);
audiourl.value = '';
await player.setUrl(musicaudio['audiourl']);
audiourl.value = musicaudio['audiourl'];
player.play();
} else {
player.play();
}
isPlay.value = true; isPlay.value = true;
audiourl.value = card['url'] as String;
} catch (e) { } catch (e) {
Logger.error('加载异常: $e'); Logger.error('加载异常: $e');
} }
@ -472,8 +518,12 @@ class ChatController extends GetxController {
// 暂停播放音乐 // 暂停播放音乐
void pause() async { void pause() async {
await player.pause(); try {
isPlay.value = false; _spotifService.pause();
isPlay.value = false;
} catch (e) {
Logger.error('加载异常: $e');
}
} }
// 更新或添加最终响应 // 更新或添加最终响应
@ -539,6 +589,65 @@ class ChatController extends GetxController {
} }
} }
// 按下开始说话
void talkLongPress() async {
_talkDebounceTimer?.cancel();
if (isHoldSpeak.value) return;
_voiceInteractionService.pauseVoiceInteraction();
// 中断当前响应
_interruptCurrentResponse("发送新消息,中断当前响应");
try {
final recognitionStream = await _asrService.startContinuousRecognition();
await Future.delayed(const Duration(milliseconds: 150));
isHoldSpeak.value = true;
recognizedText.value = '';
finalResult = '';
Haptics.vibrate(HapticsType.success);
// 监听识别事件
_recognitionSubscription = recognitionStream.listen(
_handleRecognitionEvent,
onError: _handleRecognitionError,
);
} catch (e) {
Logger.error('启动语音识别失败', e);
isHoldSpeak.value = false;
}
}
// 松开停止说话
void talkLongPressUp() async {
_talkDebounceTimer?.cancel();
_talkDebounceTimer = Timer(const Duration(milliseconds: 200), () async {
if (!isHoldSpeak.value) return;
try {
await _asrService.stopContinuousRecognition();
_recognitionSubscription?.cancel();
_recognitionSubscription = null;
String sendText = '';
final intermediateIndex =
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
if (intermediateIndex >= 0) {
sendText = messages[intermediateIndex].content;
messages.removeAt(intermediateIndex);
}
sendMessage(sendText);
recognizedText.value = '';
finalResult = '';
isHoldSpeak.value = false;
} catch (e) {
Logger.error('停止语音识别失败', e);
}
});
}
// 开始语音识别 // 开始语音识别
Future<void> startVoiceRecognition() async { Future<void> startVoiceRecognition() async {
if (isVoiceRecognitionActive.value) return; if (isVoiceRecognitionActive.value) return;
@ -651,7 +760,7 @@ class ChatController extends GetxController {
if (intermediateIndex >= 0) { if (intermediateIndex >= 0) {
// 更新已有的中间状态消息 // 更新已有的中间状态消息
messages[intermediateIndex] = messages[intermediateIndex].copyWith( messages[intermediateIndex] = messages[intermediateIndex].copyWith(
content: text, content: isVoiceRecognitionActive.value ? text : finalResult + text,
); );
messages.refresh(); messages.refresh();
} else { } else {
@ -672,26 +781,44 @@ class ChatController extends GetxController {
if (text.isEmpty) return; if (text.isEmpty) return;
Logger.info('处理语音识别最终结果: $text'); Logger.info('处理语音识别最终结果: $text');
recognizedText.value = text;
// 更新活动时间 // 更新活动时间
_updateActivityTime(); _updateActivityTime();
// 查找并移除用户的中间状态消息 if (isVoiceRecognitionActive.value) {
final intermediateIndex = // 一直语音说话
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
if (intermediateIndex >= 0) {
messages.removeAt(intermediateIndex);
}
// 发送识别到的文本 recognizedText.value = text;
sendMessage(text); // 查找并移除用户的中间状态消息
final intermediateIndex =
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
if (intermediateIndex >= 0) {
messages.removeAt(intermediateIndex);
}
// 重置识别文本,但不关闭语音识别框 // 发送识别到的文本
recognizedText.value = ''; sendMessage(text);
// 重置识别文本,但不关闭语音识别框
recognizedText.value = '';
// 重置语音检测状态
isSpeechDetected.value = false;
} else {
// 按住说话
// 重置语音检测状态 finalResult += text;
isSpeechDetected.value = false; recognizedText.value = finalResult;
final intermediateIndex =
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
if (intermediateIndex >= 0) {
// 更新已有的中间状态消息
messages[intermediateIndex] = messages[intermediateIndex].copyWith(
content: finalResult,
);
messages.refresh();
}
}
} }
// 处理语音识别事件错误 // 处理语音识别事件错误
@ -816,6 +943,7 @@ class ChatController extends GetxController {
role: chatMsg.sender, role: chatMsg.sender,
content: chatMsg.message, content: chatMsg.message,
timestamp: DateTime.parse(chatMsg.timestamp), timestamp: DateTime.parse(chatMsg.timestamp),
meta: chatMsg.metadata != null ? jsonDecode(chatMsg.metadata!) : null,
); );
}).toList(); }).toList();
@ -856,6 +984,7 @@ class ChatController extends GetxController {
role: chatMsg.sender, role: chatMsg.sender,
content: chatMsg.message, content: chatMsg.message,
timestamp: DateTime.parse(chatMsg.timestamp), timestamp: DateTime.parse(chatMsg.timestamp),
meta: chatMsg.metadata != null ? jsonDecode(chatMsg.metadata!) : null,
); );
}).toList(); }).toList();
@ -877,6 +1006,7 @@ class ChatController extends GetxController {
sessionId: agent.id, sessionId: agent.id,
message: message.content, message: message.content,
sender: message.role, sender: message.role,
metadata: jsonEncode(message.meta),
); );
} catch (e) { } catch (e) {
Logger.error('保存消息到数据库失败', e); Logger.error('保存消息到数据库失败', e);

12
lib/modules/chat/models/message_model.dart

@ -1,7 +1,7 @@
class Message { class Message {
final String role; // 'user' or 'assistant' final String role; // 'user' or 'assistant'
final String content; final String content;
final Map? data; final Map? meta;
final DateTime timestamp; final DateTime timestamp;
final bool isIntermediate; // 中间状态标记,替代isLoading final bool isIntermediate; // 中间状态标记,替代isLoading
@ -9,7 +9,7 @@ class Message {
required this.role, required this.role,
required this.content, required this.content,
required this.timestamp, required this.timestamp,
this.data, this.meta,
this.isIntermediate = false, // 默认不是中间状态 this.isIntermediate = false, // 默认不是中间状态
}); });
@ -18,7 +18,7 @@ class Message {
return Message( return Message(
role: json['role'] as String, role: json['role'] as String,
content: json['content'] as String, content: json['content'] as String,
data: json['data'], meta: json['meta'],
timestamp: DateTime.parse(json['timestamp'] as String), timestamp: DateTime.parse(json['timestamp'] as String),
isIntermediate: json['isIntermediate'] as bool? ?? false, isIntermediate: json['isIntermediate'] as bool? ?? false,
); );
@ -29,7 +29,7 @@ class Message {
return { return {
'role': role, 'role': role,
'content': content, 'content': content,
'data': data, 'meta': meta,
'timestamp': timestamp.toIso8601String(), 'timestamp': timestamp.toIso8601String(),
'isIntermediate': isIntermediate, 'isIntermediate': isIntermediate,
}; };
@ -49,14 +49,14 @@ class Message {
Message copyWith({ Message copyWith({
String? role, String? role,
String? content, String? content,
Map? data, Map? meta,
DateTime? timestamp, DateTime? timestamp,
bool? isIntermediate, bool? isIntermediate,
}) { }) {
return Message( return Message(
role: role ?? this.role, role: role ?? this.role,
content: content ?? this.content, content: content ?? this.content,
data: data ?? this.data, meta: meta ?? this.meta,
timestamp: timestamp ?? this.timestamp, timestamp: timestamp ?? this.timestamp,
isIntermediate: isIntermediate ?? this.isIntermediate, isIntermediate: isIntermediate ?? this.isIntermediate,
); );

69
lib/modules/chat/views/animated_gradient_background.dart

@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
class AnimatedGradientBackground extends StatefulWidget {
final Widget child;
const AnimatedGradientBackground(this.child, {super.key});
@override
State<AnimatedGradientBackground> createState() =>
_AnimatedGradientBackgroundState();
}
class _AnimatedGradientBackgroundState extends State<AnimatedGradientBackground>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Alignment?> _beginAnimation;
late Animation<Alignment?> _endAnimation;
final Color color1 = const Color(0xFFfbcfdd);
final Color color2 = const Color(0xFFe2e9fb);
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 10),
)..repeat(reverse: true);
// 创建起始点动画
_beginAnimation = AlignmentTween(
begin: Alignment.topCenter,
end: Alignment.bottomLeft,
).animate(_controller);
// 创建结束点动画
_endAnimation = AlignmentTween(
begin: Alignment.bottomCenter,
end: Alignment.topRight,
).animate(_controller);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [color1, color2],
begin: _beginAnimation.value!,
end: _endAnimation.value!,
),
),
child: child,
);
},
child: widget.child,
);
}
}

36
lib/modules/chat/views/chat_music_card.dart

@ -12,8 +12,8 @@ class ChatMusicCard extends GetView<ChatController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Map musiccards = data?['musiccards'].first; // final Map musiccards = data?['musiccards'].first;
final Map musicaudio = data?['musicaudio']; // final Map musicaudio = data?['musicaudio'];
return Row( return Row(
children: [ children: [
@ -25,14 +25,14 @@ class ChatMusicCard extends GetView<ChatController> {
borderRadius: BorderRadius.circular(6.r), borderRadius: BorderRadius.circular(6.r),
), ),
child: Image.network( child: Image.network(
musiccards['image'], data?['image'],
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
10.horizontalSpace, 10.horizontalSpace,
Expanded( Expanded(
child: Text( child: Text(
musiccards['name'], data?['name'],
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black, color: isDarkMode ? Colors.white : Colors.black,
@ -43,7 +43,7 @@ class ChatMusicCard extends GetView<ChatController> {
), ),
10.horizontalSpace, 10.horizontalSpace,
Obx( Obx(
() => controller.audiourl == musicaudio['audiourl'] && () => controller.audiourl.value == data?["url"] &&
controller.isPlay.value controller.isPlay.value
? GestureDetector( ? GestureDetector(
onTap: () { onTap: () {
@ -52,30 +52,6 @@ class ChatMusicCard extends GetView<ChatController> {
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
SizedBox(
width: 38.w,
height: 38.w,
child: StreamBuilder<Duration>(
stream: controller.player.positionStream,
builder: (context, snapshot) {
final position = snapshot.data ?? Duration.zero;
final total =
controller.player.duration ?? Duration.zero;
final progress = total.inSeconds > 0
? position.inSeconds / total.inSeconds
: 0.0;
return CircularProgressIndicator(
value: progress,
backgroundColor: isDarkMode
? Colors.grey[700]!
: Colors.grey[300]!,
valueColor: AlwaysStoppedAnimation<Color>(
isDarkMode ? Colors.white : Colors.black),
strokeWidth: 2, // 线条粗细
);
},
),
),
Icon( Icon(
Icons.pause_rounded, Icons.pause_rounded,
color: isDarkMode ? Colors.white : Colors.black, color: isDarkMode ? Colors.white : Colors.black,
@ -85,7 +61,7 @@ class ChatMusicCard extends GetView<ChatController> {
) )
: GestureDetector( : GestureDetector(
onTap: () { onTap: () {
controller.play(musicaudio); controller.play(data!);
}, },
child: Container( child: Container(
width: 38.w, width: 38.w,

60
lib/modules/chat/views/chat_navigation_card.dart

@ -3,15 +3,17 @@ import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:amap_flutter/amap_flutter.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
class ChatNavigationCard extends StatefulWidget { class ChatNavigationCard extends StatefulWidget {
final String content;
final Map? data; final Map? data;
final bool isDarkMode; final bool isDarkMode;
const ChatNavigationCard(this.data, this.isDarkMode, {super.key}); const ChatNavigationCard(this.data, this.content, this.isDarkMode,
{super.key});
@override @override
State<ChatNavigationCard> createState() => _ChatNavigationCardState(); State<ChatNavigationCard> createState() => _ChatNavigationCardState();
@ -22,10 +24,7 @@ class _ChatNavigationCardState extends State<ChatNavigationCard> {
LatLng startPoint = LatLng(116.397428, 39.90923); // 起点坐标 LatLng startPoint = LatLng(116.397428, 39.90923); // 起点坐标
LatLng endPoint = LatLng(116.397026, 39.903303); // 终点坐标 LatLng endPoint = LatLng(116.397026, 39.903303); // 终点坐标
final _amapFlutter = AmapFlutter(); late Future _mapUrl;
StreamSubscription? _locationStream;
String _mapUrl = '';
@override @override
void initState() { void initState() {
@ -33,30 +32,18 @@ class _ChatNavigationCardState extends State<ChatNavigationCard> {
_getLocation(); _getLocation();
} }
@override void _getLocation() {
void dispose() { List startPointList = widget.data?['location'].split(',');
_amapFlutter.dispose(); List endPointList = widget.data?['destination'].split(',');
_locationStream?.cancel(); startPoint = LatLng(
super.dispose(); double.parse(startPointList[0]),
} double.parse(startPointList[1]),
);
void _getLocation() async {
List endPointList = widget.data?['navigationcard']['location'].split(',');
endPoint = LatLng( endPoint = LatLng(
double.parse(endPointList[0]), double.parse(endPointList[0]),
double.parse(endPointList[1]), double.parse(endPointList[1]),
); );
await _amapFlutter.initLocation(); _mapUrl = _getMapUrl();
_locationStream = _amapFlutter.locationStream().listen((data) async {
startPoint = LatLng(
data['longitude'],
data['latitude'],
);
_mapUrl = await _getMapUrl();
_amapFlutter.stopLocation();
setState(() {});
});
_amapFlutter.startLocation();
} }
Future<String> _getMapUrl() async { Future<String> _getMapUrl() async {
@ -190,9 +177,24 @@ class _ChatNavigationCardState extends State<ChatNavigationCard> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
_mapUrl.isNotEmpty FutureBuilder(
? Image.network(_mapUrl) future: _mapUrl,
: const CircularProgressIndicator(), builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return Image.network(snapshot.data);
}
return const CircularProgressIndicator();
},
),
10.verticalSpace,
Text(
widget.content,
style: TextStyle(
color: widget.isDarkMode ? Colors.white : Colors.black,
fontSize: 16.sp,
height: 1.5,
),
),
], ],
); );
} }

590
lib/modules/chat/views/chat_view.dart

@ -5,6 +5,7 @@ import 'dart:math';
import '../controllers/chat_controller.dart'; import '../controllers/chat_controller.dart';
import '../models/message_model.dart'; import '../models/message_model.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import 'animated_gradient_background.dart';
import 'chat_music_card.dart'; import 'chat_music_card.dart';
import 'chat_navigation_card.dart'; import 'chat_navigation_card.dart';
import 'weather_day_card.dart'; import 'weather_day_card.dart';
@ -16,10 +17,17 @@ class ChatView extends GetView<ChatController> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark; final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold( return Obx(
backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50], () => Scaffold(
appBar: _buildAppBar(isDarkMode), backgroundColor:
body: _buildBody(isDarkMode), isDarkMode ? AppColors.darkBackground : Colors.grey[50],
appBar: controller.isVoiceRecognitionActive.value
? null
: _buildAppBar(isDarkMode),
body: controller.isVoiceRecognitionActive.value
? _buildVoiceInputPanel(isDarkMode)
: _buildBody(isDarkMode),
),
); );
} }
@ -47,7 +55,7 @@ class ChatView extends GetView<ChatController> {
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.1), color: Colors.black.withOpacity(0.1),
blurRadius: 3, blurRadius: 3,
offset: Offset(0, 1), offset: const Offset(0, 1),
), ),
], ],
), ),
@ -92,7 +100,7 @@ class ChatView extends GetView<ChatController> {
Container( Container(
width: 6.r, width: 6.r,
height: 6.r, height: 6.r,
decoration: BoxDecoration( decoration: const BoxDecoration(
color: Colors.green, color: Colors.green,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
@ -113,6 +121,13 @@ class ChatView extends GetView<ChatController> {
], ],
), ),
actions: [ actions: [
IconButton(
icon: Icon(
Icons.phone,
color: isDarkMode ? Colors.white : Colors.black87,
),
onPressed: controller.startVoiceRecognition,
),
// TTS开关 // TTS开关
Obx(() => IconButton( Obx(() => IconButton(
icon: Icon( icon: Icon(
@ -157,7 +172,7 @@ class ChatView extends GetView<ChatController> {
value: 'clear', value: 'clear',
child: Row( child: Row(
children: [ children: [
Icon(Icons.delete_outline, color: Colors.red), const Icon(Icons.delete_outline, color: Colors.red),
SizedBox(width: 8.w), SizedBox(width: 8.w),
Text( Text(
'clearChatHistory'.tr, // 清空聊天记录 'clearChatHistory'.tr, // 清空聊天记录
@ -198,7 +213,7 @@ class ChatView extends GetView<ChatController> {
), ),
// 底部输入区域 // 底部输入区域
_buildInputArea(isDarkMode), _buildTextInputPanel(isDarkMode),
], ],
); );
} }
@ -355,314 +370,345 @@ class ChatView extends GetView<ChatController> {
), ),
); );
} }
if (message.data != null) { if (message.meta != null) {
if (message.data?['weatherDays'] != null) { if (message.meta?['card_weather'] != null) {
box = WeatherDayCard(message.data, isDarkMode); box = WeatherDayCard(message.meta?['card_weather'], isDarkMode);
} }
if (message.data?['musicaudio'] != null) { if (message.meta?['card_spotify'] != null) {
box = ChatMusicCard(message.data, isDarkMode); box = ChatMusicCard(message.meta?['card_spotify'], isDarkMode);
} }
if (message.data?['navigationcard'] != null) { if (message.meta?['card_navigation'] != null) {
box = ChatNavigationCard(message.data, isDarkMode); box = ChatNavigationCard(
message.meta?['card_navigation'], message.content, isDarkMode);
} }
} }
return box; return box;
} }
// 构建输入区域 // 构建文本输入面板
Widget _buildInputArea(bool isDarkMode) { Widget _buildTextInputPanel(bool isDarkMode) {
final textController = final textController =
TextEditingController(text: controller.inputText.value); TextEditingController(text: controller.inputText.value);
// 检查是否处于语音识别模式
return Obx(() {
if (controller.isVoiceRecognitionActive.value) {
return _buildVoiceInputPanel(isDarkMode);
} else {
return _buildTextInputPanel(isDarkMode, textController);
}
});
}
// 构建文本输入面板
Widget _buildTextInputPanel(
bool isDarkMode, TextEditingController textController) {
return Container( return Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), height: 48.w,
margin: EdgeInsets.only(left: 16.w, right: 16.w, top: 10.w, bottom: 20.w),
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode ? AppColors.cardBackground : Colors.white, color: controller.isHoldSpeak.value
? AppColors.primary
: isDarkMode
? AppColors.cardBackground
: Colors.white,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.05), color: Colors.black.withOpacity(0.05),
blurRadius: 3, blurRadius: 3,
offset: Offset(0, -1), offset: const Offset(0, -1),
), ),
], ],
), ),
child: SafeArea( child: Row(
child: Row( children: [
children: [ if (!controller.isHoldSpeak.value) 6.horizontalSpace,
// 语音识别按钮 if (!controller.isHoldSpeak.value)
Container( Padding(
width: 40.w, padding: EdgeInsets.all(6.w),
height: 40.w, child: Icon(
decoration: BoxDecoration( Icons.camera_alt_outlined,
color: isDarkMode size: 28.w,
? Colors.white.withOpacity(0.1)
: Colors.grey[100],
shape: BoxShape.circle,
),
child: IconButton(
icon: Icon(
Icons.mic_none,
color: isDarkMode ? Colors.white : Colors.grey[700],
size: 20.r,
),
padding: EdgeInsets.zero,
onPressed: controller.startVoiceRecognition,
), ),
), ),
Expanded(
SizedBox(width: 8.w), child: Obx(
() => controller.isTextInput.value
// 输入框 ? Center(
Expanded( child: TextField(
child: TextField( controller: textController,
controller: textController, style: TextStyle(
decoration: InputDecoration( fontSize: 15.sp,
hintText: 'inputMessage'.tr, // 输入消息... color: isDarkMode ? Colors.white : Colors.black87,
hintStyle: TextStyle( ),
color: isDarkMode ? Colors.grey[500] : Colors.grey[500]), textInputAction: TextInputAction.send,
filled: true, decoration: InputDecoration(
fillColor: isDarkMode hintText: 'inputMessage'.tr, // 输入消息...
? Colors.white.withOpacity(0.1) hintStyle: TextStyle(color: Colors.grey[500]),
: Colors.grey[100], filled: false,
border: OutlineInputBorder( border: InputBorder.none,
borderRadius: BorderRadius.circular(20.r), focusedBorder: InputBorder.none,
borderSide: BorderSide.none, contentPadding: EdgeInsets.only(left: 10.w),
),
onSubmitted: (value) {
if (value.trim().isNotEmpty) {
controller.sendMessage(value);
textController.clear();
controller.inputText.value = ''; // 清空输入框
}
},
),
)
: Listener(
onPointerDown: (event) {
controller.talkLongPress();
},
onPointerUp: (event) {
controller.talkLongPressUp();
},
child: Container(
padding: EdgeInsets.only(
left: controller.isHoldSpeak.value ? 0 : 40.w),
color: Colors.transparent,
child: controller.isHoldSpeak.value
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(40, (int index) {
return Container(
width: 3.w,
height: 10.w,
margin:
EdgeInsets.symmetric(horizontal: 2.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(3.r),
),
);
}),
)
: Center(
child: Text(
'按住说话',
style: TextStyle(
fontSize: 16.sp,
color: isDarkMode
? Colors.white
: Colors.black87,
),
),
),
),
),
),
),
if (!controller.isHoldSpeak.value)
GestureDetector(
onTap: () {
controller.isTextInput.value = !controller.isTextInput.value;
},
child: Container(
width: 28.w,
height: 28.w,
margin: EdgeInsets.all(6.w),
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28.r),
border: Border.all(
color: isDarkMode ? Colors.white : Colors.black87,
width: 2,
), ),
contentPadding: EdgeInsets.symmetric( ),
horizontal: 16.w, child: Obx(
vertical: 10.h, () => Icon(
controller.isTextInput.value ? Icons.mic : Icons.keyboard,
size: 20.w,
), ),
), ),
maxLines: null, ),
textInputAction: TextInputAction.newline, ),
style: TextStyle( if (!controller.isHoldSpeak.value)
fontSize: 15.sp, Container(
width: 28.w,
height: 28.w,
margin: EdgeInsets.all(6.w),
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28.r),
border: Border.all(
color: isDarkMode ? Colors.white : Colors.black87, color: isDarkMode ? Colors.white : Colors.black87,
width: 2,
), ),
onChanged: (value) => controller.inputText.value = value, ),
onSubmitted: (value) { child: Icon(
if (value.trim().isNotEmpty) { Icons.add,
controller.sendMessage(value); size: 20.w,
textController.clear();
controller.inputText.value = ''; // 清空输入框
}
},
), ),
), ),
if (!controller.isHoldSpeak.value) 6.horizontalSpace,
SizedBox(width: 8.w), ],
// 发送按钮
Obx(() {
final hasText = controller.inputText.value.isNotEmpty;
return Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: hasText
? AppColors.primary
: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200],
shape: BoxShape.circle,
),
child: IconButton(
icon: Icon(
Icons.send,
color: hasText
? Colors.white
: isDarkMode
? Colors.white.withOpacity(0.5)
: Colors.grey[400],
size: 18.r,
),
padding: EdgeInsets.zero,
onPressed: hasText
? () {
controller.sendMessage(controller.inputText.value);
textController.clear();
controller.inputText.value = ''; // 清空输入框
}
: null,
),
);
}),
],
),
), ),
); );
} }
// 构建语音输入面板 // 构建语音输入面板
Widget _buildVoiceInputPanel(bool isDarkMode) { Widget _buildVoiceInputPanel(bool isDarkMode) {
return Container( return AnimatedGradientBackground(
decoration: BoxDecoration( SafeArea(
color: isDarkMode ? AppColors.cardBackground : Colors.white, child: Column(
boxShadow: [ children: [
BoxShadow( Row(
color: Colors.black.withOpacity(0.05), children: [],
blurRadius: 3, ),
offset: Offset(0, -1), Expanded(
), child: Container(
], width: 200.w,
), height: 200.w,
child: SafeArea( decoration: BoxDecoration(
child: SizedBox( color:
height: 80.h, isDarkMode ? Colors.white.withOpacity(0.1) : Colors.white,
child: Padding( shape: BoxShape.circle,
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 语音录制按钮
Container(
width: 60.r,
height: 60.r,
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
spreadRadius: 1,
),
],
),
child: Center(
child: Obx(() => Icon(
controller.isListening.value
? Icons.mic
: Icons.mic_none,
color: controller.isListening.value
? AppColors.primary
: Colors.grey.shade400,
size: 28.r,
)),
),
), ),
),
),
SizedBox(
height: 80.h,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 语音录制按钮
Container(
width: 60.r,
height: 60.r,
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
spreadRadius: 1,
),
],
),
child: Center(
child: Obx(() => Icon(
controller.isListening.value
? Icons.mic
: Icons.mic_none,
color: controller.isListening.value
? AppColors.primary
: Colors.grey.shade400,
size: 28.r,
)),
),
),
// 中间音频波形和状态文本 // 中间音频波形和状态文本
Expanded( Expanded(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// 音频波形区域 // 音频波形区域
Container( Container(
height: 50.h, height: 50.h,
child: Obx(() { child: Obx(() {
final isSpeaking = controller.isSpeechDetected.value; final isSpeaking =
return Center( controller.isSpeechDetected.value;
child: Container( return Center(
width: 220.w, child: Container(
height: 30.h, width: 220.w,
child: Row( height: 30.h,
mainAxisSize: MainAxisSize.min, child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min,
children: List.generate( mainAxisAlignment: MainAxisAlignment.center,
23, children: List.generate(
(index) { 23,
// 随机高度模拟波形 (index) {
double height = isSpeaking // 随机高度模拟波形
? (Random().nextDouble() * 20.0 + 5.0).h double height = isSpeaking
: 2.h; ? (Random().nextDouble() * 20.0 +
5.0)
.h
: 2.h;
return SizedBox( return SizedBox(
width: 6.w, width: 6.w,
child: Center( child: Center(
child: AnimatedContainer( child: AnimatedContainer(
duration: Duration(milliseconds: 500), duration: const Duration(
width: 2.5.w, milliseconds: 500),
height: height, width: 2.5.w,
margin: EdgeInsets.symmetric( height: height,
horizontal: 1.5.w), margin: EdgeInsets.symmetric(
decoration: BoxDecoration( horizontal: 1.5.w),
color: isSpeaking decoration: BoxDecoration(
? AppColors.primary color: isSpeaking
: isDarkMode ? AppColors.primary
? Colors.grey.shade600 : isDarkMode
: Colors.grey.shade400, ? Colors.grey.shade600
borderRadius: : Colors.grey.shade400,
BorderRadius.circular(2.r), borderRadius:
BorderRadius.circular(2.r),
),
),
), ),
), );
), },
); ),
}, ),
), ),
), );
), }),
); ),
}),
),
SizedBox(height: 8.h),
// 语音状态文本 // 语音状态文本
Obx(() => Text( Obx(() => Text(
controller.isSpeechDetected.value controller.isSpeechDetected.value
? 'imListening'.tr // 正在聆听... ? 'imListening'.tr // 正在聆听...
: 'canStartTalking'.tr, // 你可以开始说话 : 'canStartTalking'.tr, // 你可以开始说话
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: isDarkMode color: isDarkMode
? Colors.grey[400] ? Colors.grey[400]
: Colors.grey[700], : Colors.grey[700],
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
)), )),
], ],
), ),
), ),
// 关闭按钮 // 关闭按钮
Container( Container(
width: 60.r, width: 60.r,
height: 60.r, height: 60.r,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode color: isDarkMode
? Colors.white.withOpacity(0.1) ? Colors.white.withOpacity(0.1)
: Colors.white, : Colors.white,
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.1), color: Colors.black.withOpacity(0.1),
blurRadius: 8, blurRadius: 8,
spreadRadius: 1, spreadRadius: 1,
),
],
), ),
], child: Center(
), child: IconButton(
child: Center( icon: Icon(
child: IconButton( Icons.close,
icon: Icon( color: isDarkMode
Icons.close, ? Colors.white
color: isDarkMode ? Colors.white : Colors.grey.shade600, : Colors.grey.shade600,
size: 28.r, size: 28.r,
),
padding: EdgeInsets.zero,
onPressed: controller.stopVoiceRecognition,
),
), ),
padding: EdgeInsets.zero,
onPressed: controller.stopVoiceRecognition,
), ),
), ],
), ),
], ),
), ),
), ],
), ),
), ),
); );
@ -682,7 +728,7 @@ class ChatView extends GetView<ChatController> {
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.05), color: Colors.black.withOpacity(0.05),
blurRadius: 2, blurRadius: 2,
offset: Offset(0, 1), offset: const Offset(0, 1),
), ),
], ],
), ),
@ -720,7 +766,7 @@ class ChatView extends GetView<ChatController> {
// 构建加载动画点 // 构建加载动画点
Widget _buildLoadingDot(int index, bool isDarkMode) { Widget _buildLoadingDot(int index, bool isDarkMode) {
return AnimatedBuilder( return AnimatedBuilder(
animation: AlwaysStoppedAnimation(0), animation: const AlwaysStoppedAnimation(0),
builder: (context, child) { builder: (context, child) {
return Container( return Container(
margin: EdgeInsets.symmetric(horizontal: 2.w), margin: EdgeInsets.symmetric(horizontal: 2.w),

16
lib/modules/chat/views/weather_day_card.dart

@ -9,8 +9,6 @@ class WeatherDayCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Map weatherDays = data?['weatherDays'].first;
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -19,14 +17,14 @@ class WeatherDayCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
weatherDays['date'], data?['city'],
style: TextStyle( style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black, color: isDarkMode ? Colors.white : Colors.black,
fontSize: 10.sp, fontSize: 10.sp,
), ),
), ),
Text( Text(
'${weatherDays['maxTemperature']}°', '${data?['temperature']}°',
style: TextStyle( style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black, color: isDarkMode ? Colors.white : Colors.black,
fontSize: 30.sp, fontSize: 30.sp,
@ -43,7 +41,7 @@ class WeatherDayCard extends StatelessWidget {
size: 18.w, size: 18.w,
), ),
Text( Text(
'${weatherDays['weatherCondition']},${weatherDays['windDirection']}', '${data?['weatherCondition']},${data?['windDirection']}风',
style: TextStyle( style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black, color: isDarkMode ? Colors.white : Colors.black,
fontSize: 14.sp, fontSize: 14.sp,
@ -52,7 +50,7 @@ class WeatherDayCard extends StatelessWidget {
Row( Row(
children: [ children: [
Text( Text(
'最\n高', '湿\n度',
style: TextStyle( style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black, color: isDarkMode ? Colors.white : Colors.black,
fontSize: 10.sp, fontSize: 10.sp,
@ -61,7 +59,7 @@ class WeatherDayCard extends StatelessWidget {
), ),
5.horizontalSpace, 5.horizontalSpace,
Text( Text(
'${weatherDays['maxTemperature']}°', '${data?['humidity']}',
style: TextStyle( style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black, color: isDarkMode ? Colors.white : Colors.black,
fontSize: 20.sp, fontSize: 20.sp,
@ -69,7 +67,7 @@ class WeatherDayCard extends StatelessWidget {
), ),
10.horizontalSpace, 10.horizontalSpace,
Text( Text(
'最\n低', '风\n力',
style: TextStyle( style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black, color: isDarkMode ? Colors.white : Colors.black,
fontSize: 10.sp, fontSize: 10.sp,
@ -78,7 +76,7 @@ class WeatherDayCard extends StatelessWidget {
), ),
5.horizontalSpace, 5.horizontalSpace,
Text( Text(
'${weatherDays['minTemperature']}°', '${data?['windForce']}',
style: TextStyle( style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black, color: isDarkMode ? Colors.white : Colors.black,
fontSize: 20.sp, fontSize: 20.sp,

33
lib/modules/login/controllers/login_controller.dart

@ -1,3 +1,4 @@
import 'package:deep_voice/data/models/appconfig_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:logger/web.dart'; import 'package:logger/web.dart';
@ -471,10 +472,13 @@ class LoginController extends GetxController {
return; return;
} }
_isLoading.value = true; _isLoading.value = true;
// 模拟验证过程
// Future.delayed(const Duration(seconds: 2), () {
// _isLoading.value = false;
// // 验证成功,跳转到配对页面
// Get.offAllNamed(Routes.pairing);
// });
try { try {
await Future.delayed(const Duration(seconds: 2));
// 模拟验证过程
switch (loginType) { switch (loginType) {
case 0: case 0:
{ {
@ -692,7 +696,8 @@ class LoginController extends GetxController {
//final prefs = await GetStorage(); //final prefs = await GetStorage();
await _storage.write( await _storage.write(
'user', jsonEncode(user.toJson())); // 使用 user.toJson() 'user', jsonEncode(user.toJson())); // 使用 user.toJson()
//获取app的配置信息
await getAppConfig();
Get.offAllNamed(Routes.pairing); Get.offAllNamed(Routes.pairing);
// } else { // } else {
// throw FormatException('API 响应缺少 user 字段'); // throw FormatException('API 响应缺少 user 字段');
@ -722,4 +727,24 @@ class LoginController extends GetxController {
colorText: Colors.blue, colorText: Colors.blue,
); );
} }
Future<bool> getAppConfig() async {
try {
print('getAppConfig:进来了');
//if (User.getname() != "") {
final response = await Api.getappconfig();
print('getAppConfig: $response');
if (response == null) return false;
print('getAppConfig:进来了');
// 根据API设计调整
final resp = UserGetAppConfigResp.fromJson(response);
// 存储用户信息(示例)
_storage.write('ENV', resp.env);
_storage.write('MCPS', resp.mcps);
return true;
} catch (e) {
print('getAppConfig 取失败: $e');
return false;
}
}
} }

9
lib/modules/opus_test/bindings/opus_test_binding.dart

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import '../controllers/opus_test_controller.dart';
class OpusTestBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<OpusTestController>(() => OpusTestController());
}
}

404
lib/modules/opus_test/controllers/opus_test_controller.dart

@ -0,0 +1,404 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:path_provider/path_provider.dart';
import 'package:just_audio/just_audio.dart';
import 'package:jl_opus/jl_opus.dart';
import 'package:permission_handler/permission_handler.dart';
class OpusTestController extends GetxController {
// 选中的文件
final selectedFiles = <PlatformFile>[].obs;
// 是否携带协议头
final hasHeader = false.obs;
// 数据包长度
final packetLength = '40'.obs;
// 采样率选项
final sampleRateOptions = [8000, 16000, 24000, 32000, 44100, 48000];
final selectedSampleRate = 16000.obs;
// 通道数量
final isMono = true.obs;
// 播放状态
final isPlaying = false.obs;
final isDecoding = false.obs;
final statusMessage = ''.obs;
// 播放器
final player = AudioPlayer();
// 临时文件路径
String? tempPcmPath;
// 杰理OPUS解码器
late JlOpus jlOpus;
StreamSubscription? _eventSubscription;
@override
void onInit() {
super.onInit();
requestPermissions();
_initOpusDecoder();
}
@override
void onClose() {
player.dispose();
_eventSubscription?.cancel();
jlOpus.dispose();
super.onClose();
}
// 初始化OPUS解码器
Future<void> _initOpusDecoder() async {
jlOpus = JlOpus();
// 监听解码器事件
_eventSubscription = jlOpus.eventStream.listen((event) {
switch (event.event) {
case 'onStart':
statusMessage.value = '开始${event.type == "file" ? "文件" : "流"}解码...';
break;
case 'onComplete':
if (event.type == 'file') {
statusMessage.value = '解码完成: ${event.filePath}';
} else {
statusMessage.value = '流解码完成';
}
break;
case 'onError':
statusMessage.value = '解码错误: [${event.errorCode}] ${event.errorMessage}';
isDecoding.value = false;
break;
}
});
// 初始化OPUS解码器
final initialized = await jlOpus.initOpusDecoder();
if (!initialized) {
statusMessage.value = 'OPUS解码器初始化失败';
}
}
// 请求必要权限
Future<void> requestPermissions() async {
if (Platform.isAndroid) {
await Permission.storage.request();
}
}
// 选择文件
Future<void> pickFiles() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['opus'],
allowMultiple: false,
);
if (result != null && result.files.isNotEmpty) {
selectedFiles.clear();
selectedFiles.add(result.files.first);
statusMessage.value = '已选择文件:${result.files.first.name}';
}
} catch (e) {
statusMessage.value = '选择文件失败: $e';
}
}
// 切换通道模式
void toggleChannel(bool isSingleChannel) {
isMono.value = isSingleChannel;
}
// 设置采样率
void setSampleRate(int rate) {
selectedSampleRate.value = rate;
}
// 设置数据包长度
void setPacketLength(String length) {
packetLength.value = length;
}
// 切换是否携带协议头
void toggleHeader(bool value) {
hasHeader.value = value;
}
// 播放音频
Future<void> playAudio() async {
if (isPlaying.value) {
await player.stop();
isPlaying.value = false;
statusMessage.value = '已停止播放';
return;
}
if (selectedFiles.isEmpty) {
statusMessage.value = '请先选择Opus文件';
return;
}
try {
isDecoding.value = true;
statusMessage.value = '正在解码...';
// 使用杰理OPUS库进行解码
final inPath = selectedFiles.first.path!;
// 创建WAV输出文件路径
final tempDir = await getTemporaryDirectory();
final wavFileName = 'decoded_opus_${DateTime.now().millisecondsSinceEpoch}.wav';
final outPath = '${tempDir.path}/$wavFileName';
// 创建解码参数
final option = OpusOption(
hasHeader: hasHeader.value,
channel: isMono.value ? 1 : 2,
sampleRate: selectedSampleRate.value,
packetSize: int.tryParse(packetLength.value) ?? 40,
);
// 解码文件
final pcmPath = await jlOpus.decodeOpusFile(inPath, outPath, option);
if (pcmPath == null) {
statusMessage.value = '解码失败';
isDecoding.value = false;
return;
}
tempPcmPath = pcmPath;
// 将PCM文件转换为WAV文件
final wavFile = await _convertPcmToWav(
pcmPath,
'${tempDir.path}/$wavFileName',
selectedSampleRate.value,
isMono.value ? 1 : 2
);
statusMessage.value = '解码完成,准备播放';
// 播放WAV文件
await player.setFilePath(wavFile.path);
await player.play();
isPlaying.value = true;
statusMessage.value = '正在播放解码后的音频';
// 监听播放完成
player.playerStateStream.listen((state) {
if (state.processingState == ProcessingState.completed) {
isPlaying.value = false;
statusMessage.value = '播放完成';
}
});
} catch (e) {
statusMessage.value = '解码或播放失败: $e';
} finally {
isDecoding.value = false;
}
}
// 将PCM转换为WAV格式
Future<File> _convertPcmToWav(String pcmPath, String wavPath, int sampleRate, int channels) async {
final pcmFile = File(pcmPath);
final pcmData = await pcmFile.readAsBytes();
// 创建WAV文件 (44字节头部 + PCM数据)
final wavSize = 44 + pcmData.length;
final wavData = ByteData(wavSize);
// 写入WAV头部
_writeWavHeader(wavData, pcmData.length, sampleRate, channels);
// 写入PCM数据
for (int i = 0; i < pcmData.length; i++) {
wavData.setUint8(44 + i, pcmData[i]);
}
// 保存为WAV文件
final wavFile = File(wavPath);
await wavFile.writeAsBytes(wavData.buffer.asUint8List());
return wavFile;
}
// 写入WAV头部到ByteData
void _writeWavHeader(ByteData data, int pcmLength, int sampleRate, int channels) {
final bytesPerSample = 2; // 16位PCM
final byteRate = sampleRate * channels * bytesPerSample;
final blockAlign = channels * bytesPerSample;
// "RIFF"标识
data.setUint8(0, 0x52); // 'R'
data.setUint8(1, 0x49); // 'I'
data.setUint8(2, 0x46); // 'F'
data.setUint8(3, 0x46); // 'F'
// 文件大小 (减去8个字节的RIFF头部和大小字段)
data.setUint32(4, 36 + pcmLength, Endian.little);
// "WAVE"标识
data.setUint8(8, 0x57); // 'W'
data.setUint8(9, 0x41); // 'A'
data.setUint8(10, 0x56); // 'V'
data.setUint8(11, 0x45); // 'E'
// "fmt "子块
data.setUint8(12, 0x66); // 'f'
data.setUint8(13, 0x6D); // 'm'
data.setUint8(14, 0x74); // 't'
data.setUint8(15, 0x20); // ' '
// 子块大小 (16 for PCM)
data.setUint32(16, 16, Endian.little);
// 音频格式 (1 表示PCM)
data.setUint16(20, 1, Endian.little);
// 通道数量
data.setUint16(22, channels, Endian.little);
// 采样率
data.setUint32(24, sampleRate, Endian.little);
// 字节率 = 采样率 * 通道数 * 每样本字节数
data.setUint32(28, byteRate, Endian.little);
// 块对齐 = 通道数 * 每样本字节数
data.setUint16(32, blockAlign, Endian.little);
// 每个样本位数
data.setUint16(34, bytesPerSample * 8, Endian.little);
// "data"子块
data.setUint8(36, 0x64); // 'd'
data.setUint8(37, 0x61); // 'a'
data.setUint8(38, 0x74); // 't'
data.setUint8(39, 0x61); // 'a'
// 数据大小
data.setUint32(40, pcmLength, Endian.little);
}
}
// PCM音频源
class PCMAudioSource extends StreamAudioSource {
final Uint8List pcmData;
final int sampleRate;
final int channels;
PCMAudioSource({
required this.pcmData,
this.sampleRate = 16000,
this.channels = 1,
});
@override
Future<StreamAudioResponse> request([int? start, int? end]) async {
start = start ?? 0;
end = end ?? pcmData.length;
// 转换PCM到WAV格式
final wavHeader = _createWavHeader(pcmData.length, sampleRate, channels);
final wavData = Uint8List(wavHeader.length + pcmData.length);
wavData.setRange(0, wavHeader.length, wavHeader);
wavData.setRange(wavHeader.length, wavHeader.length + pcmData.length, pcmData);
// 调整请求范围来适应WAV头部
int adjustedStart = start;
int adjustedEnd = end;
if (start < wavHeader.length) {
adjustedStart = start;
} else {
adjustedStart = start + wavHeader.length;
}
if (end <= wavHeader.length) {
adjustedEnd = end;
} else {
adjustedEnd = end + wavHeader.length;
adjustedEnd = adjustedEnd > wavData.length ? wavData.length : adjustedEnd;
}
return StreamAudioResponse(
sourceLength: wavData.length,
contentLength: adjustedEnd - adjustedStart,
offset: adjustedStart,
stream: Stream.value(wavData.sublist(adjustedStart, adjustedEnd)),
contentType: 'audio/wav',
);
}
// 创建WAV头部
Uint8List _createWavHeader(int dataLength, int sampleRate, int channels) {
final bytesPerSample = 2; // 16位PCM
final byteRate = sampleRate * channels * bytesPerSample;
final blockAlign = channels * bytesPerSample;
final header = ByteData(44); // WAV头部固定44字节
// "RIFF"标识
header.setUint8(0, 0x52); // 'R'
header.setUint8(1, 0x49); // 'I'
header.setUint8(2, 0x46); // 'F'
header.setUint8(3, 0x46); // 'F'
// 文件大小 (减去8个字节的RIFF头部和大小字段)
header.setUint32(4, 36 + dataLength, Endian.little);
// "WAVE"标识
header.setUint8(8, 0x57); // 'W'
header.setUint8(9, 0x41); // 'A'
header.setUint8(10, 0x56); // 'V'
header.setUint8(11, 0x45); // 'E'
// "fmt "子块
header.setUint8(12, 0x66); // 'f'
header.setUint8(13, 0x6D); // 'm'
header.setUint8(14, 0x74); // 't'
header.setUint8(15, 0x20); // ' '
// 子块大小 (16 for PCM)
header.setUint32(16, 16, Endian.little);
// 音频格式 (1 表示PCM)
header.setUint16(20, 1, Endian.little);
// 通道数量
header.setUint16(22, channels, Endian.little);
// 采样率
header.setUint32(24, sampleRate, Endian.little);
// 字节率 = 采样率 * 通道数 * 每样本字节数
header.setUint32(28, byteRate, Endian.little);
// 块对齐 = 通道数 * 每样本字节数
header.setUint16(32, blockAlign, Endian.little);
// 每个样本位数
header.setUint16(34, bytesPerSample * 8, Endian.little);
// "data"子块
header.setUint8(36, 0x64); // 'd'
header.setUint8(37, 0x61); // 'a'
header.setUint8(38, 0x74); // 't'
header.setUint8(39, 0x61); // 'a'
// 数据大小
header.setUint32(40, dataLength, Endian.little);
return header.buffer.asUint8List();
}
}

445
lib/modules/opus_test/views/opus_test_view.dart

@ -0,0 +1,445 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/opus_test_controller.dart';
class OpusTestView extends GetView<OpusTestController> {
const OpusTestView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// 获取主题模式
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return DefaultTabController(
length: 2,
child: Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
elevation: 0.5,
title: Text(
'OPUS解码',
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
centerTitle: true,
bottom: TabBar(
indicatorColor: Colors.blue,
indicatorWeight: 3,
labelColor: isDarkMode ? Colors.white : Colors.black,
unselectedLabelColor: isDarkMode ? Colors.grey : Colors.grey,
tabs: const [
Tab(text: "解码"),
Tab(text: "输出"),
],
),
),
body: TabBarView(
children: [
_buildDecodeTab(isDarkMode),
_buildEncodeTab(isDarkMode),
],
),
),
);
}
// 解码标签页
Widget _buildDecodeTab(bool isDarkMode) {
return ListView(
padding: EdgeInsets.all(16.w),
children: [
// 文件选择部分
_buildSection(
title: '选择文件',
isDarkMode: isDarkMode,
child: Column(
children: [
Obx(() => controller.selectedFiles.isEmpty
? const SizedBox.shrink()
: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.selectedFiles.length,
itemBuilder: (context, index) {
final file = controller.selectedFiles[index];
return ListTile(
title: Text(
file.name,
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black87,
fontSize: 14.sp,
),
),
subtitle: Text(
'${(file.size / 1024).toStringAsFixed(2)} Kb',
style: TextStyle(
color: isDarkMode ? Colors.white38 : Colors.black54,
fontSize: 12.sp,
),
),
trailing: Radio<int>(
value: index,
groupValue: 0,
onChanged: (value) {},
activeColor: Colors.blue,
),
);
},
)),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
icon: Icon(
Icons.refresh,
color: isDarkMode ? Colors.blue[300] : Colors.blue,
),
onPressed: () {
controller.selectedFiles.clear();
},
),
],
),
],
),
),
SizedBox(height: 16.h),
// 解码参数部分
_buildSection(
title: '解码参数',
isDarkMode: isDarkMode,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Obx(() => Checkbox(
value: controller.hasHeader.value,
onChanged: (value) => controller.toggleHeader(value!),
activeColor: Colors.blue,
)),
Text(
'是否携带协议头',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black87,
fontSize: 14.sp,
),
),
],
),
// 数据包长度
Row(
children: [
Text(
'数据包长度',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black87,
fontSize: 14.sp,
),
),
SizedBox(width: 16.w),
Expanded(
child: Obx(() => TextField(
controller: TextEditingController(
text: controller.packetLength.value),
onChanged: (value) => controller.setPacketLength(value),
keyboardType: TextInputType.number,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black,
),
decoration: InputDecoration(
isDense: true,
suffix: Text(
'Byte',
style: TextStyle(
color: isDarkMode
? Colors.white60
: Colors.black54,
fontSize: 12.sp,
),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: isDarkMode
? Colors.white30
: Colors.black26,
),
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Colors.blue,
),
),
),
)),
),
],
),
SizedBox(height: 20.h),
// 采样率
Row(
children: [
Text(
'采样率',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black87,
fontSize: 14.sp,
),
),
SizedBox(width: 16.w),
Expanded(
child: Obx(() => DropdownButton<int>(
value: controller.selectedSampleRate.value,
isExpanded: true,
dropdownColor:
isDarkMode ? Colors.grey[800] : Colors.white,
items: controller.sampleRateOptions.map((rate) {
return DropdownMenuItem<int>(
value: rate,
child: Text(
rate.toString(),
style: TextStyle(
color: isDarkMode
? Colors.white
: Colors.black87,
),
),
);
}).toList(),
onChanged: (value) =>
controller.setSampleRate(value!),
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black,
),
)),
),
],
),
SizedBox(height: 20.h),
// 通道数量
Text(
'通道数量',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black87,
fontSize: 14.sp,
),
),
Row(
children: [
Expanded(
child: Row(
children: [
Obx(() => Radio<bool>(
value: true,
groupValue: controller.isMono.value,
onChanged: (value) =>
controller.toggleChannel(value!),
activeColor: Colors.blue,
)),
Text(
'单通道',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black87,
fontSize: 14.sp,
),
),
],
),
),
Expanded(
child: Row(
children: [
Obx(() => Radio<bool>(
value: false,
groupValue: controller.isMono.value,
onChanged: (value) =>
controller.toggleChannel(value!),
activeColor: Colors.blue,
)),
Text(
'双通道',
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black87,
fontSize: 14.sp,
),
),
],
),
),
],
),
],
),
),
SizedBox(height: 16.h),
// 状态消息
Obx(() => controller.statusMessage.value.isNotEmpty
? Container(
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 16.w),
margin: EdgeInsets.only(bottom: 16.h),
decoration: BoxDecoration(
color: isDarkMode
? Colors.blue.withOpacity(0.2)
: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(4.r),
border: Border.all(
color: isDarkMode
? Colors.blue.withOpacity(0.3)
: Colors.blue.withOpacity(0.2),
width: 1,
),
),
child: Text(
controller.statusMessage.value,
style: TextStyle(
color: isDarkMode ? Colors.blue[300] : Colors.blue[700],
fontSize: 12.sp,
),
),
)
: const SizedBox.shrink()),
// 播放按钮
Container(
margin: EdgeInsets.symmetric(vertical: 8.h),
child: ElevatedButton.icon(
icon: Obx(() => Icon(
controller.isPlaying.value
? Icons.stop
: Icons.play_arrow,
color: Colors.white,
)),
label: Obx(() => Text(
controller.isPlaying.value ? '停止播放' : '播放音频',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
),
)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
onPressed: controller.isDecoding.value
? null
: () {
controller.playAudio();
},
),
),
// 解码按钮
Container(
margin: EdgeInsets.symmetric(vertical: 8.h),
child: Obx(() => ElevatedButton(
onPressed: controller.selectedFiles.isEmpty ||
controller.isDecoding.value
? null
: () {
controller.pickFiles();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text(
'开始解码',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
),
),
)),
),
],
);
}
// 编码标签页(暂未实现)
Widget _buildEncodeTab(bool isDarkMode) {
return Center(
child: Text(
'编码功能尚未实现',
style: TextStyle(
color: isDarkMode ? Colors.white54 : Colors.black54,
fontSize: 16.sp,
),
),
);
}
// 构建带标题的区段
Widget _buildSection({
required String title,
required Widget child,
required bool isDarkMode,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
SizedBox(width: 8.w),
Expanded(
child: Container(
height: 1,
color: isDarkMode ? Colors.white24 : Colors.black12,
),
),
if (title == '选择文件')
IconButton(
icon: Icon(
Icons.add_circle_outline,
color: isDarkMode ? Colors.blue[300] : Colors.blue,
),
onPressed: () {
controller.pickFiles();
},
),
],
),
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(8.r),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.03),
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: child,
),
],
);
}
}

23
lib/modules/settings/views/settings_view.dart

@ -503,6 +503,29 @@ class SettingsView extends GetView<SettingsController> {
? Colors.white.withOpacity(0.1) ? Colors.white.withOpacity(0.1)
: Colors.grey[200]), : Colors.grey[200]),
// OPUS测试
_buildSimpleNavigationSetting(
title: 'OPUS解码测试',
subtitle: '测试OPUS音频文件的解码和播放',
icon: Icons.audio_file,
iconBgColor: isDarkMode
? Colors.purple[900]!.withOpacity(0.3)
: Colors.purple[100]!,
iconColor:
isDarkMode ? Colors.purple[300]! : Colors.purple[600]!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () {
Get.toNamed(Routes.opusTest);
},
isDarkMode: isDarkMode,
),
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// 退出登录 // 退出登录
_buildSimpleNavigationSetting( _buildSimpleNavigationSetting(
title: 'logout'.tr, // 退出登录 title: 'logout'.tr, // 退出登录

23
lib/modules/splash/controllers/splash_controller.dart

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:amap_flutter/amap_flutter.dart'; import 'package:amap_flutter/amap_flutter.dart';
import 'package:deep_voice/data/models/appconfig_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
@ -88,6 +89,7 @@ class SplashController extends GetxController {
isLoggedIn = await getLoginToken(); isLoggedIn = await getLoginToken();
} }
if (isLoggedIn) { if (isLoggedIn) {
await getAppConfig();
Get.offAllNamed(Routes.pairing); Get.offAllNamed(Routes.pairing);
} else { } else {
Get.offAllNamed(Routes.login); // 跳转到登录页 Get.offAllNamed(Routes.login); // 跳转到登录页
@ -118,4 +120,25 @@ class SplashController extends GetxController {
return false; return false;
} }
} }
Future<bool> getAppConfig() async {
try {
print('getAppConfig:进来了');
//if (User.getname() != "") {
final response = await Api.getappconfig();
print('getAppConfig: $response');
if (response == null) return false;
// 根据API设计调整
final resp = UserGetAppConfigResp.fromJson(response);
print('getAppConfig:进来了');
// 存储用户信息(示例)
_storage.write('ENV', resp.env);
_storage.write('MCPS', resp.mcps);
return true;
} catch (e) {
print('getAppConfig 取失败: $e');
return false;
}
}
} }

310
lib/modules/translation/controllers/translation_controller.dart

@ -10,14 +10,37 @@ import '../../../core/utils/logger.dart';
import '../../../data/models/events/voice_interaction_event.dart'; import '../../../data/models/events/voice_interaction_event.dart';
import '../../../data/services/voice_interaction_service.dart'; import '../../../data/services/voice_interaction_service.dart';
import '../../../data/services/asr_service.dart'; import '../../../data/services/asr_service.dart';
import 'package:jieli/jieli.dart';
class TranslationController extends GetxController { class TranslationController extends GetxController {
late Jieli jieli;
// 事件订阅
StreamSubscription? _scanSubscription;
StreamSubscription? _connectionSubscription;
StreamSubscription? _wakeupSubscription;
StreamSubscription? _audioSubscription;
StreamSubscription? _commandResultSubscription;
// 状态变量
var isScanning = false.obs;
var deviceInfo = '未获取'.obs;
var connectStatus = '未连接'.obs;
var currentDevice = Rx<BleDevice?>(null);
var scannedDevices = RxList<BleDevice>([]);
var wakeupReceived = false.obs;
var audioDataSize = 0.obs;
var receivedFrames = 0.obs;
var audioDataReceiving = false.obs;
var volumeCommandResult = ''.obs;
// 服务 // 服务
final AsrService _asrService = Get.find<AsrService>(); final AsrService _asrService = Get.find<AsrService>();
final VolcanoTranslationService _translationService = Get.find<VolcanoTranslationService>(); final VolcanoTranslationService _translationService =
Get.find<VolcanoTranslationService>();
final TtsService _ttsService = Get.find<TtsService>(); final TtsService _ttsService = Get.find<TtsService>();
final LanguageManager _languageManager = Get.find<LanguageManager>(); final LanguageManager _languageManager = Get.find<LanguageManager>();
final GetStorage _storage = GetStorage(); final GetStorage _storage = GetStorage();
final VoiceInteractionService _voiceInteractionService = Get.find<VoiceInteractionService>(); final VoiceInteractionService _voiceInteractionService =
Get.find<VoiceInteractionService>();
// 存储相关 // 存储相关
static const String _historyKey = 'translation_history'; static const String _historyKey = 'translation_history';
@ -29,75 +52,80 @@ class TranslationController extends GetxController {
final isRecognizing = false.obs; final isRecognizing = false.obs;
final isTranslating = false.obs; final isTranslating = false.obs;
final isTtsEnabled = true.obs; final isTtsEnabled = true.obs;
// 语言相关 // 语言相关
final sourceLanguage = '中文(简体)'.obs; final sourceLanguage = '中文(简体)'.obs;
final targetLanguage = '英语'.obs; final targetLanguage = '英语'.obs;
final sourceLanguageCode = 'zh-CN'.obs; final sourceLanguageCode = 'zh-CN'.obs;
final targetLanguageCode = 'en-US'.obs; final targetLanguageCode = 'en-US'.obs;
var detectedLanguageCode = ''; var detectedLanguageCode = '';
// 翻译历史和当前项 // 翻译历史和当前项
final translationHistory = <TranslationItem>[].obs; final translationHistory = <TranslationItem>[].obs;
String? currentSessionId; String? currentSessionId;
// 兼容旧版视图的计算属性 // 兼容旧版视图的计算属性
Rx<TranslationItem?> get currentItem { Rx<TranslationItem?> get currentItem {
if (translationHistory.isNotEmpty && translationHistory.last.isIntermediate) { if (translationHistory.isNotEmpty &&
translationHistory.last.isIntermediate) {
return Rx<TranslationItem?>(translationHistory.last); return Rx<TranslationItem?>(translationHistory.last);
} }
return Rx<TranslationItem?>(null); return Rx<TranslationItem?>(null);
} }
// UI相关 // UI相关
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
// 其他 // 其他
StreamSubscription? _recognitionSubscription; StreamSubscription? _recognitionSubscription;
StreamSubscription? _voiceInteractionSubscription; StreamSubscription? _voiceInteractionSubscription;
Timer? _translationDebounceTimer; Timer? _translationDebounceTimer;
// 获取支持的语言列表 // 获取支持的语言列表
Map<String, String> get supportedLanguages => _languageManager.getChineseNameToAsrCodeMap(); Map<String, String> get supportedLanguages =>
_languageManager.getChineseNameToAsrCodeMap();
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
jieli = Jieli();
//_setupEventListeners();
// 加载上次保存的语言设置 // 加载上次保存的语言设置
_loadSavedLanguages(); _loadSavedLanguages();
// 初始化语言代码 // 初始化语言代码
final sourceAsrCode = _languageManager.getAsrCodeByChineseName(sourceLanguage.value); final sourceAsrCode =
_languageManager.getAsrCodeByChineseName(sourceLanguage.value);
if (sourceAsrCode != null) { if (sourceAsrCode != null) {
sourceLanguageCode.value = sourceAsrCode; sourceLanguageCode.value = sourceAsrCode;
} }
final targetAsrCode = _languageManager.getAsrCodeByChineseName(targetLanguage.value); final targetAsrCode =
_languageManager.getAsrCodeByChineseName(targetLanguage.value);
if (targetAsrCode != null) { if (targetAsrCode != null) {
targetLanguageCode.value = targetAsrCode; targetLanguageCode.value = targetAsrCode;
} }
// 订阅原生服务事件 // 订阅原生服务事件
_subscribeToVoiceInteractionService(); _subscribeToVoiceInteractionService();
// 加载历史记录 // 加载历史记录
loadTranslationHistory(); loadTranslationHistory();
// 初始化服务 // 初始化服务
_initServices(); _initServices();
// 使用微任务在布局完成后自动滚动到底部 // 使用微任务在布局完成后自动滚动到底部
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom(); _scrollToBottom();
}); });
} }
/// 订阅原生服务事件 /// 订阅原生服务事件
void _subscribeToVoiceInteractionService() { void _subscribeToVoiceInteractionService() {
if (_voiceInteractionService.eventStream != null) { if (_voiceInteractionService.eventStream != null) {
_voiceInteractionSubscription = _voiceInteractionService.eventStream!.listen((event) { _voiceInteractionSubscription =
_voiceInteractionService.eventStream!.listen((event) {
if (event is RecognitionStartedEvent) { if (event is RecognitionStartedEvent) {
_handleVoiceInteractionStarted(); _handleVoiceInteractionStarted();
} }
@ -109,35 +137,54 @@ class TranslationController extends GetxController {
void _loadSavedLanguages() { void _loadSavedLanguages() {
final savedSourceLanguage = _storage.read(_sourceLanguageKey); final savedSourceLanguage = _storage.read(_sourceLanguageKey);
final savedTargetLanguage = _storage.read(_targetLanguageKey); final savedTargetLanguage = _storage.read(_targetLanguageKey);
if (savedSourceLanguage != null && supportedLanguages.containsKey(savedSourceLanguage)) { if (savedSourceLanguage != null &&
supportedLanguages.containsKey(savedSourceLanguage)) {
sourceLanguage.value = savedSourceLanguage; sourceLanguage.value = savedSourceLanguage;
} }
if (savedTargetLanguage != null && supportedLanguages.containsKey(savedTargetLanguage)) { if (savedTargetLanguage != null &&
supportedLanguages.containsKey(savedTargetLanguage)) {
targetLanguage.value = savedTargetLanguage; targetLanguage.value = savedTargetLanguage;
} }
} }
// 保存当前语言设置 // 保存当前语言设置
void _saveLanguageSettings() { void _saveLanguageSettings() {
_storage.write(_sourceLanguageKey, sourceLanguage.value); _storage.write(_sourceLanguageKey, sourceLanguage.value);
_storage.write(_targetLanguageKey, targetLanguage.value); _storage.write(_targetLanguageKey, targetLanguage.value);
} }
// /// 设置事件监听器
// void _setupEventListeners() {
// // 监听扫描结果
// _scanSubscription = jieli.scanResults.listen((devices) {
// scannedDevices.assignAll(devices);
// });
// // 监听唤醒信号
// _wakeupSubscription = jieli.wakeupStream.listen((wakeup) {
// wakeupReceived.value = true;
// Get.snackbar('收到唤醒信号', '耳机发送了唤醒信号');
// });
// }
// 初始化服务 // 初始化服务
Future<void> _initServices() async { Future<void> _initServices() async {
try { try {
await _translationService.initialize(); await _translationService.initialize();
// 初始化 ASR 服务时,明确指定需要支持的语言 // 初始化 ASR 服务时,明确指定需要支持的语言
final List<String> asrSupportedLanguages = [sourceLanguageCode.value, targetLanguageCode.value]; final List<String> asrSupportedLanguages = [
sourceLanguageCode.value,
targetLanguageCode.value
];
Logger.info('初始化ASR服务,支持语言: $asrSupportedLanguages'); Logger.info('初始化ASR服务,支持语言: $asrSupportedLanguages');
await _asrService.initialize( await _asrService.initialize(
supportedLanguages: asrSupportedLanguages, supportedLanguages: asrSupportedLanguages,
); );
Logger.info('所有服务初始化完成'); Logger.info('所有服务初始化完成');
} catch (e) { } catch (e) {
Logger.error('初始化服务失败: $e'); Logger.error('初始化服务失败: $e');
@ -159,20 +206,26 @@ class TranslationController extends GetxController {
// 开始语音识别 // 开始语音识别
Future<void> startRecognition() async { Future<void> startRecognition() async {
Logger.info('开始语音识别');
if (isRecognizing.value) return; if (isRecognizing.value) return;
_voiceInteractionService.pauseVoiceInteraction(); _voiceInteractionService.pauseVoiceInteraction();
try { try {
Logger.info('开始语音识别1');
// 每次开始识别前检查 ASR 服务是否已配置为当前语言对 // 每次开始识别前检查 ASR 服务是否已配置为当前语言对
final List<String> currentLanguages = [sourceLanguageCode.value, targetLanguageCode.value]; final List<String> currentLanguages = [
final bool needsReinit = !_asrService.supportedLanguages.toSet().containsAll(currentLanguages); sourceLanguageCode.value,
targetLanguageCode.value
];
final bool needsReinit =
!_asrService.supportedLanguages.toSet().containsAll(currentLanguages);
if (needsReinit) { if (needsReinit) {
Logger.info('识别开始前发现语言变更,重新初始化ASR服务'); Logger.info('识别开始前发现语言变更,重新初始化ASR服务');
await _reinitializeAsrService(); await _reinitializeAsrService();
} }
currentSessionId = DateTime.now().millisecondsSinceEpoch.toString(); currentSessionId = DateTime.now().millisecondsSinceEpoch.toString();
// // 初始化新的翻译项 // // 初始化新的翻译项
@ -186,17 +239,25 @@ class TranslationController extends GetxController {
// isFirstInSession: true, // isFirstInSession: true,
// isIntermediate: true, // isIntermediate: true,
// ); // );
// // 添加到历史记录 // // 添加到历史记录
// translationHistory.add(newItem); // translationHistory.add(newItem);
// 开始连续语音识别 // 开始连续语音识别
final recognitionStream = await _asrService.startContinuousRecognition(); final recognitionStream = await _asrService.startContinuousRecognition();
Logger.info('翻译开启,开始监');
// 监听音频数据
_audioSubscription = jieli.audioDataStream.listen((data) async {
// Logger.info('翻译开启,开始监听解码后的数据=${data}');
final pushAudioDataStream = await _asrService.pushAudioData(data);
});
isRecognizing.value = true; isRecognizing.value = true;
// 监听识别结果 // 监听识别结果
_recognitionSubscription?.cancel(); _recognitionSubscription?.cancel();
_recognitionSubscription = recognitionStream.listen(_handleRecognitionEvent); _recognitionSubscription =
recognitionStream.listen(_handleRecognitionEvent);
} catch (e) { } catch (e) {
isRecognizing.value = false; isRecognizing.value = false;
Logger.error('启动语音识别失败: $e'); Logger.error('启动语音识别失败: $e');
@ -205,10 +266,12 @@ class TranslationController extends GetxController {
// 处理识别事件 // 处理识别事件
void _handleRecognitionEvent(RecognitionEvent event) { void _handleRecognitionEvent(RecognitionEvent event) {
if (event.type == RecognitionEventType.finalResult && event.text.isNotEmpty) { if (event.type == RecognitionEventType.finalResult &&
event.text.isNotEmpty) {
detectedLanguageCode = event.detectedLanguage; detectedLanguageCode = event.detectedLanguage;
handleFinalResult(event.text); handleFinalResult(event.text);
} else if (event.type == RecognitionEventType.intermediateResult && event.text.isNotEmpty) { } else if (event.type == RecognitionEventType.intermediateResult &&
event.text.isNotEmpty) {
if (event.detectedLanguage.isNotEmpty) { if (event.detectedLanguage.isNotEmpty) {
detectedLanguageCode = event.detectedLanguage; detectedLanguageCode = event.detectedLanguage;
} }
@ -231,21 +294,25 @@ class TranslationController extends GetxController {
_recognitionSubscription?.cancel(); _recognitionSubscription?.cancel();
_recognitionSubscription = null; _recognitionSubscription = null;
isRecognizing.value = false; isRecognizing.value = false;
// 如果当前有中间结果,将其作为最终结果处理 // 如果当前有中间结果,将其作为最终结果处理
if (translationHistory.isNotEmpty && translationHistory.last.isIntermediate && translationHistory.last.sourceText.isNotEmpty) { if (translationHistory.isNotEmpty &&
translationHistory.last.isIntermediate &&
translationHistory.last.sourceText.isNotEmpty) {
// 标记为最终结果 // 标记为最终结果
translationHistory.last.isIntermediate = false; translationHistory.last.isIntermediate = false;
translationHistory.refresh(); translationHistory.refresh();
// 如果还没有翻译结果,尝试翻译 // 如果还没有翻译结果,尝试翻译
if (translationHistory.last.translatedText.isEmpty) { if (translationHistory.last.translatedText.isEmpty) {
await translateText(translationHistory.last.sourceText, translationHistory.last.timestamp, isFinal: true); await translateText(translationHistory.last.sourceText,
translationHistory.last.timestamp,
isFinal: true);
} }
saveTranslationHistory(); saveTranslationHistory();
} }
currentSessionId = null; currentSessionId = null;
} catch (e) { } catch (e) {
Logger.error('停止语音识别失败: $e'); Logger.error('停止语音识别失败: $e');
@ -265,10 +332,11 @@ class TranslationController extends GetxController {
targetLanguageCode: targetLanguageCode.value, targetLanguageCode: targetLanguageCode.value,
timestamp: DateTime.now(), timestamp: DateTime.now(),
sessionId: currentSessionId!, sessionId: currentSessionId!,
isFirstInSession: translationHistory.isEmpty || translationHistory.last.sessionId != currentSessionId, isFirstInSession: translationHistory.isEmpty ||
translationHistory.last.sessionId != currentSessionId,
isIntermediate: true, isIntermediate: true,
); );
translationHistory.add(newItem); translationHistory.add(newItem);
// 添加新项后滚动到底部 // 添加新项后滚动到底部
_scrollToBottom(); _scrollToBottom();
@ -283,8 +351,12 @@ class TranslationController extends GetxController {
// 防抖处理翻译请求 // 防抖处理翻译请求
_translationDebounceTimer?.cancel(); _translationDebounceTimer?.cancel();
_translationDebounceTimer = Timer(const Duration(milliseconds: 300), () { _translationDebounceTimer = Timer(const Duration(milliseconds: 300), () {
if (translationHistory.isNotEmpty && translationHistory.last.isIntermediate && translationHistory.last.sourceText.isNotEmpty) { if (translationHistory.isNotEmpty &&
translateText(translationHistory.last.sourceText, translationHistory.last.timestamp, isFinal: false); translationHistory.last.isIntermediate &&
translationHistory.last.sourceText.isNotEmpty) {
translateText(translationHistory.last.sourceText,
translationHistory.last.timestamp,
isFinal: false);
} }
}); });
} }
@ -301,7 +373,7 @@ class TranslationController extends GetxController {
// 更新后滚动到底部 // 更新后滚动到底部
_scrollToBottom(); _scrollToBottom();
saveTranslationHistory(); saveTranslationHistory();
// 使用项目的时间戳进行翻译 // 使用项目的时间戳进行翻译
translateText(text, translationHistory.last.timestamp, isFinal: true); translateText(text, translationHistory.last.timestamp, isFinal: true);
} else { } else {
@ -312,16 +384,17 @@ class TranslationController extends GetxController {
sourceLanguageCode: sourceLanguageCode.value, sourceLanguageCode: sourceLanguageCode.value,
targetLanguageCode: targetLanguageCode.value, targetLanguageCode: targetLanguageCode.value,
timestamp: DateTime.now(), timestamp: DateTime.now(),
sessionId: currentSessionId ?? DateTime.now().millisecondsSinceEpoch.toString(), sessionId: currentSessionId ??
isFirstInSession: translationHistory.isEmpty || translationHistory.last.sessionId != currentSessionId, DateTime.now().millisecondsSinceEpoch.toString(),
isFirstInSession: translationHistory.isEmpty ||
translationHistory.last.sessionId != currentSessionId,
isIntermediate: false, isIntermediate: false,
); );
translationHistory.add(newItem); translationHistory.add(newItem);
translationHistory.refresh(); translationHistory.refresh();
_scrollToBottom(); _scrollToBottom();
saveTranslationHistory(); saveTranslationHistory();
// 使用新项目的时间戳进行翻译 // 使用新项目的时间戳进行翻译
translateText(text, newItem.timestamp, isFinal: true); translateText(text, newItem.timestamp, isFinal: true);
@ -329,7 +402,8 @@ class TranslationController extends GetxController {
} }
// 翻译文本 // 翻译文本
Future<String?> translateText(String sourceText, DateTime timestamp, {bool isFinal = false}) async { Future<String?> translateText(String sourceText, DateTime timestamp,
{bool isFinal = false}) async {
if (sourceText.isEmpty) return null; if (sourceText.isEmpty) return null;
isTranslating.value = true; isTranslating.value = true;
@ -338,8 +412,10 @@ class TranslationController extends GetxController {
try { try {
// 根据检测到的语言确定源语言和目标语言 // 根据检测到的语言确定源语言和目标语言
final bool shouldSwap = detectedLanguageCode == sourceLanguageCode.value; final bool shouldSwap = detectedLanguageCode == sourceLanguageCode.value;
final detectedSourceLanguageCode = shouldSwap ? sourceLanguageCode.value : targetLanguageCode.value; final detectedSourceLanguageCode =
final detectedTargetLanguageCode = shouldSwap ? targetLanguageCode.value : sourceLanguageCode.value; shouldSwap ? sourceLanguageCode.value : targetLanguageCode.value;
final detectedTargetLanguageCode =
shouldSwap ? targetLanguageCode.value : sourceLanguageCode.value;
// 调用翻译服务 // 调用翻译服务
translationResult = await _translationService.translateText( translationResult = await _translationService.translateText(
@ -352,25 +428,28 @@ class TranslationController extends GetxController {
// 查找匹配的项目 - 优先使用timestamp查找 // 查找匹配的项目 - 优先使用timestamp查找
int index = -1; int index = -1;
index = translationHistory.indexWhere((item) => index = translationHistory
item.timestamp.isAtSameMomentAs(timestamp)); .indexWhere((item) => item.timestamp.isAtSameMomentAs(timestamp));
if (index != -1) { if (index != -1) {
// 更新历史项 // 更新历史项
translationHistory[index].translatedText = translationResult; translationHistory[index].translatedText = translationResult;
translationHistory[index].sourceLanguageCode = detectedSourceLanguageCode; translationHistory[index].sourceLanguageCode =
translationHistory[index].targetLanguageCode = detectedTargetLanguageCode; detectedSourceLanguageCode;
translationHistory[index].targetLanguageCode =
detectedTargetLanguageCode;
translationHistory.refresh(); translationHistory.refresh();
saveTranslationHistory(); saveTranslationHistory();
// 更新后滚动到底部 // 更新后滚动到底部
_scrollToBottom(); _scrollToBottom();
// 播放TTS - 只在最终结果时播放 // 播放TTS - 只在最终结果时播放
if (isTtsEnabled.value && isFinal) { if (isTtsEnabled.value && isFinal) {
// Logger.info('播放翻译文本: ${translationResult}'); // Logger.info('播放翻译文本: ${translationResult}');
await playTranslatedText(translationResult); await playTranslatedText(translationResult);
} }
} else { } else {
Logger.info('未找到匹配的历史项 ${sourceText} ${timestamp.toIso8601String() ?? "无时间戳"}'); Logger.info(
'未找到匹配的历史项 ${sourceText} ${timestamp.toIso8601String() ?? "无时间戳"}');
} }
} }
} catch (e) { } catch (e) {
@ -388,9 +467,12 @@ class TranslationController extends GetxController {
try { try {
final bool shouldSwap = detectedLanguageCode == targetLanguageCode.value; final bool shouldSwap = detectedLanguageCode == targetLanguageCode.value;
final detectedTargetLanguageCode = shouldSwap ? sourceLanguageCode.value : targetLanguageCode.value; final detectedTargetLanguageCode =
final voiceName = _languageManager.getTtsVoiceNameByAsrCode(detectedTargetLanguageCode) ?? 'en-US-AriaNeural'; shouldSwap ? sourceLanguageCode.value : targetLanguageCode.value;
final voiceName = _languageManager
.getTtsVoiceNameByAsrCode(detectedTargetLanguageCode) ??
'en-US-AriaNeural';
await _ttsService.setVoice(voiceName); await _ttsService.setVoice(voiceName);
await _ttsService.speakOnce(text); await _ttsService.speakOnce(text);
} catch (e) { } catch (e) {
@ -404,7 +486,7 @@ class TranslationController extends GetxController {
if (isRecognizing.value) { if (isRecognizing.value) {
await stopRecognition(); await stopRecognition();
} }
_translationDebounceTimer?.cancel(); _translationDebounceTimer?.cancel();
await _ttsService.stop(); await _ttsService.stop();
} catch (e) { } catch (e) {
@ -415,9 +497,11 @@ class TranslationController extends GetxController {
// 播放特定翻译项 // 播放特定翻译项
Future<void> playTranslationItem(TranslationItem item) async { Future<void> playTranslationItem(TranslationItem item) async {
if (item.translatedText.isEmpty) return; if (item.translatedText.isEmpty) return;
try { try {
final voiceName = _languageManager.getTtsVoiceNameByAsrCode(item.targetLanguageCode) ?? 'en-US-AriaNeural'; final voiceName =
_languageManager.getTtsVoiceNameByAsrCode(item.targetLanguageCode) ??
'en-US-AriaNeural';
await _ttsService.setVoice(voiceName); await _ttsService.setVoice(voiceName);
await _ttsService.speakOnce(item.translatedText); await _ttsService.speakOnce(item.translatedText);
} catch (e) { } catch (e) {
@ -428,12 +512,15 @@ class TranslationController extends GetxController {
// 播放文本 // 播放文本
Future<void> playText(String text, {String? languageCode}) async { Future<void> playText(String text, {String? languageCode}) async {
if (text.isEmpty) return; if (text.isEmpty) return;
try { try {
final voiceName = languageCode != null final voiceName = languageCode != null
? _languageManager.getTtsVoiceNameByAsrCode(languageCode) ?? 'en-US-AriaNeural' ? _languageManager.getTtsVoiceNameByAsrCode(languageCode) ??
: _languageManager.getTtsVoiceNameByAsrCode(targetLanguageCode.value) ?? 'en-US-AriaNeural'; 'en-US-AriaNeural'
: _languageManager
.getTtsVoiceNameByAsrCode(targetLanguageCode.value) ??
'en-US-AriaNeural';
await _ttsService.setVoice(voiceName); await _ttsService.setVoice(voiceName);
await _ttsService.speakOnce(text); await _ttsService.speakOnce(text);
} catch (e) { } catch (e) {
@ -443,7 +530,8 @@ class TranslationController extends GetxController {
// 滚动到底部 // 滚动到底部
void _scrollToBottom() { void _scrollToBottom() {
if (scrollController.hasClients && (translationHistory.isNotEmpty || currentSessionId != null)) { if (scrollController.hasClients &&
(translationHistory.isNotEmpty || currentSessionId != null)) {
try { try {
scrollController.animateTo( scrollController.animateTo(
scrollController.position.maxScrollExtent, scrollController.position.maxScrollExtent,
@ -460,33 +548,33 @@ class TranslationController extends GetxController {
void setupScrollListener() { void setupScrollListener() {
// 简化后的空方法,保留接口兼容性 // 简化后的空方法,保留接口兼容性
} }
// 底部控制栏高度 // 底部控制栏高度
double _bottomBarHeight = 0.0; double _bottomBarHeight = 0.0;
// 设置底部控制栏高度 // 设置底部控制栏高度
void setBottomBarHeight(double height) { void setBottomBarHeight(double height) {
_bottomBarHeight = height; _bottomBarHeight = height;
} }
// 获取底部控制栏高度 // 获取底部控制栏高度
double get bottomBarHeight => _bottomBarHeight; double get bottomBarHeight => _bottomBarHeight;
// 切换源语言和目标语言 // 切换源语言和目标语言
void swapLanguages() { void swapLanguages() {
stopAll(); stopAll();
final tempName = sourceLanguage.value; final tempName = sourceLanguage.value;
sourceLanguage.value = targetLanguage.value; sourceLanguage.value = targetLanguage.value;
targetLanguage.value = tempName; targetLanguage.value = tempName;
final tempCode = sourceLanguageCode.value; final tempCode = sourceLanguageCode.value;
sourceLanguageCode.value = targetLanguageCode.value; sourceLanguageCode.value = targetLanguageCode.value;
targetLanguageCode.value = tempCode; targetLanguageCode.value = tempCode;
// 保存语言设置 // 保存语言设置
_saveLanguageSettings(); _saveLanguageSettings();
// 更新ASR支持的语言并重新初始化 // 更新ASR支持的语言并重新初始化
_reinitializeAsrService(); _reinitializeAsrService();
} }
@ -495,17 +583,17 @@ class TranslationController extends GetxController {
void setSourceLanguage(String language) { void setSourceLanguage(String language) {
if (sourceLanguage.value != language) { if (sourceLanguage.value != language) {
stopAll(); stopAll();
sourceLanguage.value = language; sourceLanguage.value = language;
final asrCode = _languageManager.getAsrCodeByChineseName(language); final asrCode = _languageManager.getAsrCodeByChineseName(language);
if (asrCode != null) { if (asrCode != null) {
sourceLanguageCode.value = asrCode; sourceLanguageCode.value = asrCode;
} }
// 保存语言设置 // 保存语言设置
_saveLanguageSettings(); _saveLanguageSettings();
// 更新ASR支持的语言并重新初始化 // 更新ASR支持的语言并重新初始化
_reinitializeAsrService(); _reinitializeAsrService();
} }
@ -515,35 +603,38 @@ class TranslationController extends GetxController {
void setTargetLanguage(String language) { void setTargetLanguage(String language) {
if (targetLanguage.value != language) { if (targetLanguage.value != language) {
stopAll(); stopAll();
targetLanguage.value = language; targetLanguage.value = language;
final asrCode = _languageManager.getAsrCodeByChineseName(language); final asrCode = _languageManager.getAsrCodeByChineseName(language);
if (asrCode != null) { if (asrCode != null) {
targetLanguageCode.value = asrCode; targetLanguageCode.value = asrCode;
} }
// 保存语言设置 // 保存语言设置
_saveLanguageSettings(); _saveLanguageSettings();
// 更新ASR支持的语言并重新初始化 // 更新ASR支持的语言并重新初始化
_reinitializeAsrService(); _reinitializeAsrService();
} }
} }
// 重新初始化ASR服务以支持新的语言 // 重新初始化ASR服务以支持新的语言
Future<void> _reinitializeAsrService() async { Future<void> _reinitializeAsrService() async {
try { try {
// 更新ASR支持的语言 // 更新ASR支持的语言
final List<String> asrSupportedLanguages = [sourceLanguageCode.value, targetLanguageCode.value]; final List<String> asrSupportedLanguages = [
sourceLanguageCode.value,
targetLanguageCode.value
];
Logger.info('重新初始化ASR服务,支持语言: $asrSupportedLanguages'); Logger.info('重新初始化ASR服务,支持语言: $asrSupportedLanguages');
// 重新初始化ASR服务 // 重新初始化ASR服务
await _asrService.initialize( await _asrService.initialize(
supportedLanguages: asrSupportedLanguages, supportedLanguages: asrSupportedLanguages,
); );
Logger.info('ASR服务重新初始化成功'); Logger.info('ASR服务重新初始化成功');
} catch (e) { } catch (e) {
Logger.error('重新初始化ASR服务失败: $e'); Logger.error('重新初始化ASR服务失败: $e');
@ -556,15 +647,17 @@ class TranslationController extends GetxController {
if (historyJson != null) { if (historyJson != null) {
try { try {
final List<dynamic> historyList = jsonDecode(historyJson); final List<dynamic> historyList = jsonDecode(historyJson);
final List<TranslationItem> allHistory = historyList.map((item) => TranslationItem.fromJson(item)).toList(); final List<TranslationItem> allHistory =
historyList.map((item) => TranslationItem.fromJson(item)).toList();
// 只保留最新的10条记录 // 只保留最新的10条记录
if (allHistory.length > _maxHistoryItems) { if (allHistory.length > _maxHistoryItems) {
translationHistory.value = allHistory.sublist(allHistory.length - _maxHistoryItems); translationHistory.value =
allHistory.sublist(allHistory.length - _maxHistoryItems);
} else { } else {
translationHistory.value = allHistory; translationHistory.value = allHistory;
} }
// 使用微任务确保在视图完全渲染后滚动到底部 // 使用微任务确保在视图完全渲染后滚动到底部
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom(); _scrollToBottom();
@ -579,11 +672,13 @@ class TranslationController extends GetxController {
// 保存翻译历史 // 保存翻译历史
void saveTranslationHistory() { void saveTranslationHistory() {
if (translationHistory.length > _maxHistoryItems) { if (translationHistory.length > _maxHistoryItems) {
translationHistory.removeRange(0, translationHistory.length - _maxHistoryItems); translationHistory.removeRange(
0, translationHistory.length - _maxHistoryItems);
} }
try { try {
final List<Map<String, dynamic>> historyJson = translationHistory.map((item) => item.toJson()).toList(); final List<Map<String, dynamic>> historyJson =
translationHistory.map((item) => item.toJson()).toList();
_storage.write(_historyKey, jsonEncode(historyJson)); _storage.write(_historyKey, jsonEncode(historyJson));
} catch (e) { } catch (e) {
Logger.error('保存翻译历史失败: $e'); Logger.error('保存翻译历史失败: $e');
@ -644,7 +739,8 @@ class TranslationItem {
sourceLanguageCode: json['sourceLanguageCode'] ?? 'zh-CN', sourceLanguageCode: json['sourceLanguageCode'] ?? 'zh-CN',
targetLanguageCode: json['targetLanguageCode'] ?? 'en-US', targetLanguageCode: json['targetLanguageCode'] ?? 'en-US',
timestamp: timestamp, timestamp: timestamp,
sessionId: json['sessionId'] ?? DateTime.now().millisecondsSinceEpoch.toString(), sessionId:
json['sessionId'] ?? DateTime.now().millisecondsSinceEpoch.toString(),
isFirstInSession: json['isFirstInSession'] ?? false, isFirstInSession: json['isFirstInSession'] ?? false,
isIntermediate: false, // 从存储加载的都是最终结果 isIntermediate: false, // 从存储加载的都是最终结果
); );
@ -662,16 +758,18 @@ class TranslationItem {
// 不保存isIntermediate,因为存储的都是最终结果 // 不保存isIntermediate,因为存储的都是最终结果
}; };
} }
// 获取源语言的中文名称 // 获取源语言的中文名称
String get sourceLanguageName { String get sourceLanguageName {
final languageManager = Get.find<LanguageManager>(); final languageManager = Get.find<LanguageManager>();
return languageManager.getChineseNameByAsrCode(sourceLanguageCode) ?? '未知语言'; return languageManager.getChineseNameByAsrCode(sourceLanguageCode) ??
'未知语言';
} }
// 获取目标语言的中文名称 // 获取目标语言的中文名称
String get targetLanguageName { String get targetLanguageName {
final languageManager = Get.find<LanguageManager>(); final languageManager = Get.find<LanguageManager>();
return languageManager.getChineseNameByAsrCode(targetLanguageCode) ?? '未知语言'; return languageManager.getChineseNameByAsrCode(targetLanguageCode) ??
'未知语言';
} }
} }

7
lib/routes/app_pages.dart

@ -35,6 +35,8 @@ import '../modules/settings/views/settings_view.dart';
import '../modules/settings/bindings/settings_binding.dart'; import '../modules/settings/bindings/settings_binding.dart';
import '../modules/jieli_test/views/jieli_test_view.dart'; import '../modules/jieli_test/views/jieli_test_view.dart';
import '../modules/jieli_test/bindings/jieli_test_binding.dart'; import '../modules/jieli_test/bindings/jieli_test_binding.dart';
import '../modules/opus_test/views/opus_test_view.dart';
import '../modules/opus_test/bindings/opus_test_binding.dart';
abstract class AppPages { abstract class AppPages {
static final pages = [ static final pages = [
@ -133,5 +135,10 @@ abstract class AppPages {
page: () => const JieliTestView(), page: () => const JieliTestView(),
binding: JieliTestBinding(), binding: JieliTestBinding(),
), ),
GetPage(
name: Routes.opusTest,
page: () => const OpusTestView(),
binding: OpusTestBinding(),
),
]; ];
} }

1
lib/routes/app_routes.dart

@ -26,4 +26,5 @@ abstract class Routes {
static const editProfile = '/edit_profile'; static const editProfile = '/edit_profile';
static const feedback = '/feedback'; static const feedback = '/feedback';
static const jieliTest = '/jieli_test'; static const jieliTest = '/jieli_test';
static const opusTest = '/opus_test';
} }

36
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -28,12 +28,17 @@ class AzureAsrHelper(private val context: Context) {
private var supportedLanguages = arrayOf("zh-CN") private var supportedLanguages = arrayOf("zh-CN")
// 是否使用回音消除 - 内部控制常量 // 是否使用回音消除 - 内部控制常量
private val useEchoCancellation = false private val useEchoCancellation = true
// 自定义音频处理相关 // 自定义音频处理相关
private var customAudioProcessor: CustomAudioProcessor? = null private var customAudioProcessor: CustomAudioProcessor? = null
private var pushStream: PushAudioInputStream? = null private var pushStream: PushAudioInputStream? = null
private var audioConfig: AudioConfig? = null private var audioConfig: AudioConfig? = null
// 公共方法:允许外部写入音频数据
fun pushAudioData(data: ByteArray) {
FileLogger.d(TAG, "pushAudioData,data=${data},pushStream=${pushStream}")
pushStream?.write(data) // 使用 SDK 的正确方法(如 push)
}
// 初始化SDK并创建recognizer // 初始化SDK并创建recognizer
fun initialize(subscriptionKey: String, region: String, fun initialize(subscriptionKey: String, region: String,
@ -81,6 +86,7 @@ class AzureAsrHelper(private val context: Context) {
// 创建识别器 // 创建识别器
try { try {
if (useEchoCancellation) { if (useEchoCancellation) {
FileLogger.d(TAG, "如果使用回音消除,创建自定义音频输入流")
// 如果使用回音消除,创建自定义音频输入流 // 如果使用回音消除,创建自定义音频输入流
setupCustomAudioProcessing() setupCustomAudioProcessing()
@ -91,13 +97,13 @@ class AzureAsrHelper(private val context: Context) {
recognizer = SpeechRecognizer(speechConfig, audioConfig) recognizer = SpeechRecognizer(speechConfig, audioConfig)
} }
} else { } else {
// 使用默认麦克风输入 // // 使用默认麦克风输入
if (isAutoDetectLanguage) { // if (isAutoDetectLanguage) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList()) // val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig) // recognizer = SpeechRecognizer(speechConfig, autoDetectConfig)
} else { // } else {
recognizer = SpeechRecognizer(speechConfig) // recognizer = SpeechRecognizer(speechConfig)
} // }
} }
FileLogger.d(TAG, "Azure 语音服务初始化成功") FileLogger.d(TAG, "Azure 语音服务初始化成功")
@ -136,13 +142,13 @@ class AzureAsrHelper(private val context: Context) {
recognizer = SpeechRecognizer(speechConfig, audioConfig) recognizer = SpeechRecognizer(speechConfig, audioConfig)
} }
} else { } else {
// 使用默认麦克风输入 // // 使用默认麦克风输入
if (isAutoDetectLanguage) { // if (isAutoDetectLanguage) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList()) // val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig) // recognizer = SpeechRecognizer(speechConfig, autoDetectConfig)
} else { // } else {
recognizer = SpeechRecognizer(speechConfig) // recognizer = SpeechRecognizer(speechConfig)
} // }
} }
return true return true
} else { } else {

16
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt

@ -4,6 +4,7 @@ import android.content.Context
import android.app.Activity import android.app.Activity
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log
import androidx.annotation.NonNull import androidx.annotation.NonNull
import com.yunqiinnovation.azure_speech.utils.FileLogger import com.yunqiinnovation.azure_speech.utils.FileLogger
@ -91,6 +92,21 @@ class AzureSpeechPlugin: FlutterPlugin {
} catch (e: Exception) { } catch (e: Exception) {
result.error("INITIALIZATION_ERROR", e.message, null) result.error("INITIALIZATION_ERROR", e.message, null)
} }
}
"pushAudioData" -> {
val audioData = call.argument<ByteArray>("AudioData")
try {
Log.d("","AsrMethodHandler,pushAudioData${audioData}")
if (audioData != null) {
val success = azureAsrHelper.pushAudioData(audioData)
result.success(success)
}
} catch (e: Exception) {
result.error("pushAudioData_ERROR", e.message, null)
}
} }
"recognizeOnce" -> { "recognizeOnce" -> {
azureAsrHelper.recognizeOnce(object : AzureAsrHelper.RecognizeCallback { azureAsrHelper.recognizeOnce(object : AzureAsrHelper.RecognizeCallback {

5
local_plugins/chat_storage/lib/chat_storage.dart

@ -77,6 +77,7 @@ class ChatStorage {
'sessionId': sessionId, 'sessionId': sessionId,
'message': message, 'message': message,
'sender': sender, 'sender': sender,
'metadata': metadata,
}); });
return Map<String, dynamic>.from(result); return Map<String, dynamic>.from(result);
} catch (e) { } catch (e) {
@ -99,7 +100,7 @@ class ChatStorage {
final Map<String, dynamic> data = jsonDecode(result); final Map<String, dynamic> data = jsonDecode(result);
final List<dynamic> messagesList = data['messages']; final List<dynamic> messagesList = data['messages'];
List<ChatMessage> messages = messagesList.map((item) { List<ChatMessage> messages = messagesList.map((item) {
return ChatMessage.fromMap(Map<String, dynamic>.from(item)); return ChatMessage.fromMap(Map<String, dynamic>.from(item));
}).toList(); }).toList();
@ -151,4 +152,4 @@ class ChatStorage {
return {'success': false, 'error': e.toString()}; return {'success': false, 'error': e.toString()};
} }
} }
} }

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

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

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

@ -10,7 +10,13 @@ import android.util.Log
import androidx.lifecycle.LifecycleService import androidx.lifecycle.LifecycleService
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.util.* import java.util.*
import com.yunqiinnovation.azure_speech.AzureAsrHelper
import com.yunqiinnovation.azure_speech.AzureTtsHelper
import com.yunqiinnovation.jl_opus.JlOpusPlugin
/** /**
* BLE服务类,提供核心蓝牙功能 * BLE服务类,提供核心蓝牙功能
@ -18,26 +24,31 @@ import java.util.*
class BleService(private val context: Context) { class BleService(private val context: Context) {
companion object { companion object {
private const val TAG = "JieliBleService" private const val TAG = "JieliBleService"
// 服务和特征UUID - 根据协议规范重新定义 // 服务和特征UUID - 根据协议规范重新定义
private const val AUDIO_SERVICE_UUID = "0000faa0-0000-1000-8000-00805f9b34fb" // 主服务UUID // private const val AUDIO_SERVICE_UUID = "0000faa0-0000-1000-8000-00805f9b34fb" // 主服务UUID
private const val NOTIFY_CHAR_UUID = "0000faa2-0000-1000-8000-00805f9b34fb" // 用于接收数据的特征(notify) private const val AUDIO_SERVICE_UUID = "0000abc0-0000-1111-2222-123456789abc" // 主服务UUID
private const val WRITE_CHAR_UUID = "0000faa1-0000-1000-8000-00805f9b34fb" // 用于发送命令的特征(write without response) private const val NOTIFY_CHAR_UUID =
private const val CLIENT_CHAR_CONFIG_UUID = "00002902-0000-1000-8000-00805f9b34fb" // 标准的客户端特征配置描述符 "0000abc2-0000-1111-2222-123456789abc" // 用于接收数据的特征(notify)
private const val WRITE_CHAR_UUID =
"0000abc1-0000-1111-2222-123456789abc" // 用于发送命令的特征(write without response)
private const val CLIENT_CHAR_CONFIG_UUID =
"00002902-0000-1000-8000-00805f9b34fb" // 标准的客户端特征配置描述符
// 连接状态常量 // 连接状态常量
const val STATE_DISCONNECTED = 0 const val STATE_DISCONNECTED = 0
const val STATE_CONNECTING = 1 const val STATE_CONNECTING = 1
const val STATE_CONNECTED = 2 const val STATE_CONNECTED = 2
const val STATE_DISCONNECTING = 3 const val STATE_DISCONNECTING = 3
const val STATE_ERROR = 4 const val STATE_ERROR = 4
// 扫描结果更新间隔(毫秒) // 扫描结果更新间隔(毫秒)
private const val SCAN_RESULT_UPDATE_INTERVAL = 500L private const val SCAN_RESULT_UPDATE_INTERVAL = 500L
// 命令类型常量 // 命令类型常量
const val COMMAND_VOLUME = 1 const val COMMAND_VOLUME = 1
} }
/** /**
* 回调接口 * 回调接口
*/ */
@ -48,33 +59,44 @@ class BleService(private val context: Context) {
fun onWakeupSignalReceived() fun onWakeupSignalReceived()
fun onCommandResult(commandType: Int, success: Boolean) fun onCommandResult(commandType: Int, success: Boolean)
} }
// region 文件相关
private var outputFile: File? = null
private var fos: FileOutputStream? = null
// 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 private val bluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
private val bluetoothLeScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner private val bluetoothLeScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner
// GATT连接和特征 // GATT连接和特征
private var bluetoothGatt: BluetoothGatt? = null private var bluetoothGatt: BluetoothGatt? = null
private var notifyChar: BluetoothGattCharacteristic? = null private var notifyChar: BluetoothGattCharacteristic? = null
private var writeChar: BluetoothGattCharacteristic? = null private var writeChar: BluetoothGattCharacteristic? = null
// 扫描相关 // 扫描相关
private val scanHandler = Handler(Looper.getMainLooper()) private val scanHandler = Handler(Looper.getMainLooper())
private val scanResults = ArrayList<ScanResult>() private val scanResults = ArrayList<ScanResult>()
private var isScanning = false private var isScanning = false
private var isPendingScanResultUpdate = false private var isPendingScanResultUpdate = false
private val scanResultUpdateRunnable = Runnable { private val scanResultUpdateRunnable = Runnable {
notifyScanResults() notifyScanResults()
isPendingScanResultUpdate = false isPendingScanResultUpdate = false
} }
// 连接状态LiveData // 连接状态LiveData
val connectionState = MutableLiveData<Int>(STATE_DISCONNECTED) val connectionState = MutableLiveData<Int>(STATE_DISCONNECTED)
// 统一回调接口 // 统一回调接口
var callback: Callback? = null var callback: Callback? = null
/** /**
* 开始扫描BLE设备 * 开始扫描BLE设备
*/ */
@ -82,32 +104,35 @@ class BleService(private val context: Context) {
if (bluetoothLeScanner == null || isScanning) { if (bluetoothLeScanner == null || isScanning) {
return false return false
} }
Log.i(TAG, "开始扫描BLE设备...") Log.i(TAG, "开始扫描BLE设备...")
scanResults.clear() scanResults.clear()
val serviceUuid = UUID.fromString(AUDIO_SERVICE_UUID) val serviceUuid = UUID.fromString(AUDIO_SERVICE_UUID)
val scanFilter = ScanFilter.Builder() val scanFilter = ScanFilter.Builder()
// .setServiceUuid(ParcelUuid(serviceUuid)) // .setServiceUuid(ParcelUuid(serviceUuid))
.build() .build()
val scanFilters = listOf(scanFilter) val scanFilters = listOf(scanFilter)
val scanSettings = ScanSettings.Builder() val scanSettings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build() .build()
bluetoothLeScanner.startScan(scanFilters, scanSettings, scanCallback) bluetoothLeScanner.startScan(scanFilters, scanSettings, scanCallback)
isScanning = true isScanning = true
// 设置超时停止扫描 // 设置超时停止扫描
scanHandler.postDelayed({ scanHandler.postDelayed({
Log.d(TAG, "定时器触发,准备停止扫描 [主线程:${Looper.myLooper() == Looper.getMainLooper()}]") Log.d(
TAG,
"定时器触发,准备停止扫描 [主线程:${Looper.myLooper() == Looper.getMainLooper()}]"
)
stopScan() stopScan()
}, scanTimeout) }, scanTimeout)
return true return true
} }
/** /**
* 停止扫描 * 停止扫描
*/ */
@ -117,17 +142,17 @@ class BleService(private val context: Context) {
if (bluetoothLeScanner == null || !isScanning) { if (bluetoothLeScanner == null || !isScanning) {
return false return false
} }
bluetoothLeScanner.stopScan(scanCallback) bluetoothLeScanner.stopScan(scanCallback)
scanHandler.removeCallbacksAndMessages(null) scanHandler.removeCallbacksAndMessages(null)
isScanning = false isScanning = false
// 最后一次发送扫描结果 // 最后一次发送扫描结果
notifyScanResults() notifyScanResults()
return true return true
} }
/** /**
* 扫描回调 * 扫描回调
*/ */
@ -137,30 +162,54 @@ class BleService(private val context: Context) {
val deviceName = device.name ?: "未知设备" val deviceName = device.name ?: "未知设备"
val deviceAddress = device.address val deviceAddress = device.address
val rssi = result.rssi val rssi = result.rssi
val scanRecord = result.scanRecord
// Log.d(TAG, "发现设备: $deviceName [$deviceAddress], RSSI: $rssi") if (!deviceName.contains("zxd")) {
return
}
Log.d(TAG, "============发现设备: $deviceName [$deviceAddress], RSSI: $rssi")
// 打印所有发现的UUID以便调试
val serviceUuids = scanRecord?.serviceUuids
if (serviceUuids != null) {
for (uuid in serviceUuids) {
Log.d(TAG, "设备UUID: ${uuid.uuid}")
}
}
// 检查制造商数据
for (i in 0 until 65535) {
val manufacturerData = scanRecord?.getManufacturerSpecificData(i)
if (manufacturerData != null) {
// Log.d(TAG, "发现厂商ID: $i, 数据长度: ${manufacturerData.size}")
// 这里可以根据已知的厂商ID和格式解析数据
}
}
// 避免重复添加 // 避免重复添加
val existingIndex = scanResults.indexOfFirst { it.device.address == deviceAddress } val existingIndex = scanResults.indexOfFirst { it.device.address == deviceAddress }
if (existingIndex >= 0) { if (existingIndex >= 0) {
scanResults[existingIndex] = result scanResults[existingIndex] = result
} else { } else {
Log.d(TAG, "扫描回调A, scanResults[existingIndex]ADD")
scanResults.add(result) scanResults.add(result)
} }
// 如果没有待处理的更新,安排一个 // 如果没有待处理的更新,安排一个
if (!isPendingScanResultUpdate) { if (!isPendingScanResultUpdate) {
isPendingScanResultUpdate = true isPendingScanResultUpdate = true
scanHandler.postDelayed(scanResultUpdateRunnable, SCAN_RESULT_UPDATE_INTERVAL) scanHandler.postDelayed(scanResultUpdateRunnable, SCAN_RESULT_UPDATE_INTERVAL)
} }
} }
override fun onScanFailed(errorCode: Int) { override fun onScanFailed(errorCode: Int) {
Log.e(TAG, "扫描失败,错误码: $errorCode") Log.e(TAG, "扫描失败,错误码: $errorCode")
isScanning = false isScanning = false
} }
} }
/** /**
* 将扫描结果通知回调 * 将扫描结果通知回调
*/ */
@ -175,7 +224,7 @@ class BleService(private val context: Context) {
callback?.onScanResult(devicesJson) callback?.onScanResult(devicesJson)
} }
/** /**
* 连接到指定设备 * 连接到指定设备
*/ */
@ -185,24 +234,25 @@ class BleService(private val context: Context) {
updateConnectionState(STATE_ERROR) updateConnectionState(STATE_ERROR)
return return
} }
// 先断开现有连接 // 先断开现有连接
bluetoothGatt?.close() bluetoothGatt?.close()
bluetoothGatt = null bluetoothGatt = null
try { try {
val device = bluetoothAdapter.getRemoteDevice(deviceAddress) val device = bluetoothAdapter.getRemoteDevice(deviceAddress)
updateConnectionState(STATE_CONNECTING) updateConnectionState(STATE_CONNECTING)
// 连接GATT服务器 // 连接GATT服务器
Log.i(TAG, "正在连接设备: ${device.name ?: "未知设备"} [$deviceAddress]") Log.i(TAG, "正在连接设备: ${device.name ?: "未知设备"} [$deviceAddress]")
bluetoothGatt = device.connectGatt(context, false, gattCallback) bluetoothGatt =
device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "连接设备时发生错误: ${e.message}") Log.e(TAG, "连接设备时发生错误: ${e.message}")
updateConnectionState(STATE_ERROR) updateConnectionState(STATE_ERROR)
} }
} }
/** /**
* 断开连接 * 断开连接
*/ */
@ -210,7 +260,7 @@ class BleService(private val context: Context) {
bluetoothGatt?.disconnect() bluetoothGatt?.disconnect()
updateConnectionState(STATE_DISCONNECTING) updateConnectionState(STATE_DISCONNECTING)
} }
/** /**
* 更新连接状态并通知回调 * 更新连接状态并通知回调
*/ */
@ -218,22 +268,24 @@ class BleService(private val context: Context) {
connectionState.postValue(state) connectionState.postValue(state)
callback?.onConnectionStateChanged(state) callback?.onConnectionStateChanged(state)
} }
/** /**
* GATT回调 * GATT回调
*/ */
private val gattCallback = object : BluetoothGattCallback() { private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) { if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) {
Log.i(TAG, "设备已连接,开始发现服务...") Log.i(TAG, "设备已连接,开始发现服务...")
updateConnectionState(STATE_CONNECTED) updateConnectionState(STATE_CONNECTED)
// 开始搜索服务 // 开始搜索服务
gatt.discoverServices() gatt.discoverServices()
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) { } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.i(TAG, "设备已断开连接") Log.i(TAG, "设备已断开连接")
updateConnectionState(STATE_DISCONNECTED) updateConnectionState(STATE_DISCONNECTED)
// 清理资源 // 清理资源
gatt.close() gatt.close()
bluetoothGatt = null bluetoothGatt = null
@ -242,33 +294,44 @@ class BleService(private val context: Context) {
} else if (status != BluetoothGatt.GATT_SUCCESS) { } else if (status != BluetoothGatt.GATT_SUCCESS) {
Log.e(TAG, "连接失败,status=$status") Log.e(TAG, "连接失败,status=$status")
updateConnectionState(STATE_ERROR) updateConnectionState(STATE_ERROR)
gatt.close() gatt.close()
bluetoothGatt = null bluetoothGatt = null
} }
} }
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) { if (status == BluetoothGatt.GATT_SUCCESS) {
Log.i(TAG, "服务发现成功") Log.i(TAG, "服务发现成功")
// 查找目标服务 // 查找目标服务
val audioService = gatt.getService(UUID.fromString(AUDIO_SERVICE_UUID)) val audioService = gatt.getService(UUID.fromString(AUDIO_SERVICE_UUID))
if (audioService != null) { if (audioService != null) {
Log.i(TAG, "找到音频服务") Log.i(TAG, "找到音频服务,开始遍历特征...")
audioService.characteristics.forEach { char ->
Log.i(TAG, "特征UUID: ${char.uuid}")
}
Log.i(TAG, "找到音频服务$")
// 获取特征 // 获取特征
notifyChar = audioService.getCharacteristic(UUID.fromString(NOTIFY_CHAR_UUID)) notifyChar = audioService.getCharacteristic(UUID.fromString(NOTIFY_CHAR_UUID))
writeChar = audioService.getCharacteristic(UUID.fromString(WRITE_CHAR_UUID)) writeChar = audioService.getCharacteristic(UUID.fromString(WRITE_CHAR_UUID))
if (notifyChar != null && writeChar != null) { if (notifyChar != null && writeChar != null) {
Log.i(TAG, "找到所需特征") Log.i(TAG, "找到所需特征")
// 设置特征写入类型为无响应模式 // 设置特征写入类型为无响应模式
writeChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE writeChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
// 注册特征通知 // 注册特征通知
setupNotifications(gatt) setupNotifications(gatt)
opusPlugin?.startOpusDecodeStream(false, 1, 16000, 40, object :
JlOpusPlugin.Callback1 {
override fun onAudioDataReceived1(pcmData: ByteArray) {
// Log.i(TAG, "收到解码控制通知: ${pcmData.contentToString()}")
callback?.onAudioDataReceived(pcmData)
// 处理解码后的 PCM 数据(如播放或写入文件)
}})
} else { } else {
Log.e(TAG, "未找到所需特征") Log.e(TAG, "未找到所需特征")
updateConnectionState(STATE_ERROR) updateConnectionState(STATE_ERROR)
@ -282,43 +345,66 @@ class BleService(private val context: Context) {
updateConnectionState(STATE_ERROR) updateConnectionState(STATE_ERROR)
} }
} }
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { // 添加扩展函数(在文件顶部声明)
fun List<Byte>.toByteArray(): ByteArray {
return ByteArray(this.size) { this[it] }
}
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
val characteristicUuid = characteristic.uuid.toString() val characteristicUuid = characteristic.uuid.toString()
Log.d(TAG, "收到特征变化通知: $characteristicUuid") // Log.d(TAG, "收到特征变化通知: $characteristicUuid")
when (characteristicUuid) { when (characteristicUuid) {
// NOTIFY_CHAR_UUID -> { // NOTIFY_CHAR_UUID -> {
// // 收到音频数据 // // 收到音频数据
// val audioData = characteristic.value // val audioData = characteristic.value
// Log.d(TAG, "收到音频数据帧,大小: ${audioData.size} 字节") // Log.d(TAG, "收到音频数据帧,大小: ${audioData.size} 字节")
// callback?.onAudioDataReceived(audioData) // callback?.onAudioDataReceived(audioData)
// } // }
NOTIFY_CHAR_UUID -> { NOTIFY_CHAR_UUID -> {
// 处理控制消息 // 处理控制消息
val value = characteristic.value val value = characteristic.value
Log.i(TAG, "收到控制通知: ${value.contentToString()}") // Log.i(TAG, "收到控制通知: ${value.size}")
// 判断是否收到音频数据
if (value.isNotEmpty() && (value[0].toInt() and 0xFF == 0x4b || value[0].toInt() and 0xFF == 0x48)) {
// 处理设备返回的协议数据
fos?.write(value)
opusPlugin?.handleDecodeRequest(value)
}
// 判断是否是协议格式响应 // 判断是否是协议格式响应
if (value.isNotEmpty() && value[0].toInt() and 0xFF == 0xA5) { if (value.isNotEmpty() && value[0].toInt() and 0xFF == 0xA5) {
// 处理设备返回的协议数据 // 处理设备返回的协议数据
processResponseData(value) processResponseData(value)
} }
// 假设0x01表示唤醒信号 // 假设0x01表示唤醒信号
else if (value.isNotEmpty() && value[0].toInt() == 0x01) { else if (value.isNotEmpty() && value[0].toInt() == 0x01) {
Log.i(TAG, "收到唤醒信号") Log.i(TAG, "收到唤醒信号")
callback?.onWakeupSignalReceived() callback?.onWakeupSignalReceived()
// 收到唤醒后自动发送开始录音命令 // 收到唤醒后自动发送开始录音命令
sendStartRecordingCommand() sendStartRecordingCommand()
} }
} }
} }
} }
override fun onCharacteristicWrite(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
if (characteristic.uuid.toString() == WRITE_CHAR_UUID) { if (characteristic.uuid.toString() == WRITE_CHAR_UUID) {
if (status == BluetoothGatt.GATT_SUCCESS) { if (status == BluetoothGatt.GATT_SUCCESS) {
Log.i(TAG, "命令发送成功: ${characteristic.value.contentToString()}") Log.i(TAG, "命令发送成功: ${characteristic.value.contentToString()}")
@ -327,8 +413,12 @@ class BleService(private val context: Context) {
} }
} }
} }
override fun onDescriptorWrite(gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int) { override fun onDescriptorWrite(
gatt: BluetoothGatt,
descriptor: BluetoothGattDescriptor,
status: Int
) {
if (status == BluetoothGatt.GATT_SUCCESS) { if (status == BluetoothGatt.GATT_SUCCESS) {
Log.i(TAG, "描述符写入成功: ${descriptor.uuid}") Log.i(TAG, "描述符写入成功: ${descriptor.uuid}")
Log.i(TAG, "特征通知设置成功,设备就绪") Log.i(TAG, "特征通知设置成功,设备就绪")
@ -338,7 +428,7 @@ class BleService(private val context: Context) {
} }
} }
} }
/** /**
* 设置特征通知 * 设置特征通知
*/ */
@ -346,10 +436,10 @@ class BleService(private val context: Context) {
// 启用 notify 特征通知 // 启用 notify 特征通知
if (notifyChar?.properties?.and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) { if (notifyChar?.properties?.and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) {
gatt.setCharacteristicNotification(notifyChar, true) gatt.setCharacteristicNotification(notifyChar, true)
val descriptor = notifyChar?.getDescriptor(UUID.fromString(CLIENT_CHAR_CONFIG_UUID)) val descriptor = notifyChar?.getDescriptor(UUID.fromString(CLIENT_CHAR_CONFIG_UUID))
descriptor?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE descriptor?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
if (descriptor != null) { if (descriptor != null) {
gatt.writeDescriptor(descriptor) gatt.writeDescriptor(descriptor)
Log.i(TAG, "设置数据通知特征通知") Log.i(TAG, "设置数据通知特征通知")
@ -358,10 +448,11 @@ class BleService(private val context: Context) {
Log.e(TAG, "数据通知特征不支持通知") Log.e(TAG, "数据通知特征不支持通知")
} }
} }
/** /**
* 处理设备返回的数据 * 处理设备返回的数据
* 协议格式: * 协议格式:
* 协议头(0xA5) + 字段类型(1byte) + 数据长度(1byte) + 数据域(N byte) + 校验和(1byte) * 协议头(0xA5) + 字段类型(1byte) + 数据长度(1byte) + 数据域(N byte) + 校验和(1byte)
*/ */
private fun processResponseData(data: ByteArray) { private fun processResponseData(data: ByteArray) {
@ -369,36 +460,36 @@ class BleService(private val context: Context) {
Log.e(TAG, "返回数据格式错误,长度不足") Log.e(TAG, "返回数据格式错误,长度不足")
return return
} }
// 判断协议头 // 判断协议头
if (data[0].toInt() and 0xFF != 0xA5) { if (data[0].toInt() and 0xFF != 0xA5) {
Log.e(TAG, "返回数据协议头错误: ${data[0].toInt() and 0xFF}") Log.e(TAG, "返回数据协议头错误: ${data[0].toInt() and 0xFF}")
return return
} }
val fieldType = data[1].toInt() and 0xFF val fieldType = data[1].toInt() and 0xFF
val dataLen = data[2].toInt() and 0xFF val dataLen = data[2].toInt() and 0xFF
// 验证长度 // 验证长度
if (data.size != dataLen + 4) { // 协议头+字段类型+数据长度+数据域+校验和 if (data.size != dataLen + 4) { // 协议头+字段类型+数据长度+数据域+校验和
Log.e(TAG, "返回数据长度不匹配: 应为${dataLen + 4},实际为${data.size}") Log.e(TAG, "返回数据长度不匹配: 应为${dataLen + 4},实际为${data.size}")
return return
} }
// 计算校验和 // 计算校验和
var checksum = 0 var checksum = 0
for (i in 0 until data.size - 1) { for (i in 0 until data.size - 1) {
checksum += data[i].toInt() and 0xFF checksum += data[i].toInt() and 0xFF
} }
checksum = checksum and 0xFF checksum = checksum and 0xFF
val receivedChecksum = data[data.size - 1].toInt() and 0xFF val receivedChecksum = data[data.size - 1].toInt() and 0xFF
if (checksum != receivedChecksum) { if (checksum != receivedChecksum) {
Log.e(TAG, "校验和错误: 计算值=$checksum, 接收值=$receivedChecksum") Log.e(TAG, "校验和错误: 计算值=$checksum, 接收值=$receivedChecksum")
return return
} }
// 解析字段类型 // 解析字段类型
when (fieldType) { when (fieldType) {
0x01 -> { 0x01 -> {
@ -414,7 +505,7 @@ class BleService(private val context: Context) {
} }
} }
} }
/** /**
* 发送开始录音命令 * 发送开始录音命令
*/ */
@ -423,26 +514,44 @@ class BleService(private val context: Context) {
Log.e(TAG, "无法发送开始录音命令,设备未连接") Log.e(TAG, "无法发送开始录音命令,设备未连接")
return false return false
} }
try { try {
val commandData = byteArrayOf(0x01) val commandData = byteArrayOf(0x01)
writeChar?.value = commandData writeChar?.value = commandData
// 获取外部存储目录路径
// 生成带时间戳的文件名(格式:yyyyMMdd_HHmmss_audio.opus)
val timeStamp =
SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val fileName = "${timeStamp}_audio.opus"
val externalDir = context.getExternalFilesDir(null)
outputFile = File(externalDir, fileName)
fos = FileOutputStream(outputFile).also {
Log.i(TAG, "音频文件创建成功:${outputFile?.absolutePath}")
}
// 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 val success = bluetoothGatt?.writeCharacteristic(writeChar) ?: false
if (success) { if (success) {
Log.i(TAG, "开始录音命令已发送") Log.i(TAG, "开始录音命令已发送")
} else { } else {
Log.e(TAG, "开始录音命令发送失败") Log.e(TAG, "开始录音命令发送失败")
} }
return success return success
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "发送开始录音命令时发生异常: ${e.message}") Log.e(TAG, "发送开始录音命令时发生异常: ${e.message}")
return false return false
} }
} }
/** /**
* 发送停止录音命令 * 发送停止录音命令
*/ */
@ -451,74 +560,79 @@ class BleService(private val context: Context) {
Log.e(TAG, "无法发送停止录音命令,设备未连接") Log.e(TAG, "无法发送停止录音命令,设备未连接")
return false return false
} }
try { try {
val commandData = byteArrayOf(0x02) val commandData = byteArrayOf(0x02)
writeChar?.value = commandData writeChar?.value = commandData
val success = bluetoothGatt?.writeCharacteristic(writeChar) ?: false val success = bluetoothGatt?.writeCharacteristic(writeChar) ?: false
fos?.close()
if (success) { if (success) {
Log.i(TAG, "停止录音命令已发送") Log.i(TAG, "停止录音命令已发送")
} else { } else {
Log.e(TAG, "停止录音命令发送失败") Log.e(TAG, "停止录音命令发送失败")
} }
return success return success
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "发送停止录音命令时发生异常: ${e.message}") Log.e(TAG, "发送停止录音命令时发生异常: ${e.message}")
return false return false
} }
} }
/** /**
* 发送音量调整命令 * 发送音量调整命令
* 协议格式: * 协议格式:
* 协议头(0xAA) + 字段类型(0x01) + 数据长度(0x01) + 音量值(0~16) + 校验和 * 协议头(0xAA) + 字段类型(0x01) + 数据长度(0x01) + 音量值(0~16) + 校验和
*/ */
fun adjustVolume(volume: Int): Boolean { fun adjustVolume(volume: Int): Boolean {
Log.e(
TAG,
"adjustVolume,bluetoothGatt=${bluetoothGatt},writeChar=${writeChar},connectionState=${connectionState.value}"
)
if (bluetoothGatt == null || writeChar == null || connectionState.value != STATE_CONNECTED) { if (bluetoothGatt == null || writeChar == null || connectionState.value != STATE_CONNECTED) {
Log.e(TAG, "无法发送音量调整命令,设备未连接") Log.e(TAG, "无法发送音量调整命令,设备未连接")
return false return false
} }
// 限制音量范围为0-16 // 限制音量范围为0-16
val safeVolume = volume.coerceIn(0, 16) val safeVolume = volume.coerceIn(0, 16)
try { try {
// 构建命令数据 // 构建命令数据
val header: Byte = 0xAA.toByte() val header: Byte = 0xAA.toByte()
val fieldType: Byte = 0x01 val fieldType: Byte = 0x01
val dataLen: Byte = 0x01 val dataLen: Byte = 0x01
val volumeData: Byte = safeVolume.toByte() val volumeData: Byte = safeVolume.toByte()
// 计算校验和 // 计算校验和
val checksum = ((header.toInt() and 0xFF) + val checksum = ((header.toInt() and 0xFF) +
(fieldType.toInt() and 0xFF) + (fieldType.toInt() and 0xFF) +
(dataLen.toInt() and 0xFF) + (dataLen.toInt() and 0xFF) +
(volumeData.toInt() and 0xFF)) and 0xFF (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 writeChar?.value = commandData
val success = bluetoothGatt?.writeCharacteristic(writeChar) ?: false val success = bluetoothGatt?.writeCharacteristic(writeChar) ?: false
if (success) { if (success) {
Log.i(TAG, "音量调整命令已发送: $safeVolume") Log.i(TAG, "音量调整命令已发送: $safeVolume")
} else { } else {
Log.e(TAG, "音量调整命令发送失败") Log.e(TAG, "音量调整命令发送失败")
} }
return success return success
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "发送音量调整命令时发生异常: ${e.message}") Log.e(TAG, "发送音量调整命令时发生异常: ${e.message}")
return false return false
} }
} }
/** /**
* 增加音量 * 增加音量
*/ */
@ -526,7 +640,7 @@ class BleService(private val context: Context) {
// 假设当前音量保存在设备中,我们只需发送增加命令 // 假设当前音量保存在设备中,我们只需发送增加命令
return adjustVolume(16) // 发送最大音量命令 return adjustVolume(16) // 发送最大音量命令
} }
/** /**
* 减小音量 * 减小音量
*/ */
@ -534,7 +648,7 @@ class BleService(private val context: Context) {
// 假设当前音量保存在设备中,我们只需发送减小命令 // 假设当前音量保存在设备中,我们只需发送减小命令
return adjustVolume(0) // 发送最小音量命令 return adjustVolume(0) // 发送最小音量命令
} }
/** /**
* 清理资源 * 清理资源
*/ */
@ -546,4 +660,4 @@ class BleService(private val context: Context) {
notifyChar = null notifyChar = null
writeChar = 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.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.util.Log
import androidx.annotation.NonNull import androidx.annotation.NonNull
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -91,6 +92,7 @@ class JieliPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, EventChannel
override fun onAudioDataReceived(data: ByteArray) { override fun onAudioDataReceived(data: ByteArray) {
pluginScope.launch(Dispatchers.Main) { pluginScope.launch(Dispatchers.Main) {
// Log.d("","数据流onAudioDataReceived${data}")
sendEvent(EVENT_TYPE_AUDIO_DATA, data) sendEvent(EVENT_TYPE_AUDIO_DATA, data)
} }
} }

81
local_plugins/jieli/lib/jieli.dart

@ -51,34 +51,37 @@ class EventType {
/// 杰理BLE插件主类 /// 杰理BLE插件主类
class Jieli { class Jieli {
static const MethodChannel _methodChannel = MethodChannel('com.yunqiinnovation.jieli'); static const MethodChannel _methodChannel =
static const EventChannel _eventChannel = EventChannel('com.yunqiinnovation.jieli/events'); MethodChannel('com.yunqiinnovation.jieli');
static const EventChannel _eventChannel =
EventChannel('com.yunqiinnovation.jieli/events');
/// 扫描结果流 /// 扫描结果流
static final StreamController<List<BleDevice>> _scanController = static final StreamController<List<BleDevice>> _scanController =
StreamController<List<BleDevice>>.broadcast(); StreamController<List<BleDevice>>.broadcast();
Stream<List<BleDevice>> get scanResults => _scanController.stream; Stream<List<BleDevice>> get scanResults => _scanController.stream;
/// 连接状态变化流 /// 连接状态变化流
static final StreamController<ConnectionState> _connectionController = static final StreamController<ConnectionState> _connectionController =
StreamController<ConnectionState>.broadcast(); StreamController<ConnectionState>.broadcast();
Stream<ConnectionState> get connectionState => _connectionController.stream; Stream<ConnectionState> get connectionState => _connectionController.stream;
/// 音频数据流 /// 音频数据流
static final StreamController<Uint8List> _audioController = static final StreamController<Uint8List> _audioController =
StreamController<Uint8List>.broadcast(); StreamController<Uint8List>.broadcast();
Stream<Uint8List> get audioDataStream => _audioController.stream; Stream<Uint8List> get audioDataStream => _audioController.stream;
/// 唤醒信号流 /// 唤醒信号流
static final StreamController<bool> _wakeupController = static final StreamController<bool> _wakeupController =
StreamController<bool>.broadcast(); StreamController<bool>.broadcast();
Stream<bool> get wakeupStream => _wakeupController.stream; Stream<bool> get wakeupStream => _wakeupController.stream;
/// 命令结果流 /// 命令结果流
static final StreamController<CommandResult> _commandResultController = static final StreamController<CommandResult> _commandResultController =
StreamController<CommandResult>.broadcast(); StreamController<CommandResult>.broadcast();
Stream<CommandResult> get commandResultStream => _commandResultController.stream; Stream<CommandResult> get commandResultStream =>
_commandResultController.stream;
/// 原始事件流 /// 原始事件流
Stream<Map<String, dynamic>>? _eventStream; Stream<Map<String, dynamic>>? _eventStream;
@ -95,22 +98,24 @@ class Jieli {
/// 设置事件监听器 /// 设置事件监听器
void _setupEventListener() { void _setupEventListener() {
_eventSubscription = _eventChannel.receiveBroadcastStream().listen(_handleEvent); _eventSubscription =
_eventChannel.receiveBroadcastStream().listen(_handleEvent);
} }
/// 处理来自原生平台的事件 /// 处理来自原生平台的事件
void _handleEvent(dynamic event) { void _handleEvent(dynamic event) {
if (event is! Map<dynamic, dynamic>) return; if (event is! Map<dynamic, dynamic>) return;
final eventData = Map<String, dynamic>.from(event); final eventData = Map<String, dynamic>.from(event);
final String eventType = eventData['type'] as String; final String eventType = eventData['type'] as String;
final dynamic data = eventData['data']; final dynamic data = eventData['data'];
switch (eventType) { switch (eventType) {
case EventType.scanResult: case EventType.scanResult:
if (data is List<dynamic>) { if (data is List<dynamic>) {
final deviceList = data final deviceList = data
.map((item) => BleDevice.fromJson(Map<String, dynamic>.from(item))) .map(
(item) => BleDevice.fromJson(Map<String, dynamic>.from(item)))
.toList(); .toList();
_scanController.add(deviceList); _scanController.add(deviceList);
} }
@ -122,6 +127,7 @@ class Jieli {
break; break;
case EventType.audioData: case EventType.audioData:
if (data is Uint8List) { if (data is Uint8List) {
//print('收到ble通道消息: $data');
_audioController.add(data); _audioController.add(data);
} }
break; break;
@ -175,37 +181,41 @@ class Jieli {
/// 发送开始录音命令 /// 发送开始录音命令
Future<bool> sendStartRecordingCommand() async { Future<bool> sendStartRecordingCommand() async {
final result = await _methodChannel.invokeMethod<bool>('sendStartRecordingCommand'); final result =
await _methodChannel.invokeMethod<bool>('sendStartRecordingCommand');
return result ?? false; return result ?? false;
} }
/// 发送停止录音命令 /// 发送停止录音命令
Future<bool> sendStopRecordingCommand() async { Future<bool> sendStopRecordingCommand() async {
final result = await _methodChannel.invokeMethod<bool>('sendStopRecordingCommand'); final result =
await _methodChannel.invokeMethod<bool>('sendStopRecordingCommand');
return result ?? false; return result ?? false;
} }
/// 关闭插件,释放资源 /// 关闭插件,释放资源
Future<void> dispose() async { Future<void> dispose() async {
await stopScan(); await stopScan();
await disconnect(); // await disconnect();
_eventSubscription?.cancel(); // _eventSubscription?.cancel();
_scanController.close(); // _scanController.close();
_connectionController.close(); // _connectionController.close();
_audioController.close(); // _audioController.close();
_wakeupController.close(); // _wakeupController.close();
_commandResultController.close(); // _commandResultController.close();
} }
/// 检查蓝牙是否已启用 /// 检查蓝牙是否已启用
Future<bool> isBluetoothEnabled() async { Future<bool> isBluetoothEnabled() async {
final result = await _methodChannel.invokeMethod<bool>('isBluetoothEnabled'); final result =
await _methodChannel.invokeMethod<bool>('isBluetoothEnabled');
return result ?? false; return result ?? false;
} }
/// 请求打开蓝牙 /// 请求打开蓝牙
Future<bool> requestBluetoothEnable() async { Future<bool> requestBluetoothEnable() async {
final result = await _methodChannel.invokeMethod<bool>('requestBluetoothEnable'); final result =
await _methodChannel.invokeMethod<bool>('requestBluetoothEnable');
return result ?? false; return result ?? false;
} }
@ -223,7 +233,7 @@ class Jieli {
return false; return false;
} }
} }
/// 增加音量 /// 增加音量
Future<bool> increaseVolume() async { Future<bool> increaseVolume() async {
try { try {
@ -234,7 +244,7 @@ class Jieli {
return false; return false;
} }
} }
/// 减小音量 /// 减小音量
Future<bool> decreaseVolume() async { Future<bool> decreaseVolume() async {
try { try {
@ -261,12 +271,13 @@ class BluetoothDevice {
class CommandResult { class CommandResult {
final int commandType; final int commandType;
final bool success; final bool success;
CommandResult({ CommandResult({
required this.commandType, required this.commandType,
required this.success, required this.success,
}); });
@override @override
String toString() => 'CommandResult{commandType: $commandType, success: $success}'; String toString() =>
} 'CommandResult{commandType: $commandType, success: $success}';
}

126
local_plugins/jl_opus/README.md

@ -0,0 +1,126 @@
# JL OPUS Plugin
基于杰理音频编解码库的Flutter插件,提供OPUS音频格式的解码功能。
## 功能
- 支持OPUS文件解码为PCM格式
- 支持OPUS数据流解码
- 支持多种采样率和通道配置
- 支持带协议头/不带协议头的OPUS数据
## 引入方式
在pubspec.yaml中添加依赖:
```yaml
dependencies:
jl_opus:
path: ./local_plugins/jl_opus
```
## 使用方法
### 初始化插件
```dart
import 'package:jl_opus/jl_opus.dart';
final jlOpus = JlOpus();
```
### 解码OPUS文件
```dart
Future<void> decodeOpusFile() async {
final inPath = '/path/to/input.opus';
final outPath = '/path/to/output.pcm';
// 创建解码选项
final option = OpusOption(
hasHeader: false, // 是否有协议头
channel: 1, // 通道数:1=单声道,2=立体声
sampleRate: 16000, // 采样率
packetSize: 40, // 数据包大小(仅当hasHeader=false时生效)
);
try {
// 开始解码文件
final outFilePath = await jlOpus.decodeOpusFile(inPath, outPath, option);
print('解码成功: $outFilePath');
} catch (e) {
print('解码失败: $e');
}
}
```
### 解码OPUS数据流
```dart
Future<void> decodeOpusStream() async {
// 监听解码后的PCM数据
jlOpus.decodedDataStream.listen((pcmData) {
print('收到解码后的PCM数据: ${pcmData.length} 字节');
// 处理解码后的PCM数据
});
// 创建解码选项
final option = OpusOption(
hasHeader: false,
channel: 1,
sampleRate: 16000,
packetSize: 40,
);
// 开始解码数据流
final success = await jlOpus.startDecodeStream(option);
if (success) {
print('开始解码数据流');
// 写入OPUS数据进行解码
final opusData = Uint8List.fromList([/* OPUS数据 */]);
await jlOpus.writeAudioStream(opusData);
// 数据处理完成后停止解码
await jlOpus.stopDecodeStream();
}
}
```
### 监听事件
```dart
void listenToEvents() {
jlOpus.eventStream.listen((event) {
switch (event.event) {
case 'onStart':
print('开始${event.type == "file" ? "文件" : "流"}解码');
break;
case 'onComplete':
print('完成${event.type == "file" ? "文件" : "流"}解码');
if (event.filePath != null) {
print('输出文件: ${event.filePath}');
}
break;
case 'onError':
print('解码错误: [${event.errorCode}] ${event.errorMessage}');
break;
}
});
}
```
### 资源释放
```dart
Future<void> disposePlugin() async {
await jlOpus.dispose();
print('插件资源已释放');
}
```
## 注意事项
1. OPUS解码需要正确设置参数,特别是采样率和通道数必须与原始音频匹配
2. 使用前请确保jl_audio_decode_V2.0.0_20005.aar包已添加到libs目录
3. 本插件仅支持Android平台

62
local_plugins/jl_opus/android/build.gradle.kts

@ -0,0 +1,62 @@
plugins {
// Android Library 插件
id("com.android.library")
// Kotlin Android 插件
id("org.jetbrains.kotlin.android")
id("com.github.johnrengelman.shadow") version "8.1.1"
}
// 添加mavenLocal()仓库
repositories {
mavenLocal()
google()
mavenCentral()
}
android {
// 命名空间,对应你插件的包名(需与代码内包名保持一致)
namespace = "com.yunqiinnovation.jl_opus"
// 目标 SDK 版本
compileSdk = 33
defaultConfig {
// 最低 SDK 版本
minSdk = 21
targetSdk = 33
}
// Java 语言级别兼容配置
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
// Kotlin 语言级别
kotlinOptions {
jvmTarget = "11"
}
// 添加packaging配置,排除冲突文件
packaging {
resources {
excludes.add("META-INF/DEPENDENCIES")
excludes.add("META-INF/LICENSE")
excludes.add("META-INF/LICENSE.txt")
excludes.add("META-INF/license.txt")
excludes.add("META-INF/NOTICE")
excludes.add("META-INF/NOTICE.txt")
excludes.add("META-INF/notice.txt")
excludes.add("META-INF/*.kotlin_module")
}
}
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10")
implementation("androidx.annotation:annotation:1.5.0")
implementation("androidx.core:core-ktx:1.9.0")
// 使用从本地Maven仓库引用的AAR依赖
implementation("com.jieli.audio:jl_audio_decode:2.0.0")
}

1
local_plugins/jl_opus/android/settings.gradle.kts

@ -0,0 +1 @@
rootProject.name = "jl_opus"

9
local_plugins/jl_opus/android/src/main/AndroidManifest.xml

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.jl_opus">
<!-- 外部存储权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
</manifest>

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

@ -0,0 +1,377 @@
package com.yunqiinnovation.jl_opus
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.annotation.NonNull
import com.jieli.jl_audio_decode.opus.model.OpusOption
import com.jieli.jl_audio_decode.callback.OnDecodeStreamCallback
import com.jieli.jl_audio_decode.callback.OnStateCallback
import com.jieli.jl_audio_decode.opus.OpusManager
import com.jieli.jl_audio_decode.exceptions.OpusException
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import java.io.File
/** JlOpusPlugin */
class JlOpusPlugin: FlutterPlugin, MethodCallHandler {
private lateinit var channel: MethodChannel
private lateinit var eventChannel: EventChannel
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
}
override fun onCancel(arguments: Any?) {
streamSink = null
}
})
// 初始化OpusManager
try {
opusManager = OpusManager()
Log.d(TAG, "OpusManager初始化成功")
} catch (e: OpusException) {
Log.e(TAG, "OpusManager初始化失败: ${e.message}", e)
}
}
// 在主线程上安全发送事件
private fun sendEventOnMainThread(event: Map<String, Any?>) {
if (streamSink == null) return
if (Looper.myLooper() == Looper.getMainLooper()) {
// 如果已经在主线程,直接发送
streamSink?.success(event)
} else {
// 否则切换到主线程
mainHandler.post {
streamSink?.success(event)
}
}
}
// 在主线程上安全返回结果
private fun sendResultOnMainThread(result: Result, value: Any?) {
if (Looper.myLooper() == Looper.getMainLooper()) {
// 如果已经在主线程,直接返回
result.success(value)
} else {
// 否则切换到主线程
mainHandler.post {
result.success(value)
}
}
}
// 在主线程上安全返回错误
private fun sendErrorOnMainThread(result: Result, errorCode: String, errorMessage: String, errorDetails: Any?) {
if (Looper.myLooper() == Looper.getMainLooper()) {
// 如果已经在主线程,直接返回
result.error(errorCode, errorMessage, errorDetails)
} else {
// 否则切换到主线程
mainHandler.post {
result.error(errorCode, errorMessage, errorDetails)
}
}
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
if (opusManager == null) {
result.error("OPUS_MANAGER_NULL", "OpusManager未初始化", null)
return
}
when (call.method) {
"initOpusDecoder" -> {
result.success(opusManager != null)
}
"decodeOpusFile" -> {
val inPath = call.argument<String>("inPath")
val outPath = call.argument<String>("outPath")
val hasHeader = call.argument<Boolean>("hasHeader") ?: false
val channel = call.argument<Int>("channel") ?: 1
val sampleRate = call.argument<Int>("sampleRate") ?: 16000
val packetSize = call.argument<Int>("packetSize") ?: 40
if (inPath == null || outPath == null) {
result.error("INVALID_ARGS", "输入或输出路径不能为空", null)
return
}
// 确保输入文件存在
val inFile = File(inPath)
if (!inFile.exists()) {
result.error("FILE_NOT_FOUND", "输入文件不存在: $inPath", null)
return
}
// 创建解码参数
val option = OpusOption()
.setHasHead(hasHeader)
.setChannel(channel)
.setSampleRate(sampleRate)
.setPacketSize(packetSize)
Log.d(TAG, "开始解码文件, 参数: $option, 输入: $inPath, 输出: $outPath")
opusManager?.decodeFile(inPath, outPath, option, object : OnStateCallback {
override fun onStart() {
Log.d(TAG, "文件解码开始")
sendEventOnMainThread(mapOf(
"event" to "onStart",
"type" to "file"
))
}
override fun onComplete(outFilePath: String?) {
Log.d(TAG, "文件解码完成: $outFilePath")
sendEventOnMainThread(mapOf(
"event" to "onComplete",
"type" to "file",
"filePath" to outFilePath
))
sendResultOnMainThread(result, outFilePath)
}
override fun onError(code: Int, message: String?) {
Log.e(TAG, "文件解码错误: [$code] $message")
sendEventOnMainThread(mapOf(
"event" to "onError",
"type" to "file",
"code" to code,
"message" to message
))
sendErrorOnMainThread(result, "DECODE_ERROR", "解码出错: [$code] $message", null)
}
})
}
"startDecodeStream" -> {
val hasHeader = call.argument<Boolean>("hasHeader") ?: false
val channel = call.argument<Int>("channel") ?: 1
val sampleRate = call.argument<Int>("sampleRate") ?: 16000
val packetSize = call.argument<Int>("packetSize") ?: 40
// 如果已经在解码流,先停止
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) {
sendEventOnMainThread(mapOf(
"event" to "onDecodeStream",
"data" to data
))
}
}
override fun onStart() {
Log.d(TAG, "数据流解码开始")
sendEventOnMainThread(mapOf(
"event" to "onStart",
"type" to "stream"
))
sendResultOnMainThread(result, true)
}
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
))
sendErrorOnMainThread(result, "DECODE_STREAM_ERROR", "数据流解码出错: [$code] $message", null)
}
})
}
"stopDecodeStream" -> {
if (opusManager?.isDecodeStream == true) {
opusManager?.stopDecodeStream()
result.success(true)
} else {
result.success(false)
}
}
"writeAudioStream" -> {
val data = call.argument<ByteArray>("data")
if (data == null) {
result.error("INVALID_ARGS", "音频数据不能为空", null)
return
}
if (opusManager?.isDecodeStream == true) {
opusManager?.writeAudioStream(data)
result.success(true)
} else {
result.error("NOT_DECODING", "当前没有处于解码状态", null)
}
}
"isDecoding" -> {
result.success(opusManager?.isDecodeStream == true)
}
"dispose" -> {
opusManager?.let {
if (it.isDecodeStream) {
it.stopDecodeStream()
}
it.release()
}
opusManager = null
result.success(true)
}
else -> {
result.notImplemented()
}
}
}
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 {
if (it.isDecodeStream) {
it.stopDecodeStream()
}
it.release()
}
opusManager = null
}
}

BIN
local_plugins/jl_opus/android/src/main/libs/jl_audio_decode_V2.0.0_20005.aar

Binary file not shown.

221
local_plugins/jl_opus/lib/jl_opus.dart

@ -0,0 +1,221 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/services.dart';
/// OPUS解码参数选项
class OpusOption {
/// 是否具有协议头
final bool hasHeader;
/// 通道数。取值范围:[1, 2]
final int channel;
/// 采样率。参考值:{8000, 16000, 24000, 32000, 44100, 48000}
final int sampleRate;
/// 数据包长度。仅hasHeader = false时生效。
final int packetSize;
/// 构造函数
OpusOption({
this.hasHeader = false,
this.channel = 1,
this.sampleRate = 16000,
this.packetSize = 40,
});
/// 转换为参数Map
Map<String, dynamic> toMap() {
return {
'hasHeader': hasHeader,
'channel': channel,
'sampleRate': sampleRate,
'packetSize': packetSize,
};
}
}
/// 杰理OPUS编解码器事件
class OpusEvent {
/// 事件类型
final String event;
/// 数据类型 (file或stream)
final String? type;
/// 解码后的数据 (仅在流解码时有效)
final Uint8List? data;
/// 输出文件路径 (仅在文件解码完成时有效)
final String? filePath;
/// 错误码 (仅在发生错误时有效)
final int? errorCode;
/// 错误信息 (仅在发生错误时有效)
final String? errorMessage;
/// 构造函数
OpusEvent({
required this.event,
this.type,
this.data,
this.filePath,
this.errorCode,
this.errorMessage,
});
/// 从Map构造对象
factory OpusEvent.fromMap(Map<dynamic, dynamic> map) {
return OpusEvent(
event: map['event'],
type: map['type'],
data: map['data'],
filePath: map['filePath'],
errorCode: map['code'],
errorMessage: map['message'],
);
}
}
/// 杰理OPUS编解码插件
class JlOpus {
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) {
if (event is Map) {
return OpusEvent.fromMap(event);
}
throw PlatformException(
code: 'INVALID_EVENT',
message: '无效的事件数据',
);
});
return _eventStream!;
}
/// 解码后的数据流
final StreamController<Uint8List> _decodedDataController =
StreamController<Uint8List>.broadcast();
/// 获取解码数据流
Stream<Uint8List> get decodedDataStream => _decodedDataController.stream;
/// 构造函数,设置事件监听
JlOpus() {
_setupEventListener();
}
/// 设置事件监听
void _setupEventListener() {
eventStream.listen((event) {
if (event.event == 'onDecodeStream' && event.data != null) {
_decodedDataController.add(event.data!);
}
});
}
/// 初始化OPUS解码器
Future<bool> initOpusDecoder() async {
try {
final result = await _methodChannel.invokeMethod<bool>('initOpusDecoder');
return result ?? false;
} catch (e) {
return false;
}
}
/// 解码OPUS文件
///
/// [inPath] 输入文件路径
/// [outPath] 输出文件路径
/// [option] 解码参数
Future<String?> decodeOpusFile(
String inPath, String outPath, OpusOption option) async {
try {
final result = await _methodChannel.invokeMethod<String>(
'decodeOpusFile',
{
'inPath': inPath,
'outPath': outPath,
...option.toMap(),
},
);
return result;
} catch (e) {
rethrow;
}
}
/// 开始解码OPUS数据流
///
/// [option] 解码参数
Future<bool> startDecodeStream(OpusOption option) async {
try {
final result = await _methodChannel.invokeMethod<bool>(
'startDecodeStream',
option.toMap(),
);
return result ?? false;
} catch (e) {
rethrow;
}
}
/// 停止解码OPUS数据流
Future<bool> stopDecodeStream() async {
try {
final result =
await _methodChannel.invokeMethod<bool>('stopDecodeStream');
return result ?? false;
} catch (e) {
return false;
}
}
/// 写入OPUS音频数据进行解码
///
/// [data] OPUS音频数据
Future<bool> writeAudioStream(Uint8List data) async {
try {
final result = await _methodChannel.invokeMethod<bool>(
'writeAudioStream',
{'data': data},
);
return result ?? false;
} catch (e) {
rethrow;
}
}
/// 判断是否正在解码
Future<bool> isDecoding() async {
try {
final result = await _methodChannel.invokeMethod<bool>('isDecoding');
return result ?? false;
} catch (e) {
return false;
}
}
/// 释放资源
Future<bool> dispose() async {
try {
_decodedDataController.close();
final result = await _methodChannel.invokeMethod<bool>('dispose');
return result ?? false;
} catch (e) {
return false;
}
}
}

25
local_plugins/jl_opus/pubspec.yaml

@ -0,0 +1,25 @@
name: jl_opus
description: 杰理OPUS编解码库Flutter插件
version: 0.0.1
homepage:
environment:
sdk: ">=2.12.0 <3.0.0"
flutter: ">=2.0.0"
dependencies:
flutter:
sdk: flutter
path_provider: ^2.0.0
permission_handler: ^11.4.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
plugin:
platforms:
android:
package: com.yunqiinnovation.jl_opus
pluginClass: JlOpusPlugin

3
local_plugins/open_ai_service/android/build.gradle.kts

@ -4,6 +4,7 @@ plugins {
// Kotlin Android 插件 // Kotlin Android 插件
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("com.github.johnrengelman.shadow") version "8.1.1" id("com.github.johnrengelman.shadow") version "8.1.1"
kotlin("plugin.serialization") version "1.9.24"
} }
android { android {
@ -57,4 +58,6 @@ dependencies {
implementation("io.ktor:ktor-client-cio:3.1.2") implementation("io.ktor:ktor-client-cio:3.1.2")
implementation("io.ktor:ktor-client-okhttp:3.1.2") implementation("io.ktor:ktor-client-okhttp:3.1.2")
implementation("io.ktor:ktor-client-logging:3.1.2") implementation("io.ktor:ktor-client-logging:3.1.2")
implementation("com.google.code.gson:gson:2.10.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
} }

304
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt

@ -34,7 +34,18 @@ import io.ktor.client.request.*
import io.ktor.http.* import io.ktor.http.*
import com.yunqiinnovation.open_ai_service.mcp.CustomSseClientTransport import com.yunqiinnovation.open_ai_service.mcp.CustomSseClientTransport
import io.ktor.util.valuesOf import io.ktor.util.valuesOf
import okio.Buffer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
@Serializable
data class ServerConfig(
val servername: String,
val url: String,
val tools: List<String>? = null
)
/** /**
* 工具类型枚举 * 工具类型枚举
*/ */
@ -61,13 +72,13 @@ interface FunctionHandler {
*/ */
class MCPSubClient( class MCPSubClient(
private val serverId: String, private val serverId: String,
serverUrl: String, private val config: ServerConfig,
private val customClient: HttpClient? = null private val customClient: HttpClient? = null
) : AutoCloseable { ) : AutoCloseable {
private val TAG = "MCPSubClient[$serverId]" private val TAG = "MCPSubClient[$serverId]"
// 保存服务器URL // 保存服务器URL
private val serverUrl: String = serverUrl private val serverUrl: String = config.url
init { init {
Log.d(TAG, "MCPSubClient初始化,URL: $serverUrl") Log.d(TAG, "MCPSubClient初始化,URL: $serverUrl")
@ -80,7 +91,6 @@ class MCPSubClient(
version = "1.0.0" version = "1.0.0"
) )
) )
// 服务器提供的工具列表 // 服务器提供的工具列表
private var tools: List<ToolUnion> = emptyList() private var tools: List<ToolUnion> = emptyList()
@ -122,10 +132,10 @@ class MCPSubClient(
urlString = serverUrl, urlString = serverUrl,
requestBuilder = { requestBuilder = {
// 添加基本请求头 // 添加基本请求头
headers { // headers {
append(HttpHeaders.Accept, "text/event-stream") // append(HttpHeaders.Accept, "text/event-stream")
append(HttpHeaders.CacheControl, "no-cache") // append(HttpHeaders.CacheControl, "no-cache")
} // }
} }
) )
@ -156,109 +166,106 @@ class MCPSubClient(
* 处理工具列表 * 处理工具列表
*/ */
private fun processToolsList(toolsResult: ListToolsResult?) { private fun processToolsList(toolsResult: ListToolsResult?) {
tools = toolsResult?.tools?.map { tool -> try {
// 确保输入模式有效 // 将MCP工具列表转换为ToolUnion列表
val inputType = tool.inputSchema?.type ?: "object" tools = toolsResult?.tools?.filter { tool -> config.tools.isNullOrEmpty() || config.tools.contains(tool.name)
val inputProperties = tool.inputSchema?.properties ?: emptyMap() }?.map { tool ->
val inputRequired = tool.inputSchema?.required ?: emptyList<String>() // 转换为ToolUnion格式
ToolUnion.ofTool(
ToolUnion.ofTool( Tool.builder()
Tool.builder() .name(tool.name)
.name(tool.name) .description(tool.description ?: "")
.description(tool.description ?: "") .inputSchema(
.inputSchema( Tool.InputSchema.builder()
Tool.InputSchema.builder() .type(JsonValue.from(tool.inputSchema?.type ?: "object"))
.type(JsonValue.from(inputType)) .properties(if (tool.inputSchema?.properties is JsonObject)
.properties(if (inputProperties is JsonObject) inputProperties.toJsonValue() else JsonValue.from(emptyMap<String, String>())) (tool.inputSchema.properties as JsonObject).toJsonValue()
.putAdditionalProperty("required", JsonValue.from(inputRequired)) else JsonValue.from(emptyMap<String, String>()))
.build() .putAdditionalProperty("required", JsonValue.from(tool.inputSchema?.required ?: emptyList<String>()))
) .build()
.build() )
) .build()
} ?: emptyList() )
} ?: emptyList()
// 更新工具映射
tools.forEach { toolUnion -> // 更新工具映射
val tool = toolUnion.tool().get() toolMapping.clear()
toolMapping[tool.name()] = toolUnion tools.forEach { toolUnion ->
val tool = toolUnion.tool().get()
toolMapping[tool.name()] = toolUnion
}
// 更新工具Map列表
updateToolMaps()
Log.d(TAG, "成功处理 ${tools.size} 个工具")
} catch (e: Exception) {
Log.e(TAG, "处理工具列表失败: ${e.message}", e)
} }
// 更新工具Map列表
updateToolMaps()
} }
/** /**
* 更新工具Map列表,用于OpenAI请求 * 更新工具Map列表,用于OpenAI请求
* 将MCP工具转换为OpenAI函数调用格式
*/ */
private fun updateToolMaps() { private fun updateToolMaps() {
toolMaps.clear() toolMaps.clear()
// 添加MCP工具 try {
tools.forEach { toolUnion -> // 转换MCP工具为OpenAI函数格式
try { tools.forEach { toolUnion ->
val toolOptional = toolUnion.tool() val tool = toolUnion.tool().getOrNull() ?: return@forEach
if (!toolOptional.isPresent) {
return@forEach
}
val tool = toolOptional.get()
// 创建基本参数结构
val parametersMap = mutableMapOf<String, Any>()
parametersMap["type"] = "object" // 默认值
parametersMap["properties"] = mutableMapOf<String, Any>()
parametersMap["required"] = emptyList<String>()
// 从工具的输入模式中提取信息
try { try {
// 获取输入模式并转换为字符串 // 获取工具的输入模式
val schema = tool.inputSchema() val inputSchema = tool.inputSchema() ?: return@forEach
val schemaStr = schema.toString()
Log.d(TAG, "解析工具Schema: $schemaStr")
// 如果toString()提供了足够的信息,就使用正则表达式解析 // 使用toString()获取完整JSON字符串
// 这是最可靠的方法,因为它不依赖于API的细节变化 val schemaStr = inputSchema.toString()
Log.d(TAG, "工具 ${tool.name()} 的Schema: $schemaStr")
// 解析类型 // 解析properties和required
val typePattern = "type=([^,}]+)".toRegex() val propertiesMap = HashMap<String, Any>()
val typeMatch = typePattern.find(schemaStr) val requiredList = ArrayList<String>()
if (typeMatch != null) {
parametersMap["type"] = typeMatch.groupValues[1].trim()
}
// 尝试从字符串中解析完整的JSON结构
try { try {
// 直接从Schema字符串中提取属性结构,手动构建properties映射 // 匹配properties内部的内容
val propertiesMap = mutableMapOf<String, Any>() val propertiesPattern = "properties=\\{(.*?)\\}(?:,|\\s|$)".toRegex(RegexOption.DOT_MATCHES_ALL)
// 匹配属性定义: 形如 fieldName={...}
val propertiesPattern = "properties=\\{([^}]+)\\}".toRegex()
val propertiesMatch = propertiesPattern.find(schemaStr) val propertiesMatch = propertiesPattern.find(schemaStr)
if (propertiesMatch != null) { if (propertiesMatch != null) {
// 匹配属性定义: 形如 fieldName={...} val propertiesContent = propertiesMatch.groupValues[1]
val fieldPattern = "([\\w]+)=\\{([^{}]+(?:\\{[^{}]*\\}[^{}]*)*)\\}".toRegex()
val fieldMatches = fieldPattern.findAll(propertiesMatch.groupValues[1]) // 匹配每个属性字段,例如 query={description=搜索信息, type=string}
val fieldPattern = "([a-zA-Z0-9_]+)=\\{([^{}]+?)\\}".toRegex()
val fieldMatches = fieldPattern.findAll(propertiesContent)
fieldMatches.forEach { fieldMatch -> fieldMatches.forEach { fieldMatch ->
val fieldName = fieldMatch.groupValues[1] val fieldName = fieldMatch.groupValues[1]
val fieldContent = fieldMatch.groupValues[2] val fieldContent = fieldMatch.groupValues[2]
// 为属性创建一个Map // 解析字段属性
val fieldMap = mutableMapOf<String, Any>() val fieldMap = HashMap<String, Any>()
// 解析字段内容中的键值对 // 解析如 description=搜索信息, type=string 格式的键值对
val keyValuePattern = "(\\w+)=([^,}]+)".toRegex() // 同时处理带引号和不带引号的情况
val keyValuePattern = "([a-zA-Z0-9_]+)=(\"[^\"]*\"|'[^']*'|[^,}\\s]+)".toRegex()
val keyValueMatches = keyValuePattern.findAll(fieldContent) val keyValueMatches = keyValuePattern.findAll(fieldContent)
keyValueMatches.forEach { kvMatch -> keyValueMatches.forEach { kvMatch ->
val key = kvMatch.groupValues[1] val key = kvMatch.groupValues[1]
val value = kvMatch.groupValues[2].trim() var value = kvMatch.groupValues[2].trim()
// 根据值的类型进行适当处理 // 去除引号(如果有)
if ((value.startsWith("\"") && value.endsWith("\"")) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.substring(1, value.length - 1)
}
// 处理不同类型的值
val processedValue: Any = when { val processedValue: Any = when {
value == "true" -> true value.equals("true", ignoreCase = true) -> true
value == "false" -> false value.equals("false", ignoreCase = true) -> false
value.matches("\\d+".toRegex()) -> value.toIntOrNull() ?: value value.matches("\\d+".toRegex()) -> value.toIntOrNull() ?: value
value.matches("\\d+\\.\\d+".toRegex()) -> value.toDoubleOrNull() ?: value value.matches("\\d+\\.\\d+".toRegex()) -> value.toDoubleOrNull() ?: value
else -> value // 保持为字符串 else -> value // 保持为字符串
@ -267,51 +274,63 @@ class MCPSubClient(
fieldMap[key] = processedValue fieldMap[key] = processedValue
} }
// 添加到属性映射
propertiesMap[fieldName] = fieldMap propertiesMap[fieldName] = fieldMap
} }
parametersMap["properties"] = propertiesMap
Log.d(TAG, "成功解析属性: $propertiesMap") Log.d(TAG, "成功解析属性: $propertiesMap")
} else {
parametersMap["properties"] = emptyMap<String, Any>()
} }
// 提取required部分 // 解析required
val requiredPattern = "required=\\[([^\\]]+)\\]".toRegex() val requiredPattern = "required=\\[(.*?)\\]".toRegex()
val requiredMatch = requiredPattern.find(schemaStr) val requiredMatch = requiredPattern.find(schemaStr)
if (requiredMatch != null) { if (requiredMatch != null) {
val requiredStr = requiredMatch.groupValues[1] val requiredStr = requiredMatch.groupValues[1]
val requiredList = requiredStr.split(",").map { it.trim() } if (requiredStr.isNotEmpty()) {
parametersMap["required"] = requiredList // 处理可能带引号的字段名
} else { val fieldNames = requiredStr.split(",").map {
parametersMap["required"] = emptyList<String>() val trimmed = it.trim()
if ((trimmed.startsWith("\"") && trimmed.endsWith("\"")) ||
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
trimmed.substring(1, trimmed.length - 1)
} else {
trimmed
}
}
requiredList.addAll(fieldNames)
Log.d(TAG, "解析到required字段: $requiredList")
}
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "从字符串解析schema细节失败: ${e.message}") Log.e(TAG, "解析Schema字符串失败: ${e.message}", e)
// 使用默认值 Log.e(TAG, "原始Schema: $schemaStr")
parametersMap["properties"] = emptyMap<String, Any>()
parametersMap["required"] = emptyList<String>()
} }
} catch (e: Exception) {
Log.e(TAG, "解析工具输入模式失败: ${e.message}", e) // 创建OpenAI函数格式
// 使用默认值继续 val functionMap = mapOf(
}
// 创建工具Map
toolMaps.add(mapOf(
"type" to "function",
"function" to mapOf(
"name" to tool.name(), "name" to tool.name(),
"description" to (tool.description() ?: ""), "description" to (tool.description() ?: ""),
"parameters" to parametersMap "parameters" to mapOf(
"type" to "object",
"properties" to propertiesMap,
"required" to requiredList
)
) )
))
// 添加到工具列表
Log.d(TAG, "工具 ${tool.name()} 成功添加到工具映射") toolMaps.add(mapOf(
"type" to "function",
} catch (e: Exception) { "function" to functionMap
Log.e(TAG, "转换工具到Map失败: ${e.message}") ))
Log.d(TAG, "成功转换工具 ${tool.name()} 到OpenAI函数格式")
} catch (e: Exception) {
Log.e(TAG, "转换工具 ${tool.name()} 失败: ${e.message}")
}
} }
} catch (e: Exception) {
Log.e(TAG, "更新工具映射失败: ${e.message}")
} }
Log.d(TAG, "更新工具映射完成,共 ${toolMaps.size} 个工具") Log.d(TAG, "更新工具映射完成,共 ${toolMaps.size} 个工具")
@ -380,6 +399,9 @@ class MCPSubClient(
} }
return try { return try {
// 直接使用原始参数
Log.d(TAG, "调用工具 $name 使用参数: $arguments")
val result = mcp.callTool( val result = mcp.callTool(
name = name, name = name,
arguments = arguments arguments = arguments
@ -391,10 +413,10 @@ class MCPSubClient(
else -> "" else -> ""
} }
} }
JSONObject().apply {put("meta",result?._meta?.toMap()); put("context",context) } JSONObject().apply { put("name",name);put("meta",result?._meta); put("context",context) }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "调用MCP工具失败: ${e.message}", e) Log.e(TAG, "调用MCP工具失败: ${e.message}", e)
JSONObject().apply { put("context", "调用MCP工具失败: ${e.message}") } JSONObject().apply { put("context", "调用MCP工具失败: ${e.message}")}
} }
} }
@ -476,7 +498,7 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
// 清除现有连接 // 清除现有连接
closeAllConnections() closeAllConnections()
val serverMap = mutableMapOf<String, String>() val serverMap = mutableMapOf<String, ServerConfig>()
var configLoaded = false var configLoaded = false
// 尝试解析传入的JSON配置 // 尝试解析传入的JSON配置
@ -484,17 +506,19 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
if (mcpConfigJson.isNotEmpty()) { if (mcpConfigJson.isNotEmpty()) {
val mapper = ObjectMapper() val mapper = ObjectMapper()
try { try {
val config = mapper.readValue(mcpConfigJson, Map::class.java) Log.d(TAG, "传入JSON:${mcpConfigJson}")
val configMap: Map<String, ServerConfig> = Json.decodeFromString(mcpConfigJson)
// val config = mapper.readValue(mcpConfigJson, Map::class.java)
// 处理mcpServers格式 // 处理mcpServers格式
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val mcpServers = config["mcpServers"] as? Map<String, Map<String, String>> // val mcpServers = config["mcpServers"] as? Map<String, Map<String, String>>
if (mcpServers != null && mcpServers.isNotEmpty()) { if (configMap.isNotEmpty()) {
// 从服务器配置中提取URL // 从服务器配置中提取URL
mcpServers.forEach { (serverId, serverConfig) -> configMap.forEach { (serverId, serverConfig) ->
val url = serverConfig["url"] val url = serverConfig.url
if (!url.isNullOrEmpty()) { if (!url.isNullOrEmpty()) {
serverMap[serverId] = url serverMap[serverId] = serverConfig
Log.d(TAG, "从配置中添加服务器 [$serverId]: $url") Log.d(TAG, "从配置中添加服务器 [$serverId]: $url")
} }
} }
@ -513,10 +537,10 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
// 创建并连接所有子客户端 // 创建并连接所有子客户端
val connectionResults = mutableMapOf<String, Boolean>() val connectionResults = mutableMapOf<String, Boolean>()
for ((serverId, serverUrl) in serverMap) { for ((serverId, serverConfig) in serverMap) {
try { try {
Log.d(TAG, "为服务器 [$serverId],[$serverUrl] 创建子客户端") Log.d(TAG, "为服务器 [$serverId],[${serverConfig.url}] 创建子客户端")
val subClient = MCPSubClient(serverId, serverUrl, sharedHttpClient) val subClient = MCPSubClient(serverId, serverConfig, sharedHttpClient)
val connected = subClient.connect() val connected = subClient.connect()
if (connected) { if (connected) {
@ -862,6 +886,9 @@ private fun createSslTrustAllClient(): HttpClient {
Log.d("OkHttp-URL", "实际请求URL: ${request.url}") Log.d("OkHttp-URL", "实际请求URL: ${request.url}")
Log.d("OkHttp-Headers", "请求头: ${request.headers}") Log.d("OkHttp-Headers", "请求头: ${request.headers}")
// 生成curl命令用于调试
logAsCurlCommand(request)
val response = chain.proceed(request) val response = chain.proceed(request)
Log.d("OkHttp-Response", "状态码: ${response.code} - ${response.message}") Log.d("OkHttp-Response", "状态码: ${response.code} - ${response.message}")
@ -889,4 +916,39 @@ private fun createSslTrustAllClient(): HttpClient {
// 安装SSE插件 // 安装SSE插件
install(SSE) install(SSE)
} }
}
/**
* 将OkHttp请求转换为curl命令并输出到日志
*/
private fun logAsCurlCommand(request: okhttp3.Request) {
try {
val curlCmd = StringBuilder("curl -X ${request.method}")
// 添加URL
curlCmd.append(" \"${request.url}\"")
// 添加请求头
request.headers.forEach { (name, value) ->
curlCmd.append(" \\\n -H \"$name: $value\"")
}
// 添加请求体
val requestBody = request.body
if (requestBody != null) {
val buffer = okio.Buffer()
requestBody.writeTo(buffer)
val bodyContent = buffer.readUtf8()
// 请求体不为空时添加
if (bodyContent.isNotEmpty()) {
curlCmd.append(" \\\n -d '${bodyContent.replace("'", "\\'")}'")
}
}
// 输出到日志
Log.d("CURL-Command", curlCmd.toString())
} catch (e: Exception) {
Log.e("CURL-Command", "生成curl命令失败: ${e.message}", e)
}
} }

38
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIServicePlugin.kt

@ -22,6 +22,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonObject
import kotlin.math.min
/** OpenAIServicePlugin */ /** OpenAIServicePlugin */
class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler { class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
@ -164,24 +166,34 @@ class OpenAIServicePlugin : FlutterPlugin, MethodCallHandler, StreamHandler {
} }
override fun onFunctionCall(functionCall: JSONObject) { override fun onFunctionCall(functionCall: JSONObject) {
Log.e("OpenAIServicePlugin", "工具调用结果: ${functionCall.toString()}") Log.e("OpenAIServicePlugin", "context:${functionCall.toString()}")
val context = functionCall["context"] val context = functionCall["context"]
// val meta = functionCall["meta"] if (functionCall.has("meta")) {
val meta: Map<String, Any>? = functionCall.optJSONObject("meta")?.let { metaJson -> val meta = functionCall.get("meta")
metaJson.keys().asSequence().associateWith { key -> val metaJsonObject:JsonObject = meta as JsonObject
metaJson.get(key) // 或 opt(key),更安全 Log.e("OpenAIServicePlugin", "工具调用结果 context:${context} meta:${meta.toString()}")
// 发送函数调用事件
mainHandler.post {
eventSink?.success(mapOf("type" to "functionCall", "content" to context,"meta" to meta.toString()))
}
if (metaJsonObject.containsKey("card_spotify")) {
mainHandler.post {
eventSink?.success(mapOf("type" to "complete"))
}
} }
} }
val card_spotifyKey = meta?.containsKey("card_spotify") ?: false
if (card_spotifyKey) { //是否是音乐卡片
} // val meta: Map<String, Any>? = functionCall.optJSONObject("meta")?.let { metaJson ->
// metaJson.keys().asSequence().associateWith { key ->
// metaJson.get(key) // 或 opt(key),更安全
// }
// } ?: mapOf(
// // 如果 null,就用默认值
// "card_spotify" to {
// "url" to "spotify:track:58kNJana4w5BIjlZE2wq5m"
// },
// )
// 发送函数调用事件
mainHandler.post {
// val functionCallMap = functionCall.toMap()
eventSink?.success(mapOf("type" to "functionCall", "content" to context,"meta" to meta))
}
} }
} }
) )

9
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt

@ -120,8 +120,13 @@ class CustomSseClientTransport(
val eventData = event.data ?: "" val eventData = event.data ?: ""
Log.d(TAG, "收到endpoint事件: $eventData") Log.d(TAG, "收到endpoint事件: $eventData")
// 使用主机部分构建endpoint // 检查eventData是否已包含hostPart
val fullEndpoint = if (eventData.startsWith("/")) { val fullEndpoint = if (eventData.contains(hostPart)) {
// 如果已包含hostPart,则直接使用eventData
Log.d(TAG, "eventData已包含hostPart,直接使用")
eventData
} else if (eventData.startsWith("/")) {
// 如果以/开头但不包含hostPart,则拼接
"$hostPart$eventData" "$hostPart$eventData"
} else { } else {
"$eventData" "$eventData"

7
local_plugins/open_ai_service/lib/open_ai_service.dart

@ -25,17 +25,19 @@ enum OpenAIEventType {
class OpenAIEvent { class OpenAIEvent {
final OpenAIEventType type; final OpenAIEventType type;
final dynamic content; final dynamic content;
final dynamic meta; final Map<String, dynamic>? meta;
OpenAIEvent({required this.type, this.content, this.meta}); OpenAIEvent({required this.type, this.content, this.meta});
factory OpenAIEvent.fromMap(Map<String, dynamic> map) { factory OpenAIEvent.fromMap(Map<String, dynamic> map) {
final typeStr = map['type'] as String; final typeStr = map['type'] as String;
final content = map['content']; final content = map['content'];
final rawMeta = map['meta'] as String?;
final meta = rawMeta != null ? jsonDecode(rawMeta) : null;
return OpenAIEvent( return OpenAIEvent(
type: _typeFromString(typeStr), type: _typeFromString(typeStr),
content: content, content: content,
meta: meta,
); );
} }
@ -77,6 +79,7 @@ class OpenAIService {
// 监听原生事件 // 监听原生事件
_eventChannel.receiveBroadcastStream().listen( _eventChannel.receiveBroadcastStream().listen(
(dynamic event) { (dynamic event) {
print("收到事件 OpenAIEvent: $event");
if (event is Map<dynamic, dynamic>) { if (event is Map<dynamic, dynamic>) {
final eventMap = Map<String, dynamic>.from(event); final eventMap = Map<String, dynamic>.from(event);
final openAIEvent = OpenAIEvent.fromMap(eventMap); final openAIEvent = OpenAIEvent.fromMap(eventMap);

BIN
local_plugins/opus/.gradle/8.10/checksums/checksums.lock

Binary file not shown.

0
local_plugins/opus/.gradle/8.10/dependencies-accessors/gc.properties

BIN
local_plugins/opus/.gradle/8.10/fileChanges/last-build.bin

Binary file not shown.

BIN
local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.bin

Binary file not shown.

BIN
local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock

Binary file not shown.

0
local_plugins/opus/.gradle/8.10/gc.properties

BIN
local_plugins/opus/.gradle/8.9/checksums/checksums.lock

Binary file not shown.

0
local_plugins/opus/.gradle/8.9/dependencies-accessors/gc.properties

BIN
local_plugins/opus/.gradle/8.9/fileChanges/last-build.bin

Binary file not shown.

BIN
local_plugins/opus/.gradle/8.9/fileHashes/fileHashes.lock

Binary file not shown.

0
local_plugins/opus/.gradle/8.9/gc.properties

BIN
local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock

Binary file not shown.

2
local_plugins/opus/.gradle/buildOutputCleanup/cache.properties

@ -0,0 +1,2 @@
#Thu Apr 24 10:52:53 CST 2025
gradle.version=8.9

1
local_plugins/opus/.gradle/nb-cache/trust/404236D79074CDA6A1B2372C665DCA9E587766B94C3493E9904CD4249D51B6BF

@ -0,0 +1 @@
C7E6339E39539FBB82728828DAB8AC59D6F180C3E383F85B403CD9B85CA1DF6A

0
local_plugins/opus/.gradle/vcs-1/gc.properties

92
local_plugins/opus/README.md

@ -0,0 +1,92 @@
# Opus Plugin for Flutter
一个用于在 Flutter 中使用 Opus 音频编解码器的插件,支持 Android 和 iOS 平台。
## 功能
- Opus 编码:将 PCM 音频数据编码为 Opus 格式
- Opus 解码:将 Opus 数据解码为 PCM 音频
- 支持灵活设置编码参数:采样率、声道数、应用类型、复杂度等
- 兼容 Android 和 iOS 平台
## 使用方法
### 添加依赖
由于这是本地插件,在 `pubspec.yaml` 中添加:
```yaml
dependencies:
opus:
path: local_plugins/opus
```
### 初始化编解码器
```dart
// 创建编码器
await Opus.createEncoder(
sampleRate: 48000, // 采样率 (Hz)
channels: 2, // 声道数 (1=单声道, 2=立体声)
application: OpusApplicationType.audio, // 应用类型
complexity: 10, // 复杂度 (0-10)
frameSize: 960, // 帧大小
);
// 创建解码器
await Opus.createDecoder(
sampleRate: 48000,
channels: 2,
);
```
### 编码和解码
```dart
// 编码 PCM 数据
Uint8List pcmData = ...; // 16位 PCM 音频数据
Uint8List? opusData = await Opus.encodeData(pcmData);
// 解码 Opus 数据
Uint8List opusData = ...; // Opus 编码数据
Uint8List? pcmData = await Opus.decodeData(opusData, frameSize: 960);
```
### 释放资源
```dart
// 释放编码器资源
await Opus.disposeEncoder();
// 释放解码器资源
await Opus.disposeDecoder();
```
## 关于 Opus
Opus 是一种开源、免版税的音频编解码器,专为通过互联网传输音频而设计。它可变比特率从 6 kbit/s 到 510 kbit/s,帧大小从 2.5 ms 到 60 ms,支持采样率从 8 kHz(窄带)到 48 kHz(全频带)。
Opus 特点:
- 低延迟 (5-20ms)
- 高音质
- 灵活的比特率
- 支持语音和音乐
- 强大的丢包恢复能力
## 配置说明
### Android 配置
需要将 libopus.so 文件放在以下目录:
- android/src/main/jniLibs/armeabi-v7a/libopus.so
- android/src/main/jniLibs/arm64-v8a/libopus.so
> 注意:若需要自行编译 Opus,可参考 https://opus-codec.org/ 官方指南。
### iOS 配置
需要将 opus.framework 或 libopus.a 放在 ios/Frameworks 目录下。
## 示例
查看 [example](example) 目录中的示例代码,了解如何使用此插件生成、编码和解码音频数据。

21
local_plugins/opus/android/CMakeLists.txt

@ -0,0 +1,21 @@
cmake_minimum_required(VERSION 3.4.1)
# 设置opus源码路径,使用我们自己的头文件
set(OPUS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libs/opus)
# 添加opus头文件路径
include_directories(${OPUS_DIR}/include)
# 创建我们自己的opus库(桩实现)
add_library(opus SHARED
src/main/cpp/opus_stub.cpp)
# 添加JNI库
add_library(opus_jni SHARED
src/main/cpp/opus_jni.cpp)
# 链接库
target_link_libraries(opus_jni
android
log
opus)

67
local_plugins/opus/android/build.gradle.kts

@ -0,0 +1,67 @@
plugins {
// Android Library 插件
id("com.android.library")
// Kotlin Android 插件
id("org.jetbrains.kotlin.android")
}
group = "com.yunqiinnovation.opus"
version = "1.0-SNAPSHOT"
android {
namespace = "com.yunqiinnovation.opus"
// 目标 SDK 版本
compileSdk = 31
defaultConfig {
// 最低 SDK 版本
minSdk = 21
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// 设置启用CMake构建
externalNativeBuild {
cmake {
arguments += "-DANDROID_STL=c++_shared"
cppFlags += "-std=c++11"
}
}
// 设置支持的ABIs
ndk {
abiFilters += listOf("armeabi-v7a", "arm64-v8a")
}
}
// Java 语言级别兼容配置
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
// Kotlin 语言级别
kotlinOptions {
jvmTarget = "1.8"
}
// CMake配置
externalNativeBuild {
cmake {
path = file("CMakeLists.txt")
}
}
// 处理jniLibs
sourceSets {
getByName("main") {
jniLibs.srcDirs("src/main/jniLibs")
java.srcDir("src/main/kotlin")
}
}
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10")
implementation("androidx.annotation:annotation:1.3.0")
}

6
local_plugins/opus/android/libs/opus/AUTHORS

@ -0,0 +1,6 @@
Jean-Marc Valin (jmvalin@jmvalin.ca)
Koen Vos (koenvos74@gmail.com)
Timothy Terriberry (tterribe@xiph.org)
Karsten Vandborg Sorensen (karsten.vandborg.sorensen@skype.net)
Soren Skak Jensen (ssjensen@gn.com)
Gregory Maxwell (greg@xiph.org)

744
local_plugins/opus/android/libs/opus/CMakeLists.txt

@ -0,0 +1,744 @@
cmake_minimum_required(VERSION 3.16)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
include(OpusPackageVersion)
get_package_version(PACKAGE_VERSION PROJECT_VERSION)
project(Opus LANGUAGES C VERSION ${PROJECT_VERSION})
include(OpusFunctions)
include(OpusBuildtype)
include(OpusConfig)
include(OpusSources)
include(GNUInstallDirs)
include(CMakeDependentOption)
include(FeatureSummary)
set(OPUS_BUILD_SHARED_LIBRARY_HELP_STR "build shared library.")
option(OPUS_BUILD_SHARED_LIBRARY ${OPUS_BUILD_SHARED_LIBRARY_HELP_STR} OFF)
if(OPUS_BUILD_SHARED_LIBRARY OR BUILD_SHARED_LIBS OR OPUS_BUILD_FRAMEWORK)
# Global flag to cause add_library() to create shared libraries if on.
set(BUILD_SHARED_LIBS ON)
set(OPUS_BUILD_SHARED_LIBRARY ON)
endif()
add_feature_info(OPUS_BUILD_SHARED_LIBRARY OPUS_BUILD_SHARED_LIBRARY ${OPUS_BUILD_SHARED_LIBRARY_HELP_STR})
set(OPUS_BUILD_TESTING_HELP_STR "build tests.")
option(OPUS_BUILD_TESTING ${OPUS_BUILD_TESTING_HELP_STR} OFF)
if(OPUS_BUILD_TESTING OR BUILD_TESTING)
set(OPUS_BUILD_TESTING ON)
set(BUILD_TESTING ON)
endif()
add_feature_info(OPUS_BUILD_TESTING OPUS_BUILD_TESTING ${OPUS_BUILD_TESTING_HELP_STR})
set(OPUS_CUSTOM_MODES_HELP_STR "enable non-Opus modes, e.g. 44.1 kHz & 2^n frames.")
option(OPUS_CUSTOM_MODES ${OPUS_CUSTOM_MODES_HELP_STR} OFF)
add_feature_info(OPUS_CUSTOM_MODES OPUS_CUSTOM_MODES ${OPUS_CUSTOM_MODES_HELP_STR})
set(OPUS_BUILD_PROGRAMS_HELP_STR "build programs.")
option(OPUS_BUILD_PROGRAMS ${OPUS_BUILD_PROGRAMS_HELP_STR} OFF)
add_feature_info(OPUS_BUILD_PROGRAMS OPUS_BUILD_PROGRAMS ${OPUS_BUILD_PROGRAMS_HELP_STR})
set(OPUS_DISABLE_INTRINSICS_HELP_STR "disable all intrinsics optimizations.")
option(OPUS_DISABLE_INTRINSICS ${OPUS_DISABLE_INTRINSICS_HELP_STR} OFF)
add_feature_info(OPUS_DISABLE_INTRINSICS OPUS_DISABLE_INTRINSICS ${OPUS_DISABLE_INTRINSICS_HELP_STR})
set(OPUS_FIXED_POINT_HELP_STR "compile as fixed-point (for machines without a fast enough FPU).")
option(OPUS_FIXED_POINT ${OPUS_FIXED_POINT_HELP_STR} OFF)
add_feature_info(OPUS_FIXED_POINT OPUS_FIXED_POINT ${OPUS_FIXED_POINT_HELP_STR})
set(OPUS_ENABLE_FLOAT_API_HELP_STR "compile with the floating point API (for machines with float library).")
option(OPUS_ENABLE_FLOAT_API ${OPUS_ENABLE_FLOAT_API_HELP_STR} ON)
add_feature_info(OPUS_ENABLE_FLOAT_API OPUS_ENABLE_FLOAT_API ${OPUS_ENABLE_FLOAT_API_HELP_STR})
set(OPUS_FLOAT_APPROX_HELP_STR "enable floating point approximations (Ensure your platform supports IEEE 754 before enabling).")
option(OPUS_FLOAT_APPROX ${OPUS_FLOAT_APPROX_HELP_STR} OFF)
add_feature_info(OPUS_FLOAT_APPROX OPUS_FLOAT_APPROX ${OPUS_FLOAT_APPROX_HELP_STR})
set(OPUS_ASSERTIONS_HELP_STR "additional software error checking.")
option(OPUS_ASSERTIONS ${OPUS_ASSERTIONS_HELP_STR} OFF)
add_feature_info(OPUS_ASSERTIONS OPUS_ASSERTIONS ${OPUS_ASSERTIONS_HELP_STR})
set(OPUS_HARDENING_HELP_STR "run-time checks that are cheap and safe for use in production.")
option(OPUS_HARDENING ${OPUS_HARDENING_HELP_STR} ON)
add_feature_info(OPUS_HARDENING OPUS_HARDENING ${OPUS_HARDENING_HELP_STR})
set(OPUS_FUZZING_HELP_STR "causes the encoder to make random decisions (do not use in production).")
option(OPUS_FUZZING ${OPUS_FUZZING_HELP_STR} OFF)
add_feature_info(OPUS_FUZZING OPUS_FUZZING ${OPUS_FUZZING_HELP_STR})
set(OPUS_CHECK_ASM_HELP_STR "enable bit-exactness checks between optimized and c implementations.")
option(OPUS_CHECK_ASM ${OPUS_CHECK_ASM_HELP_STR} OFF)
add_feature_info(OPUS_CHECK_ASM OPUS_CHECK_ASM ${OPUS_CHECK_ASM_HELP_STR})
set(OPUS_DNN_FLOAT_DEBUG_HELP_STR "Run DNN computations as float for debugging purposes.")
option(OPUS_DNN_FLOAT_DEBUG ${OPUS_DNN_FLOAT_DEBUG_HELP_STR} OFF)
add_feature_info(OPUS_DNN_FLOAT_DEBUG OPUS_DNN_FLOAT_DEBUG ${OPUS_DNN_FLOAT_DEBUG_HELP_STR})
set(OPUS_INSTALL_PKG_CONFIG_MODULE_HELP_STR "install pkg-config module.")
option(OPUS_INSTALL_PKG_CONFIG_MODULE ${OPUS_INSTALL_PKG_CONFIG_MODULE_HELP_STR} ON)
add_feature_info(OPUS_INSTALL_PKG_CONFIG_MODULE OPUS_INSTALL_PKG_CONFIG_MODULE ${OPUS_INSTALL_PKG_CONFIG_MODULE_HELP_STR})
set(OPUS_INSTALL_CMAKE_CONFIG_MODULE_HELP_STR "install CMake package config module.")
option(OPUS_INSTALL_CMAKE_CONFIG_MODULE ${OPUS_INSTALL_CMAKE_CONFIG_MODULE_HELP_STR} ON)
add_feature_info(OPUS_INSTALL_CMAKE_CONFIG_MODULE OPUS_INSTALL_CMAKE_CONFIG_MODULE ${OPUS_INSTALL_CMAKE_CONFIG_MODULE_HELP_STR})
set(OPUS_DRED_HELP_STR "enable DRED.")
option(OPUS_DRED ${OPUS_DRED_HELP_STR} OFF)
add_feature_info(OPUS_DRED OPUS_DRED ${OPUS_DRED_HELP_STR})
set(OPUS_OSCE_HELP_STR "enable OSCE.")
option(OPUS_OSCE ${OPUS_OSCE_HELP_STR} OFF)
add_feature_info(OPUS_OSCE OPUS_OSCE ${OPUS_OSCE_HELP_STR})
if(APPLE)
set(OPUS_BUILD_FRAMEWORK_HELP_STR "build Framework bundle for Apple systems.")
option(OPUS_BUILD_FRAMEWORK ${OPUS_BUILD_FRAMEWORK_HELP_STR} OFF)
add_feature_info(OPUS_BUILD_FRAMEWORK OPUS_BUILD_FRAMEWORK ${OPUS_BUILD_FRAMEWORK_HELP_STR})
endif()
if(MSVC)
set(OPUS_STATIC_RUNTIME_HELP_STR "build with static runtime library.")
option(OPUS_STATIC_RUNTIME ${OPUS_STATIC_RUNTIME_HELP_STR} OFF)
add_feature_info(OPUS_STATIC_RUNTIME OPUS_STATIC_RUNTIME ${OPUS_STATIC_RUNTIME_HELP_STR})
endif()
set(OPUS_FIXED_POINT_DEBUG_HELP_STR "debug fixed-point implementation.")
cmake_dependent_option(OPUS_FIXED_POINT_DEBUG
${OPUS_FIXED_POINT_DEBUG_HELP_STR}
ON
"OPUS_FIXED_POINT; OPUS_FIXED_POINT_DEBUG"
OFF)
add_feature_info(OPUS_FIXED_POINT_DEBUG OPUS_FIXED_POINT_DEBUG ${OPUS_FIXED_POINT_DEBUG_HELP_STR})
set(OPUS_VAR_ARRAYS_HELP_STR "use variable length arrays for stack arrays.")
cmake_dependent_option(OPUS_VAR_ARRAYS
${OPUS_VAR_ARRAYS_HELP_STR}
ON
"VLA_SUPPORTED; NOT OPUS_USE_ALLOCA; NOT OPUS_NONTHREADSAFE_PSEUDOSTACK"
OFF)
add_feature_info(OPUS_VAR_ARRAYS OPUS_VAR_ARRAYS ${OPUS_VAR_ARRAYS_HELP_STR})
set(OPUS_USE_ALLOCA_HELP_STR "use alloca for stack arrays (on non-C99 compilers).")
cmake_dependent_option(OPUS_USE_ALLOCA
${OPUS_USE_ALLOCA_HELP_STR}
ON
"USE_ALLOCA_SUPPORTED; NOT OPUS_VAR_ARRAYS; NOT OPUS_NONTHREADSAFE_PSEUDOSTACK"
OFF)
add_feature_info(OPUS_USE_ALLOCA OPUS_USE_ALLOCA ${OPUS_USE_ALLOCA_HELP_STR})
set(OPUS_NONTHREADSAFE_PSEUDOSTACK_HELP_STR "use a non threadsafe pseudostack when neither variable length arrays or alloca is supported.")
cmake_dependent_option(OPUS_NONTHREADSAFE_PSEUDOSTACK
${OPUS_NONTHREADSAFE_PSEUDOSTACK_HELP_STR}
ON
"NOT OPUS_VAR_ARRAYS; NOT OPUS_USE_ALLOCA"
OFF)
add_feature_info(OPUS_NONTHREADSAFE_PSEUDOSTACK OPUS_NONTHREADSAFE_PSEUDOSTACK ${OPUS_NONTHREADSAFE_PSEUDOSTACK_HELP_STR})
set(OPUS_FAST_MATH_HELP_STR "enable fast math (unsupported and discouraged use, as code is not well tested with this build option).")
cmake_dependent_option(OPUS_FAST_MATH
${OPUS_FAST_MATH_HELP_STR}
ON
"OPUS_FLOAT_APPROX; OPUS_FAST_MATH; FAST_MATH_SUPPORTED"
OFF)
add_feature_info(OPUS_FAST_MATH OPUS_FAST_MATH ${OPUS_FAST_MATH_HELP_STR})
set(OPUS_STACK_PROTECTOR_HELP_STR "use stack protection.")
cmake_dependent_option(OPUS_STACK_PROTECTOR
${OPUS_STACK_PROTECTOR_HELP_STR}
ON
"STACK_PROTECTOR_SUPPORTED"
OFF)
add_feature_info(OPUS_STACK_PROTECTOR OPUS_STACK_PROTECTOR ${OPUS_STACK_PROTECTOR_HELP_STR})
if(NOT MSVC)
set(OPUS_FORTIFY_SOURCE_HELP_STR "add protection against buffer overflows.")
cmake_dependent_option(OPUS_FORTIFY_SOURCE
${OPUS_FORTIFY_SOURCE_HELP_STR}
ON
"FORTIFY_SOURCE_SUPPORTED"
OFF)
add_feature_info(OPUS_FORTIFY_SOURCE OPUS_FORTIFY_SOURCE ${OPUS_FORTIFY_SOURCE_HELP_STR})
endif()
if(MINGW AND (OPUS_FORTIFY_SOURCE OR OPUS_STACK_PROTECTOR))
# ssp lib is needed for security features for MINGW
list(APPEND OPUS_REQUIRED_LIBRARIES ssp)
endif()
if(OPUS_CPU_X86 OR OPUS_CPU_X64)
set(OPUS_X86_MAY_HAVE_SSE_HELP_STR "does runtime check for SSE1 support.")
cmake_dependent_option(OPUS_X86_MAY_HAVE_SSE
${OPUS_X86_MAY_HAVE_SSE_HELP_STR}
ON
"SSE1_SUPPORTED; NOT OPUS_DISABLE_INTRINSICS"
OFF)
add_feature_info(OPUS_X86_MAY_HAVE_SSE OPUS_X86_MAY_HAVE_SSE ${OPUS_X86_MAY_HAVE_SSE_HELP_STR})
set(OPUS_X86_MAY_HAVE_SSE2_HELP_STR "does runtime check for SSE2 support.")
cmake_dependent_option(OPUS_X86_MAY_HAVE_SSE2
${OPUS_X86_MAY_HAVE_SSE2_HELP_STR}
ON
"SSE2_SUPPORTED; NOT OPUS_DISABLE_INTRINSICS"
OFF)
add_feature_info(OPUS_X86_MAY_HAVE_SSE2 OPUS_X86_MAY_HAVE_SSE2 ${OPUS_X86_MAY_HAVE_SSE2_HELP_STR})
set(OPUS_X86_MAY_HAVE_SSE4_1_HELP_STR "does runtime check for SSE4.1 support.")
cmake_dependent_option(OPUS_X86_MAY_HAVE_SSE4_1
${OPUS_X86_MAY_HAVE_SSE4_1_HELP_STR}
ON
"SSE4_1_SUPPORTED; NOT OPUS_DISABLE_INTRINSICS"
OFF)
add_feature_info(OPUS_X86_MAY_HAVE_SSE4_1 OPUS_X86_MAY_HAVE_SSE4_1 ${OPUS_X86_MAY_HAVE_SSE4_1_HELP_STR})
set(OPUS_X86_MAY_HAVE_AVX2_HELP_STR "does runtime check for AVX FMA AVX2 support.")
cmake_dependent_option(OPUS_X86_MAY_HAVE_AVX2
${OPUS_X86_MAY_HAVE_AVX2_HELP_STR}
ON
"AVX2_SUPPORTED; NOT OPUS_DISABLE_INTRINSICS"
OFF)
add_feature_info(OPUS_X86_MAY_HAVE_AVX2 OPUS_X86_MAY_HAVE_AVX2 ${OPUS_X86_MAY_HAVE_AVX2_HELP_STR})
# PRESUME depends on MAY HAVE, but PRESUME will override runtime detection
set(OPUS_X86_PRESUME_SSE_HELP_STR "assume target CPU has SSE1 support (override runtime check).")
set(OPUS_X86_PRESUME_SSE2_HELP_STR "assume target CPU has SSE2 support (override runtime check).")
if(OPUS_CPU_X64) # Assume x86_64 has up to SSE2 support
cmake_dependent_option(OPUS_X86_PRESUME_SSE
${OPUS_X86_PRESUME_SSE_HELP_STR}
ON
"OPUS_X86_MAY_HAVE_SSE; NOT OPUS_DISABLE_INTRINSICS"
OFF)
cmake_dependent_option(OPUS_X86_PRESUME_SSE2
${OPUS_X86_PRESUME_SSE2_HELP_STR}
ON
"OPUS_X86_MAY_HAVE_SSE2; NOT OPUS_DISABLE_INTRINSICS"
OFF)
else()
cmake_dependent_option(OPUS_X86_PRESUME_SSE
${OPUS_X86_PRESUME_SSE_HELP_STR}
OFF
"OPUS_X86_MAY_HAVE_SSE; NOT OPUS_DISABLE_INTRINSICS"
OFF)
cmake_dependent_option(OPUS_X86_PRESUME_SSE2
${OPUS_X86_PRESUME_SSE2_HELP_STR}
OFF
"OPUS_X86_MAY_HAVE_SSE2; NOT OPUS_DISABLE_INTRINSICS"
OFF)
endif()
add_feature_info(OPUS_X86_PRESUME_SSE OPUS_X86_PRESUME_SSE ${OPUS_X86_PRESUME_SSE_HELP_STR})
add_feature_info(OPUS_X86_PRESUME_SSE2 OPUS_X86_PRESUME_SSE2 ${OPUS_X86_PRESUME_SSE2_HELP_STR})
set(OPUS_X86_PRESUME_SSE4_1_HELP_STR "assume target CPU has SSE4.1 support (override runtime check).")
cmake_dependent_option(OPUS_X86_PRESUME_SSE4_1
${OPUS_X86_PRESUME_SSE4_1_HELP_STR}
OFF
"OPUS_X86_MAY_HAVE_SSE4_1; NOT OPUS_DISABLE_INTRINSICS"
OFF)
add_feature_info(OPUS_X86_PRESUME_SSE4_1 OPUS_X86_PRESUME_SSE4_1 ${OPUS_X86_PRESUME_SSE4_1_HELP_STR})
set(OPUS_X86_PRESUME_AVX2_HELP_STR "assume target CPU has AVX FMA AVX2 support (override runtime check).")
cmake_dependent_option(OPUS_X86_PRESUME_AVX2
${OPUS_X86_PRESUME_AVX2_HELP_STR}
OFF
"OPUS_X86_MAY_HAVE_AVX2; NOT OPUS_DISABLE_INTRINSICS"
OFF)
add_feature_info(OPUS_X86_PRESUME_AVX2 OPUS_X86_PRESUME_AVX2 ${OPUS_X86_PRESUME_AVX2_HELP_STR})
endif()
feature_summary(WHAT ALL)
set_package_properties(Git
PROPERTIES
TYPE
REQUIRED
DESCRIPTION
"fast, scalable, distributed revision control system"
URL
"https://git-scm.com/"
PURPOSE
"required to set up package version")
set(Opus_PUBLIC_HEADER
${CMAKE_CURRENT_SOURCE_DIR}/include/opus.h
${CMAKE_CURRENT_SOURCE_DIR}/include/opus_defines.h
${CMAKE_CURRENT_SOURCE_DIR}/include/opus_multistream.h
${CMAKE_CURRENT_SOURCE_DIR}/include/opus_projection.h
${CMAKE_CURRENT_SOURCE_DIR}/include/opus_types.h)
if(OPUS_CUSTOM_MODES)
list(APPEND Opus_PUBLIC_HEADER ${CMAKE_CURRENT_SOURCE_DIR}/include/opus_custom.h)
endif()
if(MSVC)
if(OPUS_STATIC_RUNTIME)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
else()
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
endif()
endif()
add_library(opus ${opus_headers} ${opus_sources} ${opus_sources_float} ${Opus_PUBLIC_HEADER})
add_library(Opus::opus ALIAS opus)
get_library_version(OPUS_LIBRARY_VERSION OPUS_LIBRARY_VERSION_MAJOR)
message(DEBUG "Opus library version: ${OPUS_LIBRARY_VERSION}")
set_target_properties(opus
PROPERTIES SOVERSION
${OPUS_LIBRARY_VERSION_MAJOR}
VERSION
${OPUS_LIBRARY_VERSION}
PUBLIC_HEADER
"${Opus_PUBLIC_HEADER}")
target_include_directories(
opus
PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/opus>
PRIVATE ${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/dnn
celt
silk)
target_link_libraries(opus PRIVATE ${OPUS_REQUIRED_LIBRARIES})
target_compile_definitions(opus PRIVATE OPUS_BUILD)
if(OPUS_FIXED_POINT_DEBUG)
target_compile_definitions(opus PRIVATE FIXED_DEBUG)
endif()
if(OPUS_FORTIFY_SOURCE AND NOT MSVC)
target_compile_definitions(opus PRIVATE
$<$<NOT:$<CONFIG:debug>>:_FORTIFY_SOURCE=2>)
endif()
if(OPUS_FLOAT_APPROX)
target_compile_definitions(opus PRIVATE FLOAT_APPROX)
endif()
if(OPUS_ASSERTIONS)
target_compile_definitions(opus PRIVATE ENABLE_ASSERTIONS)
endif()
if(OPUS_HARDENING)
target_compile_definitions(opus PRIVATE ENABLE_HARDENING)
endif()
if(OPUS_FUZZING)
target_compile_definitions(opus PRIVATE FUZZING)
endif()
if(OPUS_CHECK_ASM)
target_compile_definitions(opus PRIVATE OPUS_CHECK_ASM)
endif()
if(NOT OPUS_DNN_FLOAT_DEBUG)
target_compile_definitions(opus PRIVATE DISABLE_DEBUG_FLOAT)
endif()
if(OPUS_VAR_ARRAYS)
target_compile_definitions(opus PRIVATE VAR_ARRAYS)
elseif(OPUS_USE_ALLOCA)
target_compile_definitions(opus PRIVATE USE_ALLOCA)
elseif(OPUS_NONTHREADSAFE_PSEUDOSTACK)
target_compile_definitions(opus PRIVATE NONTHREADSAFE_PSEUDOSTACK)
else()
message(ERROR "Need to set a define for stack allocation")
endif()
if(OPUS_CUSTOM_MODES)
target_compile_definitions(opus PRIVATE CUSTOM_MODES)
endif()
if(OPUS_FAST_MATH)
if(MSVC)
target_compile_options(opus PRIVATE /fp:fast)
else()
target_compile_options(opus PRIVATE -ffast-math)
endif()
endif()
if(OPUS_STACK_PROTECTOR)
if(MSVC)
target_compile_options(opus PRIVATE /GS)
else()
target_compile_options(opus PRIVATE -fstack-protector-strong)
endif()
elseif(STACK_PROTECTOR_DISABLED_SUPPORTED)
target_compile_options(opus PRIVATE /GS-)
endif()
if(BUILD_SHARED_LIBS)
if(WIN32)
target_compile_definitions(opus PRIVATE DLL_EXPORT)
elseif(HIDDEN_VISIBILITY_SUPPORTED)
set_target_properties(opus PROPERTIES C_VISIBILITY_PRESET hidden)
endif()
endif()
add_sources_group(opus silk ${silk_headers} ${silk_sources})
add_sources_group(opus celt ${celt_headers} ${celt_sources})
if(OPUS_FIXED_POINT)
add_sources_group(opus silk ${silk_sources_fixed})
target_include_directories(opus PRIVATE silk/fixed)
target_compile_definitions(opus PRIVATE FIXED_POINT=1)
else()
add_sources_group(opus silk ${silk_sources_float})
target_include_directories(opus PRIVATE silk/float)
endif()
if(NOT OPUS_ENABLE_FLOAT_API)
target_compile_definitions(opus PRIVATE DISABLE_FLOAT_API)
endif()
if (OPUS_DEEP_PLC OR OPUS_DRED OR OPUS_OSCE)
add_sources_group(opus lpcnet ${deep_plc_headers} ${deep_plc_sources})
set(OPUS_DNN TRUE)
else()
set(OPUS_DNN FALSE)
endif()
if (OPUS_DNN)
add_sources_group(opus lpcnet ${deep_plc_headers} ${deep_plc_sources})
target_compile_definitions(opus PRIVATE ENABLE_DEEP_PLC)
endif()
if (OPUS_DRED)
add_sources_group(opus lpcnet ${dred_headers} ${dred_sources})
target_compile_definitions(opus PRIVATE ENABLE_DRED)
endif()
if (OPUS_OSCE)
add_sources_group(opus lpcnet ${osce_headers} ${osce_sources})
target_compile_definitions(opus PRIVATE ENABLE_OSCE)
endif()
if(NOT OPUS_DISABLE_INTRINSICS)
if(((OPUS_X86_MAY_HAVE_SSE AND NOT OPUS_X86_PRESUME_SSE) OR
(OPUS_X86_MAY_HAVE_SSE2 AND NOT OPUS_X86_PRESUME_SSE2) OR
(OPUS_X86_MAY_HAVE_SSE4_1 AND NOT OPUS_X86_PRESUME_SSE4_1) OR
(OPUS_X86_MAY_HAVE_AVX2 AND NOT OPUS_X86_PRESUME_AVX2)) AND
RUNTIME_CPU_CAPABILITY_DETECTION)
target_compile_definitions(opus PRIVATE OPUS_HAVE_RTCD)
if(NOT MSVC)
if(CPU_INFO_BY_ASM_SUPPORTED)
target_compile_definitions(opus PRIVATE CPU_INFO_BY_ASM)
elseif(CPU_INFO_BY_C_SUPPORTED)
target_compile_definitions(opus PRIVATE CPU_INFO_BY_C)
else()
message(ERROR "Runtime cpu capability detection is enabled while CPU_INFO is not supported")
endif()
endif()
add_sources_group(opus celt ${celt_sources_x86_rtcd})
add_sources_group(opus silk ${silk_sources_x86_rtcd})
if (OPUS_DNN)
add_sources_group(opus lpcnet ${dnn_sources_x86_rtcd})
endif()
endif()
if(SSE1_SUPPORTED)
if(OPUS_X86_MAY_HAVE_SSE)
add_sources_group(opus celt ${celt_sources_sse})
target_compile_definitions(opus PRIVATE OPUS_X86_MAY_HAVE_SSE)
if(NOT MSVC)
set_source_files_properties(${celt_sources_sse} PROPERTIES COMPILE_FLAGS -msse)
endif()
endif()
if(OPUS_X86_PRESUME_SSE)
target_compile_definitions(opus PRIVATE OPUS_X86_PRESUME_SSE)
if(NOT MSVC)
target_compile_options(opus PRIVATE -msse)
endif()
endif()
endif()
if(SSE2_SUPPORTED)
if(OPUS_X86_MAY_HAVE_SSE2)
add_sources_group(opus celt ${celt_sources_sse2})
if (OPUS_DNN)
add_sources_group(opus lpcnet ${dnn_sources_sse2})
endif()
target_compile_definitions(opus PRIVATE OPUS_X86_MAY_HAVE_SSE2)
if(NOT MSVC)
set_source_files_properties(${celt_sources_sse2} ${dnn_sources_sse2} PROPERTIES COMPILE_FLAGS -msse2)
endif()
endif()
if(OPUS_X86_PRESUME_SSE2)
target_compile_definitions(opus PRIVATE OPUS_X86_PRESUME_SSE2)
if(NOT MSVC)
target_compile_options(opus PRIVATE -msse2)
endif()
endif()
endif()
if(SSE4_1_SUPPORTED)
if(OPUS_X86_MAY_HAVE_SSE4_1)
add_sources_group(opus celt ${celt_sources_sse4_1})
add_sources_group(opus silk ${silk_sources_sse4_1})
if (OPUS_DNN)
add_sources_group(opus lpcnet ${dnn_sources_sse4_1})
endif()
target_compile_definitions(opus PRIVATE OPUS_X86_MAY_HAVE_SSE4_1)
if(NOT MSVC)
set_source_files_properties(${celt_sources_sse4_1} ${silk_sources_sse4_1} ${dnn_sources_sse4_1} PROPERTIES COMPILE_FLAGS -msse4.1)
endif()
if(OPUS_FIXED_POINT)
add_sources_group(opus silk ${silk_sources_fixed_sse4_1})
if(NOT MSVC)
set_source_files_properties(${silk_sources_fixed_sse4_1} PROPERTIES COMPILE_FLAGS -msse4.1)
endif()
endif()
endif()
if(OPUS_X86_PRESUME_SSE4_1)
target_compile_definitions(opus PRIVATE OPUS_X86_PRESUME_SSE4_1)
if(NOT MSVC)
target_compile_options(opus PRIVATE -msse4.1)
endif()
endif()
endif()
if(AVX2_SUPPORTED)
if(OPUS_X86_MAY_HAVE_AVX2)
add_sources_group(opus celt ${celt_sources_avx2})
add_sources_group(opus silk ${silk_sources_avx2})
add_sources_group(opus silk ${silk_sources_float_avx2})
if (OPUS_DNN)
add_sources_group(opus lpcnet ${dnn_sources_avx2})
endif()
target_compile_definitions(opus PRIVATE OPUS_X86_MAY_HAVE_AVX2)
if(MSVC)
set(AVX2_FLAGS "${AVX2_FLAGS} /arch:AVX2")
else()
set(AVX2_FLAGS "${AVX2_FLAGS} -mavx2 -mfma -mavx")
endif()
set_source_files_properties(${celt_sources_avx2} PROPERTIES COMPILE_FLAGS ${AVX2_FLAGS})
set_source_files_properties(${silk_sources_avx2} PROPERTIES COMPILE_FLAGS ${AVX2_FLAGS})
set_source_files_properties(${silk_sources_float_avx2} PROPERTIES COMPILE_FLAGS ${AVX2_FLAGS})
set_source_files_properties(${dnn_sources_avx2} PROPERTIES COMPILE_FLAGS ${AVX2_FLAGS})
endif()
if(OPUS_X86_PRESUME_AVX2)
target_compile_definitions(opus PRIVATE OPUS_X86_PRESUME_AVX2)
target_compile_definitions(opus PRIVATE OPUS_X86_PRESUME_SSE4_1)
if(NOT MSVC)
target_compile_options(opus PRIVATE -mavx2 -mfma -mavx)
endif()
endif()
endif()
if(MSVC)
if(AVX2_SUPPORTED AND OPUS_X86_PRESUME_AVX2) # on 64 bit and 32 bits
add_definitions(/arch:AVX2)
elseif(OPUS_CPU_X86) # if AVX not supported then set SSE flag
if((SSE4_1_SUPPORTED AND OPUS_X86_PRESUME_SSE4_1)
OR (SSE2_SUPPORTED AND OPUS_X86_PRESUME_SSE2))
target_compile_definitions(opus PRIVATE /arch:SSE2)
elseif(SSE1_SUPPORTED AND OPUS_X86_PRESUME_SSE)
target_compile_definitions(opus PRIVATE /arch:SSE)
endif()
endif()
endif()
if(COMPILER_SUPPORT_NEON)
if(OPUS_MAY_HAVE_NEON)
if(RUNTIME_CPU_CAPABILITY_DETECTION)
message(STATUS "OPUS_MAY_HAVE_NEON enabling runtime detection")
target_compile_definitions(opus PRIVATE OPUS_HAVE_RTCD)
add_sources_group(opus celt ${celt_sources_arm_rtcd})
add_sources_group(opus silk ${silk_sources_arm_rtcd})
else()
message(ERROR "Runtime cpu capability detection needed for MAY_HAVE_NEON")
endif()
# Do runtime check for NEON
target_compile_definitions(opus
PRIVATE
OPUS_ARM_MAY_HAVE_NEON
OPUS_ARM_MAY_HAVE_NEON_INTR)
endif()
add_sources_group(opus celt ${celt_sources_arm_neon_intr})
add_sources_group(opus silk ${silk_sources_arm_neon_intr})
if (OPUS_DNN)
add_sources_group(opus lpcnet ${dnn_sources_arm_neon})
endif()
# silk arm neon depends on main_Fix.h
target_include_directories(opus PRIVATE silk/fixed)
if(OPUS_FIXED_POINT)
add_sources_group(opus silk ${silk_sources_fixed_arm_neon_intr})
endif()
if(OPUS_PRESUME_NEON)
target_compile_definitions(opus
PRIVATE
OPUS_ARM_PRESUME_NEON
OPUS_ARM_PRESUME_NEON_INTR)
endif()
endif()
endif()
target_compile_definitions(opus
PRIVATE
$<$<BOOL:${HAVE_LRINT}>:HAVE_LRINT>
$<$<BOOL:${HAVE_LRINTF}>:HAVE_LRINTF>)
if(OPUS_BUILD_FRAMEWORK)
set_target_properties(opus PROPERTIES
FRAMEWORK TRUE
FRAMEWORK_VERSION ${PROJECT_VERSION}
MACOSX_FRAMEWORK_IDENTIFIER org.xiph.opus
MACOSX_FRAMEWORK_SHORT_VERSION_STRING ${PROJECT_VERSION}
MACOSX_FRAMEWORK_BUNDLE_VERSION ${PROJECT_VERSION}
XCODE_ATTRIBUTE_INSTALL_PATH "@rpath"
OUTPUT_NAME Opus)
endif()
install(TARGETS opus
EXPORT OpusTargets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
FRAMEWORK DESTINATION ${CMAKE_INSTALL_PREFIX}
PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/opus)
if(OPUS_INSTALL_PKG_CONFIG_MODULE)
set(prefix ${CMAKE_INSTALL_PREFIX})
set(exec_prefix ${CMAKE_INSTALL_PREFIX})
set(libdir ${CMAKE_INSTALL_FULL_LIBDIR})
set(includedir ${CMAKE_INSTALL_FULL_INCLUDEDIR})
set(VERSION ${PACKAGE_VERSION})
if(HAVE_LIBM)
set(LIBM "-lm")
endif()
configure_file(opus.pc.in opus.pc)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/opus.pc
DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
endif()
if(OPUS_INSTALL_CMAKE_CONFIG_MODULE)
set(CPACK_GENERATOR TGZ)
include(CPack)
set(CMAKE_INSTALL_PACKAGEDIR ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})
install(EXPORT OpusTargets
NAMESPACE Opus::
DESTINATION ${CMAKE_INSTALL_PACKAGEDIR})
include(CMakePackageConfigHelpers)
set(INCLUDE_INSTALL_DIR ${CMAKE_INSTALL_INCLUDEDIR})
configure_package_config_file(${PROJECT_SOURCE_DIR}/cmake/OpusConfig.cmake.in
OpusConfig.cmake
INSTALL_DESTINATION
${CMAKE_INSTALL_PACKAGEDIR}
PATH_VARS
INCLUDE_INSTALL_DIR
INSTALL_PREFIX
${CMAKE_INSTALL_PREFIX})
write_basic_package_version_file(OpusConfigVersion.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/OpusConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/OpusConfigVersion.cmake
DESTINATION ${CMAKE_INSTALL_PACKAGEDIR})
endif()
if(OPUS_BUILD_PROGRAMS)
# demo
if(OPUS_CUSTOM_MODES)
add_executable(opus_custom_demo ${opus_custom_demo_sources})
target_include_directories(opus_custom_demo
PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(opus_custom_demo PRIVATE opus)
target_compile_definitions(opus_custom_demo PRIVATE OPUS_BUILD)
endif()
add_executable(opus_demo ${opus_demo_sources})
target_include_directories(opus_demo PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(opus_demo PRIVATE silk) # debug.h
target_include_directories(opus_demo PRIVATE celt) # arch.h
target_include_directories(opus_demo PRIVATE dnn)
target_link_libraries(opus_demo PRIVATE opus ${OPUS_REQUIRED_LIBRARIES})
target_compile_definitions(opus_demo PRIVATE OPUS_BUILD)
# compare
add_executable(opus_compare ${opus_compare_sources})
target_include_directories(opus_compare PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(opus_compare PRIVATE opus ${OPUS_REQUIRED_LIBRARIES})
endif()
if(BUILD_TESTING AND NOT BUILD_SHARED_LIBS)
enable_testing()
# tests
add_executable(test_opus_decode ${test_opus_decode_sources})
target_include_directories(test_opus_decode
PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(test_opus_decode PRIVATE opus)
target_compile_definitions(test_opus_decode PRIVATE OPUS_BUILD)
if(OPUS_FIXED_POINT)
target_compile_definitions(test_opus_decode PRIVATE DISABLE_FLOAT_API)
endif()
add_test(NAME test_opus_decode COMMAND ${CMAKE_COMMAND}
-DTEST_EXECUTABLE=$<TARGET_FILE:test_opus_decode>
-DCMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAME}
-P "${PROJECT_SOURCE_DIR}/cmake/RunTest.cmake")
add_executable(test_opus_padding ${test_opus_padding_sources})
target_include_directories(test_opus_padding
PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(test_opus_padding PRIVATE opus)
add_test(NAME test_opus_padding COMMAND ${CMAKE_COMMAND}
-DTEST_EXECUTABLE=$<TARGET_FILE:test_opus_padding>
-DCMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAME}
-P "${PROJECT_SOURCE_DIR}/cmake/RunTest.cmake")
add_executable(test_opus_api ${test_opus_api_sources})
target_include_directories(test_opus_api
PRIVATE ${CMAKE_CURRENT_BINARY_DIR} celt)
target_link_libraries(test_opus_api PRIVATE opus)
target_compile_definitions(test_opus_api PRIVATE OPUS_BUILD)
if(OPUS_FIXED_POINT)
target_compile_definitions(test_opus_api PRIVATE DISABLE_FLOAT_API)
endif()
add_test(NAME test_opus_api COMMAND ${CMAKE_COMMAND}
-DTEST_EXECUTABLE=$<TARGET_FILE:test_opus_api>
-DCMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAME}
-P "${PROJECT_SOURCE_DIR}/cmake/RunTest.cmake")
add_executable(test_opus_encode ${test_opus_encode_sources})
target_include_directories(test_opus_encode
PRIVATE ${CMAKE_CURRENT_BINARY_DIR} celt dnn)
target_link_libraries(test_opus_encode PRIVATE opus)
target_compile_definitions(test_opus_encode PRIVATE OPUS_BUILD)
add_test(NAME test_opus_encode COMMAND ${CMAKE_COMMAND}
-DTEST_EXECUTABLE=$<TARGET_FILE:test_opus_encode>
-DCMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAME}
-P "${PROJECT_SOURCE_DIR}/cmake/RunTest.cmake")
add_executable(test_opus_extensions ${test_opus_extensions_sources})
target_include_directories(test_opus_extensions
PRIVATE ${CMAKE_CURRENT_BINARY_DIR} celt dnn)
target_link_libraries(test_opus_extensions PRIVATE opus)
target_compile_definitions(test_opus_extensions PRIVATE OPUS_BUILD)
add_test(NAME test_opus_extensions COMMAND ${CMAKE_COMMAND}
-DTEST_EXECUTABLE=$<TARGET_FILE:test_opus_extensions>
-DCMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAME}
-P "${PROJECT_SOURCE_DIR}/cmake/RunTest.cmake")
if(OPUS_DRED)
add_executable(test_opus_dred ${test_opus_dred_sources})
target_include_directories(test_opus_dred
PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(test_opus_dred PRIVATE opus)
target_compile_definitions(test_opus_dred PRIVATE OPUS_BUILD)
add_test(NAME test_opus_dred COMMAND ${CMAKE_COMMAND}
-DTEST_EXECUTABLE=$<TARGET_FILE:test_opus_dred>
-DCMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAME}
-P "${PROJECT_SOURCE_DIR}/cmake/RunTest.cmake")
endif()
endif()

44
local_plugins/opus/android/libs/opus/COPYING

@ -0,0 +1,44 @@
Copyright 2001-2023 Xiph.Org, Skype Limited, Octasic,
Jean-Marc Valin, Timothy B. Terriberry,
CSIRO, Gregory Maxwell, Mark Borgerding,
Erik de Castro Lopo, Mozilla, Amazon
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
- Neither the name of Internet Society, IETF or IETF Trust, nor the
names of specific contributors, may be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Opus is subject to the royalty-free patent licenses which are
specified at:
Xiph.Org Foundation:
https://datatracker.ietf.org/ipr/1524/
Microsoft Corporation:
https://datatracker.ietf.org/ipr/1914/
Broadcom Corporation:
https://datatracker.ietf.org/ipr/1526/

0
local_plugins/opus/android/libs/opus/ChangeLog

368
local_plugins/opus/android/libs/opus/INSTALL

@ -0,0 +1,368 @@
Installation Instructions
*************************
Copyright (C) 1994-1996, 1999-2002, 2004-2017, 2020-2021 Free
Software Foundation, Inc.
Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. This file is offered as-is,
without warranty of any kind.
Basic Installation
==================
Briefly, the shell command './configure && make && make install'
should configure, build, and install this package. The following
more-detailed instructions are generic; see the 'README' file for
instructions specific to this package. Some packages provide this
'INSTALL' file but do not implement all of the features documented
below. The lack of an optional feature in a given package is not
necessarily a bug. More recommendations for GNU packages can be found
in *note Makefile Conventions: (standards)Makefile Conventions.
The 'configure' shell script attempts to guess correct values for
various system-dependent variables used during compilation. It uses
those values to create a 'Makefile' in each directory of the package.
It may also create one or more '.h' files containing system-dependent
definitions. Finally, it creates a shell script 'config.status' that
you can run in the future to recreate the current configuration, and a
file 'config.log' containing compiler output (useful mainly for
debugging 'configure').
It can also use an optional file (typically called 'config.cache' and
enabled with '--cache-file=config.cache' or simply '-C') that saves the
results of its tests to speed up reconfiguring. Caching is disabled by
default to prevent problems with accidental use of stale cache files.
If you need to do unusual things to compile the package, please try
to figure out how 'configure' could check whether to do them, and mail
diffs or instructions to the address given in the 'README' so they can
be considered for the next release. If you are using the cache, and at
some point 'config.cache' contains results you don't want to keep, you
may remove or edit it.
The file 'configure.ac' (or 'configure.in') is used to create
'configure' by a program called 'autoconf'. You need 'configure.ac' if
you want to change it or regenerate 'configure' using a newer version of
'autoconf'.
The simplest way to compile this package is:
1. 'cd' to the directory containing the package's source code and type
'./configure' to configure the package for your system.
Running 'configure' might take a while. While running, it prints
some messages telling which features it is checking for.
2. Type 'make' to compile the package.
3. Optionally, type 'make check' to run any self-tests that come with
the package, generally using the just-built uninstalled binaries.
4. Type 'make install' to install the programs and any data files and
documentation. When installing into a prefix owned by root, it is
recommended that the package be configured and built as a regular
user, and only the 'make install' phase executed with root
privileges.
5. Optionally, type 'make installcheck' to repeat any self-tests, but
this time using the binaries in their final installed location.
This target does not install anything. Running this target as a
regular user, particularly if the prior 'make install' required
root privileges, verifies that the installation completed
correctly.
6. You can remove the program binaries and object files from the
source code directory by typing 'make clean'. To also remove the
files that 'configure' created (so you can compile the package for
a different kind of computer), type 'make distclean'. There is
also a 'make maintainer-clean' target, but that is intended mainly
for the package's developers. If you use it, you may have to get
all sorts of other programs in order to regenerate files that came
with the distribution.
7. Often, you can also type 'make uninstall' to remove the installed
files again. In practice, not all packages have tested that
uninstallation works correctly, even though it is required by the
GNU Coding Standards.
8. Some packages, particularly those that use Automake, provide 'make
distcheck', which can by used by developers to test that all other
targets like 'make install' and 'make uninstall' work correctly.
This target is generally not run by end users.
Compilers and Options
=====================
Some systems require unusual options for compilation or linking that
the 'configure' script does not know about. Run './configure --help'
for details on some of the pertinent environment variables.
You can give 'configure' initial values for configuration parameters
by setting variables in the command line or in the environment. Here is
an example:
./configure CC=c99 CFLAGS=-g LIBS=-lposix
*Note Defining Variables::, for more details.
Compiling For Multiple Architectures
====================================
You can compile the package for more than one kind of computer at the
same time, by placing the object files for each architecture in their
own directory. To do this, you can use GNU 'make'. 'cd' to the
directory where you want the object files and executables to go and run
the 'configure' script. 'configure' automatically checks for the source
code in the directory that 'configure' is in and in '..'. This is known
as a "VPATH" build.
With a non-GNU 'make', it is safer to compile the package for one
architecture at a time in the source code directory. After you have
installed the package for one architecture, use 'make distclean' before
reconfiguring for another architecture.
On MacOS X 10.5 and later systems, you can create libraries and
executables that work on multiple system types--known as "fat" or
"universal" binaries--by specifying multiple '-arch' options to the
compiler but only a single '-arch' option to the preprocessor. Like
this:
./configure CC="gcc -arch i386 -arch x86_64 -arch ppc -arch ppc64" \
CXX="g++ -arch i386 -arch x86_64 -arch ppc -arch ppc64" \
CPP="gcc -E" CXXCPP="g++ -E"
This is not guaranteed to produce working output in all cases, you
may have to build one architecture at a time and combine the results
using the 'lipo' tool if you have problems.
Installation Names
==================
By default, 'make install' installs the package's commands under
'/usr/local/bin', include files under '/usr/local/include', etc. You
can specify an installation prefix other than '/usr/local' by giving
'configure' the option '--prefix=PREFIX', where PREFIX must be an
absolute file name.
You can specify separate installation prefixes for
architecture-specific files and architecture-independent files. If you
pass the option '--exec-prefix=PREFIX' to 'configure', the package uses
PREFIX as the prefix for installing programs and libraries.
Documentation and other data files still use the regular prefix.
In addition, if you use an unusual directory layout you can give
options like '--bindir=DIR' to specify different values for particular
kinds of files. Run 'configure --help' for a list of the directories
you can set and what kinds of files go in them. In general, the default
for these options is expressed in terms of '${prefix}', so that
specifying just '--prefix' will affect all of the other directory
specifications that were not explicitly provided.
The most portable way to affect installation locations is to pass the
correct locations to 'configure'; however, many packages provide one or
both of the following shortcuts of passing variable assignments to the
'make install' command line to change installation locations without
having to reconfigure or recompile.
The first method involves providing an override variable for each
affected directory. For example, 'make install
prefix=/alternate/directory' will choose an alternate location for all
directory configuration variables that were expressed in terms of
'${prefix}'. Any directories that were specified during 'configure',
but not in terms of '${prefix}', must each be overridden at install time
for the entire installation to be relocated. The approach of makefile
variable overrides for each directory variable is required by the GNU
Coding Standards, and ideally causes no recompilation. However, some
platforms have known limitations with the semantics of shared libraries
that end up requiring recompilation when using this method, particularly
noticeable in packages that use GNU Libtool.
The second method involves providing the 'DESTDIR' variable. For
example, 'make install DESTDIR=/alternate/directory' will prepend
'/alternate/directory' before all installation names. The approach of
'DESTDIR' overrides is not required by the GNU Coding Standards, and
does not work on platforms that have drive letters. On the other hand,
it does better at avoiding recompilation issues, and works well even
when some directory options were not specified in terms of '${prefix}'
at 'configure' time.
Optional Features
=================
If the package supports it, you can cause programs to be installed
with an extra prefix or suffix on their names by giving 'configure' the
option '--program-prefix=PREFIX' or '--program-suffix=SUFFIX'.
Some packages pay attention to '--enable-FEATURE' options to
'configure', where FEATURE indicates an optional part of the package.
They may also pay attention to '--with-PACKAGE' options, where PACKAGE
is something like 'gnu-as' or 'x' (for the X Window System). The
'README' should mention any '--enable-' and '--with-' options that the
package recognizes.
For packages that use the X Window System, 'configure' can usually
find the X include and library files automatically, but if it doesn't,
you can use the 'configure' options '--x-includes=DIR' and
'--x-libraries=DIR' to specify their locations.
Some packages offer the ability to configure how verbose the
execution of 'make' will be. For these packages, running './configure
--enable-silent-rules' sets the default to minimal output, which can be
overridden with 'make V=1'; while running './configure
--disable-silent-rules' sets the default to verbose, which can be
overridden with 'make V=0'.
Particular systems
==================
On HP-UX, the default C compiler is not ANSI C compatible. If GNU CC
is not installed, it is recommended to use the following options in
order to use an ANSI C compiler:
./configure CC="cc -Ae -D_XOPEN_SOURCE=500"
and if that doesn't work, install pre-built binaries of GCC for HP-UX.
HP-UX 'make' updates targets which have the same timestamps as their
prerequisites, which makes it generally unusable when shipped generated
files such as 'configure' are involved. Use GNU 'make' instead.
On OSF/1 a.k.a. Tru64, some versions of the default C compiler cannot
parse its '<wchar.h>' header file. The option '-nodtk' can be used as a
workaround. If GNU CC is not installed, it is therefore recommended to
try
./configure CC="cc"
and if that doesn't work, try
./configure CC="cc -nodtk"
On Solaris, don't put '/usr/ucb' early in your 'PATH'. This
directory contains several dysfunctional programs; working variants of
these programs are available in '/usr/bin'. So, if you need '/usr/ucb'
in your 'PATH', put it _after_ '/usr/bin'.
On Haiku, software installed for all users goes in '/boot/common',
not '/usr/local'. It is recommended to use the following options:
./configure --prefix=/boot/common
Specifying the System Type
==========================
There may be some features 'configure' cannot figure out
automatically, but needs to determine by the type of machine the package
will run on. Usually, assuming the package is built to be run on the
_same_ architectures, 'configure' can figure that out, but if it prints
a message saying it cannot guess the machine type, give it the
'--build=TYPE' option. TYPE can either be a short name for the system
type, such as 'sun4', or a canonical name which has the form:
CPU-COMPANY-SYSTEM
where SYSTEM can have one of these forms:
OS
KERNEL-OS
See the file 'config.sub' for the possible values of each field. If
'config.sub' isn't included in this package, then this package doesn't
need to know the machine type.
If you are _building_ compiler tools for cross-compiling, you should
use the option '--target=TYPE' to select the type of system they will
produce code for.
If you want to _use_ a cross compiler, that generates code for a
platform different from the build platform, you should specify the
"host" platform (i.e., that on which the generated programs will
eventually be run) with '--host=TYPE'.
Sharing Defaults
================
If you want to set default values for 'configure' scripts to share,
you can create a site shell script called 'config.site' that gives
default values for variables like 'CC', 'cache_file', and 'prefix'.
'configure' looks for 'PREFIX/share/config.site' if it exists, then
'PREFIX/etc/config.site' if it exists. Or, you can set the
'CONFIG_SITE' environment variable to the location of the site script.
A warning: not all 'configure' scripts look for a site script.
Defining Variables
==================
Variables not defined in a site shell script can be set in the
environment passed to 'configure'. However, some packages may run
configure again during the build, and the customized values of these
variables may be lost. In order to avoid this problem, you should set
them in the 'configure' command line, using 'VAR=value'. For example:
./configure CC=/usr/local2/bin/gcc
causes the specified 'gcc' to be used as the C compiler (unless it is
overridden in the site shell script).
Unfortunately, this technique does not work for 'CONFIG_SHELL' due to an
Autoconf limitation. Until the limitation is lifted, you can use this
workaround:
CONFIG_SHELL=/bin/bash ./configure CONFIG_SHELL=/bin/bash
'configure' Invocation
======================
'configure' recognizes the following options to control how it
operates.
'--help'
'-h'
Print a summary of all of the options to 'configure', and exit.
'--help=short'
'--help=recursive'
Print a summary of the options unique to this package's
'configure', and exit. The 'short' variant lists options used only
in the top level, while the 'recursive' variant lists options also
present in any nested packages.
'--version'
'-V'
Print the version of Autoconf used to generate the 'configure'
script, and exit.
'--cache-file=FILE'
Enable the cache: use and save the results of the tests in FILE,
traditionally 'config.cache'. FILE defaults to '/dev/null' to
disable caching.
'--config-cache'
'-C'
Alias for '--cache-file=config.cache'.
'--quiet'
'--silent'
'-q'
Do not print messages saying which checks are being made. To
suppress all normal output, redirect it to '/dev/null' (any error
messages will still be shown).
'--srcdir=DIR'
Look for the package's source code in directory DIR. Usually
'configure' can determine that directory automatically.
'--prefix=DIR'
Use DIR as the installation prefix. *note Installation Names:: for
more details, including other options available for fine-tuning the
installation locations.
'--no-create'
'-n'
Run the configure checks, but stop before creating any output
files.
'configure' also accepts some other, not widely useful, options. Run
'configure --help' for more details.

489
local_plugins/opus/android/libs/opus/Makefile.am

@ -0,0 +1,489 @@
# Provide the full test output for failed tests when using the parallel
# test suite (which is enabled by default with automake 1.13+).
export VERBOSE = yes
AUTOMAKE_OPTIONS = subdir-objects
ACLOCAL_AMFLAGS = -I m4
lib_LTLIBRARIES = libopus.la
DIST_SUBDIRS = doc
AM_CPPFLAGS = -I$(top_srcdir)/include -I$(top_srcdir)/celt -I$(top_srcdir)/silk \
-I$(top_srcdir)/silk/float -I$(top_srcdir)/silk/fixed $(NE10_CFLAGS) \
-I$(top_srcdir)/dnn
include celt_sources.mk
include lpcnet_sources.mk
include silk_sources.mk
include opus_sources.mk
LPCNET_SOURCES =
if ENABLE_DEEP_PLC
LPCNET_SOURCES += $(DEEP_PLC_SOURCES)
endif
if ENABLE_DRED
LPCNET_SOURCES += $(DRED_SOURCES)
endif
if ENABLE_OSCE
LPCNET_SOURCES += $(OSCE_SOURCES)
endif
if FIXED_POINT
SILK_SOURCES += $(SILK_SOURCES_FIXED)
if HAVE_SSE4_1
SILK_SOURCES += $(SILK_SOURCES_SSE4_1) $(SILK_SOURCES_FIXED_SSE4_1)
endif
if HAVE_ARM_NEON_INTR
SILK_SOURCES += $(SILK_SOURCES_FIXED_ARM_NEON_INTR)
endif
else
SILK_SOURCES += $(SILK_SOURCES_FLOAT)
if HAVE_SSE4_1
SILK_SOURCES += $(SILK_SOURCES_SSE4_1)
endif
if HAVE_AVX2
SILK_SOURCES += $(SILK_SOURCES_FLOAT_AVX2)
endif
endif
if DISABLE_FLOAT_API
else
OPUS_SOURCES += $(OPUS_SOURCES_FLOAT)
endif
if CPU_X86
if HAVE_RTCD
CELT_SOURCES += $(CELT_SOURCES_X86_RTCD)
SILK_SOURCES += $(SILK_SOURCES_X86_RTCD)
if ENABLE_DEEP_PLC
LPCNET_SOURCES += $(DNN_SOURCES_X86_RTCD)
endif
endif
if HAVE_SSE
CELT_SOURCES += $(CELT_SOURCES_SSE)
endif
if HAVE_SSE2
CELT_SOURCES += $(CELT_SOURCES_SSE2)
if ENABLE_DEEP_PLC
LPCNET_SOURCES += $(DNN_SOURCES_SSE2)
endif
endif
if HAVE_SSE4_1
CELT_SOURCES += $(CELT_SOURCES_SSE4_1)
if ENABLE_DEEP_PLC
LPCNET_SOURCES += $(DNN_SOURCES_SSE4_1)
endif
endif
if HAVE_AVX2
SILK_SOURCES += $(SILK_SOURCES_AVX2)
CELT_SOURCES += $(CELT_SOURCES_AVX2)
if ENABLE_DEEP_PLC
LPCNET_SOURCES += $(DNN_SOURCES_AVX2)
endif
endif
endif
if CPU_ARM
if HAVE_RTCD
CELT_SOURCES += $(CELT_SOURCES_ARM_RTCD)
SILK_SOURCES += $(SILK_SOURCES_ARM_RTCD)
if ENABLE_DEEP_PLC
LPCNET_SOURCES += $(DNN_SOURCES_ARM_RTCD)
endif
endif
if ENABLE_DEEP_PLC
if HAVE_ARM_DOTPROD
LPCNET_SOURCES += $(DNN_SOURCES_DOTPROD)
endif
if HAVE_ARM_NEON_INTR
LPCNET_SOURCES += $(DNN_SOURCES_NEON)
endif
endif
if HAVE_ARM_NEON_INTR
CELT_SOURCES += $(CELT_SOURCES_ARM_NEON_INTR)
SILK_SOURCES += $(SILK_SOURCES_ARM_NEON_INTR)
endif
if HAVE_ARM_NE10
CELT_SOURCES += $(CELT_SOURCES_ARM_NE10)
endif
if OPUS_ARM_EXTERNAL_ASM
noinst_LTLIBRARIES = libarmasm.la
libarmasm_la_SOURCES = $(CELT_SOURCES_ARM_ASM:.s=-gnu.S)
BUILT_SOURCES = $(CELT_SOURCES_ARM_ASM:.s=-gnu.S) \
$(CELT_AM_SOURCES_ARM_ASM:.s.in=.s) \
$(CELT_AM_SOURCES_ARM_ASM:.s.in=-gnu.S)
endif
endif
CLEANFILES = $(CELT_SOURCES_ARM_ASM:.s=-gnu.S) \
$(CELT_AM_SOURCES_ARM_ASM:.s.in=-gnu.S)
include celt_headers.mk
include lpcnet_headers.mk
include silk_headers.mk
include opus_headers.mk
LPCNET_HEAD =
if ENABLE_DEEP_PLC
LPCNET_HEAD += $(DEEP_PLC_HEAD)
endif
if ENABLE_DRED
LPCNET_HEAD += $(DRED_HEAD)
endif
if ENABLE_OSCE
LPCNET_HEAD += $(OSCE_HEAD)
endif
if ENABLE_LOSSGEN
LPCNET_HEAD += $(LOSSGEN_HEAD)
endif
libopus_la_SOURCES = $(CELT_SOURCES) $(SILK_SOURCES) $(LPCNET_SOURCES) $(OPUS_SOURCES)
libopus_la_LDFLAGS = -no-undefined -version-info @OPUS_LT_CURRENT@:@OPUS_LT_REVISION@:@OPUS_LT_AGE@
libopus_la_LIBADD = $(NE10_LIBS) $(LIBM)
if OPUS_ARM_EXTERNAL_ASM
libopus_la_LIBADD += libarmasm.la
endif
pkginclude_HEADERS = include/opus.h include/opus_multistream.h include/opus_types.h include/opus_defines.h include/opus_projection.h
noinst_HEADERS = $(OPUS_HEAD) $(SILK_HEAD) $(CELT_HEAD) $(LPCNET_HEAD)
if EXTRA_PROGRAMS
noinst_PROGRAMS = celt/tests/test_unit_cwrs32 \
celt/tests/test_unit_dft \
celt/tests/test_unit_entropy \
celt/tests/test_unit_laplace \
celt/tests/test_unit_mathops \
celt/tests/test_unit_mdct \
celt/tests/test_unit_rotation \
celt/tests/test_unit_types \
opus_compare \
opus_demo \
repacketizer_demo \
silk/tests/test_unit_LPC_inv_pred_gain \
tests/test_opus_api \
tests/test_opus_decode \
tests/test_opus_dred \
tests/test_opus_encode \
tests/test_opus_extensions \
tests/test_opus_padding \
tests/test_opus_projection \
trivial_example
TESTS = celt/tests/test_unit_cwrs32 \
celt/tests/test_unit_dft \
celt/tests/test_unit_entropy \
celt/tests/test_unit_laplace \
celt/tests/test_unit_mathops \
celt/tests/test_unit_mdct \
celt/tests/test_unit_rotation \
celt/tests/test_unit_types \
silk/tests/test_unit_LPC_inv_pred_gain \
tests/test_opus_api \
tests/test_opus_decode \
tests/test_opus_encode \
tests/test_opus_extensions \
tests/test_opus_padding \
tests/test_opus_projection
opus_demo_SOURCES = src/opus_demo.c
if ENABLE_LOSSGEN
opus_demo_SOURCES += $(LOSSGEN_SOURCES)
endif
opus_demo_LDADD = libopus.la $(NE10_LIBS) $(LIBM)
repacketizer_demo_SOURCES = src/repacketizer_demo.c
repacketizer_demo_LDADD = libopus.la $(NE10_LIBS) $(LIBM)
opus_compare_SOURCES = src/opus_compare.c
opus_compare_LDADD = $(LIBM)
trivial_example_SOURCES = doc/trivial_example.c
trivial_example_LDADD = libopus.la $(LIBM)
tests_test_opus_api_SOURCES = tests/test_opus_api.c tests/test_opus_common.h
tests_test_opus_api_LDADD = libopus.la $(NE10_LIBS) $(LIBM)
tests_test_opus_encode_SOURCES = tests/test_opus_encode.c tests/opus_encode_regressions.c tests/test_opus_common.h
tests_test_opus_encode_LDADD = libopus.la $(NE10_LIBS) $(LIBM)
tests_test_opus_decode_SOURCES = tests/test_opus_decode.c tests/test_opus_common.h
tests_test_opus_decode_LDADD = libopus.la $(NE10_LIBS) $(LIBM)
tests_test_opus_padding_SOURCES = tests/test_opus_padding.c tests/test_opus_common.h
tests_test_opus_padding_LDADD = libopus.la $(NE10_LIBS) $(LIBM)
tests_test_opus_dred_SOURCES = tests/test_opus_dred.c tests/test_opus_common.h
tests_test_opus_dred_LDADD = libopus.la $(NE10_LIBS) $(LIBM)
CELT_OBJ = $(CELT_SOURCES:.c=.lo)
SILK_OBJ = $(SILK_SOURCES:.c=.lo)
LPCNET_OBJ = $(LPCNET_SOURCES:.c=.lo)
OPUS_OBJ = $(OPUS_SOURCES:.c=.lo)
tests_test_opus_extensions_SOURCES = tests/test_opus_extensions.c tests/test_opus_common.h
tests_test_opus_extensions_LDADD = $(OPUS_OBJ) $(SILK_OBJ) $(LPCNET_OBJ) $(CELT_OBJ) $(NE10_LIBS) $(LIBM)
if OPUS_ARM_EXTERNAL_ASM
tests_test_opus_extensions_LDADD += libarmasm.la
endif
tests_test_opus_projection_SOURCES = tests/test_opus_projection.c tests/test_opus_common.h
tests_test_opus_projection_LDADD = $(OPUS_OBJ) $(SILK_OBJ) $(LPCNET_OBJ) $(CELT_OBJ) $(NE10_LIBS) $(LIBM)
if OPUS_ARM_EXTERNAL_ASM
tests_test_opus_projection_LDADD += libarmasm.la
endif
silk_tests_test_unit_LPC_inv_pred_gain_SOURCES = silk/tests/test_unit_LPC_inv_pred_gain.c
silk_tests_test_unit_LPC_inv_pred_gain_LDADD = $(SILK_OBJ) $(LPCNET_OBJ) $(CELT_OBJ) $(NE10_LIBS) $(LIBM)
if OPUS_ARM_EXTERNAL_ASM
silk_tests_test_unit_LPC_inv_pred_gain_LDADD += libarmasm.la
endif
celt_tests_test_unit_cwrs32_SOURCES = celt/tests/test_unit_cwrs32.c
celt_tests_test_unit_cwrs32_LDADD = $(LIBM)
celt_tests_test_unit_dft_SOURCES = celt/tests/test_unit_dft.c
celt_tests_test_unit_dft_LDADD = $(CELT_OBJ) $(LPCNET_OBJ) $(NE10_LIBS) $(LIBM)
if OPUS_ARM_EXTERNAL_ASM
celt_tests_test_unit_dft_LDADD += libarmasm.la
endif
celt_tests_test_unit_entropy_SOURCES = celt/tests/test_unit_entropy.c
celt_tests_test_unit_entropy_LDADD = $(LIBM)
celt_tests_test_unit_laplace_SOURCES = celt/tests/test_unit_laplace.c
celt_tests_test_unit_laplace_LDADD = $(LIBM)
celt_tests_test_unit_mathops_SOURCES = celt/tests/test_unit_mathops.c
celt_tests_test_unit_mathops_LDADD = $(CELT_OBJ) $(LPCNET_OBJ) $(NE10_LIBS) $(LIBM)
if OPUS_ARM_EXTERNAL_ASM
celt_tests_test_unit_mathops_LDADD += libarmasm.la
endif
celt_tests_test_unit_mdct_SOURCES = celt/tests/test_unit_mdct.c
celt_tests_test_unit_mdct_LDADD = $(CELT_OBJ) $(LPCNET_OBJ) $(NE10_LIBS) $(LIBM)
if OPUS_ARM_EXTERNAL_ASM
celt_tests_test_unit_mdct_LDADD += libarmasm.la
endif
celt_tests_test_unit_rotation_SOURCES = celt/tests/test_unit_rotation.c
celt_tests_test_unit_rotation_LDADD = $(CELT_OBJ) $(LPCNET_OBJ) $(NE10_LIBS) $(LIBM)
if OPUS_ARM_EXTERNAL_ASM
celt_tests_test_unit_rotation_LDADD += libarmasm.la
endif
celt_tests_test_unit_types_SOURCES = celt/tests/test_unit_types.c
celt_tests_test_unit_types_LDADD = $(LIBM)
endif
if CUSTOM_MODES
pkginclude_HEADERS += include/opus_custom.h
if EXTRA_PROGRAMS
noinst_PROGRAMS += opus_custom_demo
opus_custom_demo_SOURCES = celt/opus_custom_demo.c
opus_custom_demo_LDADD = libopus.la $(LIBM)
endif
endif
if EXTRA_PROGRAMS
if ENABLE_DEEP_PLC
noinst_PROGRAMS += fargan_demo dump_data dump_weights_blob
fargan_demo_SOURCES = dnn/fargan_demo.c
fargan_demo_LDADD = $(LPCNET_OBJ) $(CELT_OBJ) $(LIBM)
dump_data_SOURCES = dnn/dump_data.c
dump_data_LDADD = $(LPCNET_OBJ) $(CELT_OBJ) $(LIBM)
dump_weights_blob_SOURCES = dnn/write_lpcnet_weights.c
dump_weights_blob_LDADD = $(LIBM)
dump_weights_blob_CFLAGS = $(AM_CFLAGS) -DDUMP_BINARY_WEIGHTS
endif
if ENABLE_DRED
TESTS += tests/test_opus_dred
endif
if ENABLE_LOSSGEN
noinst_PROGRAMS += lossgen_demo
lossgen_demo_SOURCES = dnn/lossgen_demo.c $(LOSSGEN_SOURCES)
lossgen_demo_LDADD = $(LIBM)
endif
endif
EXTRA_DIST = opus.pc.in \
opus-uninstalled.pc.in \
opus.m4 \
Makefile.mips \
Makefile.unix \
CMakeLists.txt \
cmake/CFeatureCheck.cmake \
cmake/OpusBuildtype.cmake \
cmake/OpusConfig.cmake \
cmake/OpusConfig.cmake.in \
cmake/OpusFunctions.cmake \
cmake/OpusPackageVersion.cmake \
cmake/OpusSources.cmake \
cmake/README.md \
cmake/RunTest.cmake \
cmake/config.h.cmake.in \
cmake/vla.c \
cmake/cpu_info_by_asm.c \
cmake/cpu_info_by_c.c \
meson/get-version.py \
meson/read-sources-list.py \
meson/README.md \
meson.build \
meson_options.txt \
include/meson.build \
celt/meson.build \
celt/tests/meson.build \
dnn/meson.build \
dnn/README.md \
silk/meson.build \
silk/tests/meson.build \
src/meson.build \
tests/meson.build \
doc/meson.build \
tests/run_vectors.sh \
celt/arm/arm2gnu.pl \
celt/arm/celt_pitch_xcorr_arm.s
pkgconfigdir = $(libdir)/pkgconfig
pkgconfig_DATA = opus.pc
m4datadir = $(datadir)/aclocal
m4data_DATA = opus.m4
# Targets to build and install just the library without the docs
opus check-opus install-opus: export NO_DOXYGEN = 1
opus: all
check-opus: check
install-opus: install
# Or just the docs
docs:
( cd doc && $(MAKE) $(AM_MAKEFLAGS) )
install-docs:
( cd doc && $(MAKE) $(AM_MAKEFLAGS) install )
# Or everything (by default)
all-local:
@[ -n "$(NO_DOXYGEN)" ] || ( cd doc && $(MAKE) $(AM_MAKEFLAGS) )
install-data-local:
@[ -n "$(NO_DOXYGEN)" ] || ( cd doc && $(MAKE) $(AM_MAKEFLAGS) install )
clean-local:
-( cd doc && $(MAKE) $(AM_MAKEFLAGS) clean )
uninstall-local:
( cd doc && $(MAKE) $(AM_MAKEFLAGS) uninstall )
# We check this every time make is run, with configure.ac being touched to
# trigger an update of the build system files if update_version changes the
# current PACKAGE_VERSION (or if package_version was modified manually by a
# user with either AUTO_UPDATE=no or no update_version script present - the
# latter being the normal case for tarball releases).
#
# We can't just add the package_version file to CONFIGURE_DEPENDENCIES since
# simply running autoconf will not actually regenerate configure for us when
# the content of that file changes (due to autoconf dependency checking not
# knowing about that without us creating yet another file for it to include).
#
# The MAKECMDGOALS check is a gnu-make'ism, but will degrade 'gracefully' for
# makes that don't support it. The only loss of functionality is not forcing
# an update of package_version for `make dist` if AUTO_UPDATE=no, but that is
# unlikely to be a real problem for any real user.
$(top_srcdir)/configure.ac: force
@case "$(MAKECMDGOALS)" in \
dist-hook) exit 0 ;; \
dist-* | dist | distcheck | distclean) _arg=release ;; \
esac; \
if ! $(top_srcdir)/update_version $$_arg 2> /dev/null; then \
if [ ! -e $(top_srcdir)/package_version ]; then \
echo 'PACKAGE_VERSION="unknown"' > $(top_srcdir)/package_version; \
fi; \
. $(top_srcdir)/package_version || exit 1; \
[ "$(PACKAGE_VERSION)" != "$$PACKAGE_VERSION" ] || exit 0; \
fi; \
touch $@
force:
# Create a minimal package_version file when make dist is run.
dist-hook:
echo 'PACKAGE_VERSION="$(PACKAGE_VERSION)"' > $(top_distdir)/package_version
.PHONY: opus check-opus install-opus docs install-docs
# automake doesn't do dependency tracking for asm files, that I can tell
$(CELT_SOURCES_ARM_ASM:%.s=%-gnu.S): celt/arm/armopts-gnu.S
$(CELT_SOURCES_ARM_ASM:%.s=%-gnu.S): $(top_srcdir)/celt/arm/arm2gnu.pl
# convert ARM asm to GNU as format
%-gnu.S: $(top_srcdir)/%.s
$(top_srcdir)/celt/arm/arm2gnu.pl @ARM2GNU_PARAMS@ < $< > $@
# For autoconf-modified sources (e.g., armopts.s)
%-gnu.S: %.s
$(top_srcdir)/celt/arm/arm2gnu.pl @ARM2GNU_PARAMS@ < $< > $@
OPT_UNIT_TEST_OBJ = $(celt_tests_test_unit_mathops_SOURCES:.c=.o) \
$(celt_tests_test_unit_rotation_SOURCES:.c=.o) \
$(celt_tests_test_unit_mdct_SOURCES:.c=.o) \
$(celt_tests_test_unit_dft_SOURCES:.c=.o) \
$(silk_tests_test_unit_LPC_inv_pred_gain_SOURCES:.c=.o)
if HAVE_SSE
SSE_OBJ = $(CELT_SOURCES_SSE:.c=.lo)
$(SSE_OBJ): CFLAGS += $(OPUS_X86_SSE_CFLAGS)
endif
if HAVE_SSE2
SSE2_OBJ = $(CELT_SOURCES_SSE2:.c=.lo) \
$(DNN_SOURCES_SSE2:.c=.lo)
$(SSE2_OBJ): CFLAGS += $(OPUS_X86_SSE2_CFLAGS)
endif
if HAVE_SSE4_1
SSE4_1_OBJ = $(CELT_SOURCES_SSE4_1:.c=.lo) \
$(DNN_SOURCES_SSE4_1:.c=.lo) \
$(SILK_SOURCES_SSE4_1:.c=.lo) \
$(SILK_SOURCES_FIXED_SSE4_1:.c=.lo)
$(SSE4_1_OBJ): CFLAGS += $(OPUS_X86_SSE4_1_CFLAGS)
endif
if HAVE_AVX2
AVX2_OBJ = $(CELT_SOURCES_AVX2:.c=.lo) \
$(SILK_SOURCES_AVX2:.c=.lo) \
$(SILK_SOURCES_FLOAT_AVX2:.c=.lo) \
$(DNN_SOURCES_AVX2:.c=.lo)
$(AVX2_OBJ): CFLAGS += $(OPUS_X86_AVX2_CFLAGS)
endif
if HAVE_ARM_NEON_INTR
ARM_NEON_INTR_OBJ = $(CELT_SOURCES_ARM_NEON_INTR:.c=.lo) \
$(SILK_SOURCES_ARM_NEON_INTR:.c=.lo) \
$(DNN_SOURCES_NEON:.c=.lo) \
$(SILK_SOURCES_FIXED_ARM_NEON_INTR:.c=.lo)
$(ARM_NEON_INTR_OBJ): CFLAGS += \
$(OPUS_ARM_NEON_INTR_CFLAGS) $(NE10_CFLAGS)
endif
if HAVE_ARM_DOTPROD
ARM_DOTPROD_OBJ = $(DNN_SOURCES_DOTPROD:.c=.lo)
$(ARM_DOTPROD_OBJ): CFLAGS += $(ARM_DOTPROD_INTR_CFLAGS)
endif

4853
local_plugins/opus/android/libs/opus/Makefile.in

File diff suppressed because it is too large

169
local_plugins/opus/android/libs/opus/Makefile.mips

@ -0,0 +1,169 @@
#################### COMPILE OPTIONS #######################
# Uncomment this for fixed-point build
FIXED_POINT=1
# It is strongly recommended to uncomment one of these
# VAR_ARRAYS: Use C99 variable-length arrays for stack allocation
# USE_ALLOCA: Use alloca() for stack allocation
# If none is defined, then the fallback is a non-threadsafe global array
CFLAGS := -DUSE_ALLOCA $(CFLAGS)
#CFLAGS := -DVAR_ARRAYS $(CFLAGS)
# These options affect performance
# HAVE_LRINTF: Use C99 intrinsics to speed up float-to-int conversion
CFLAGS := -DHAVE_LRINTF $(CFLAGS)
###################### END OF OPTIONS ######################
-include package_version
include silk_sources.mk
include celt_sources.mk
include opus_sources.mk
ifdef FIXED_POINT
SILK_SOURCES += $(SILK_SOURCES_FIXED)
else
SILK_SOURCES += $(SILK_SOURCES_FLOAT)
OPUS_SOURCES += $(OPUS_SOURCES_FLOAT)
endif
EXESUFFIX =
LIBPREFIX = lib
LIBSUFFIX = .a
OBJSUFFIX = .o
CC = $(TOOLCHAIN_PREFIX)cc$(TOOLCHAIN_SUFFIX)
AR = $(TOOLCHAIN_PREFIX)ar
RANLIB = $(TOOLCHAIN_PREFIX)ranlib
CP = $(TOOLCHAIN_PREFIX)cp
cppflags-from-defines = $(addprefix -D,$(1))
cppflags-from-includes = $(addprefix -I,$(1))
ldflags-from-ldlibdirs = $(addprefix -L,$(1))
ldlibs-from-libs = $(addprefix -l,$(1))
WARNINGS = -Wall -W -Wstrict-prototypes -Wextra -Wcast-align -Wnested-externs -Wshadow
CFLAGS += -mips32r2 -mno-mips16 -std=gnu99 -O2 -g $(WARNINGS) -DENABLE_ASSERTIONS -DMIPSr1_ASM -DOPUS_BUILD -mdspr2 -march=74kc -mtune=74kc -mmt -mgp32
CINCLUDES = include silk celt
ifdef FIXED_POINT
CFLAGS += -DFIXED_POINT=1 -DDISABLE_FLOAT_API
CINCLUDES += silk/fixed
else
CINCLUDES += silk/float
endif
LIBS = m
LDLIBDIRS = ./
CFLAGS += $(call cppflags-from-defines,$(CDEFINES))
CFLAGS += $(call cppflags-from-includes,$(CINCLUDES))
LDFLAGS += $(call ldflags-from-ldlibdirs,$(LDLIBDIRS))
LDLIBS += $(call ldlibs-from-libs,$(LIBS))
COMPILE.c.cmdline = $(CC) -c $(CFLAGS) -o $@ $<
LINK.o = $(CC) $(LDPREFLAGS) $(LDFLAGS)
LINK.o.cmdline = $(LINK.o) $^ $(LDLIBS) -o $@$(EXESUFFIX)
ARCHIVE.cmdline = $(AR) $(ARFLAGS) $@ $^ && $(RANLIB) $@
%$(OBJSUFFIX):%.c
$(COMPILE.c.cmdline)
%$(OBJSUFFIX):%.cpp
$(COMPILE.cpp.cmdline)
# Directives
# Variable definitions
LIB_NAME = opus
TARGET = $(LIBPREFIX)$(LIB_NAME)$(LIBSUFFIX)
SRCS_C = $(SILK_SOURCES) $(CELT_SOURCES) $(OPUS_SOURCES)
OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(SRCS_C))
OPUSDEMO_SRCS_C = src/opus_demo.c
OPUSDEMO_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(OPUSDEMO_SRCS_C))
TESTOPUSAPI_SRCS_C = tests/test_opus_api.c
TESTOPUSAPI_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(TESTOPUSAPI_SRCS_C))
TESTOPUSDECODE_SRCS_C = tests/test_opus_decode.c
TESTOPUSDECODE_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(TESTOPUSDECODE_SRCS_C))
TESTOPUSENCODE_SRCS_C = tests/test_opus_encode.c tests/opus_encode_regressions.c
TESTOPUSENCODE_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(TESTOPUSENCODE_SRCS_C))
TESTOPUSEXTENSIONS_SRCS_C = tests/test_opus_extensions.c
TESTOPUSEXTENSIONS_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(TESTOPUSEXTENSIONS_SRCS_C))
TESTOPUSPADDING_SRCS_C = tests/test_opus_padding.c
TESTOPUSPADDING_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(TESTOPUSPADDING_SRCS_C))
OPUSCOMPARE_SRCS_C = src/opus_compare.c
OPUSCOMPARE_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(OPUSCOMPARE_SRCS_C))
TESTS := test_opus_api test_opus_decode test_opus_encode test_opus_extensions test_opus_padding
# Rules
all: lib opus_demo opus_compare $(TESTS)
lib: $(TARGET)
check: all
for test in $(TESTS); do ./$$test; done
$(TARGET): $(OBJS)
$(ARCHIVE.cmdline)
opus_demo$(EXESUFFIX): $(OPUSDEMO_OBJS) $(TARGET)
$(LINK.o.cmdline)
test_opus_api$(EXESUFFIX): $(TESTOPUSAPI_OBJS) $(TARGET)
$(LINK.o.cmdline)
test_opus_decode$(EXESUFFIX): $(TESTOPUSDECODE_OBJS) $(TARGET)
$(LINK.o.cmdline)
test_opus_encode$(EXESUFFIX): $(TESTOPUSENCODE_OBJS) $(TARGET)
$(LINK.o.cmdline)
test_opus_extensions$(EXESUFFIX): $(TESTOPUSEXTENSIONS_OBJS) $(TARGET)
$(LINK.o.cmdline)
test_opus_padding$(EXESUFFIX): $(TESTOPUSPADDING_OBJS) $(TARGET)
$(LINK.o.cmdline)
opus_compare$(EXESUFFIX): $(OPUSCOMPARE_OBJS)
$(LINK.o.cmdline)
celt/celt.o: CFLAGS += -DPACKAGE_VERSION='$(PACKAGE_VERSION)'
celt/celt.o: package_version
package_version: force
@if [ -x ./update_version ]; then \
./update_version || true; \
elif [ ! -e ./package_version ]; then \
echo 'PACKAGE_VERSION="unknown"' > ./package_version; \
fi
force:
clean:
rm -f opus_demo$(EXESUFFIX) opus_compare$(EXESUFFIX) $(TARGET) \
test_opus_api$(EXESUFFIX) test_opus_decode$(EXESUFFIX) \
test_opus_encode$(EXESUFFIX) test_opus_extensions$(EXESUFFIX) \
test_opus_padding$(EXESUFFIX)
$(OBJS) $(OPUSDEMO_OBJS) $(OPUSCOMPARE_OBJS) $(TESTOPUSAPI_OBJS) \
$(TESTOPUSDECODE_OBJS) $(TESTOPUSENCODE_OBJS) \
$(TESTOPUSEXTENSIONS_OBJS) $(TESTOPUSPADDING_OBJS)
.PHONY: all lib clean force check

167
local_plugins/opus/android/libs/opus/Makefile.unix

@ -0,0 +1,167 @@
#################### COMPILE OPTIONS #######################
# Uncomment this for fixed-point build
#FIXED_POINT=1
# It is strongly recommended to uncomment one of these
# VAR_ARRAYS: Use C99 variable-length arrays for stack allocation
# USE_ALLOCA: Use alloca() for stack allocation
# If none is defined, then the fallback is a non-threadsafe global array
CFLAGS := -DUSE_ALLOCA $(CFLAGS)
#CFLAGS := -DVAR_ARRAYS $(CFLAGS)
# These options affect performance
# HAVE_LRINTF: Use C99 intrinsics to speed up float-to-int conversion
#CFLAGS := -DHAVE_LRINTF $(CFLAGS)
###################### END OF OPTIONS ######################
-include package_version
include silk_sources.mk
include celt_sources.mk
include opus_sources.mk
ifdef FIXED_POINT
SILK_SOURCES += $(SILK_SOURCES_FIXED)
else
SILK_SOURCES += $(SILK_SOURCES_FLOAT)
OPUS_SOURCES += $(OPUS_SOURCES_FLOAT)
endif
EXESUFFIX =
LIBPREFIX = lib
LIBSUFFIX = .a
OBJSUFFIX = .o
CC = $(TOOLCHAIN_PREFIX)cc$(TOOLCHAIN_SUFFIX)
AR = $(TOOLCHAIN_PREFIX)ar
RANLIB = $(TOOLCHAIN_PREFIX)ranlib
CP = $(TOOLCHAIN_PREFIX)cp
cppflags-from-defines = $(addprefix -D,$(1))
cppflags-from-includes = $(addprefix -I,$(1))
ldflags-from-ldlibdirs = $(addprefix -L,$(1))
ldlibs-from-libs = $(addprefix -l,$(1))
WARNINGS = -Wall -W -Wstrict-prototypes -Wextra -Wcast-align -Wnested-externs -Wshadow
CFLAGS += -O2 -g $(WARNINGS) -DOPUS_BUILD
CINCLUDES = include silk celt
ifdef FIXED_POINT
CFLAGS += -DFIXED_POINT=1 -DDISABLE_FLOAT_API
CINCLUDES += silk/fixed
else
CINCLUDES += silk/float
endif
LIBS = m
LDLIBDIRS = ./
CFLAGS += $(call cppflags-from-defines,$(CDEFINES))
CFLAGS += $(call cppflags-from-includes,$(CINCLUDES))
LDFLAGS += $(call ldflags-from-ldlibdirs,$(LDLIBDIRS))
LDLIBS += $(call ldlibs-from-libs,$(LIBS))
COMPILE.c.cmdline = $(CC) -c $(CFLAGS) -o $@ $<
LINK.o = $(CC) $(LDPREFLAGS) $(LDFLAGS)
LINK.o.cmdline = $(LINK.o) $^ $(LDLIBS) -o $@$(EXESUFFIX)
ARCHIVE.cmdline = $(AR) $(ARFLAGS) $@ $^ && $(RANLIB) $@
%$(OBJSUFFIX):%.c
$(COMPILE.c.cmdline)
%$(OBJSUFFIX):%.cpp
$(COMPILE.cpp.cmdline)
# Directives
# Variable definitions
LIB_NAME = opus
TARGET = $(LIBPREFIX)$(LIB_NAME)$(LIBSUFFIX)
SRCS_C = $(SILK_SOURCES) $(CELT_SOURCES) $(OPUS_SOURCES)
OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(SRCS_C))
OPUSDEMO_SRCS_C = src/opus_demo.c
OPUSDEMO_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(OPUSDEMO_SRCS_C))
TESTOPUSAPI_SRCS_C = tests/test_opus_api.c
TESTOPUSAPI_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(TESTOPUSAPI_SRCS_C))
TESTOPUSDECODE_SRCS_C = tests/test_opus_decode.c
TESTOPUSDECODE_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(TESTOPUSDECODE_SRCS_C))
TESTOPUSENCODE_SRCS_C = tests/test_opus_encode.c tests/opus_encode_regressions.c
TESTOPUSENCODE_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(TESTOPUSENCODE_SRCS_C))
TESTOPUSEXTENSIONS_SRCS_C = tests/test_opus_extensions.c
TESTOPUSEXTENSIONS_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(TESTOPUSEXTENSIONS_SRCS_C))
TESTOPUSPADDING_SRCS_C = tests/test_opus_padding.c
TESTOPUSPADDING_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(TESTOPUSPADDING_SRCS_C))
OPUSCOMPARE_SRCS_C = src/opus_compare.c
OPUSCOMPARE_OBJS := $(patsubst %.c,%$(OBJSUFFIX),$(OPUSCOMPARE_SRCS_C))
TESTS := test_opus_api test_opus_decode test_opus_encode test_opus_extensions test_opus_padding
# Rules
all: lib opus_demo opus_compare $(TESTS)
lib: $(TARGET)
check: all
for test in $(TESTS); do ./$$test; done
$(TARGET): $(OBJS)
$(ARCHIVE.cmdline)
opus_demo$(EXESUFFIX): $(OPUSDEMO_OBJS) $(TARGET)
$(LINK.o.cmdline)
test_opus_api$(EXESUFFIX): $(TESTOPUSAPI_OBJS) $(TARGET)
$(LINK.o.cmdline)
test_opus_decode$(EXESUFFIX): $(TESTOPUSDECODE_OBJS) $(TARGET)
$(LINK.o.cmdline)
test_opus_encode$(EXESUFFIX): $(TESTOPUSENCODE_OBJS) $(TARGET)
$(LINK.o.cmdline)
test_opus_extensions$(EXESUFFIX): $(TESTOPUSEXTENSIONS_OBJS) $(TARGET)
$(LINK.o.cmdline)
test_opus_padding$(EXESUFFIX): $(TESTOPUSPADDING_OBJS) $(TARGET)
$(LINK.o.cmdline)
opus_compare$(EXESUFFIX): $(OPUSCOMPARE_OBJS)
$(LINK.o.cmdline)
celt/celt.o: CFLAGS += -DPACKAGE_VERSION='$(PACKAGE_VERSION)'
celt/celt.o: package_version
package_version: force
@if [ -x ./update_version ]; then \
./update_version || true; \
elif [ ! -e ./package_version ]; then \
echo 'PACKAGE_VERSION="unknown"' > ./package_version; \
fi
force:
clean:
rm -f opus_demo$(EXESUFFIX) opus_compare$(EXESUFFIX) $(TARGET) \
test_opus_api$(EXESUFFIX) test_opus_decode$(EXESUFFIX) \
test_opus_encode$(EXESUFFIX) test_opus_extensions$(EXESUFFIX) \
test_opus_padding$(EXESUFFIX)
$(OBJS) $(OPUSDEMO_OBJS) $(OPUSCOMPARE_OBJS) $(TESTOPUSAPI_OBJS) \
$(TESTOPUSDECODE_OBJS) $(TESTOPUSENCODE_OBJS) \
$(TESTOPUSEXTENSIONS_OBJS) $(TESTOPUSPADDING_OBJS)
.PHONY: all lib clean force check

0
local_plugins/opus/android/libs/opus/NEWS

189
local_plugins/opus/android/libs/opus/README

@ -0,0 +1,189 @@
== Opus audio codec ==
Opus is a codec for interactive speech and audio transmission over the Internet.
Opus can handle a wide range of interactive audio applications, including
Voice over IP, videoconferencing, in-game chat, and even remote live music
performances. It can scale from low bit-rate narrowband speech to very high
quality stereo music.
Opus, when coupled with an appropriate container format, is also suitable
for non-realtime stored-file applications such as music distribution, game
soundtracks, portable music players, jukeboxes, and other applications that
have historically used high latency formats such as MP3, AAC, or Vorbis.
Opus is specified by IETF RFC 6716:
https://tools.ietf.org/html/rfc6716
The Opus format and this implementation of it are subject to the royalty-
free patent and copyright licenses specified in the file COPYING.
This package implements a shared library for encoding and decoding raw Opus
bitstreams. Raw Opus bitstreams should be used over RTP according to
https://tools.ietf.org/html/rfc7587
The package also includes a number of test tools used for testing the
correct operation of the library. The bitstreams read/written by these
tools should not be used for Opus file distribution: They include
additional debugging data and cannot support seeking.
Opus stored in files should use the Ogg encapsulation for Opus which is
described at:
https://tools.ietf.org/html/rfc7845
An opus-tools package is available which provides encoding and decoding of
Ogg encapsulated Opus files and includes a number of useful features.
Opus-tools can be found at:
https://gitlab.xiph.org/xiph/opus-tools.git
or on the main Opus website:
https://opus-codec.org/
== Deep Learning and Opus ==
Lossy networks continue to be a challenge for real-time communications.
While the original implementation of Opus provides an excellent packet loss
concealment mechanism, the team has continued to advance the methodology used
to improve audio quality in challenge network environments.
In Opus 1.5, we added a deep learning based redundancy encoder that enhances
audio in lossy networks by embedding one second of recovery data in the padding
data of each packet. The underlying algorithm behind encoding and decoding the
recovery data is called the deep redundancy (DRED) algorithm. By leveraging
the padding data within the packet, Opus 1.5 is fully backward compatible with
prior revisions of Opus. Please see the README under the "dnn" subdirectory to
understand DRED.
DRED was developed by a team that Amazon Web Services initially sponsored,
who open-sourced the implementation as well as began the
standardization process at the IETF:
https://datatracker.ietf.org/doc/draft-ietf-mlcodec-opus-extension/
The license behind Opus or the intellectual property position of Opus does
not change with Opus 1.5.
== Compiling libopus ==
To build from a distribution tarball, you only need to do the following:
% ./configure
% make
To build from the git repository, the following steps are necessary:
0) Set up a development environment:
On an Ubuntu or Debian family Linux distribution:
% sudo apt-get install git autoconf automake libtool gcc make
On a Fedora/Redhat based Linux:
% sudo dnf install git autoconf automake libtool gcc make
Or for older Redhat/Centos Linux releases:
% sudo yum install git autoconf automake libtool gcc make
On Apple macOS, install Xcode and brew.sh, then in the Terminal enter:
% brew install autoconf automake libtool
1) Clone the repository:
% git clone https://gitlab.xiph.org/xiph/opus.git
% cd opus
2) Compiling the source
% ./autogen.sh
% ./configure
% make
On x86, it's a good idea to use a -march= option that allows the use of AVX2.
3) Install the codec libraries (optional)
% sudo make install
Once you have compiled the codec, there will be a opus_demo executable
in the top directory.
Usage: opus_demo [-e] <application> <sampling rate (Hz)> <channels (1/2)>
<bits per second> [options] <input> <output>
opus_demo -d <sampling rate (Hz)> <channels (1/2)> [options]
<input> <output>
mode: voip | audio | restricted-lowdelay
options:
-e : only runs the encoder (output the bit-stream)
-d : only runs the decoder (reads the bit-stream as input)
-cbr : enable constant bitrate; default: variable bitrate
-cvbr : enable constrained variable bitrate; default:
unconstrained
-bandwidth <NB|MB|WB|SWB|FB>
: audio bandwidth (from narrowband to fullband);
default: sampling rate
-framesize <2.5|5|10|20|40|60>
: frame size in ms; default: 20
-max_payload <bytes>
: maximum payload size in bytes, default: 1024
-complexity <comp>
: complexity, 0 (lowest) ... 10 (highest); default: 10
-inbandfec : enable SILK inband FEC
-forcemono : force mono encoding, even for stereo input
-dtx : enable SILK DTX
-loss <perc> : simulate packet loss, in percent (0-100); default: 0
input and output are little-endian signed 16-bit PCM files or opus
bitstreams with simple opus_demo proprietary framing.
== Testing ==
This package includes a collection of automated unit and system tests
which SHOULD be run after compiling the package especially the first
time it is run on a new platform.
To run the integrated tests:
% make check
There is also collection of standard test vectors which are not
included in this package for size reasons but can be obtained from:
https://opus-codec.org/docs/opus_testvectors-rfc8251.tar.gz
To run compare the code to these test vectors:
% curl -OL https://opus-codec.org/docs/opus_testvectors-rfc8251.tar.gz
% tar -zxf opus_testvectors-rfc8251.tar.gz
% ./tests/run_vectors.sh ./ opus_newvectors 48000
== Compiling libopus for Windows and alternative build systems ==
See cmake/README.md or meson/README.md.
== Portability notes ==
This implementation uses floating-point by default but can be compiled to
use only fixed-point arithmetic by setting --enable-fixed-point (if using
autoconf) or by defining the FIXED_POINT macro (if building manually).
The fixed point implementation has somewhat lower audio quality and is
slower on platforms with fast FPUs, it is normally only used in embedded
environments.
The implementation can be compiled with either a C89 or a C99 compiler.
While it does not rely on any _undefined behavior_ as defined by C89 or
C99, it relies on common _implementation-defined behavior_ for two's
complement architectures:
o Right shifts of negative values are consistent with two's
complement arithmetic, so that a>>b is equivalent to
floor(a/(2^b)),
o For conversion to a signed integer of N bits, the value is reduced
modulo 2^N to be within range of the type,
o The result of integer division of a negative value is truncated
towards zero, and
o The compiler provides a 64-bit integer type (a C99 requirement
which is supported by most C89 compilers).

1214
local_plugins/opus/android/libs/opus/aclocal.m4

File diff suppressed because it is too large

182
local_plugins/opus/android/libs/opus/celt/_kiss_fft_guts.h

@ -0,0 +1,182 @@
/*Copyright (c) 2003-2004, Mark Borgerding
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.*/
#ifndef KISS_FFT_GUTS_H
#define KISS_FFT_GUTS_H
#define MIN(a,b) ((a)<(b) ? (a):(b))
#define MAX(a,b) ((a)>(b) ? (a):(b))
/* kiss_fft.h
defines kiss_fft_scalar as either short or a float type
and defines
typedef struct { kiss_fft_scalar r; kiss_fft_scalar i; }kiss_fft_cpx; */
#include "kiss_fft.h"
/*
Explanation of macros dealing with complex math:
C_MUL(m,a,b) : m = a*b
C_FIXDIV( c , div ) : if a fixed point impl., c /= div. noop otherwise
C_SUB( res, a,b) : res = a - b
C_SUBFROM( res , a) : res -= a
C_ADDTO( res , a) : res += a
* */
#ifdef FIXED_POINT
#include "arch.h"
#define SAMP_MAX 2147483647
#define TWID_MAX 32767
#define TRIG_UPSCALE 1
#define SAMP_MIN -SAMP_MAX
# define S_MUL(a,b) MULT16_32_Q15(b, a)
# define C_MUL(m,a,b) \
do{ (m).r = SUB32_ovflw(S_MUL((a).r,(b).r) , S_MUL((a).i,(b).i)); \
(m).i = ADD32_ovflw(S_MUL((a).r,(b).i) , S_MUL((a).i,(b).r)); }while(0)
# define C_MULC(m,a,b) \
do{ (m).r = ADD32_ovflw(S_MUL((a).r,(b).r) , S_MUL((a).i,(b).i)); \
(m).i = SUB32_ovflw(S_MUL((a).i,(b).r) , S_MUL((a).r,(b).i)); }while(0)
# define C_MULBYSCALAR( c, s ) \
do{ (c).r = S_MUL( (c).r , s ) ;\
(c).i = S_MUL( (c).i , s ) ; }while(0)
# define DIVSCALAR(x,k) \
(x) = S_MUL( x, (TWID_MAX-((k)>>1))/(k)+1 )
# define C_FIXDIV(c,div) \
do { DIVSCALAR( (c).r , div); \
DIVSCALAR( (c).i , div); }while (0)
#define C_ADD( res, a,b)\
do {(res).r=ADD32_ovflw((a).r,(b).r); (res).i=ADD32_ovflw((a).i,(b).i); \
}while(0)
#define C_SUB( res, a,b)\
do {(res).r=SUB32_ovflw((a).r,(b).r); (res).i=SUB32_ovflw((a).i,(b).i); \
}while(0)
#define C_ADDTO( res , a)\
do {(res).r = ADD32_ovflw((res).r, (a).r); (res).i = ADD32_ovflw((res).i,(a).i);\
}while(0)
#define C_SUBFROM( res , a)\
do {(res).r = ADD32_ovflw((res).r,(a).r); (res).i = SUB32_ovflw((res).i,(a).i); \
}while(0)
#if defined(OPUS_ARM_INLINE_ASM)
#include "arm/kiss_fft_armv4.h"
#endif
#if defined(OPUS_ARM_INLINE_EDSP)
#include "arm/kiss_fft_armv5e.h"
#endif
#if defined(MIPSr1_ASM)
#include "mips/kiss_fft_mipsr1.h"
#endif
#else /* not FIXED_POINT*/
# define S_MUL(a,b) ( (a)*(b) )
#define C_MUL(m,a,b) \
do{ (m).r = (a).r*(b).r - (a).i*(b).i;\
(m).i = (a).r*(b).i + (a).i*(b).r; }while(0)
#define C_MULC(m,a,b) \
do{ (m).r = (a).r*(b).r + (a).i*(b).i;\
(m).i = (a).i*(b).r - (a).r*(b).i; }while(0)
#define C_MUL4(m,a,b) C_MUL(m,a,b)
# define C_FIXDIV(c,div) /* NOOP */
# define C_MULBYSCALAR( c, s ) \
do{ (c).r *= (s);\
(c).i *= (s); }while(0)
#endif
#ifndef CHECK_OVERFLOW_OP
# define CHECK_OVERFLOW_OP(a,op,b) /* noop */
#endif
#ifndef C_ADD
#define C_ADD( res, a,b)\
do { \
CHECK_OVERFLOW_OP((a).r,+,(b).r)\
CHECK_OVERFLOW_OP((a).i,+,(b).i)\
(res).r=(a).r+(b).r; (res).i=(a).i+(b).i; \
}while(0)
#define C_SUB( res, a,b)\
do { \
CHECK_OVERFLOW_OP((a).r,-,(b).r)\
CHECK_OVERFLOW_OP((a).i,-,(b).i)\
(res).r=(a).r-(b).r; (res).i=(a).i-(b).i; \
}while(0)
#define C_ADDTO( res , a)\
do { \
CHECK_OVERFLOW_OP((res).r,+,(a).r)\
CHECK_OVERFLOW_OP((res).i,+,(a).i)\
(res).r += (a).r; (res).i += (a).i;\
}while(0)
#define C_SUBFROM( res , a)\
do {\
CHECK_OVERFLOW_OP((res).r,-,(a).r)\
CHECK_OVERFLOW_OP((res).i,-,(a).i)\
(res).r -= (a).r; (res).i -= (a).i; \
}while(0)
#endif /* C_ADD defined */
#ifdef FIXED_POINT
/*# define KISS_FFT_COS(phase) TRIG_UPSCALE*floor(MIN(32767,MAX(-32767,.5+32768 * cos (phase))))
# define KISS_FFT_SIN(phase) TRIG_UPSCALE*floor(MIN(32767,MAX(-32767,.5+32768 * sin (phase))))*/
# define KISS_FFT_COS(phase) floor(.5+TWID_MAX*cos (phase))
# define KISS_FFT_SIN(phase) floor(.5+TWID_MAX*sin (phase))
# define HALF_OF(x) ((x)>>1)
#elif defined(USE_SIMD)
# define KISS_FFT_COS(phase) _mm_set1_ps( cos(phase) )
# define KISS_FFT_SIN(phase) _mm_set1_ps( sin(phase) )
# define HALF_OF(x) ((x)*_mm_set1_ps(.5f))
#else
# define KISS_FFT_COS(phase) (kiss_fft_scalar) cos(phase)
# define KISS_FFT_SIN(phase) (kiss_fft_scalar) sin(phase)
# define HALF_OF(x) ((x)*.5f)
#endif
#define kf_cexp(x,phase) \
do{ \
(x)->r = KISS_FFT_COS(phase);\
(x)->i = KISS_FFT_SIN(phase);\
}while(0)
#define kf_cexp2(x,phase) \
do{ \
(x)->r = TRIG_UPSCALE*celt_cos_norm((phase));\
(x)->i = TRIG_UPSCALE*celt_cos_norm((phase)-32768);\
}while(0)
#endif /* KISS_FFT_GUTS_H */

291
local_plugins/opus/android/libs/opus/celt/arch.h

@ -0,0 +1,291 @@
/* Copyright (c) 2003-2008 Jean-Marc Valin
Copyright (c) 2007-2008 CSIRO
Copyright (c) 2007-2009 Xiph.Org Foundation
Written by Jean-Marc Valin */
/**
@file arch.h
@brief Various architecture definitions for CELT
*/
/*
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef ARCH_H
#define ARCH_H
#include "opus_types.h"
#include "opus_defines.h"
# if !defined(__GNUC_PREREQ)
# if defined(__GNUC__)&&defined(__GNUC_MINOR__)
# define __GNUC_PREREQ(_maj,_min) \
((__GNUC__<<16)+__GNUC_MINOR__>=((_maj)<<16)+(_min))
# else
# define __GNUC_PREREQ(_maj,_min) 0
# endif
# endif
#if OPUS_GNUC_PREREQ(3, 0)
#define opus_likely(x) (__builtin_expect(!!(x), 1))
#define opus_unlikely(x) (__builtin_expect(!!(x), 0))
#else
#define opus_likely(x) (!!(x))
#define opus_unlikely(x) (!!(x))
#endif
#define CELT_SIG_SCALE 32768.f
#define CELT_FATAL(str) celt_fatal(str, __FILE__, __LINE__);
#if defined(ENABLE_ASSERTIONS) || defined(ENABLE_HARDENING)
#ifdef __GNUC__
__attribute__((noreturn))
#endif
void celt_fatal(const char *str, const char *file, int line);
#if defined(CELT_C) && !defined(OVERRIDE_celt_fatal)
#include <stdio.h>
#include <stdlib.h>
#ifdef __GNUC__
__attribute__((noreturn))
#endif
void celt_fatal(const char *str, const char *file, int line)
{
fprintf (stderr, "Fatal (internal) error in %s, line %d: %s\n", file, line, str);
#if defined(_MSC_VER)
_set_abort_behavior( 0, _WRITE_ABORT_MSG);
#endif
abort();
}
#endif
#define celt_assert(cond) {if (!(cond)) {CELT_FATAL("assertion failed: " #cond);}}
#define celt_assert2(cond, message) {if (!(cond)) {CELT_FATAL("assertion failed: " #cond "\n" message);}}
#define MUST_SUCCEED(call) celt_assert((call) == OPUS_OK)
#else
#define celt_assert(cond)
#define celt_assert2(cond, message)
#define MUST_SUCCEED(call) do {if((call) != OPUS_OK) {RESTORE_STACK; return OPUS_INTERNAL_ERROR;} } while (0)
#endif
#if defined(ENABLE_ASSERTIONS)
#define celt_sig_assert(cond) {if (!(cond)) {CELT_FATAL("signal assertion failed: " #cond);}}
#else
#define celt_sig_assert(cond)
#endif
#define IMUL32(a,b) ((a)*(b))
#define MIN16(a,b) ((a) < (b) ? (a) : (b)) /**< Minimum 16-bit value. */
#define MAX16(a,b) ((a) > (b) ? (a) : (b)) /**< Maximum 16-bit value. */
#define MIN32(a,b) ((a) < (b) ? (a) : (b)) /**< Minimum 32-bit value. */
#define MAX32(a,b) ((a) > (b) ? (a) : (b)) /**< Maximum 32-bit value. */
#define IMIN(a,b) ((a) < (b) ? (a) : (b)) /**< Minimum int value. */
#define IMAX(a,b) ((a) > (b) ? (a) : (b)) /**< Maximum int value. */
#define UADD32(a,b) ((a)+(b))
#define USUB32(a,b) ((a)-(b))
/* Set this if opus_int64 is a native type of the CPU. */
/* Assume that all LP64 architectures have fast 64-bit types; also x86_64
(which can be ILP32 for x32) and Win64 (which is LLP64). */
#if defined(__x86_64__) || defined(__LP64__) || defined(_WIN64)
#define OPUS_FAST_INT64 1
#else
#define OPUS_FAST_INT64 0
#endif
#define PRINT_MIPS(file)
#ifdef FIXED_POINT
typedef opus_int16 opus_val16;
typedef opus_int32 opus_val32;
typedef opus_int64 opus_val64;
typedef opus_val32 celt_sig;
typedef opus_val16 celt_norm;
typedef opus_val32 celt_ener;
#define celt_isnan(x) 0
#define Q15ONE 32767
#define SIG_SHIFT 12
/* Safe saturation value for 32-bit signals. Should be less than
2^31*(1-0.85) to avoid blowing up on DC at deemphasis.*/
#define SIG_SAT (300000000)
#define NORM_SCALING 16384
#define DB_SHIFT 10
#define EPSILON 1
#define VERY_SMALL 0
#define VERY_LARGE16 ((opus_val16)32767)
#define Q15_ONE ((opus_val16)32767)
#define SCALEIN(a) (a)
#define SCALEOUT(a) (a)
#define ABS16(x) ((x) < 0 ? (-(x)) : (x))
#define ABS32(x) ((x) < 0 ? (-(x)) : (x))
static OPUS_INLINE opus_int16 SAT16(opus_int32 x) {
return x > 32767 ? 32767 : x < -32768 ? -32768 : (opus_int16)x;
}
#ifdef FIXED_DEBUG
#include "fixed_debug.h"
#else
#include "fixed_generic.h"
#ifdef OPUS_ARM_PRESUME_AARCH64_NEON_INTR
#include "arm/fixed_arm64.h"
#elif defined (OPUS_ARM_INLINE_EDSP)
#include "arm/fixed_armv5e.h"
#elif defined (OPUS_ARM_INLINE_ASM)
#include "arm/fixed_armv4.h"
#elif defined (BFIN_ASM)
#include "fixed_bfin.h"
#elif defined (TI_C5X_ASM)
#include "fixed_c5x.h"
#elif defined (TI_C6X_ASM)
#include "fixed_c6x.h"
#endif
#endif
#else /* FIXED_POINT */
typedef float opus_val16;
typedef float opus_val32;
typedef float opus_val64;
typedef float celt_sig;
typedef float celt_norm;
typedef float celt_ener;
#ifdef FLOAT_APPROX
/* This code should reliably detect NaN/inf even when -ffast-math is used.
Assumes IEEE 754 format. */
static OPUS_INLINE int celt_isnan(float x)
{
union {float f; opus_uint32 i;} in;
in.f = x;
return ((in.i>>23)&0xFF)==0xFF && (in.i&0x007FFFFF)!=0;
}
#else
#ifdef __FAST_MATH__
#error Cannot build libopus with -ffast-math unless FLOAT_APPROX is defined. This could result in crashes on extreme (e.g. NaN) input
#endif
#define celt_isnan(x) ((x)!=(x))
#endif
#define Q15ONE 1.0f
#define NORM_SCALING 1.f
#define EPSILON 1e-15f
#define VERY_SMALL 1e-30f
#define VERY_LARGE16 1e15f
#define Q15_ONE ((opus_val16)1.f)
/* This appears to be the same speed as C99's fabsf() but it's more portable. */
#define ABS16(x) ((float)fabs(x))
#define ABS32(x) ((float)fabs(x))
#define QCONST16(x,bits) (x)
#define QCONST32(x,bits) (x)
#define NEG16(x) (-(x))
#define NEG32(x) (-(x))
#define NEG32_ovflw(x) (-(x))
#define EXTRACT16(x) (x)
#define EXTEND32(x) (x)
#define SHR16(a,shift) (a)
#define SHL16(a,shift) (a)
#define SHR32(a,shift) (a)
#define SHL32(a,shift) (a)
#define PSHR32(a,shift) (a)
#define VSHR32(a,shift) (a)
#define PSHR(a,shift) (a)
#define SHR(a,shift) (a)
#define SHL(a,shift) (a)
#define SATURATE(x,a) (x)
#define SATURATE16(x) (x)
#define ROUND16(a,shift) (a)
#define SROUND16(a,shift) (a)
#define HALF16(x) (.5f*(x))
#define HALF32(x) (.5f*(x))
#define ADD16(a,b) ((a)+(b))
#define SUB16(a,b) ((a)-(b))
#define ADD32(a,b) ((a)+(b))
#define SUB32(a,b) ((a)-(b))
#define ADD32_ovflw(a,b) ((a)+(b))
#define SUB32_ovflw(a,b) ((a)-(b))
#define MULT16_16_16(a,b) ((a)*(b))
#define MULT16_16(a,b) ((opus_val32)(a)*(opus_val32)(b))
#define MAC16_16(c,a,b) ((c)+(opus_val32)(a)*(opus_val32)(b))
#define MULT16_32_Q15(a,b) ((a)*(b))
#define MULT16_32_Q16(a,b) ((a)*(b))
#define MULT32_32_Q31(a,b) ((a)*(b))
#define MAC16_32_Q15(c,a,b) ((c)+(a)*(b))
#define MAC16_32_Q16(c,a,b) ((c)+(a)*(b))
#define MULT16_16_Q11_32(a,b) ((a)*(b))
#define MULT16_16_Q11(a,b) ((a)*(b))
#define MULT16_16_Q13(a,b) ((a)*(b))
#define MULT16_16_Q14(a,b) ((a)*(b))
#define MULT16_16_Q15(a,b) ((a)*(b))
#define MULT16_16_P15(a,b) ((a)*(b))
#define MULT16_16_P13(a,b) ((a)*(b))
#define MULT16_16_P14(a,b) ((a)*(b))
#define MULT16_32_P16(a,b) ((a)*(b))
#define DIV32_16(a,b) (((opus_val32)(a))/(opus_val16)(b))
#define DIV32(a,b) (((opus_val32)(a))/(opus_val32)(b))
#define SCALEIN(a) ((a)*CELT_SIG_SCALE)
#define SCALEOUT(a) ((a)*(1/CELT_SIG_SCALE))
#define SIG2WORD16(x) (x)
#endif /* !FIXED_POINT */
#ifndef GLOBAL_STACK_SIZE
#ifdef FIXED_POINT
#define GLOBAL_STACK_SIZE 120000
#else
#define GLOBAL_STACK_SIZE 120000
#endif
#endif
#endif /* ARCH_H */

353
local_plugins/opus/android/libs/opus/celt/arm/arm2gnu.pl

@ -0,0 +1,353 @@
#!/usr/bin/perl
# Copyright (C) 2002-2013 Xiph.org Foundation
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# - Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# - Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
# OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
my $bigend; # little/big endian
my $nxstack;
my $apple = 0;
my $symprefix = "";
$nxstack = 0;
eval 'exec /usr/local/bin/perl -S $0 ${1+"$@"}'
if $running_under_some_shell;
while ($ARGV[0] =~ /^-/) {
$_ = shift;
last if /^--$/;
if (/^-n$/) {
$nflag++;
next;
}
if (/^--apple$/) {
$apple = 1;
$symprefix = "_";
next;
}
die "I don't recognize this switch: $_\\n";
}
$printit++ unless $nflag;
$\ = "\n"; # automatically add newline on print
$n=0;
$thumb = 0; # ARM mode by default, not Thumb.
@proc_stack = ();
printf (" .syntax unified\n");
LINE:
while (<>) {
# For ADRLs we need to add a new line after the substituted one.
$addPadding = 0;
# First, we do not dare to touch *anything* inside double quotes, do we?
# Second, if you want a dollar character in the string,
# insert two of them -- that's how ARM C and assembler treat strings.
s/^([A-Za-z_]\w*)[ \t]+DCB[ \t]*\"/$1: .ascii \"/ && do { s/\$\$/\$/g; next };
s/\bDCB\b[ \t]*\"/.ascii \"/ && do { s/\$\$/\$/g; next };
s/^(\S+)\s+RN\s+(\S+)/$1 .req r$2/ && do { s/\$\$/\$/g; next };
# If there's nothing on a line but a comment, don't try to apply any further
# substitutions (this is a cheap hack to avoid mucking up the license header)
s/^([ \t]*);/$1@/ && do { s/\$\$/\$/g; next };
# If substituted -- leave immediately !
s/@/,:/;
s/;/@/;
while ( /@.*'/ ) {
s/(@.*)'/$1/g;
}
s/\{FALSE\}/0/g;
s/\{TRUE\}/1/g;
s/\{(\w\w\w\w+)\}/$1/g;
s/\bINCLUDE[ \t]*([^ \t\n]+)/.include \"$1\"/;
s/\bGET[ \t]*([^ \t\n]+)/.include \"${ my $x=$1; $x =~ s|\.s|-gnu.S|; \$x }\"/;
s/\bIMPORT\b/.extern/;
s/\bEXPORT\b\s*/.global $symprefix/;
s/^(\s+)\[/$1IF/;
s/^(\s+)\|/$1ELSE/;
s/^(\s+)\]/$1ENDIF/;
s/IF *:DEF:/ .ifdef/;
s/IF *:LNOT: *:DEF:/ .ifndef/;
s/ELSE/ .else/;
s/ENDIF/ .endif/;
if( /\bIF\b/ ) {
s/\bIF\b/ .if/;
s/=/==/;
}
if ( $n == 2) {
s/\$/\\/g;
}
if ($n == 1) {
s/\$//g;
s/label//g;
$n = 2;
}
if ( /MACRO/ ) {
s/MACRO *\n/.macro/;
$n=1;
}
if ( /\bMEND\b/ ) {
s/\bMEND\b/.endm/;
$n=0;
}
# ".rdata" doesn't work in 'as' version 2.13.2, as it is ".rodata" there.
#
if ( /\bAREA\b/ ) {
my $align;
$align = "2";
if ( /ALIGN=(\d+)/ ) {
$align = $1;
}
if ( /CODE/ ) {
$nxstack = 1;
}
s/^(.+)CODE(.+)READONLY(.*)/ .text/;
s/^(.+)DATA(.+)READONLY(.*)/ .section .rdata/;
s/^(.+)\|\|\.data\|\|(.+)/ .data/;
s/^(.+)\|\|\.bss\|\|(.+)/ .bss/;
s/$/; .p2align $align/;
# Enable NEON instructions but don't produce a binary that requires
# ARMv7. RVCT does not have equivalent directives, so we just do this
# for all CODE areas.
if ( /.text/ ) {
# Separating .arch, .fpu, etc., by semicolons does not work (gas
# thinks the semicolon is part of the arch name, even when there's
# whitespace separating them). Sadly this means our line numbers
# won't match the original source file (we could use the .line
# directive, which is documented to be obsolete, but then gdb will
# show the wrong line in the translated source file).
s/$/; .arch armv7-a\n .fpu neon\n .object_arch armv4t/ unless ($apple);
}
}
s/\|\|\.constdata\$(\d+)\|\|/.L_CONST$1/; # ||.constdata$3||
s/\|\|\.bss\$(\d+)\|\|/.L_BSS$1/; # ||.bss$2||
s/\|\|\.data\$(\d+)\|\|/.L_DATA$1/; # ||.data$2||
s/\|\|([a-zA-Z0-9_]+)\@([a-zA-Z0-9_]+)\|\|/@ $&/;
s/^(\s+)\%(\s)/ .space $1/;
s/\|(.+)\.(\d+)\|/\.$1_$2/; # |L80.123| -> .L80_123
s/\bCODE32\b/.code 32/ && do {$thumb = 0};
s/\bCODE16\b/.code 16/ && do {$thumb = 1};
if (/\bPROC\b/)
{
my $prefix;
my $proc;
/^([A-Za-z_\.]\w+)\b/;
$proc = $1;
$prefix = "";
if ($proc)
{
$prefix = $prefix.sprintf("\t.type\t%s, %%function", $proc) unless ($apple);
# Make sure we $prefix isn't empty here (for the $apple case).
# We handle mangling the label here, make sure it doesn't match
# the label handling below (if $prefix would be empty).
$prefix = $prefix."; ";
push(@proc_stack, $proc);
s/^[A-Za-z_\.]\w+/$symprefix$&:/;
}
$prefix = $prefix."\t.thumb_func; " if ($thumb);
s/\bPROC\b/@ $&/;
$_ = $prefix.$_;
}
s/^(\s*)(S|Q|SH|U|UQ|UH)ASX\b/$1$2ADDSUBX/;
s/^(\s*)(S|Q|SH|U|UQ|UH)SAX\b/$1$2SUBADDX/;
if (/\bENDP\b/)
{
my $proc;
s/\bENDP\b/@ $&/;
$proc = pop(@proc_stack);
$_ = "\t.size $proc, .-$proc".$_ if ($proc && !$apple);
}
s/\bSUBT\b/@ $&/;
s/\bDATA\b/@ $&/; # DATA directive is deprecated -- Asm guide, p.7-25
s/\bKEEP\b/@ $&/;
s/\bEXPORTAS\b/@ $&/;
s/\|\|(.)+\bEQU\b/@ $&/;
s/\|\|([\w\$]+)\|\|/$1/;
s/\bENTRY\b/@ $&/;
s/\bASSERT\b/@ $&/;
s/\bGBLL\b/@ $&/;
s/\bGBLA\b/@ $&/;
s/^\W+OPT\b/@ $&/;
s/:OR:/|/g;
s/:SHL:/<</g;
s/:SHR:/>>/g;
s/:AND:/&/g;
s/:LAND:/&&/g;
s/CPSR/cpsr/;
s/SPSR/spsr/;
s/ALIGN$/.balign 4/;
s/ALIGN\s+([0-9x]+)$/.balign $1/;
s/psr_cxsf/psr_all/;
s/LTORG/.ltorg/;
s/^([A-Za-z_]\w*)[ \t]+EQU/ .set $1,/;
s/^([A-Za-z_]\w*)[ \t]+SETL/ .set $1,/;
s/^([A-Za-z_]\w*)[ \t]+SETA/ .set $1,/;
s/^([A-Za-z_]\w*)[ \t]+\*/ .set $1,/;
# {PC} + 0xdeadfeed --> . + 0xdeadfeed
s/\{PC\} \+/ \. +/;
# Single hex constant on the line !
#
# >>> NOTE <<<
# Double-precision floats in gcc are always mixed-endian, which means
# bytes in two words are little-endian, but words are big-endian.
# So, 0x0000deadfeed0000 would be stored as 0x0000dead at low address
# and 0xfeed0000 at high address.
#
s/\bDCFD\b[ \t]+0x([a-fA-F0-9]{8})([a-fA-F0-9]{8})/.long 0x$1, 0x$2/;
# Only decimal constants on the line, no hex !
s/\bDCFD\b[ \t]+([0-9\.\-]+)/.double $1/;
# Single hex constant on the line !
# s/\bDCFS\b[ \t]+0x([a-f0-9]{8})([a-f0-9]{8})/.long 0x$1, 0x$2/;
# Only decimal constants on the line, no hex !
# s/\bDCFS\b[ \t]+([0-9\.\-]+)/.double $1/;
s/\bDCFS[ \t]+0x/.word 0x/;
s/\bDCFS\b/.float/;
s/^([A-Za-z_]\w*)[ \t]+DCD/$1 .word/;
s/\bDCD\b/.word/;
s/^([A-Za-z_]\w*)[ \t]+DCW/$1 .short/;
s/\bDCW\b/.short/;
s/^([A-Za-z_]\w*)[ \t]+DCB/$1 .byte/;
s/\bDCB\b/.byte/;
s/^([A-Za-z_]\w*)[ \t]+\%/.comm $1,/;
s/^[A-Za-z_\.]\w+/$&:/;
s/^(\d+)/$1:/;
s/\%(\d+)/$1b_or_f/;
s/\%[Bb](\d+)/$1b/;
s/\%[Ff](\d+)/$1f/;
s/\%[Ff][Tt](\d+)/$1f/;
s/&([\dA-Fa-f]+)/0x$1/;
if ( /\b2_[01]+\b/ ) {
s/\b2_([01]+)\b/conv$1&&&&/g;
while ( /[01][01][01][01]&&&&/ ) {
s/0000&&&&/&&&&0/g;
s/0001&&&&/&&&&1/g;
s/0010&&&&/&&&&2/g;
s/0011&&&&/&&&&3/g;
s/0100&&&&/&&&&4/g;
s/0101&&&&/&&&&5/g;
s/0110&&&&/&&&&6/g;
s/0111&&&&/&&&&7/g;
s/1000&&&&/&&&&8/g;
s/1001&&&&/&&&&9/g;
s/1010&&&&/&&&&A/g;
s/1011&&&&/&&&&B/g;
s/1100&&&&/&&&&C/g;
s/1101&&&&/&&&&D/g;
s/1110&&&&/&&&&E/g;
s/1111&&&&/&&&&F/g;
}
s/000&&&&/&&&&0/g;
s/001&&&&/&&&&1/g;
s/010&&&&/&&&&2/g;
s/011&&&&/&&&&3/g;
s/100&&&&/&&&&4/g;
s/101&&&&/&&&&5/g;
s/110&&&&/&&&&6/g;
s/111&&&&/&&&&7/g;
s/00&&&&/&&&&0/g;
s/01&&&&/&&&&1/g;
s/10&&&&/&&&&2/g;
s/11&&&&/&&&&3/g;
s/0&&&&/&&&&0/g;
s/1&&&&/&&&&1/g;
s/conv&&&&/0x/g;
}
if ( /commandline/)
{
if( /-bigend/)
{
$bigend=1;
}
}
if ( /\bDCDU\b/ )
{
my $cmd=$_;
my $value;
my $prefix;
my $w1;
my $w2;
my $w3;
my $w4;
s/\s+DCDU\b/@ $&/;
$cmd =~ /\bDCDU\b\s+0x(\d+)/;
$value = $1;
$value =~ /(\w\w)(\w\w)(\w\w)(\w\w)/;
$w1 = $1;
$w2 = $2;
$w3 = $3;
$w4 = $4;
if( $bigend ne "")
{
# big endian
$prefix = "\t.byte\t0x".$w1.";".
"\t.byte\t0x".$w2.";".
"\t.byte\t0x".$w3.";".
"\t.byte\t0x".$w4."; ";
}
else
{
# little endian
$prefix = "\t.byte\t0x".$w4.";".
"\t.byte\t0x".$w3.";".
"\t.byte\t0x".$w2.";".
"\t.byte\t0x".$w1."; ";
}
$_=$prefix.$_;
}
if ( /\badrl\b/i )
{
s/\badrl\s+(\w+)\s*,\s*(\w+)/ldr $1,=$2/i;
$addPadding = 1;
}
s/\bEND\b/@ END/;
} continue {
printf ("%s", $_) if $printit;
if ($addPadding != 0)
{
printf (" mov r0,r0\n");
$addPadding = 0;
}
}
#If we had a code section, mark that this object doesn't need an executable
# stack.
if ($nxstack && !$apple) {
printf (" .section\t.note.GNU-stack,\"\",\%\%progbits\n");
}

171
local_plugins/opus/android/libs/opus/celt/arm/arm_celt_map.c

@ -0,0 +1,171 @@
/* Copyright (c) 2010 Xiph.Org Foundation
* Copyright (c) 2013 Parrot */
/*
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "pitch.h"
#include "kiss_fft.h"
#include "mdct.h"
#if defined(OPUS_HAVE_RTCD)
# if defined(OPUS_ARM_MAY_HAVE_NEON_INTR) && !defined(OPUS_ARM_PRESUME_NEON_INTR)
opus_val32 (*const CELT_INNER_PROD_IMPL[OPUS_ARCHMASK+1])(const opus_val16 *x, const opus_val16 *y, int N) = {
celt_inner_prod_c, /* ARMv4 */
celt_inner_prod_c, /* EDSP */
celt_inner_prod_c, /* Media */
celt_inner_prod_neon,/* NEON */
celt_inner_prod_neon /* DOTPROD */
};
void (*const DUAL_INNER_PROD_IMPL[OPUS_ARCHMASK+1])(const opus_val16 *x, const opus_val16 *y01, const opus_val16 *y02,
int N, opus_val32 *xy1, opus_val32 *xy2) = {
dual_inner_prod_c, /* ARMv4 */
dual_inner_prod_c, /* EDSP */
dual_inner_prod_c, /* Media */
dual_inner_prod_neon,/* NEON */
dual_inner_prod_neon /* DOTPROD */
};
# endif
# if defined(FIXED_POINT)
# if ((defined(OPUS_ARM_MAY_HAVE_NEON) && !defined(OPUS_ARM_PRESUME_NEON)) || \
(defined(OPUS_ARM_MAY_HAVE_MEDIA) && !defined(OPUS_ARM_PRESUME_MEDIA)) || \
(defined(OPUS_ARM_MAY_HAVE_EDSP) && !defined(OPUS_ARM_PRESUME_EDSP)))
opus_val32 (*const CELT_PITCH_XCORR_IMPL[OPUS_ARCHMASK+1])(const opus_val16 *,
const opus_val16 *, opus_val32 *, int, int, int) = {
celt_pitch_xcorr_c, /* ARMv4 */
MAY_HAVE_EDSP(celt_pitch_xcorr), /* EDSP */
MAY_HAVE_MEDIA(celt_pitch_xcorr), /* Media */
MAY_HAVE_NEON(celt_pitch_xcorr), /* NEON */
MAY_HAVE_NEON(celt_pitch_xcorr) /* DOTPROD */
};
# endif
# else /* !FIXED_POINT */
# if defined(OPUS_ARM_MAY_HAVE_NEON_INTR) && !defined(OPUS_ARM_PRESUME_NEON_INTR)
void (*const CELT_PITCH_XCORR_IMPL[OPUS_ARCHMASK+1])(const opus_val16 *,
const opus_val16 *, opus_val32 *, int, int, int) = {
celt_pitch_xcorr_c, /* ARMv4 */
celt_pitch_xcorr_c, /* EDSP */
celt_pitch_xcorr_c, /* Media */
celt_pitch_xcorr_float_neon, /* Neon */
celt_pitch_xcorr_float_neon /* DOTPROD */
};
# endif
# endif /* FIXED_POINT */
#if defined(FIXED_POINT) && defined(OPUS_HAVE_RTCD) && \
defined(OPUS_ARM_MAY_HAVE_NEON_INTR) && !defined(OPUS_ARM_PRESUME_NEON_INTR)
void (*const XCORR_KERNEL_IMPL[OPUS_ARCHMASK + 1])(
const opus_val16 *x,
const opus_val16 *y,
opus_val32 sum[4],
int len
) = {
xcorr_kernel_c, /* ARMv4 */
xcorr_kernel_c, /* EDSP */
xcorr_kernel_c, /* Media */
xcorr_kernel_neon_fixed, /* Neon */
xcorr_kernel_neon_fixed /* DOTPROD */
};
#endif
# if defined(OPUS_ARM_MAY_HAVE_NEON_INTR)
# if defined(HAVE_ARM_NE10)
# if defined(CUSTOM_MODES)
int (*const OPUS_FFT_ALLOC_ARCH_IMPL[OPUS_ARCHMASK+1])(kiss_fft_state *st) = {
opus_fft_alloc_arch_c, /* ARMv4 */
opus_fft_alloc_arch_c, /* EDSP */
opus_fft_alloc_arch_c, /* Media */
opus_fft_alloc_arm_neon, /* Neon with NE10 library support */
opus_fft_alloc_arm_neon /* DOTPROD with NE10 library support */
};
void (*const OPUS_FFT_FREE_ARCH_IMPL[OPUS_ARCHMASK+1])(kiss_fft_state *st) = {
opus_fft_free_arch_c, /* ARMv4 */
opus_fft_free_arch_c, /* EDSP */
opus_fft_free_arch_c, /* Media */
opus_fft_free_arm_neon, /* Neon with NE10 */
opus_fft_free_arm_neon /* DOTPROD with NE10 */
};
# endif /* CUSTOM_MODES */
void (*const OPUS_FFT[OPUS_ARCHMASK+1])(const kiss_fft_state *cfg,
const kiss_fft_cpx *fin,
kiss_fft_cpx *fout) = {
opus_fft_c, /* ARMv4 */
opus_fft_c, /* EDSP */
opus_fft_c, /* Media */
opus_fft_neon, /* Neon with NE10 */
opus_fft_neon /* DOTPROD with NE10 */
};
void (*const OPUS_IFFT[OPUS_ARCHMASK+1])(const kiss_fft_state *cfg,
const kiss_fft_cpx *fin,
kiss_fft_cpx *fout) = {
opus_ifft_c, /* ARMv4 */
opus_ifft_c, /* EDSP */
opus_ifft_c, /* Media */
opus_ifft_neon, /* Neon with NE10 */
opus_ifft_neon /* DOTPROD with NE10 */
};
void (*const CLT_MDCT_FORWARD_IMPL[OPUS_ARCHMASK+1])(const mdct_lookup *l,
kiss_fft_scalar *in,
kiss_fft_scalar * OPUS_RESTRICT out,
const opus_val16 *window,
int overlap, int shift,
int stride, int arch) = {
clt_mdct_forward_c, /* ARMv4 */
clt_mdct_forward_c, /* EDSP */
clt_mdct_forward_c, /* Media */
clt_mdct_forward_neon, /* Neon with NE10 */
clt_mdct_forward_neon /* DOTPROD with NE10 */
};
void (*const CLT_MDCT_BACKWARD_IMPL[OPUS_ARCHMASK+1])(const mdct_lookup *l,
kiss_fft_scalar *in,
kiss_fft_scalar * OPUS_RESTRICT out,
const opus_val16 *window,
int overlap, int shift,
int stride, int arch) = {
clt_mdct_backward_c, /* ARMv4 */
clt_mdct_backward_c, /* EDSP */
clt_mdct_backward_c, /* Media */
clt_mdct_backward_neon, /* Neon with NE10 */
clt_mdct_backward_neon /* DOTPROD with NE10 */
};
# endif /* HAVE_ARM_NE10 */
# endif /* OPUS_ARM_MAY_HAVE_NEON_INTR */
#endif /* OPUS_HAVE_RTCD */

291
local_plugins/opus/android/libs/opus/celt/arm/armcpu.c

@ -0,0 +1,291 @@
/* Copyright (c) 2010 Xiph.Org Foundation
* Copyright (c) 2013 Parrot */
/*
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/* Original code from libtheora modified to suit to Opus */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#ifdef OPUS_HAVE_RTCD
#include "armcpu.h"
#include "cpu_support.h"
#include "os_support.h"
#include "opus_types.h"
#include "arch.h"
#define OPUS_CPU_ARM_V4_FLAG (1<<OPUS_ARCH_ARM_V4)
#define OPUS_CPU_ARM_EDSP_FLAG (1<<OPUS_ARCH_ARM_EDSP)
#define OPUS_CPU_ARM_MEDIA_FLAG (1<<OPUS_ARCH_ARM_MEDIA)
#define OPUS_CPU_ARM_NEON_FLAG (1<<OPUS_ARCH_ARM_NEON)
#define OPUS_CPU_ARM_DOTPROD_FLAG (1<<OPUS_ARCH_ARM_DOTPROD)
#if defined(_MSC_VER)
/*For GetExceptionCode() and EXCEPTION_ILLEGAL_INSTRUCTION.*/
# define WIN32_LEAN_AND_MEAN
# define WIN32_EXTRA_LEAN
# include <windows.h>
static OPUS_INLINE opus_uint32 opus_cpu_capabilities(void){
opus_uint32 flags;
flags=0;
/* MSVC has no OPUS_INLINE __asm support for ARM, but it does let you __emit
* instructions via their assembled hex code.
* All of these instructions should be essentially nops. */
# if defined(OPUS_ARM_MAY_HAVE_EDSP) || defined(OPUS_ARM_MAY_HAVE_MEDIA) \
|| defined(OPUS_ARM_MAY_HAVE_NEON) || defined(OPUS_ARM_MAY_HAVE_NEON_INTR)
__try{
/*PLD [r13]*/
__emit(0xF5DDF000);
flags|=OPUS_CPU_ARM_EDSP_FLAG;
}
__except(GetExceptionCode()==EXCEPTION_ILLEGAL_INSTRUCTION){
/*Ignore exception.*/
}
# if defined(OPUS_ARM_MAY_HAVE_MEDIA) \
|| defined(OPUS_ARM_MAY_HAVE_NEON) || defined(OPUS_ARM_MAY_HAVE_NEON_INTR)
__try{
/*SHADD8 r3,r3,r3*/
__emit(0xE6333F93);
flags|=OPUS_CPU_ARM_MEDIA_FLAG;
}
__except(GetExceptionCode()==EXCEPTION_ILLEGAL_INSTRUCTION){
/*Ignore exception.*/
}
# if defined(OPUS_ARM_MAY_HAVE_NEON) || defined(OPUS_ARM_MAY_HAVE_NEON_INTR)
__try{
/*VORR q0,q0,q0*/
__emit(0xF2200150);
flags|=OPUS_CPU_ARM_NEON_FLAG;
}
__except(GetExceptionCode()==EXCEPTION_ILLEGAL_INSTRUCTION){
/*Ignore exception.*/
}
# endif
# endif
# endif
return flags;
}
#elif defined(__linux__)
/* Linux based */
#include <stdio.h>
static opus_uint32 opus_cpu_capabilities(void)
{
opus_uint32 flags = 0;
FILE *cpuinfo;
/* Reading /proc/self/auxv would be easier, but that doesn't work reliably on
* Android */
cpuinfo = fopen("/proc/cpuinfo", "r");
if(cpuinfo != NULL)
{
/* 512 should be enough for anybody (it's even enough for all the flags that
* x86 has accumulated... so far). */
char buf[512];
while(fgets(buf, 512, cpuinfo) != NULL)
{
# if defined(OPUS_ARM_MAY_HAVE_EDSP) || defined(OPUS_ARM_MAY_HAVE_MEDIA) \
|| defined(OPUS_ARM_MAY_HAVE_NEON) || defined(OPUS_ARM_MAY_HAVE_NEON_INTR)
/* Search for edsp and neon flag */
if(memcmp(buf, "Features", 8) == 0)
{
char *p;
p = strstr(buf, " edsp");
if(p != NULL && (p[5] == ' ' || p[5] == '\n'))
flags |= OPUS_CPU_ARM_EDSP_FLAG;
# if defined(OPUS_ARM_MAY_HAVE_NEON) || defined(OPUS_ARM_MAY_HAVE_NEON_INTR)
p = strstr(buf, " neon");
if(p != NULL && (p[5] == ' ' || p[5] == '\n'))
flags |= OPUS_CPU_ARM_NEON_FLAG;
p = strstr(buf, " asimd");
if(p != NULL && (p[6] == ' ' || p[6] == '\n'))
flags |= OPUS_CPU_ARM_NEON_FLAG | OPUS_CPU_ARM_MEDIA_FLAG | OPUS_CPU_ARM_EDSP_FLAG;
# endif
# if defined(OPUS_ARM_MAY_HAVE_DOTPROD)
p = strstr(buf, " asimddp");
if(p != NULL && (p[8] == ' ' || p[8] == '\n'))
flags |= OPUS_CPU_ARM_DOTPROD_FLAG;
# endif
}
# endif
# if defined(OPUS_ARM_MAY_HAVE_MEDIA) \
|| defined(OPUS_ARM_MAY_HAVE_NEON) || defined(OPUS_ARM_MAY_HAVE_NEON_INTR)
/* Search for media capabilities (>= ARMv6) */
if(memcmp(buf, "CPU architecture:", 17) == 0)
{
int version;
version = atoi(buf+17);
if(version >= 6)
flags |= OPUS_CPU_ARM_MEDIA_FLAG;
}
# endif
}
#if defined(OPUS_ARM_PRESUME_AARCH64_NEON_INTR)
flags |= OPUS_CPU_ARM_EDSP_FLAG | OPUS_CPU_ARM_MEDIA_FLAG | OPUS_CPU_ARM_NEON_FLAG;
# if defined(OPUS_ARM_PRESUME_DOTPROD)
flags |= OPUS_CPU_ARM_DOTPROD_FLAG;
# endif
#endif
fclose(cpuinfo);
}
return flags;
}
#elif defined(__APPLE__)
#include <sys/types.h>
#include <sys/sysctl.h>
static opus_uint32 opus_cpu_capabilities(void)
{
opus_uint32 flags = 0;
#if defined(OPUS_ARM_MAY_HAVE_DOTPROD)
size_t size = sizeof(uint32_t);
uint32_t value = 0;
if (!sysctlbyname("hw.optional.arm.FEAT_DotProd", &value, &size, NULL, 0) && value)
{
flags |= OPUS_CPU_ARM_DOTPROD_FLAG;
}
#endif
#if defined(OPUS_ARM_PRESUME_AARCH64_NEON_INTR)
flags |= OPUS_CPU_ARM_EDSP_FLAG | OPUS_CPU_ARM_MEDIA_FLAG | OPUS_CPU_ARM_NEON_FLAG;
# if defined(OPUS_ARM_PRESUME_DOTPROD)
flags |= OPUS_CPU_ARM_DOTPROD_FLAG;
# endif
#endif
return flags;
}
#elif defined(__FreeBSD__)
#include <sys/auxv.h>
static opus_uint32 opus_cpu_capabilities(void)
{
long hwcap = 0;
opus_uint32 flags = 0;
# if defined(OPUS_ARM_MAY_HAVE_MEDIA) \
|| defined(OPUS_ARM_MAY_HAVE_NEON) || defined(OPUS_ARM_MAY_HAVE_NEON_INTR)
/* FreeBSD requires armv6+, which always supports media instructions */
flags |= OPUS_CPU_ARM_MEDIA_FLAG;
# endif
elf_aux_info(AT_HWCAP, &hwcap, sizeof hwcap);
# if defined(OPUS_ARM_MAY_HAVE_EDSP) || defined(OPUS_ARM_MAY_HAVE_MEDIA) \
|| defined(OPUS_ARM_MAY_HAVE_NEON) || defined(OPUS_ARM_MAY_HAVE_NEON_INTR)
# ifdef HWCAP_EDSP
if (hwcap & HWCAP_EDSP)
flags |= OPUS_CPU_ARM_EDSP_FLAG;
# endif
# if defined(OPUS_ARM_MAY_HAVE_NEON) || defined(OPUS_ARM_MAY_HAVE_NEON_INTR)
# ifdef HWCAP_NEON
if (hwcap & HWCAP_NEON)
flags |= OPUS_CPU_ARM_NEON_FLAG;
# elif defined(HWCAP_ASIMD)
if (hwcap & HWCAP_ASIMD)
flags |= OPUS_CPU_ARM_NEON_FLAG | OPUS_CPU_ARM_MEDIA_FLAG | OPUS_CPU_ARM_EDSP_FLAG;
# endif
# endif
# if defined(OPUS_ARM_MAY_HAVE_DOTPROD) && defined(HWCAP_ASIMDDP)
if (hwcap & HWCAP_ASIMDDP)
flags |= OPUS_CPU_ARM_DOTPROD_FLAG;
# endif
# endif
#if defined(OPUS_ARM_PRESUME_AARCH64_NEON_INTR)
flags |= OPUS_CPU_ARM_EDSP_FLAG | OPUS_CPU_ARM_MEDIA_FLAG | OPUS_CPU_ARM_NEON_FLAG;
# if defined(OPUS_ARM_PRESUME_DOTPROD)
flags |= OPUS_CPU_ARM_DOTPROD_FLAG;
# endif
#endif
return (flags);
}
#else
/* The feature registers which can tell us what the processor supports are
* accessible in priveleged modes only, so we can't have a general user-space
* detection method like on x86.*/
# error "Configured to use ARM asm but no CPU detection method available for " \
"your platform. Reconfigure with --disable-rtcd (or send patches)."
#endif
static int opus_select_arch_impl(void)
{
opus_uint32 flags = opus_cpu_capabilities();
int arch = 0;
if(!(flags & OPUS_CPU_ARM_EDSP_FLAG)) {
/* Asserts ensure arch values are sequential */
celt_assert(arch == OPUS_ARCH_ARM_V4);
return arch;
}
arch++;
if(!(flags & OPUS_CPU_ARM_MEDIA_FLAG)) {
celt_assert(arch == OPUS_ARCH_ARM_EDSP);
return arch;
}
arch++;
if(!(flags & OPUS_CPU_ARM_NEON_FLAG)) {
celt_assert(arch == OPUS_ARCH_ARM_MEDIA);
return arch;
}
arch++;
if(!(flags & OPUS_CPU_ARM_DOTPROD_FLAG)) {
celt_assert(arch == OPUS_ARCH_ARM_NEON);
return arch;
}
arch++;
celt_assert(arch == OPUS_ARCH_ARM_DOTPROD);
return arch;
}
int opus_select_arch(void) {
int arch = opus_select_arch_impl();
#ifdef FUZZING
arch = rand()%(arch+1);
#endif
return arch;
}
#endif

90
local_plugins/opus/android/libs/opus/celt/arm/armcpu.h

@ -0,0 +1,90 @@
/* Copyright (c) 2010 Xiph.Org Foundation
* Copyright (c) 2013 Parrot */
/*
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#if !defined(ARMCPU_H)
# define ARMCPU_H
# if defined(OPUS_ARM_MAY_HAVE_EDSP)
# define MAY_HAVE_EDSP(name) name ## _edsp
# else
# define MAY_HAVE_EDSP(name) name ## _c
# endif
# if defined(OPUS_ARM_MAY_HAVE_MEDIA)
# define MAY_HAVE_MEDIA(name) name ## _media
# else
# define MAY_HAVE_MEDIA(name) MAY_HAVE_EDSP(name)
# endif
# if defined(OPUS_ARM_MAY_HAVE_NEON)
# define MAY_HAVE_NEON(name) name ## _neon
# else
# define MAY_HAVE_NEON(name) MAY_HAVE_MEDIA(name)
# endif
# if defined(OPUS_ARM_MAY_HAVE_DOTPROD)
# define MAY_HAVE_DOTPROD(name) name ## _dotprod
# else
# define MAY_HAVE_DOTPROD(name) MAY_HAVE_NEON(name)
# endif
# if defined(OPUS_ARM_PRESUME_EDSP)
# define PRESUME_EDSP(name) name ## _edsp
# else
# define PRESUME_EDSP(name) name ## _c
# endif
# if defined(OPUS_ARM_PRESUME_MEDIA)
# define PRESUME_MEDIA(name) name ## _media
# else
# define PRESUME_MEDIA(name) PRESUME_EDSP(name)
# endif
# if defined(OPUS_ARM_PRESUME_NEON)
# define PRESUME_NEON(name) name ## _neon
# else
# define PRESUME_NEON(name) PRESUME_MEDIA(name)
# endif
# if defined(OPUS_ARM_PRESUME_DOTPROD)
# define PRESUME_DOTPROD(name) name ## _dotprod
# else
# define PRESUME_DOTPROD(name) PRESUME_NEON(name)
# endif
# if defined(OPUS_HAVE_RTCD)
int opus_select_arch(void);
#define OPUS_ARCH_ARM_V4 (0)
#define OPUS_ARCH_ARM_EDSP (1)
#define OPUS_ARCH_ARM_MEDIA (2)
#define OPUS_ARCH_ARM_NEON (3)
#define OPUS_ARCH_ARM_DOTPROD (4)
# endif
#endif

37
local_plugins/opus/android/libs/opus/celt/arm/armopts.s.in

@ -0,0 +1,37 @@
/* Copyright (C) 2013 Mozilla Corporation */
/*
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
; Set the following to 1 if we have EDSP instructions
; (LDRD/STRD, etc., ARMv5E and later).
OPUS_ARM_MAY_HAVE_EDSP * @OPUS_ARM_MAY_HAVE_EDSP@
; Set the following to 1 if we have ARMv6 media instructions.
OPUS_ARM_MAY_HAVE_MEDIA * @OPUS_ARM_MAY_HAVE_MEDIA@
; Set the following to 1 if we have NEON (some ARMv7)
OPUS_ARM_MAY_HAVE_NEON * @OPUS_ARM_MAY_HAVE_NEON@
END

173
local_plugins/opus/android/libs/opus/celt/arm/celt_fft_ne10.c

@ -0,0 +1,173 @@
/* Copyright (c) 2015 Xiph.Org Foundation
Written by Viswanath Puttagunta */
/**
@file celt_fft_ne10.c
@brief ARM Neon optimizations for fft using NE10 library
*/
/*
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef SKIP_CONFIG_H
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#endif
#include <NE10_dsp.h>
#include "os_support.h"
#include "kiss_fft.h"
#include "stack_alloc.h"
#if !defined(FIXED_POINT)
# define NE10_FFT_ALLOC_C2C_TYPE_NEON ne10_fft_alloc_c2c_float32_neon
# define NE10_FFT_CFG_TYPE_T ne10_fft_cfg_float32_t
# define NE10_FFT_STATE_TYPE_T ne10_fft_state_float32_t
# define NE10_FFT_DESTROY_C2C_TYPE ne10_fft_destroy_c2c_float32
# define NE10_FFT_CPX_TYPE_T ne10_fft_cpx_float32_t
# define NE10_FFT_C2C_1D_TYPE_NEON ne10_fft_c2c_1d_float32_neon
#else
# define NE10_FFT_ALLOC_C2C_TYPE_NEON(nfft) ne10_fft_alloc_c2c_int32_neon(nfft)
# define NE10_FFT_CFG_TYPE_T ne10_fft_cfg_int32_t
# define NE10_FFT_STATE_TYPE_T ne10_fft_state_int32_t
# define NE10_FFT_DESTROY_C2C_TYPE ne10_fft_destroy_c2c_int32
# define NE10_FFT_DESTROY_C2C_TYPE ne10_fft_destroy_c2c_int32
# define NE10_FFT_CPX_TYPE_T ne10_fft_cpx_int32_t
# define NE10_FFT_C2C_1D_TYPE_NEON ne10_fft_c2c_1d_int32_neon
#endif
#if defined(CUSTOM_MODES)
/* nfft lengths in NE10 that support scaled fft */
# define NE10_FFTSCALED_SUPPORT_MAX 4
static const int ne10_fft_scaled_support[NE10_FFTSCALED_SUPPORT_MAX] = {
480, 240, 120, 60
};
int opus_fft_alloc_arm_neon(kiss_fft_state *st)
{
int i;
size_t memneeded = sizeof(struct arch_fft_state);
st->arch_fft = (arch_fft_state *)opus_alloc(memneeded);
if (!st->arch_fft)
return -1;
for (i = 0; i < NE10_FFTSCALED_SUPPORT_MAX; i++) {
if(st->nfft == ne10_fft_scaled_support[i])
break;
}
if (i == NE10_FFTSCALED_SUPPORT_MAX) {
/* This nfft length (scaled fft) is not supported in NE10 */
st->arch_fft->is_supported = 0;
st->arch_fft->priv = NULL;
}
else {
st->arch_fft->is_supported = 1;
st->arch_fft->priv = (void *)NE10_FFT_ALLOC_C2C_TYPE_NEON(st->nfft);
if (st->arch_fft->priv == NULL) {
return -1;
}
}
return 0;
}
void opus_fft_free_arm_neon(kiss_fft_state *st)
{
NE10_FFT_CFG_TYPE_T cfg;
if (!st->arch_fft)
return;
cfg = (NE10_FFT_CFG_TYPE_T)st->arch_fft->priv;
if (cfg)
NE10_FFT_DESTROY_C2C_TYPE(cfg);
opus_free(st->arch_fft);
}
#endif
void opus_fft_neon(const kiss_fft_state *st,
const kiss_fft_cpx *fin,
kiss_fft_cpx *fout)
{
NE10_FFT_STATE_TYPE_T state;
NE10_FFT_CFG_TYPE_T cfg = &state;
VARDECL(NE10_FFT_CPX_TYPE_T, buffer);
SAVE_STACK;
ALLOC(buffer, st->nfft, NE10_FFT_CPX_TYPE_T);
if (!st->arch_fft->is_supported) {
/* This nfft length (scaled fft) not supported in NE10 */
opus_fft_c(st, fin, fout);
}
else {
memcpy((void *)cfg, st->arch_fft->priv, sizeof(NE10_FFT_STATE_TYPE_T));
state.buffer = (NE10_FFT_CPX_TYPE_T *)&buffer[0];
#if !defined(FIXED_POINT)
state.is_forward_scaled = 1;
NE10_FFT_C2C_1D_TYPE_NEON((NE10_FFT_CPX_TYPE_T *)fout,
(NE10_FFT_CPX_TYPE_T *)fin,
cfg, 0);
#else
NE10_FFT_C2C_1D_TYPE_NEON((NE10_FFT_CPX_TYPE_T *)fout,
(NE10_FFT_CPX_TYPE_T *)fin,
cfg, 0, 1);
#endif
}
RESTORE_STACK;
}
void opus_ifft_neon(const kiss_fft_state *st,
const kiss_fft_cpx *fin,
kiss_fft_cpx *fout)
{
NE10_FFT_STATE_TYPE_T state;
NE10_FFT_CFG_TYPE_T cfg = &state;
VARDECL(NE10_FFT_CPX_TYPE_T, buffer);
SAVE_STACK;
ALLOC(buffer, st->nfft, NE10_FFT_CPX_TYPE_T);
if (!st->arch_fft->is_supported) {
/* This nfft length (scaled fft) not supported in NE10 */
opus_ifft_c(st, fin, fout);
}
else {
memcpy((void *)cfg, st->arch_fft->priv, sizeof(NE10_FFT_STATE_TYPE_T));
state.buffer = (NE10_FFT_CPX_TYPE_T *)&buffer[0];
#if !defined(FIXED_POINT)
state.is_backward_scaled = 0;
NE10_FFT_C2C_1D_TYPE_NEON((NE10_FFT_CPX_TYPE_T *)fout,
(NE10_FFT_CPX_TYPE_T *)fin,
cfg, 1);
#else
NE10_FFT_C2C_1D_TYPE_NEON((NE10_FFT_CPX_TYPE_T *)fout,
(NE10_FFT_CPX_TYPE_T *)fin,
cfg, 1, 0);
#endif
}
RESTORE_STACK;
}

258
local_plugins/opus/android/libs/opus/celt/arm/celt_mdct_ne10.c

@ -0,0 +1,258 @@
/* Copyright (c) 2015 Xiph.Org Foundation
Written by Viswanath Puttagunta */
/**
@file celt_mdct_ne10.c
@brief ARM Neon optimizations for mdct using NE10 library
*/
/*
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef SKIP_CONFIG_H
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#endif
#include "kiss_fft.h"
#include "_kiss_fft_guts.h"
#include "mdct.h"
#include "stack_alloc.h"
void clt_mdct_forward_neon(const mdct_lookup *l,
kiss_fft_scalar *in,
kiss_fft_scalar * OPUS_RESTRICT out,
const opus_val16 *window,
int overlap, int shift, int stride, int arch)
{
int i;
int N, N2, N4;
VARDECL(kiss_fft_scalar, f);
VARDECL(kiss_fft_cpx, f2);
const kiss_fft_state *st = l->kfft[shift];
const kiss_twiddle_scalar *trig;
SAVE_STACK;
N = l->n;
trig = l->trig;
for (i=0;i<shift;i++)
{
N >>= 1;
trig += N;
}
N2 = N>>1;
N4 = N>>2;
ALLOC(f, N2, kiss_fft_scalar);
ALLOC(f2, N4, kiss_fft_cpx);
/* Consider the input to be composed of four blocks: [a, b, c, d] */
/* Window, shuffle, fold */
{
/* Temp pointers to make it really clear to the compiler what we're doing */
const kiss_fft_scalar * OPUS_RESTRICT xp1 = in+(overlap>>1);
const kiss_fft_scalar * OPUS_RESTRICT xp2 = in+N2-1+(overlap>>1);
kiss_fft_scalar * OPUS_RESTRICT yp = f;
const opus_val16 * OPUS_RESTRICT wp1 = window+(overlap>>1);
const opus_val16 * OPUS_RESTRICT wp2 = window+(overlap>>1)-1;
for(i=0;i<((overlap+3)>>2);i++)
{
/* Real part arranged as -d-cR, Imag part arranged as -b+aR*/
*yp++ = MULT16_32_Q15(*wp2, xp1[N2]) + MULT16_32_Q15(*wp1,*xp2);
*yp++ = MULT16_32_Q15(*wp1, *xp1) - MULT16_32_Q15(*wp2, xp2[-N2]);
xp1+=2;
xp2-=2;
wp1+=2;
wp2-=2;
}
wp1 = window;
wp2 = window+overlap-1;
for(;i<N4-((overlap+3)>>2);i++)
{
/* Real part arranged as a-bR, Imag part arranged as -c-dR */
*yp++ = *xp2;
*yp++ = *xp1;
xp1+=2;
xp2-=2;
}
for(;i<N4;i++)
{
/* Real part arranged as a-bR, Imag part arranged as -c-dR */
*yp++ = -MULT16_32_Q15(*wp1, xp1[-N2]) + MULT16_32_Q15(*wp2, *xp2);
*yp++ = MULT16_32_Q15(*wp2, *xp1) + MULT16_32_Q15(*wp1, xp2[N2]);
xp1+=2;
xp2-=2;
wp1+=2;
wp2-=2;
}
}
/* Pre-rotation */
{
kiss_fft_scalar * OPUS_RESTRICT yp = f;
const kiss_twiddle_scalar *t = &trig[0];
for(i=0;i<N4;i++)
{
kiss_fft_cpx yc;
kiss_twiddle_scalar t0, t1;
kiss_fft_scalar re, im, yr, yi;
t0 = t[i];
t1 = t[N4+i];
re = *yp++;
im = *yp++;
yr = S_MUL(re,t0) - S_MUL(im,t1);
yi = S_MUL(im,t0) + S_MUL(re,t1);
yc.r = yr;
yc.i = yi;
f2[i] = yc;
}
}
opus_fft(st, f2, (kiss_fft_cpx *)f, arch);
/* Post-rotate */
{
/* Temp pointers to make it really clear to the compiler what we're doing */
const kiss_fft_cpx * OPUS_RESTRICT fp = (kiss_fft_cpx *)f;
kiss_fft_scalar * OPUS_RESTRICT yp1 = out;
kiss_fft_scalar * OPUS_RESTRICT yp2 = out+stride*(N2-1);
const kiss_twiddle_scalar *t = &trig[0];
/* Temp pointers to make it really clear to the compiler what we're doing */
for(i=0;i<N4;i++)
{
kiss_fft_scalar yr, yi;
yr = S_MUL(fp->i,t[N4+i]) - S_MUL(fp->r,t[i]);
yi = S_MUL(fp->r,t[N4+i]) + S_MUL(fp->i,t[i]);
*yp1 = yr;
*yp2 = yi;
fp++;
yp1 += 2*stride;
yp2 -= 2*stride;
}
}
RESTORE_STACK;
}
void clt_mdct_backward_neon(const mdct_lookup *l,
kiss_fft_scalar *in,
kiss_fft_scalar * OPUS_RESTRICT out,
const opus_val16 * OPUS_RESTRICT window,
int overlap, int shift, int stride, int arch)
{
int i;
int N, N2, N4;
VARDECL(kiss_fft_scalar, f);
const kiss_twiddle_scalar *trig;
const kiss_fft_state *st = l->kfft[shift];
N = l->n;
trig = l->trig;
for (i=0;i<shift;i++)
{
N >>= 1;
trig += N;
}
N2 = N>>1;
N4 = N>>2;
ALLOC(f, N2, kiss_fft_scalar);
/* Pre-rotate */
{
/* Temp pointers to make it really clear to the compiler what we're doing */
const kiss_fft_scalar * OPUS_RESTRICT xp1 = in;
const kiss_fft_scalar * OPUS_RESTRICT xp2 = in+stride*(N2-1);
kiss_fft_scalar * OPUS_RESTRICT yp = f;
const kiss_twiddle_scalar * OPUS_RESTRICT t = &trig[0];
for(i=0;i<N4;i++)
{
kiss_fft_scalar yr, yi;
yr = S_MUL(*xp2, t[i]) + S_MUL(*xp1, t[N4+i]);
yi = S_MUL(*xp1, t[i]) - S_MUL(*xp2, t[N4+i]);
yp[2*i] = yr;
yp[2*i+1] = yi;
xp1+=2*stride;
xp2-=2*stride;
}
}
opus_ifft(st, (kiss_fft_cpx *)f, (kiss_fft_cpx*)(out+(overlap>>1)), arch);
/* Post-rotate and de-shuffle from both ends of the buffer at once to make
it in-place. */
{
kiss_fft_scalar * yp0 = out+(overlap>>1);
kiss_fft_scalar * yp1 = out+(overlap>>1)+N2-2;
const kiss_twiddle_scalar *t = &trig[0];
/* Loop to (N4+1)>>1 to handle odd N4. When N4 is odd, the
middle pair will be computed twice. */
for(i=0;i<(N4+1)>>1;i++)
{
kiss_fft_scalar re, im, yr, yi;
kiss_twiddle_scalar t0, t1;
re = yp0[0];
im = yp0[1];
t0 = t[i];
t1 = t[N4+i];
/* We'd scale up by 2 here, but instead it's done when mixing the windows */
yr = S_MUL(re,t0) + S_MUL(im,t1);
yi = S_MUL(re,t1) - S_MUL(im,t0);
re = yp1[0];
im = yp1[1];
yp0[0] = yr;
yp1[1] = yi;
t0 = t[(N4-i-1)];
t1 = t[(N2-i-1)];
/* We'd scale up by 2 here, but instead it's done when mixing the windows */
yr = S_MUL(re,t0) + S_MUL(im,t1);
yi = S_MUL(re,t1) - S_MUL(im,t0);
yp1[0] = yr;
yp0[1] = yi;
yp0 += 2;
yp1 -= 2;
}
}
/* Mirror on both sides for TDAC */
{
kiss_fft_scalar * OPUS_RESTRICT xp1 = out+overlap-1;
kiss_fft_scalar * OPUS_RESTRICT yp1 = out;
const opus_val16 * OPUS_RESTRICT wp1 = window;
const opus_val16 * OPUS_RESTRICT wp2 = window+overlap-1;
for(i = 0; i < overlap/2; i++)
{
kiss_fft_scalar x1, x2;
x1 = *xp1;
x2 = *yp1;
*yp1++ = MULT16_32_Q15(*wp2, x2) - MULT16_32_Q15(*wp1, x1);
*xp1-- = MULT16_32_Q15(*wp1, x2) + MULT16_32_Q15(*wp2, x1);
wp1++;
wp2--;
}
}
RESTORE_STACK;
}

272
local_plugins/opus/android/libs/opus/celt/arm/celt_neon_intr.c

@ -0,0 +1,272 @@
/* Copyright (c) 2014-2015 Xiph.Org Foundation
Written by Viswanath Puttagunta */
/**
@file celt_neon_intr.c
@brief ARM Neon Intrinsic optimizations for celt
*/
/*
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <arm_neon.h>
#include "../pitch.h"
#if defined(FIXED_POINT)
#include <string.h>
void xcorr_kernel_neon_fixed(const opus_val16 * x, const opus_val16 * y, opus_val32 sum[4], int len)
{
int j;
int32x4_t a = vld1q_s32(sum);
/* Load y[0...3] */
/* This requires len>0 to always be valid (which we assert in the C code). */
int16x4_t y0 = vld1_s16(y);
y += 4;
/* This loop loads one y value more than we actually need.
Therefore we have to stop as soon as there are 8 or fewer samples left
(instead of 7), to avoid reading past the end of the array. */
for (j = 0; j + 8 < len; j += 8)
{
/* Load x[0...7] */
int16x8_t xx = vld1q_s16(x);
int16x4_t x0 = vget_low_s16(xx);
int16x4_t x4 = vget_high_s16(xx);
/* Load y[4...11] */
int16x8_t yy = vld1q_s16(y);
int16x4_t y4 = vget_low_s16(yy);
int16x4_t y8 = vget_high_s16(yy);
int32x4_t a0 = vmlal_lane_s16(a, y0, x0, 0);
int32x4_t a1 = vmlal_lane_s16(a0, y4, x4, 0);
int16x4_t y1 = vext_s16(y0, y4, 1);
int16x4_t y5 = vext_s16(y4, y8, 1);
int32x4_t a2 = vmlal_lane_s16(a1, y1, x0, 1);
int32x4_t a3 = vmlal_lane_s16(a2, y5, x4, 1);
int16x4_t y2 = vext_s16(y0, y4, 2);
int16x4_t y6 = vext_s16(y4, y8, 2);
int32x4_t a4 = vmlal_lane_s16(a3, y2, x0, 2);
int32x4_t a5 = vmlal_lane_s16(a4, y6, x4, 2);
int16x4_t y3 = vext_s16(y0, y4, 3);
int16x4_t y7 = vext_s16(y4, y8, 3);
int32x4_t a6 = vmlal_lane_s16(a5, y3, x0, 3);
int32x4_t a7 = vmlal_lane_s16(a6, y7, x4, 3);
y0 = y8;
a = a7;
x += 8;
y += 8;
}
if (j + 4 < len) {
/* Load x[0...3] */
int16x4_t x0 = vld1_s16(x);
/* Load y[4...7] */
int16x4_t y4 = vld1_s16(y);
int32x4_t a0 = vmlal_lane_s16(a, y0, x0, 0);
int16x4_t y1 = vext_s16(y0, y4, 1);
int32x4_t a1 = vmlal_lane_s16(a0, y1, x0, 1);
int16x4_t y2 = vext_s16(y0, y4, 2);
int32x4_t a2 = vmlal_lane_s16(a1, y2, x0, 2);
int16x4_t y3 = vext_s16(y0, y4, 3);
int32x4_t a3 = vmlal_lane_s16(a2, y3, x0, 3);
y0 = y4;
a = a3;
x += 4;
y += 4;
j += 4;
}
if (j + 2 < len) {
/* Load x[0...1] */
int16x4x2_t xx = vld2_dup_s16(x);
int16x4_t x0 = xx.val[0];
int16x4_t x1 = xx.val[1];
/* Load y[4...5].
We would like to use vld1_dup_s32(), but casting the pointer would
break strict aliasing rules and potentially have alignment issues.
Fortunately the compiler seems capable of translating this memcpy()
and vdup_n_s32() into the equivalent vld1_dup_s32().*/
int32_t yy;
memcpy(&yy, y, sizeof(yy));
int16x4_t y4 = vreinterpret_s16_s32(vdup_n_s32(yy));
int32x4_t a0 = vmlal_s16(a, y0, x0);
int16x4_t y1 = vext_s16(y0, y4, 1);
/* Replace bottom copy of {y[5], y[4]} in y4 with {y[3], y[2]} from y0,
using VSRI instead of VEXT, since it's a data-processing
instruction. */
y0 = vreinterpret_s16_s64(vsri_n_s64(vreinterpret_s64_s16(y4),
vreinterpret_s64_s16(y0), 32));
int32x4_t a1 = vmlal_s16(a0, y1, x1);
a = a1;
x += 2;
y += 2;
j += 2;
}
if (j + 1 < len) {
/* Load next x. */
int16x4_t x0 = vld1_dup_s16(x);
int32x4_t a0 = vmlal_s16(a, y0, x0);
/* Load last y. */
int16x4_t y4 = vld1_dup_s16(y);
y0 = vreinterpret_s16_s64(vsri_n_s64(vreinterpret_s64_s16(y4),
vreinterpret_s64_s16(y0), 16));
a = a0;
x++;
}
/* Load last x. */
int16x4_t x0 = vld1_dup_s16(x);
int32x4_t a0 = vmlal_s16(a, y0, x0);
vst1q_s32(sum, a0);
}
#else
#if defined(__ARM_FEATURE_FMA) && defined(__ARM_ARCH_ISA_A64)
/* If we can, force the compiler to use an FMA instruction rather than break
* vmlaq_f32() into fmul/fadd. */
#ifdef vmlaq_lane_f32
#undef vmlaq_lane_f32
#endif
#define vmlaq_lane_f32(a,b,c,lane) vfmaq_lane_f32(a,b,c,lane)
#endif
/*
* Function: xcorr_kernel_neon_float
* ---------------------------------
* Computes 4 correlation values and stores them in sum[4]
*/
static void xcorr_kernel_neon_float(const float32_t *x, const float32_t *y,
float32_t sum[4], int len) {
float32x4_t YY[3];
float32x4_t YEXT[3];
float32x4_t XX[2];
float32x2_t XX_2;
float32x4_t SUMM;
const float32_t *xi = x;
const float32_t *yi = y;
celt_assert(len>0);
YY[0] = vld1q_f32(yi);
SUMM = vdupq_n_f32(0);
/* Consume 8 elements in x vector and 12 elements in y
* vector. However, the 12'th element never really gets
* touched in this loop. So, if len == 8, then we only
* must access y[0] to y[10]. y[11] must not be accessed
* hence make sure len > 8 and not len >= 8
*/
while (len > 8) {
yi += 4;
YY[1] = vld1q_f32(yi);
yi += 4;
YY[2] = vld1q_f32(yi);
XX[0] = vld1q_f32(xi);
xi += 4;
XX[1] = vld1q_f32(xi);
xi += 4;
SUMM = vmlaq_lane_f32(SUMM, YY[0], vget_low_f32(XX[0]), 0);
YEXT[0] = vextq_f32(YY[0], YY[1], 1);
SUMM = vmlaq_lane_f32(SUMM, YEXT[0], vget_low_f32(XX[0]), 1);
YEXT[1] = vextq_f32(YY[0], YY[1], 2);
SUMM = vmlaq_lane_f32(SUMM, YEXT[1], vget_high_f32(XX[0]), 0);
YEXT[2] = vextq_f32(YY[0], YY[1], 3);
SUMM = vmlaq_lane_f32(SUMM, YEXT[2], vget_high_f32(XX[0]), 1);
SUMM = vmlaq_lane_f32(SUMM, YY[1], vget_low_f32(XX[1]), 0);
YEXT[0] = vextq_f32(YY[1], YY[2], 1);
SUMM = vmlaq_lane_f32(SUMM, YEXT[0], vget_low_f32(XX[1]), 1);
YEXT[1] = vextq_f32(YY[1], YY[2], 2);
SUMM = vmlaq_lane_f32(SUMM, YEXT[1], vget_high_f32(XX[1]), 0);
YEXT[2] = vextq_f32(YY[1], YY[2], 3);
SUMM = vmlaq_lane_f32(SUMM, YEXT[2], vget_high_f32(XX[1]), 1);
YY[0] = YY[2];
len -= 8;
}
/* Consume 4 elements in x vector and 8 elements in y
* vector. However, the 8'th element in y never really gets
* touched in this loop. So, if len == 4, then we only
* must access y[0] to y[6]. y[7] must not be accessed
* hence make sure len>4 and not len>=4
*/
if (len > 4) {
yi += 4;
YY[1] = vld1q_f32(yi);
XX[0] = vld1q_f32(xi);
xi += 4;
SUMM = vmlaq_lane_f32(SUMM, YY[0], vget_low_f32(XX[0]), 0);
YEXT[0] = vextq_f32(YY[0], YY[1], 1);
SUMM = vmlaq_lane_f32(SUMM, YEXT[0], vget_low_f32(XX[0]), 1);
YEXT[1] = vextq_f32(YY[0], YY[1], 2);
SUMM = vmlaq_lane_f32(SUMM, YEXT[1], vget_high_f32(XX[0]), 0);
YEXT[2] = vextq_f32(YY[0], YY[1], 3);
SUMM = vmlaq_lane_f32(SUMM, YEXT[2], vget_high_f32(XX[0]), 1);
YY[0] = YY[1];
len -= 4;
}
while (--len > 0) {
XX_2 = vld1_dup_f32(xi++);
SUMM = vmlaq_lane_f32(SUMM, YY[0], XX_2, 0);
YY[0]= vld1q_f32(++yi);
}
XX_2 = vld1_dup_f32(xi);
SUMM = vmlaq_lane_f32(SUMM, YY[0], XX_2, 0);
vst1q_f32(sum, SUMM);
}
void celt_pitch_xcorr_float_neon(const opus_val16 *_x, const opus_val16 *_y,
opus_val32 *xcorr, int len, int max_pitch, int arch) {
int i;
(void)arch;
celt_assert(max_pitch > 0);
celt_sig_assert((((unsigned char *)_x-(unsigned char *)NULL)&3)==0);
for (i = 0; i < (max_pitch-3); i += 4) {
xcorr_kernel_neon_float((const float32_t *)_x, (const float32_t *)_y+i,
(float32_t *)xcorr+i, len);
}
/* In case max_pitch isn't a multiple of 4, do non-unrolled version. */
for (; i < max_pitch; i++) {
xcorr[i] = celt_inner_prod_neon(_x, _y+i, len);
}
}
#endif

555
local_plugins/opus/android/libs/opus/celt/arm/celt_pitch_xcorr_arm-gnu.S

@ -0,0 +1,555 @@
.syntax unified
@ Copyright (c) 2007-2008 CSIRO
@ Copyright (c) 2007-2009 Xiph.Org Foundation
@ Copyright (c) 2013 Parrot
@ Written by Aurélien Zanelli
@
@ Redistribution and use in source and binary forms, with or without
@ modification, are permitted provided that the following conditions
@ are met:
@
@ - Redistributions of source code must retain the above copyright
@ notice, this list of conditions and the following disclaimer.
@
@ - Redistributions in binary form must reproduce the above copyright
@ notice, this list of conditions and the following disclaimer in the
@ documentation and/or other materials provided with the distribution.
@
@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
@ ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
@ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
@ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
@ OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
@ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
@ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
@ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
@ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
@ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
.text; .p2align 2; .arch armv7-a
.fpu neon
.object_arch armv4t
.include "celt/arm/armopts-gnu.S"
.if OPUS_ARM_MAY_HAVE_EDSP
.global celt_pitch_xcorr_edsp
.endif
.if OPUS_ARM_MAY_HAVE_NEON
.global celt_pitch_xcorr_neon
.endif
.if OPUS_ARM_MAY_HAVE_NEON
@ Compute sum[k]=sum(x[j]*y[j+k],j=0...len-1), k=0...3
.type xcorr_kernel_neon, %function; xcorr_kernel_neon: @ PROC
xcorr_kernel_neon_start:
@ input:
@ r3 = int len
@ r4 = opus_val16 *x
@ r5 = opus_val16 *y
@ q0 = opus_val32 sum[4]
@ output:
@ q0 = opus_val32 sum[4]
@ preserved: r0-r3, r6-r11, d2, q4-q7, q9-q15
@ internal usage:
@ r12 = int j
@ d3 = y_3|y_2|y_1|y_0
@ q2 = y_B|y_A|y_9|y_8|y_7|y_6|y_5|y_4
@ q3 = x_7|x_6|x_5|x_4|x_3|x_2|x_1|x_0
@ q8 = scratch
@
@ Load y[0...3]
@ This requires len>0 to always be valid (which we assert in the C code).
VLD1.16 {d5}, [r5]!
SUBS r12, r3, #8
BLE xcorr_kernel_neon_process4
@ Process 8 samples at a time.
@ This loop loads one y value more than we actually need. Therefore we have to
@ stop as soon as there are 8 or fewer samples left (instead of 7), to avoid
@ reading past the end of the array.
xcorr_kernel_neon_process8:
@ This loop has 19 total instructions (10 cycles to issue, minimum), with
@ - 2 cycles of ARM insrtuctions,
@ - 10 cycles of load/store/byte permute instructions, and
@ - 9 cycles of data processing instructions.
@ On a Cortex A8, we dual-issue the maximum amount (9 cycles) between the
@ latter two categories, meaning the whole loop should run in 10 cycles per
@ iteration, barring cache misses.
@
@ Load x[0...7]
VLD1.16 {d6, d7}, [r4]!
@ Unlike VMOV, VAND is a data processsing instruction (and doesn't get
@ assembled to VMOV, like VORR would), so it dual-issues with the prior VLD1.
VAND d3, d5, d5
SUBS r12, r12, #8
@ Load y[4...11]
VLD1.16 {d4, d5}, [r5]!
VMLAL.S16 q0, d3, d6[0]
VEXT.16 d16, d3, d4, #1
VMLAL.S16 q0, d4, d7[0]
VEXT.16 d17, d4, d5, #1
VMLAL.S16 q0, d16, d6[1]
VEXT.16 d16, d3, d4, #2
VMLAL.S16 q0, d17, d7[1]
VEXT.16 d17, d4, d5, #2
VMLAL.S16 q0, d16, d6[2]
VEXT.16 d16, d3, d4, #3
VMLAL.S16 q0, d17, d7[2]
VEXT.16 d17, d4, d5, #3
VMLAL.S16 q0, d16, d6[3]
VMLAL.S16 q0, d17, d7[3]
BGT xcorr_kernel_neon_process8
@ Process 4 samples here if we have > 4 left (still reading one extra y value).
xcorr_kernel_neon_process4:
ADDS r12, r12, #4
BLE xcorr_kernel_neon_process2
@ Load x[0...3]
VLD1.16 d6, [r4]!
@ Use VAND since it's a data processing instruction again.
VAND d4, d5, d5
SUB r12, r12, #4
@ Load y[4...7]
VLD1.16 d5, [r5]!
VMLAL.S16 q0, d4, d6[0]
VEXT.16 d16, d4, d5, #1
VMLAL.S16 q0, d16, d6[1]
VEXT.16 d16, d4, d5, #2
VMLAL.S16 q0, d16, d6[2]
VEXT.16 d16, d4, d5, #3
VMLAL.S16 q0, d16, d6[3]
@ Process 2 samples here if we have > 2 left (still reading one extra y value).
xcorr_kernel_neon_process2:
ADDS r12, r12, #2
BLE xcorr_kernel_neon_process1
@ Load x[0...1]
VLD2.16 {d6[],d7[]}, [r4]!
@ Use VAND since it's a data processing instruction again.
VAND d4, d5, d5
SUB r12, r12, #2
@ Load y[4...5]
VLD1.32 {d5[]}, [r5]!
VMLAL.S16 q0, d4, d6
VEXT.16 d16, d4, d5, #1
@ Replace bottom copy of {y5,y4} in d5 with {y3,y2} from d4, using VSRI
@ instead of VEXT, since it's a data-processing instruction.
VSRI.64 d5, d4, #32
VMLAL.S16 q0, d16, d7
@ Process 1 sample using the extra y value we loaded above.
xcorr_kernel_neon_process1:
@ Load next *x
VLD1.16 {d6[]}, [r4]!
ADDS r12, r12, #1
@ y[0...3] are left in d5 from prior iteration(s) (if any)
VMLAL.S16 q0, d5, d6
MOVLE pc, lr
@ Now process 1 last sample, not reading ahead.
@ Load last *y
VLD1.16 {d4[]}, [r5]!
VSRI.64 d4, d5, #16
@ Load last *x
VLD1.16 {d6[]}, [r4]!
VMLAL.S16 q0, d4, d6
MOV pc, lr
.size xcorr_kernel_neon, .-xcorr_kernel_neon @ ENDP
@ opus_val32 celt_pitch_xcorr_neon(opus_val16 *_x, opus_val16 *_y,
@ opus_val32 *xcorr, int len, int max_pitch, int arch)
.type celt_pitch_xcorr_neon, %function; celt_pitch_xcorr_neon: @ PROC
@ input:
@ r0 = opus_val16 *_x
@ r1 = opus_val16 *_y
@ r2 = opus_val32 *xcorr
@ r3 = int len
@ output:
@ r0 = int maxcorr
@ internal usage:
@ r4 = opus_val16 *x (for xcorr_kernel_neon())
@ r5 = opus_val16 *y (for xcorr_kernel_neon())
@ r6 = int max_pitch
@ r12 = int j
@ q15 = int maxcorr[4] (q15 is not used by xcorr_kernel_neon())
@ ignored:
@ int arch
STMFD sp!, {r4-r6, lr}
LDR r6, [sp, #16]
VMOV.S32 q15, #1
@ if (max_pitch < 4) goto celt_pitch_xcorr_neon_process4_done
SUBS r6, r6, #4
BLT celt_pitch_xcorr_neon_process4_done
celt_pitch_xcorr_neon_process4:
@ xcorr_kernel_neon parameters:
@ r3 = len, r4 = _x, r5 = _y, q0 = {0, 0, 0, 0}
MOV r4, r0
MOV r5, r1
VEOR q0, q0, q0
@ xcorr_kernel_neon only modifies r4, r5, r12, and q0...q3.
@ So we don't save/restore any other registers.
BL xcorr_kernel_neon_start
SUBS r6, r6, #4
VST1.32 {q0}, [r2]!
@ _y += 4
ADD r1, r1, #8
VMAX.S32 q15, q15, q0
@ if (max_pitch < 4) goto celt_pitch_xcorr_neon_process4_done
BGE celt_pitch_xcorr_neon_process4
@ We have less than 4 sums left to compute.
celt_pitch_xcorr_neon_process4_done:
ADDS r6, r6, #4
@ Reduce maxcorr to a single value
VMAX.S32 d30, d30, d31
VPMAX.S32 d30, d30, d30
@ if (max_pitch <= 0) goto celt_pitch_xcorr_neon_done
BLE celt_pitch_xcorr_neon_done
@ Now compute each remaining sum one at a time.
celt_pitch_xcorr_neon_process_remaining:
MOV r4, r0
MOV r5, r1
VMOV.I32 q0, #0
SUBS r12, r3, #8
BLT celt_pitch_xcorr_neon_process_remaining4
@ Sum terms 8 at a time.
celt_pitch_xcorr_neon_process_remaining_loop8:
@ Load x[0...7]
VLD1.16 {q1}, [r4]!
@ Load y[0...7]
VLD1.16 {q2}, [r5]!
SUBS r12, r12, #8
VMLAL.S16 q0, d4, d2
VMLAL.S16 q0, d5, d3
BGE celt_pitch_xcorr_neon_process_remaining_loop8
@ Sum terms 4 at a time.
celt_pitch_xcorr_neon_process_remaining4:
ADDS r12, r12, #4
BLT celt_pitch_xcorr_neon_process_remaining4_done
@ Load x[0...3]
VLD1.16 {d2}, [r4]!
@ Load y[0...3]
VLD1.16 {d3}, [r5]!
SUB r12, r12, #4
VMLAL.S16 q0, d3, d2
celt_pitch_xcorr_neon_process_remaining4_done:
@ Reduce the sum to a single value.
VADD.S32 d0, d0, d1
VPADDL.S32 d0, d0
ADDS r12, r12, #4
BLE celt_pitch_xcorr_neon_process_remaining_loop_done
@ Sum terms 1 at a time.
celt_pitch_xcorr_neon_process_remaining_loop1:
VLD1.16 {d2[]}, [r4]!
VLD1.16 {d3[]}, [r5]!
SUBS r12, r12, #1
VMLAL.S16 q0, d2, d3
BGT celt_pitch_xcorr_neon_process_remaining_loop1
celt_pitch_xcorr_neon_process_remaining_loop_done:
VST1.32 {d0[0]}, [r2]!
VMAX.S32 d30, d30, d0
SUBS r6, r6, #1
@ _y++
ADD r1, r1, #2
@ if (--max_pitch > 0) goto celt_pitch_xcorr_neon_process_remaining
BGT celt_pitch_xcorr_neon_process_remaining
celt_pitch_xcorr_neon_done:
VMOV.32 r0, d30[0]
LDMFD sp!, {r4-r6, pc}
.size celt_pitch_xcorr_neon, .-celt_pitch_xcorr_neon @ ENDP
.endif
.if OPUS_ARM_MAY_HAVE_EDSP
@ This will get used on ARMv7 devices without NEON, so it has been optimized
@ to take advantage of dual-issuing where possible.
.type xcorr_kernel_edsp, %function; xcorr_kernel_edsp: @ PROC
xcorr_kernel_edsp_start:
@ input:
@ r3 = int len
@ r4 = opus_val16 *_x (must be 32-bit aligned)
@ r5 = opus_val16 *_y (must be 32-bit aligned)
@ r6...r9 = opus_val32 sum[4]
@ output:
@ r6...r9 = opus_val32 sum[4]
@ preserved: r0-r5
@ internal usage
@ r2 = int j
@ r12,r14 = opus_val16 x[4]
@ r10,r11 = opus_val16 y[4]
STMFD sp!, {r2,r4,r5,lr}
LDR r10, [r5], #4 @ Load y[0...1]
SUBS r2, r3, #4 @ j = len-4
LDR r11, [r5], #4 @ Load y[2...3]
BLE xcorr_kernel_edsp_process4_done
LDR r12, [r4], #4 @ Load x[0...1]
@ Stall
xcorr_kernel_edsp_process4:
@ The multiplies must issue from pipeline 0, and can't dual-issue with each
@ other. Every other instruction here dual-issues with a multiply, and is
@ thus "free". There should be no stalls in the body of the loop.
SMLABB r6, r12, r10, r6 @ sum[0] = MAC16_16(sum[0],x_0,y_0)
LDR r14, [r4], #4 @ Load x[2...3]
SMLABT r7, r12, r10, r7 @ sum[1] = MAC16_16(sum[1],x_0,y_1)
SUBS r2, r2, #4 @ j-=4
SMLABB r8, r12, r11, r8 @ sum[2] = MAC16_16(sum[2],x_0,y_2)
SMLABT r9, r12, r11, r9 @ sum[3] = MAC16_16(sum[3],x_0,y_3)
SMLATT r6, r12, r10, r6 @ sum[0] = MAC16_16(sum[0],x_1,y_1)
LDR r10, [r5], #4 @ Load y[4...5]
SMLATB r7, r12, r11, r7 @ sum[1] = MAC16_16(sum[1],x_1,y_2)
SMLATT r8, r12, r11, r8 @ sum[2] = MAC16_16(sum[2],x_1,y_3)
SMLATB r9, r12, r10, r9 @ sum[3] = MAC16_16(sum[3],x_1,y_4)
LDRGT r12, [r4], #4 @ Load x[0...1]
SMLABB r6, r14, r11, r6 @ sum[0] = MAC16_16(sum[0],x_2,y_2)
SMLABT r7, r14, r11, r7 @ sum[1] = MAC16_16(sum[1],x_2,y_3)
SMLABB r8, r14, r10, r8 @ sum[2] = MAC16_16(sum[2],x_2,y_4)
SMLABT r9, r14, r10, r9 @ sum[3] = MAC16_16(sum[3],x_2,y_5)
SMLATT r6, r14, r11, r6 @ sum[0] = MAC16_16(sum[0],x_3,y_3)
LDR r11, [r5], #4 @ Load y[6...7]
SMLATB r7, r14, r10, r7 @ sum[1] = MAC16_16(sum[1],x_3,y_4)
SMLATT r8, r14, r10, r8 @ sum[2] = MAC16_16(sum[2],x_3,y_5)
SMLATB r9, r14, r11, r9 @ sum[3] = MAC16_16(sum[3],x_3,y_6)
BGT xcorr_kernel_edsp_process4
xcorr_kernel_edsp_process4_done:
ADDS r2, r2, #4
BLE xcorr_kernel_edsp_done
LDRH r12, [r4], #2 @ r12 = *x++
SUBS r2, r2, #1 @ j--
@ Stall
SMLABB r6, r12, r10, r6 @ sum[0] = MAC16_16(sum[0],x,y_0)
LDRHGT r14, [r4], #2 @ r14 = *x++
SMLABT r7, r12, r10, r7 @ sum[1] = MAC16_16(sum[1],x,y_1)
SMLABB r8, r12, r11, r8 @ sum[2] = MAC16_16(sum[2],x,y_2)
SMLABT r9, r12, r11, r9 @ sum[3] = MAC16_16(sum[3],x,y_3)
BLE xcorr_kernel_edsp_done
SMLABT r6, r14, r10, r6 @ sum[0] = MAC16_16(sum[0],x,y_1)
SUBS r2, r2, #1 @ j--
SMLABB r7, r14, r11, r7 @ sum[1] = MAC16_16(sum[1],x,y_2)
LDRH r10, [r5], #2 @ r10 = y_4 = *y++
SMLABT r8, r14, r11, r8 @ sum[2] = MAC16_16(sum[2],x,y_3)
LDRHGT r12, [r4], #2 @ r12 = *x++
SMLABB r9, r14, r10, r9 @ sum[3] = MAC16_16(sum[3],x,y_4)
BLE xcorr_kernel_edsp_done
SMLABB r6, r12, r11, r6 @ sum[0] = MAC16_16(sum[0],tmp,y_2)
CMP r2, #1 @ j--
SMLABT r7, r12, r11, r7 @ sum[1] = MAC16_16(sum[1],tmp,y_3)
LDRH r2, [r5], #2 @ r2 = y_5 = *y++
SMLABB r8, r12, r10, r8 @ sum[2] = MAC16_16(sum[2],tmp,y_4)
LDRHGT r14, [r4] @ r14 = *x
SMLABB r9, r12, r2, r9 @ sum[3] = MAC16_16(sum[3],tmp,y_5)
BLE xcorr_kernel_edsp_done
SMLABT r6, r14, r11, r6 @ sum[0] = MAC16_16(sum[0],tmp,y_3)
LDRH r11, [r5] @ r11 = y_6 = *y
SMLABB r7, r14, r10, r7 @ sum[1] = MAC16_16(sum[1],tmp,y_4)
SMLABB r8, r14, r2, r8 @ sum[2] = MAC16_16(sum[2],tmp,y_5)
SMLABB r9, r14, r11, r9 @ sum[3] = MAC16_16(sum[3],tmp,y_6)
xcorr_kernel_edsp_done:
LDMFD sp!, {r2,r4,r5,pc}
.size xcorr_kernel_edsp, .-xcorr_kernel_edsp @ ENDP
.type celt_pitch_xcorr_edsp, %function; celt_pitch_xcorr_edsp: @ PROC
@ input:
@ r0 = opus_val16 *_x (must be 32-bit aligned)
@ r1 = opus_val16 *_y (only needs to be 16-bit aligned)
@ r2 = opus_val32 *xcorr
@ r3 = int len
@ output:
@ r0 = maxcorr
@ internal usage
@ r4 = opus_val16 *x
@ r5 = opus_val16 *y
@ r6 = opus_val32 sum0
@ r7 = opus_val32 sum1
@ r8 = opus_val32 sum2
@ r9 = opus_val32 sum3
@ r1 = int max_pitch
@ r12 = int j
@ ignored:
@ int arch
STMFD sp!, {r4-r11, lr}
MOV r5, r1
LDR r1, [sp, #36]
MOV r4, r0
TST r5, #3
@ maxcorr = 1
MOV r0, #1
BEQ celt_pitch_xcorr_edsp_process1u_done
@ Compute one sum at the start to make y 32-bit aligned.
SUBS r12, r3, #4
@ r14 = sum = 0
MOV r14, #0
LDRH r8, [r5], #2
BLE celt_pitch_xcorr_edsp_process1u_loop4_done
LDR r6, [r4], #4
MOV r8, r8, LSL #16
celt_pitch_xcorr_edsp_process1u_loop4:
LDR r9, [r5], #4
SMLABT r14, r6, r8, r14 @ sum = MAC16_16(sum, x_0, y_0)
LDR r7, [r4], #4
SMLATB r14, r6, r9, r14 @ sum = MAC16_16(sum, x_1, y_1)
LDR r8, [r5], #4
SMLABT r14, r7, r9, r14 @ sum = MAC16_16(sum, x_2, y_2)
SUBS r12, r12, #4 @ j-=4
SMLATB r14, r7, r8, r14 @ sum = MAC16_16(sum, x_3, y_3)
LDRGT r6, [r4], #4
BGT celt_pitch_xcorr_edsp_process1u_loop4
MOV r8, r8, LSR #16
celt_pitch_xcorr_edsp_process1u_loop4_done:
ADDS r12, r12, #4
celt_pitch_xcorr_edsp_process1u_loop1:
LDRHGE r6, [r4], #2
@ Stall
SMLABBGE r14, r6, r8, r14 @ sum = MAC16_16(sum, *x, *y)
SUBSGE r12, r12, #1
LDRHGT r8, [r5], #2
BGT celt_pitch_xcorr_edsp_process1u_loop1
@ Restore _x
SUB r4, r4, r3, LSL #1
@ Restore and advance _y
SUB r5, r5, r3, LSL #1
@ maxcorr = max(maxcorr, sum)
CMP r0, r14
ADD r5, r5, #2
MOVLT r0, r14
SUBS r1, r1, #1
@ xcorr[i] = sum
STR r14, [r2], #4
BLE celt_pitch_xcorr_edsp_done
celt_pitch_xcorr_edsp_process1u_done:
@ if (max_pitch < 4) goto celt_pitch_xcorr_edsp_process2
SUBS r1, r1, #4
BLT celt_pitch_xcorr_edsp_process2
celt_pitch_xcorr_edsp_process4:
@ xcorr_kernel_edsp parameters:
@ r3 = len, r4 = _x, r5 = _y, r6...r9 = sum[4] = {0, 0, 0, 0}
MOV r6, #0
MOV r7, #0
MOV r8, #0
MOV r9, #0
BL xcorr_kernel_edsp_start @ xcorr_kernel_edsp(_x, _y+i, xcorr+i, len)
@ maxcorr = max(maxcorr, sum0, sum1, sum2, sum3)
CMP r0, r6
@ _y+=4
ADD r5, r5, #8
MOVLT r0, r6
CMP r0, r7
MOVLT r0, r7
CMP r0, r8
MOVLT r0, r8
CMP r0, r9
MOVLT r0, r9
STMIA r2!, {r6-r9}
SUBS r1, r1, #4
BGE celt_pitch_xcorr_edsp_process4
celt_pitch_xcorr_edsp_process2:
ADDS r1, r1, #2
BLT celt_pitch_xcorr_edsp_process1a
SUBS r12, r3, #4
@ {r10, r11} = {sum0, sum1} = {0, 0}
MOV r10, #0
MOV r11, #0
LDR r8, [r5], #4
BLE celt_pitch_xcorr_edsp_process2_loop_done
LDR r6, [r4], #4
LDR r9, [r5], #4
celt_pitch_xcorr_edsp_process2_loop4:
SMLABB r10, r6, r8, r10 @ sum0 = MAC16_16(sum0, x_0, y_0)
LDR r7, [r4], #4
SMLABT r11, r6, r8, r11 @ sum1 = MAC16_16(sum1, x_0, y_1)
SUBS r12, r12, #4 @ j-=4
SMLATT r10, r6, r8, r10 @ sum0 = MAC16_16(sum0, x_1, y_1)
LDR r8, [r5], #4
SMLATB r11, r6, r9, r11 @ sum1 = MAC16_16(sum1, x_1, y_2)
LDRGT r6, [r4], #4
SMLABB r10, r7, r9, r10 @ sum0 = MAC16_16(sum0, x_2, y_2)
SMLABT r11, r7, r9, r11 @ sum1 = MAC16_16(sum1, x_2, y_3)
SMLATT r10, r7, r9, r10 @ sum0 = MAC16_16(sum0, x_3, y_3)
LDRGT r9, [r5], #4
SMLATB r11, r7, r8, r11 @ sum1 = MAC16_16(sum1, x_3, y_4)
BGT celt_pitch_xcorr_edsp_process2_loop4
celt_pitch_xcorr_edsp_process2_loop_done:
ADDS r12, r12, #2
BLE celt_pitch_xcorr_edsp_process2_1
LDR r6, [r4], #4
@ Stall
SMLABB r10, r6, r8, r10 @ sum0 = MAC16_16(sum0, x_0, y_0)
LDR r9, [r5], #4
SMLABT r11, r6, r8, r11 @ sum1 = MAC16_16(sum1, x_0, y_1)
SUB r12, r12, #2
SMLATT r10, r6, r8, r10 @ sum0 = MAC16_16(sum0, x_1, y_1)
MOV r8, r9
SMLATB r11, r6, r9, r11 @ sum1 = MAC16_16(sum1, x_1, y_2)
celt_pitch_xcorr_edsp_process2_1:
LDRH r6, [r4], #2
ADDS r12, r12, #1
@ Stall
SMLABB r10, r6, r8, r10 @ sum0 = MAC16_16(sum0, x_0, y_0)
LDRHGT r7, [r4], #2
SMLABT r11, r6, r8, r11 @ sum1 = MAC16_16(sum1, x_0, y_1)
BLE celt_pitch_xcorr_edsp_process2_done
LDRH r9, [r5], #2
SMLABT r10, r7, r8, r10 @ sum0 = MAC16_16(sum0, x_0, y_1)
SMLABB r11, r7, r9, r11 @ sum1 = MAC16_16(sum1, x_0, y_2)
celt_pitch_xcorr_edsp_process2_done:
@ Restore _x
SUB r4, r4, r3, LSL #1
@ Restore and advance _y
SUB r5, r5, r3, LSL #1
@ maxcorr = max(maxcorr, sum0)
CMP r0, r10
ADD r5, r5, #2
MOVLT r0, r10
SUB r1, r1, #2
@ maxcorr = max(maxcorr, sum1)
CMP r0, r11
@ xcorr[i] = sum
STR r10, [r2], #4
MOVLT r0, r11
STR r11, [r2], #4
celt_pitch_xcorr_edsp_process1a:
ADDS r1, r1, #1
BLT celt_pitch_xcorr_edsp_done
SUBS r12, r3, #4
@ r14 = sum = 0
MOV r14, #0
BLT celt_pitch_xcorr_edsp_process1a_loop_done
LDR r6, [r4], #4
LDR r8, [r5], #4
LDR r7, [r4], #4
LDR r9, [r5], #4
celt_pitch_xcorr_edsp_process1a_loop4:
SMLABB r14, r6, r8, r14 @ sum = MAC16_16(sum, x_0, y_0)
SUBS r12, r12, #4 @ j-=4
SMLATT r14, r6, r8, r14 @ sum = MAC16_16(sum, x_1, y_1)
LDRGE r6, [r4], #4
SMLABB r14, r7, r9, r14 @ sum = MAC16_16(sum, x_2, y_2)
LDRGE r8, [r5], #4
SMLATT r14, r7, r9, r14 @ sum = MAC16_16(sum, x_3, y_3)
LDRGE r7, [r4], #4
LDRGE r9, [r5], #4
BGE celt_pitch_xcorr_edsp_process1a_loop4
celt_pitch_xcorr_edsp_process1a_loop_done:
ADDS r12, r12, #2
LDRGE r6, [r4], #4
LDRGE r8, [r5], #4
@ Stall
SMLABBGE r14, r6, r8, r14 @ sum = MAC16_16(sum, x_0, y_0)
SUBGE r12, r12, #2
SMLATTGE r14, r6, r8, r14 @ sum = MAC16_16(sum, x_1, y_1)
ADDS r12, r12, #1
LDRHGE r6, [r4], #2
LDRHGE r8, [r5], #2
@ Stall
SMLABBGE r14, r6, r8, r14 @ sum = MAC16_16(sum, *x, *y)
@ maxcorr = max(maxcorr, sum)
CMP r0, r14
@ xcorr[i] = sum
STR r14, [r2], #4
MOVLT r0, r14
celt_pitch_xcorr_edsp_done:
LDMFD sp!, {r4-r11, pc}
.size celt_pitch_xcorr_edsp, .-celt_pitch_xcorr_edsp @ ENDP
.endif
@ END:
.section .note.GNU-stack,"",%progbits

551
local_plugins/opus/android/libs/opus/celt/arm/celt_pitch_xcorr_arm.s

@ -0,0 +1,551 @@
; Copyright (c) 2007-2008 CSIRO
; Copyright (c) 2007-2009 Xiph.Org Foundation
; Copyright (c) 2013 Parrot
; Written by Aurélien Zanelli
;
; Redistribution and use in source and binary forms, with or without
; modification, are permitted provided that the following conditions
; are met:
;
; - Redistributions of source code must retain the above copyright
; notice, this list of conditions and the following disclaimer.
;
; - Redistributions in binary form must reproduce the above copyright
; notice, this list of conditions and the following disclaimer in the
; documentation and/or other materials provided with the distribution.
;
; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
; ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
; OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
; EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
; PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
; PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
; LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
AREA |.text|, CODE, READONLY
GET celt/arm/armopts.s
IF OPUS_ARM_MAY_HAVE_EDSP
EXPORT celt_pitch_xcorr_edsp
ENDIF
IF OPUS_ARM_MAY_HAVE_NEON
EXPORT celt_pitch_xcorr_neon
ENDIF
IF OPUS_ARM_MAY_HAVE_NEON
; Compute sum[k]=sum(x[j]*y[j+k],j=0...len-1), k=0...3
xcorr_kernel_neon PROC
xcorr_kernel_neon_start
; input:
; r3 = int len
; r4 = opus_val16 *x
; r5 = opus_val16 *y
; q0 = opus_val32 sum[4]
; output:
; q0 = opus_val32 sum[4]
; preserved: r0-r3, r6-r11, d2, q4-q7, q9-q15
; internal usage:
; r12 = int j
; d3 = y_3|y_2|y_1|y_0
; q2 = y_B|y_A|y_9|y_8|y_7|y_6|y_5|y_4
; q3 = x_7|x_6|x_5|x_4|x_3|x_2|x_1|x_0
; q8 = scratch
;
; Load y[0...3]
; This requires len>0 to always be valid (which we assert in the C code).
VLD1.16 {d5}, [r5]!
SUBS r12, r3, #8
BLE xcorr_kernel_neon_process4
; Process 8 samples at a time.
; This loop loads one y value more than we actually need. Therefore we have to
; stop as soon as there are 8 or fewer samples left (instead of 7), to avoid
; reading past the end of the array.
xcorr_kernel_neon_process8
; This loop has 19 total instructions (10 cycles to issue, minimum), with
; - 2 cycles of ARM insrtuctions,
; - 10 cycles of load/store/byte permute instructions, and
; - 9 cycles of data processing instructions.
; On a Cortex A8, we dual-issue the maximum amount (9 cycles) between the
; latter two categories, meaning the whole loop should run in 10 cycles per
; iteration, barring cache misses.
;
; Load x[0...7]
VLD1.16 {d6, d7}, [r4]!
; Unlike VMOV, VAND is a data processsing instruction (and doesn't get
; assembled to VMOV, like VORR would), so it dual-issues with the prior VLD1.
VAND d3, d5, d5
SUBS r12, r12, #8
; Load y[4...11]
VLD1.16 {d4, d5}, [r5]!
VMLAL.S16 q0, d3, d6[0]
VEXT.16 d16, d3, d4, #1
VMLAL.S16 q0, d4, d7[0]
VEXT.16 d17, d4, d5, #1
VMLAL.S16 q0, d16, d6[1]
VEXT.16 d16, d3, d4, #2
VMLAL.S16 q0, d17, d7[1]
VEXT.16 d17, d4, d5, #2
VMLAL.S16 q0, d16, d6[2]
VEXT.16 d16, d3, d4, #3
VMLAL.S16 q0, d17, d7[2]
VEXT.16 d17, d4, d5, #3
VMLAL.S16 q0, d16, d6[3]
VMLAL.S16 q0, d17, d7[3]
BGT xcorr_kernel_neon_process8
; Process 4 samples here if we have > 4 left (still reading one extra y value).
xcorr_kernel_neon_process4
ADDS r12, r12, #4
BLE xcorr_kernel_neon_process2
; Load x[0...3]
VLD1.16 d6, [r4]!
; Use VAND since it's a data processing instruction again.
VAND d4, d5, d5
SUB r12, r12, #4
; Load y[4...7]
VLD1.16 d5, [r5]!
VMLAL.S16 q0, d4, d6[0]
VEXT.16 d16, d4, d5, #1
VMLAL.S16 q0, d16, d6[1]
VEXT.16 d16, d4, d5, #2
VMLAL.S16 q0, d16, d6[2]
VEXT.16 d16, d4, d5, #3
VMLAL.S16 q0, d16, d6[3]
; Process 2 samples here if we have > 2 left (still reading one extra y value).
xcorr_kernel_neon_process2
ADDS r12, r12, #2
BLE xcorr_kernel_neon_process1
; Load x[0...1]
VLD2.16 {d6[],d7[]}, [r4]!
; Use VAND since it's a data processing instruction again.
VAND d4, d5, d5
SUB r12, r12, #2
; Load y[4...5]
VLD1.32 {d5[]}, [r5]!
VMLAL.S16 q0, d4, d6
VEXT.16 d16, d4, d5, #1
; Replace bottom copy of {y5,y4} in d5 with {y3,y2} from d4, using VSRI
; instead of VEXT, since it's a data-processing instruction.
VSRI.64 d5, d4, #32
VMLAL.S16 q0, d16, d7
; Process 1 sample using the extra y value we loaded above.
xcorr_kernel_neon_process1
; Load next *x
VLD1.16 {d6[]}, [r4]!
ADDS r12, r12, #1
; y[0...3] are left in d5 from prior iteration(s) (if any)
VMLAL.S16 q0, d5, d6
MOVLE pc, lr
; Now process 1 last sample, not reading ahead.
; Load last *y
VLD1.16 {d4[]}, [r5]!
VSRI.64 d4, d5, #16
; Load last *x
VLD1.16 {d6[]}, [r4]!
VMLAL.S16 q0, d4, d6
MOV pc, lr
ENDP
; opus_val32 celt_pitch_xcorr_neon(opus_val16 *_x, opus_val16 *_y,
; opus_val32 *xcorr, int len, int max_pitch, int arch)
celt_pitch_xcorr_neon PROC
; input:
; r0 = opus_val16 *_x
; r1 = opus_val16 *_y
; r2 = opus_val32 *xcorr
; r3 = int len
; output:
; r0 = int maxcorr
; internal usage:
; r4 = opus_val16 *x (for xcorr_kernel_neon())
; r5 = opus_val16 *y (for xcorr_kernel_neon())
; r6 = int max_pitch
; r12 = int j
; q15 = int maxcorr[4] (q15 is not used by xcorr_kernel_neon())
; ignored:
; int arch
STMFD sp!, {r4-r6, lr}
LDR r6, [sp, #16]
VMOV.S32 q15, #1
; if (max_pitch < 4) goto celt_pitch_xcorr_neon_process4_done
SUBS r6, r6, #4
BLT celt_pitch_xcorr_neon_process4_done
celt_pitch_xcorr_neon_process4
; xcorr_kernel_neon parameters:
; r3 = len, r4 = _x, r5 = _y, q0 = {0, 0, 0, 0}
MOV r4, r0
MOV r5, r1
VEOR q0, q0, q0
; xcorr_kernel_neon only modifies r4, r5, r12, and q0...q3.
; So we don't save/restore any other registers.
BL xcorr_kernel_neon_start
SUBS r6, r6, #4
VST1.32 {q0}, [r2]!
; _y += 4
ADD r1, r1, #8
VMAX.S32 q15, q15, q0
; if (max_pitch < 4) goto celt_pitch_xcorr_neon_process4_done
BGE celt_pitch_xcorr_neon_process4
; We have less than 4 sums left to compute.
celt_pitch_xcorr_neon_process4_done
ADDS r6, r6, #4
; Reduce maxcorr to a single value
VMAX.S32 d30, d30, d31
VPMAX.S32 d30, d30, d30
; if (max_pitch <= 0) goto celt_pitch_xcorr_neon_done
BLE celt_pitch_xcorr_neon_done
; Now compute each remaining sum one at a time.
celt_pitch_xcorr_neon_process_remaining
MOV r4, r0
MOV r5, r1
VMOV.I32 q0, #0
SUBS r12, r3, #8
BLT celt_pitch_xcorr_neon_process_remaining4
; Sum terms 8 at a time.
celt_pitch_xcorr_neon_process_remaining_loop8
; Load x[0...7]
VLD1.16 {q1}, [r4]!
; Load y[0...7]
VLD1.16 {q2}, [r5]!
SUBS r12, r12, #8
VMLAL.S16 q0, d4, d2
VMLAL.S16 q0, d5, d3
BGE celt_pitch_xcorr_neon_process_remaining_loop8
; Sum terms 4 at a time.
celt_pitch_xcorr_neon_process_remaining4
ADDS r12, r12, #4
BLT celt_pitch_xcorr_neon_process_remaining4_done
; Load x[0...3]
VLD1.16 {d2}, [r4]!
; Load y[0...3]
VLD1.16 {d3}, [r5]!
SUB r12, r12, #4
VMLAL.S16 q0, d3, d2
celt_pitch_xcorr_neon_process_remaining4_done
; Reduce the sum to a single value.
VADD.S32 d0, d0, d1
VPADDL.S32 d0, d0
ADDS r12, r12, #4
BLE celt_pitch_xcorr_neon_process_remaining_loop_done
; Sum terms 1 at a time.
celt_pitch_xcorr_neon_process_remaining_loop1
VLD1.16 {d2[]}, [r4]!
VLD1.16 {d3[]}, [r5]!
SUBS r12, r12, #1
VMLAL.S16 q0, d2, d3
BGT celt_pitch_xcorr_neon_process_remaining_loop1
celt_pitch_xcorr_neon_process_remaining_loop_done
VST1.32 {d0[0]}, [r2]!
VMAX.S32 d30, d30, d0
SUBS r6, r6, #1
; _y++
ADD r1, r1, #2
; if (--max_pitch > 0) goto celt_pitch_xcorr_neon_process_remaining
BGT celt_pitch_xcorr_neon_process_remaining
celt_pitch_xcorr_neon_done
VMOV.32 r0, d30[0]
LDMFD sp!, {r4-r6, pc}
ENDP
ENDIF
IF OPUS_ARM_MAY_HAVE_EDSP
; This will get used on ARMv7 devices without NEON, so it has been optimized
; to take advantage of dual-issuing where possible.
xcorr_kernel_edsp PROC
xcorr_kernel_edsp_start
; input:
; r3 = int len
; r4 = opus_val16 *_x (must be 32-bit aligned)
; r5 = opus_val16 *_y (must be 32-bit aligned)
; r6...r9 = opus_val32 sum[4]
; output:
; r6...r9 = opus_val32 sum[4]
; preserved: r0-r5
; internal usage
; r2 = int j
; r12,r14 = opus_val16 x[4]
; r10,r11 = opus_val16 y[4]
STMFD sp!, {r2,r4,r5,lr}
LDR r10, [r5], #4 ; Load y[0...1]
SUBS r2, r3, #4 ; j = len-4
LDR r11, [r5], #4 ; Load y[2...3]
BLE xcorr_kernel_edsp_process4_done
LDR r12, [r4], #4 ; Load x[0...1]
; Stall
xcorr_kernel_edsp_process4
; The multiplies must issue from pipeline 0, and can't dual-issue with each
; other. Every other instruction here dual-issues with a multiply, and is
; thus "free". There should be no stalls in the body of the loop.
SMLABB r6, r12, r10, r6 ; sum[0] = MAC16_16(sum[0],x_0,y_0)
LDR r14, [r4], #4 ; Load x[2...3]
SMLABT r7, r12, r10, r7 ; sum[1] = MAC16_16(sum[1],x_0,y_1)
SUBS r2, r2, #4 ; j-=4
SMLABB r8, r12, r11, r8 ; sum[2] = MAC16_16(sum[2],x_0,y_2)
SMLABT r9, r12, r11, r9 ; sum[3] = MAC16_16(sum[3],x_0,y_3)
SMLATT r6, r12, r10, r6 ; sum[0] = MAC16_16(sum[0],x_1,y_1)
LDR r10, [r5], #4 ; Load y[4...5]
SMLATB r7, r12, r11, r7 ; sum[1] = MAC16_16(sum[1],x_1,y_2)
SMLATT r8, r12, r11, r8 ; sum[2] = MAC16_16(sum[2],x_1,y_3)
SMLATB r9, r12, r10, r9 ; sum[3] = MAC16_16(sum[3],x_1,y_4)
LDRGT r12, [r4], #4 ; Load x[0...1]
SMLABB r6, r14, r11, r6 ; sum[0] = MAC16_16(sum[0],x_2,y_2)
SMLABT r7, r14, r11, r7 ; sum[1] = MAC16_16(sum[1],x_2,y_3)
SMLABB r8, r14, r10, r8 ; sum[2] = MAC16_16(sum[2],x_2,y_4)
SMLABT r9, r14, r10, r9 ; sum[3] = MAC16_16(sum[3],x_2,y_5)
SMLATT r6, r14, r11, r6 ; sum[0] = MAC16_16(sum[0],x_3,y_3)
LDR r11, [r5], #4 ; Load y[6...7]
SMLATB r7, r14, r10, r7 ; sum[1] = MAC16_16(sum[1],x_3,y_4)
SMLATT r8, r14, r10, r8 ; sum[2] = MAC16_16(sum[2],x_3,y_5)
SMLATB r9, r14, r11, r9 ; sum[3] = MAC16_16(sum[3],x_3,y_6)
BGT xcorr_kernel_edsp_process4
xcorr_kernel_edsp_process4_done
ADDS r2, r2, #4
BLE xcorr_kernel_edsp_done
LDRH r12, [r4], #2 ; r12 = *x++
SUBS r2, r2, #1 ; j--
; Stall
SMLABB r6, r12, r10, r6 ; sum[0] = MAC16_16(sum[0],x,y_0)
LDRHGT r14, [r4], #2 ; r14 = *x++
SMLABT r7, r12, r10, r7 ; sum[1] = MAC16_16(sum[1],x,y_1)
SMLABB r8, r12, r11, r8 ; sum[2] = MAC16_16(sum[2],x,y_2)
SMLABT r9, r12, r11, r9 ; sum[3] = MAC16_16(sum[3],x,y_3)
BLE xcorr_kernel_edsp_done
SMLABT r6, r14, r10, r6 ; sum[0] = MAC16_16(sum[0],x,y_1)
SUBS r2, r2, #1 ; j--
SMLABB r7, r14, r11, r7 ; sum[1] = MAC16_16(sum[1],x,y_2)
LDRH r10, [r5], #2 ; r10 = y_4 = *y++
SMLABT r8, r14, r11, r8 ; sum[2] = MAC16_16(sum[2],x,y_3)
LDRHGT r12, [r4], #2 ; r12 = *x++
SMLABB r9, r14, r10, r9 ; sum[3] = MAC16_16(sum[3],x,y_4)
BLE xcorr_kernel_edsp_done
SMLABB r6, r12, r11, r6 ; sum[0] = MAC16_16(sum[0],tmp,y_2)
CMP r2, #1 ; j--
SMLABT r7, r12, r11, r7 ; sum[1] = MAC16_16(sum[1],tmp,y_3)
LDRH r2, [r5], #2 ; r2 = y_5 = *y++
SMLABB r8, r12, r10, r8 ; sum[2] = MAC16_16(sum[2],tmp,y_4)
LDRHGT r14, [r4] ; r14 = *x
SMLABB r9, r12, r2, r9 ; sum[3] = MAC16_16(sum[3],tmp,y_5)
BLE xcorr_kernel_edsp_done
SMLABT r6, r14, r11, r6 ; sum[0] = MAC16_16(sum[0],tmp,y_3)
LDRH r11, [r5] ; r11 = y_6 = *y
SMLABB r7, r14, r10, r7 ; sum[1] = MAC16_16(sum[1],tmp,y_4)
SMLABB r8, r14, r2, r8 ; sum[2] = MAC16_16(sum[2],tmp,y_5)
SMLABB r9, r14, r11, r9 ; sum[3] = MAC16_16(sum[3],tmp,y_6)
xcorr_kernel_edsp_done
LDMFD sp!, {r2,r4,r5,pc}
ENDP
celt_pitch_xcorr_edsp PROC
; input:
; r0 = opus_val16 *_x (must be 32-bit aligned)
; r1 = opus_val16 *_y (only needs to be 16-bit aligned)
; r2 = opus_val32 *xcorr
; r3 = int len
; output:
; r0 = maxcorr
; internal usage
; r4 = opus_val16 *x
; r5 = opus_val16 *y
; r6 = opus_val32 sum0
; r7 = opus_val32 sum1
; r8 = opus_val32 sum2
; r9 = opus_val32 sum3
; r1 = int max_pitch
; r12 = int j
; ignored:
; int arch
STMFD sp!, {r4-r11, lr}
MOV r5, r1
LDR r1, [sp, #36]
MOV r4, r0
TST r5, #3
; maxcorr = 1
MOV r0, #1
BEQ celt_pitch_xcorr_edsp_process1u_done
; Compute one sum at the start to make y 32-bit aligned.
SUBS r12, r3, #4
; r14 = sum = 0
MOV r14, #0
LDRH r8, [r5], #2
BLE celt_pitch_xcorr_edsp_process1u_loop4_done
LDR r6, [r4], #4
MOV r8, r8, LSL #16
celt_pitch_xcorr_edsp_process1u_loop4
LDR r9, [r5], #4
SMLABT r14, r6, r8, r14 ; sum = MAC16_16(sum, x_0, y_0)
LDR r7, [r4], #4
SMLATB r14, r6, r9, r14 ; sum = MAC16_16(sum, x_1, y_1)
LDR r8, [r5], #4
SMLABT r14, r7, r9, r14 ; sum = MAC16_16(sum, x_2, y_2)
SUBS r12, r12, #4 ; j-=4
SMLATB r14, r7, r8, r14 ; sum = MAC16_16(sum, x_3, y_3)
LDRGT r6, [r4], #4
BGT celt_pitch_xcorr_edsp_process1u_loop4
MOV r8, r8, LSR #16
celt_pitch_xcorr_edsp_process1u_loop4_done
ADDS r12, r12, #4
celt_pitch_xcorr_edsp_process1u_loop1
LDRHGE r6, [r4], #2
; Stall
SMLABBGE r14, r6, r8, r14 ; sum = MAC16_16(sum, *x, *y)
SUBSGE r12, r12, #1
LDRHGT r8, [r5], #2
BGT celt_pitch_xcorr_edsp_process1u_loop1
; Restore _x
SUB r4, r4, r3, LSL #1
; Restore and advance _y
SUB r5, r5, r3, LSL #1
; maxcorr = max(maxcorr, sum)
CMP r0, r14
ADD r5, r5, #2
MOVLT r0, r14
SUBS r1, r1, #1
; xcorr[i] = sum
STR r14, [r2], #4
BLE celt_pitch_xcorr_edsp_done
celt_pitch_xcorr_edsp_process1u_done
; if (max_pitch < 4) goto celt_pitch_xcorr_edsp_process2
SUBS r1, r1, #4
BLT celt_pitch_xcorr_edsp_process2
celt_pitch_xcorr_edsp_process4
; xcorr_kernel_edsp parameters:
; r3 = len, r4 = _x, r5 = _y, r6...r9 = sum[4] = {0, 0, 0, 0}
MOV r6, #0
MOV r7, #0
MOV r8, #0
MOV r9, #0
BL xcorr_kernel_edsp_start ; xcorr_kernel_edsp(_x, _y+i, xcorr+i, len)
; maxcorr = max(maxcorr, sum0, sum1, sum2, sum3)
CMP r0, r6
; _y+=4
ADD r5, r5, #8
MOVLT r0, r6
CMP r0, r7
MOVLT r0, r7
CMP r0, r8
MOVLT r0, r8
CMP r0, r9
MOVLT r0, r9
STMIA r2!, {r6-r9}
SUBS r1, r1, #4
BGE celt_pitch_xcorr_edsp_process4
celt_pitch_xcorr_edsp_process2
ADDS r1, r1, #2
BLT celt_pitch_xcorr_edsp_process1a
SUBS r12, r3, #4
; {r10, r11} = {sum0, sum1} = {0, 0}
MOV r10, #0
MOV r11, #0
LDR r8, [r5], #4
BLE celt_pitch_xcorr_edsp_process2_loop_done
LDR r6, [r4], #4
LDR r9, [r5], #4
celt_pitch_xcorr_edsp_process2_loop4
SMLABB r10, r6, r8, r10 ; sum0 = MAC16_16(sum0, x_0, y_0)
LDR r7, [r4], #4
SMLABT r11, r6, r8, r11 ; sum1 = MAC16_16(sum1, x_0, y_1)
SUBS r12, r12, #4 ; j-=4
SMLATT r10, r6, r8, r10 ; sum0 = MAC16_16(sum0, x_1, y_1)
LDR r8, [r5], #4
SMLATB r11, r6, r9, r11 ; sum1 = MAC16_16(sum1, x_1, y_2)
LDRGT r6, [r4], #4
SMLABB r10, r7, r9, r10 ; sum0 = MAC16_16(sum0, x_2, y_2)
SMLABT r11, r7, r9, r11 ; sum1 = MAC16_16(sum1, x_2, y_3)
SMLATT r10, r7, r9, r10 ; sum0 = MAC16_16(sum0, x_3, y_3)
LDRGT r9, [r5], #4
SMLATB r11, r7, r8, r11 ; sum1 = MAC16_16(sum1, x_3, y_4)
BGT celt_pitch_xcorr_edsp_process2_loop4
celt_pitch_xcorr_edsp_process2_loop_done
ADDS r12, r12, #2
BLE celt_pitch_xcorr_edsp_process2_1
LDR r6, [r4], #4
; Stall
SMLABB r10, r6, r8, r10 ; sum0 = MAC16_16(sum0, x_0, y_0)
LDR r9, [r5], #4
SMLABT r11, r6, r8, r11 ; sum1 = MAC16_16(sum1, x_0, y_1)
SUB r12, r12, #2
SMLATT r10, r6, r8, r10 ; sum0 = MAC16_16(sum0, x_1, y_1)
MOV r8, r9
SMLATB r11, r6, r9, r11 ; sum1 = MAC16_16(sum1, x_1, y_2)
celt_pitch_xcorr_edsp_process2_1
LDRH r6, [r4], #2
ADDS r12, r12, #1
; Stall
SMLABB r10, r6, r8, r10 ; sum0 = MAC16_16(sum0, x_0, y_0)
LDRHGT r7, [r4], #2
SMLABT r11, r6, r8, r11 ; sum1 = MAC16_16(sum1, x_0, y_1)
BLE celt_pitch_xcorr_edsp_process2_done
LDRH r9, [r5], #2
SMLABT r10, r7, r8, r10 ; sum0 = MAC16_16(sum0, x_0, y_1)
SMLABB r11, r7, r9, r11 ; sum1 = MAC16_16(sum1, x_0, y_2)
celt_pitch_xcorr_edsp_process2_done
; Restore _x
SUB r4, r4, r3, LSL #1
; Restore and advance _y
SUB r5, r5, r3, LSL #1
; maxcorr = max(maxcorr, sum0)
CMP r0, r10
ADD r5, r5, #2
MOVLT r0, r10
SUB r1, r1, #2
; maxcorr = max(maxcorr, sum1)
CMP r0, r11
; xcorr[i] = sum
STR r10, [r2], #4
MOVLT r0, r11
STR r11, [r2], #4
celt_pitch_xcorr_edsp_process1a
ADDS r1, r1, #1
BLT celt_pitch_xcorr_edsp_done
SUBS r12, r3, #4
; r14 = sum = 0
MOV r14, #0
BLT celt_pitch_xcorr_edsp_process1a_loop_done
LDR r6, [r4], #4
LDR r8, [r5], #4
LDR r7, [r4], #4
LDR r9, [r5], #4
celt_pitch_xcorr_edsp_process1a_loop4
SMLABB r14, r6, r8, r14 ; sum = MAC16_16(sum, x_0, y_0)
SUBS r12, r12, #4 ; j-=4
SMLATT r14, r6, r8, r14 ; sum = MAC16_16(sum, x_1, y_1)
LDRGE r6, [r4], #4
SMLABB r14, r7, r9, r14 ; sum = MAC16_16(sum, x_2, y_2)
LDRGE r8, [r5], #4
SMLATT r14, r7, r9, r14 ; sum = MAC16_16(sum, x_3, y_3)
LDRGE r7, [r4], #4
LDRGE r9, [r5], #4
BGE celt_pitch_xcorr_edsp_process1a_loop4
celt_pitch_xcorr_edsp_process1a_loop_done
ADDS r12, r12, #2
LDRGE r6, [r4], #4
LDRGE r8, [r5], #4
; Stall
SMLABBGE r14, r6, r8, r14 ; sum = MAC16_16(sum, x_0, y_0)
SUBGE r12, r12, #2
SMLATTGE r14, r6, r8, r14 ; sum = MAC16_16(sum, x_1, y_1)
ADDS r12, r12, #1
LDRHGE r6, [r4], #2
LDRHGE r8, [r5], #2
; Stall
SMLABBGE r14, r6, r8, r14 ; sum = MAC16_16(sum, *x, *y)
; maxcorr = max(maxcorr, sum)
CMP r0, r14
; xcorr[i] = sum
STR r14, [r2], #4
MOVLT r0, r14
celt_pitch_xcorr_edsp_done
LDMFD sp!, {r4-r11, pc}
ENDP
ENDIF
END

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save