Browse Source

Merge branch 'new_dev' of https://github.com/deepcloud2048/deep_voice into new_dev

newdev_shunjiawei
lxm 1 year ago
parent
commit
4e95ae6b9f
  1. 33
      lib/core/utils/synchrodata.dart
  2. 6
      lib/data/services/speech_impl/azure_tts_service.dart
  3. 2
      lib/data/services/speech_impl/voice_clone_tts_service.dart
  4. 4
      lib/data/services/tts_service.dart
  5. 62
      lib/modules/explore/views/explore_view.dart
  6. 101
      lib/modules/meeting/views/generate_bottom_sheet.dart
  7. 14
      lib/modules/meeting/views/meeting_details_view.dart
  8. 1075
      lib/modules/meeting/views/meeting_view.dart
  9. 92
      lib/modules/meeting/views/more_bottom_sheet.dart
  10. 85
      lib/modules/translation/controllers/translation_controller.dart
  11. 20
      lib/modules/translation/views/translation_list_view.dart
  12. 8
      lib/modules/translation/views/translation_view.dart
  13. 13
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  14. 14
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  15. 1
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt
  16. 110
      local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt
  17. 2
      local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt

33
lib/core/utils/synchrodata.dart

@ -8,7 +8,11 @@ import '../../data/services/network/api.dart';
class Synchrodata {
static final GetStorage _storage = GetStorage();
static List _records = _storage.read("operation_records") ?? [];
static List<Map<String, dynamic>> _records =
(_storage.read("operation_records") as List?)
?.map((e) => Map<String, dynamic>.from(e as Map))
.toList() ??
[];
static List _delids = _storage.read("operation_delids") ?? [];
static bool _isSynchro = false;
@ -30,7 +34,7 @@ class Synchrodata {
switch (method) {
case 'insert':
_records.add({
...params,
...Map<String, dynamic>.from(params),
'id': id,
'number': params['id'],
'uid': userId,
@ -38,19 +42,19 @@ class Synchrodata {
_storage.write("operation_records", _records);
break;
case 'update':
Map? foundElement = _records.firstWhere(
Map<String, dynamic>? foundElement = _records.firstWhere(
(element) => element['id'] == id,
orElse: () => null,
orElse: () => {},
);
if (foundElement != null) {
if (foundElement.isNotEmpty) {
foundElement.addAll({
...params,
...Map<String, dynamic>.from(params),
'id': id,
'number': params['id'],
});
} else {
_records.add({
...params,
...Map<String, dynamic>.from(params),
'id': id,
'number': params['id'],
'uid': userId,
@ -75,15 +79,18 @@ class Synchrodata {
// 获取需要修改的全部数据
List putRecords = [];
for (var element in _records) {
final result = await SqfliteApi.getMeetingDetails(element['number']);
final speakerList =
await SqfliteApi.getMeetingSpeaker(element['number']);
Map result = {'details': {}};
List speakerList = [];
if (!_delids.contains(element['id'])) {
result = await SqfliteApi.getMeetingDetails(element['number']);
speakerList = await SqfliteApi.getMeetingSpeaker(element['number']);
}
putRecords.add({
...result,
...result['details'],
...Map<String, dynamic>.from(result),
...Map<String, dynamic>.from(result['details']),
'rtype': result['type'],
'original': jsonEncode(speakerList),
...element,
...Map<String, dynamic>.from(element),
});
}
var result = await Api.putOperationRecord({

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

@ -415,10 +415,12 @@ class AzureTtsService extends GetxService implements TtsService {
}
@override
Future<bool> setAudioOutputDevice() async {
Future<bool> setAudioOutputDevice(int type) async {
try {
// 开始合成,传递参数
final result = await _channel.invokeMethod('setAudioOutputDevice');
final result = await _channel.invokeMethod('setAudioOutputDevice', {
'type': type,
});
return result;
} catch (e) {
Logger.error('音频输出设备类型失败: ${e.toString()}');

2
lib/data/services/speech_impl/voice_clone_tts_service.dart

@ -392,7 +392,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
}
@override
Future<bool> setAudioOutputDevice() {
Future<bool> setAudioOutputDevice(int type) {
// TODO: implement setAudioOutputDevice
throw UnimplementedError();
}

4
lib/data/services/tts_service.dart

@ -33,7 +33,9 @@ abstract class TtsService {
/// 释放资源
Future<void> dispose();
Future<bool> setAudioOutputDevice();
/// 切换音频输出设备
Future<bool> setAudioOutputDevice(int type);
}
/// TTS事件类型

62
lib/modules/explore/views/explore_view.dart

@ -126,37 +126,37 @@ class ExploreView extends GetView<ExploreController> {
],
),
// 搜索框
SizedBox(height: 16.h),
Container(
decoration: BoxDecoration(
color:
isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100],
borderRadius: BorderRadius.circular(12.r),
),
child: TextField(
controller: controller.searchController,
decoration: InputDecoration(
hintText: 'searchAi'.tr, // 搜索AI功能...
hintStyle: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[400],
),
prefixIcon: Icon(
Icons.search,
size: 20.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[400],
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 12.h),
),
textInputAction: TextInputAction.search,
onSubmitted: (_) => controller.search(),
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
// // 搜索框
// SizedBox(height: 16.h),
// Container(
// decoration: BoxDecoration(
// color:
// isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100],
// borderRadius: BorderRadius.circular(12.r),
// ),
// child: TextField(
// controller: controller.searchController,
// decoration: InputDecoration(
// hintText: 'searchAi'.tr, // 搜索AI功能...
// hintStyle: TextStyle(
// fontSize: 14.sp,
// color: isDarkMode ? Colors.grey[400] : Colors.grey[400],
// ),
// prefixIcon: Icon(
// Icons.search,
// size: 20.sp,
// color: isDarkMode ? Colors.grey[400] : Colors.grey[400],
// ),
// border: InputBorder.none,
// contentPadding: EdgeInsets.symmetric(vertical: 12.h),
// ),
// textInputAction: TextInputAction.search,
// onSubmitted: (_) => controller.search(),
// style: TextStyle(
// color: isDarkMode ? Colors.white : Colors.black,
// ),
// ),
// ),
],
),
);

101
lib/modules/meeting/views/generate_bottom_sheet.dart

@ -9,7 +9,9 @@ import 'record_language_bottom_sheet.dart';
import 'template_view.dart';
class GenerateBottomSheet extends StatefulWidget {
const GenerateBottomSheet({super.key});
final bool isSummarizeAgain;
const GenerateBottomSheet({super.key, this.isSummarizeAgain = false});
@override
State<GenerateBottomSheet> createState() => _GenerateBottomSheetState();
@ -214,56 +216,57 @@ class _GenerateBottomSheetState extends State<GenerateBottomSheet> {
},
),
),
Container(
margin: EdgeInsets.symmetric(horizontal: 12.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.r),
),
child: Column(
children: [
_row(
'区分说话人',
widget: Transform.scale(
scale: 0.8,
alignment: Alignment.centerRight,
child: Switch(
value: _isSpeaker,
onChanged: (bool value) {
setState(() {
_isSpeaker = value;
});
},
if (!widget.isSummarizeAgain)
Container(
margin: EdgeInsets.symmetric(horizontal: 12.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.r),
),
child: Column(
children: [
_row(
'区分说话人',
widget: Transform.scale(
scale: 0.8,
alignment: Alignment.centerRight,
child: Switch(
value: _isSpeaker,
onChanged: (bool value) {
setState(() {
_isSpeaker = value;
});
},
),
),
),
),
_row(
'录音语言',
value: _languageText,
isBorder: false,
onTap: () {
Get.bottomSheet(
RecordLanguageBottomSheet(_language),
isScrollControlled: true,
backgroundColor: Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
_row(
'录音语言',
value: _languageText,
isBorder: false,
onTap: () {
Get.bottomSheet(
RecordLanguageBottomSheet(_language),
isScrollControlled: true,
backgroundColor: Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
),
).then((value) {
if (value != null) {
setState(() {
_languageText = value['name'];
_language = value['value'];
});
}
});
},
),
],
).then((value) {
if (value != null) {
setState(() {
_languageText = value['name'];
_language = value['value'];
});
}
});
},
),
],
),
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w),
child: GestureDetector(
@ -279,6 +282,10 @@ class _GenerateBottomSheetState extends State<GenerateBottomSheet> {
_storage.write("meeting_last_template_Data", _templateData);
_storage.write("meeting_template_list", _templateList);
Get.back();
if (widget.isSummarizeAgain) {
_controller.meetingDetails.value.tasktype = 2;
_controller.meetingDetails.refresh();
}
_controller.summaryCreate();
},
child: Container(

14
lib/modules/meeting/views/meeting_details_view.dart

@ -10,6 +10,7 @@ import 'package:lottie/lottie.dart';
import '../../../core/theme/app_colors.dart';
import '../controllers/meeting_details_controller.dart';
import 'mind_map_tab.dart';
import 'more_bottom_sheet.dart';
import 'overview_tab.dart';
import 'share_bottom_sheet.dart';
import 'speech_tab.dart';
@ -189,7 +190,18 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
size: 16.sp,
),
),
onPressed: () {},
onPressed: () {
Get.bottomSheet(
const MoreBottomSheet(),
isScrollControlled: true,
backgroundColor: Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
);
},
),
],
);

1075
lib/modules/meeting/views/meeting_view.dart

File diff suppressed because it is too large

92
lib/modules/meeting/views/more_bottom_sheet.dart

@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/meeting_controller.dart';
import '../controllers/meeting_details_controller.dart';
import 'generate_bottom_sheet.dart';
class MoreBottomSheet extends StatefulWidget {
const MoreBottomSheet({super.key});
@override
State<MoreBottomSheet> createState() => _MoreBottomSheetState();
}
class _MoreBottomSheetState extends State<MoreBottomSheet> {
final _controller = Get.find<MeetingController>();
final _detailsController = Get.find<MeetingDetailsController>();
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 10.w),
if (_detailsController.meetingDetails.value.tasktype == 3)
_shareItem(Icons.refresh, '重新总结', onTap: () {
Get.back();
Get.bottomSheet(
const GenerateBottomSheet(
isSummarizeAgain: true,
),
isScrollControlled: true,
backgroundColor: Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
);
}),
_shareItem(Icons.delete_outline, '删除', onTap: () async {
_controller.deleteMeeting(
_detailsController.meetingData.value.id,
_detailsController.meetingData.value.filepath,
);
Get.back();
Get.back();
}),
],
),
);
}
Widget _shareItem(
IconData icon,
String title, {
VoidCallback? onTap,
}) {
return InkWell(
onTap: onTap,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 15.w),
margin: EdgeInsets.only(bottom: 10.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
children: [
Icon(
icon,
color: Colors.black87,
),
10.horizontalSpace,
Expanded(
child: Text(
title,
style: TextStyle(
fontSize: 14.sp,
color: Colors.black87,
),
),
),
],
),
),
);
}
}

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

@ -17,6 +17,13 @@ import '../../../data/models/events/voice_interaction_event.dart';
import '../../../data/services/asr_service.dart';
import '../../../data/services/ble_manager.dart';
// 切换音频输出设备的枚举
enum AudioOutputDevice {
defaultDevice, // 默认(如有耳机优先耳机,否则扬声器)
headphones, // 强制耳机
speaker, // 强制扬声器
}
class TranslationController extends GetxController {
// 服务
final AsrService _asrService = Get.find<AsrService>();
@ -55,6 +62,7 @@ class TranslationController extends GetxController {
final appDocDir = getApplicationDocumentsDirectory();
var appDir;
var dir;
Future<void> changeTranslationMode(String mode) async {
currentMode.value = mode;
currentModeTitle.value = _getModeTitle(mode);
@ -104,7 +112,7 @@ class TranslationController extends GetxController {
//_setupEventListeners();
// 加载上次保存的语言设置
_loadSavedLanguages();
_requestStoragePermission();
// 初始化语言代码
final sourceAsrCode =
_languageManager.getAsrCodeByChineseName(sourceLanguage.value);
@ -175,25 +183,27 @@ class TranslationController extends GetxController {
}
// 请求存储权限
void _requestStoragePermission() async {
Future<bool> _requestStoragePermission() async {
// final BuildContext context = Get.overlayContext!;
try {
final bool granted = await PermissionUtil.instance.requestPermission(
permissionType: Permission.storage,
permissionName: "存储",
explanationText: "我们需要访问存储权限,以便您可以选择并上传音频文件。",
permanentDenialText: "检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。",
);
PermissionUtil.instance
.requestPermission(
// context: context,
permissionType: Permission.storage,
permissionName: "存储",
explanationText: "我们需要访问存储权限,以便您可以选择并上传音频文件。",
permanentDenialText: "检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。",
)
.then((granted) {
if (granted) {
Logger.d("Permission", "用户授予了存储权限");
return true;
} else {
Logger.d("Permission", "用户未授予存储权限");
// 可以提示用户无法进行文件操作
return false;
}
});
} catch (e) {
Logger.e("Permission", "请求存储权限时出错: $e");
return false;
}
}
// /// 设置事件监听器
// void _setupEventListeners() {
@ -224,6 +234,11 @@ class TranslationController extends GetxController {
await _asrService.initialize(
supportedLanguages: asrSupportedLanguages,
);
final bool storage = await _requestStoragePermission();
if (!storage) {
Logger.d("Permission", "未授予存储权限,无法提供录音功能");
return;
}
appDir = await appDocDir; // 先 await 获取目录
dir = Directory("${appDir.path}/TranslatAudio");
if (!await dir.exists()) {
@ -266,10 +281,10 @@ class TranslationController extends GetxController {
Future<void> startRecognition() async {
Logger.info('开始语音识别');
// 等待权限请求完成并获取结果
final bool granted = await _requestRecordPermission();
final bool record = await _requestRecordPermission();
// 如果权限被拒绝,直接返回
if (!granted) {
if (!record) {
Logger.d("Permission", "录音权限被拒绝,无法开始录音");
return;
}
@ -328,6 +343,11 @@ class TranslationController extends GetxController {
await _asrService.startContinuousRecognition(_audioSourceType);
isRecognizing.value = true;
if (isRecording.value && isRecognizing.value) {
final bool storage = await _requestStoragePermission();
if (!storage) {
Logger.d("Permission", "未授予存储权限,无法提供录音功能");
return;
}
final formattedTime =
DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
await _asrService.enableRecord(
@ -516,15 +536,32 @@ class TranslationController extends GetxController {
detectedSourceLanguageCode;
translationHistory[index].targetLanguageCode =
detectedTargetLanguageCode;
translationHistory.refresh();
saveTranslationHistory();
// 更新后滚动到底部
_scrollToBottom();
// 播放TTS - 只在最终结果时播放
if (isTtsEnabled.value && isFinal) {
// Logger.info('播放翻译文本: ${translationResult}');
await playTranslatedText(translationResult);
print("判断是否是面对面模式且语言一致: ${detectedSourceLanguageCode}");
// 判断是否是面对面模式且语言一致
final bool isFaceToFaceSameLang =
detectedSourceLanguageCode == sourceLanguageCode.value &&
currentMode.value == "faceToFace";
// 设置音频输出设备
var isSuccess = await _ttsService.setAudioOutputDevice(
isFaceToFaceSameLang
? AudioOutputDevice.speaker.index
: AudioOutputDevice.headphones.index,
);
if (isSuccess) {
print('设置音频输出设备成功');
translationHistory.refresh();
saveTranslationHistory();
// 更新后滚动到底部
_scrollToBottom();
// 播放TTS - 只在最终结果时播放
if (isTtsEnabled.value && isFinal) {
// Logger.info('播放翻译文本: ${translationResult}');
await playTranslatedText(translationResult);
}
} else {
print('设置音频输出设备失败');
}
} else {
Logger.info(

20
lib/modules/translation/views/translation_list_view.dart

@ -278,15 +278,16 @@ class TranslationListView extends GetView<TranslationListController> {
),
_popupMenuEntry(
Icon(
Icons.delete_outline,
Icons.smart_button,
size: 20.w,
color: isDarkMode ? Colors.red[700] : Colors.red,
color: isDarkMode ? Colors.white : Colors.black87,
),
'删除',
isDarkMode ? Colors.red[700]! : Colors.red,
'智能转写',
isDarkMode ? Colors.white : Colors.black87,
() {
file.delete();
controller.loadTranslatAudio(); // 刷新列表
Get.toNamed(Routes.meeting, arguments: {
'filePath': file.path,
});
},
),
_popupMenuEntry(
@ -295,12 +296,11 @@ class TranslationListView extends GetView<TranslationListController> {
size: 20.w,
color: isDarkMode ? Colors.red[700] : Colors.red,
),
'导入',
'删除',
isDarkMode ? Colors.red[700]! : Colors.red,
() {
Get.toNamed(Routes.meeting, arguments: {
'filePath': file.path,
});
file.delete();
controller.loadTranslatAudio(); // 刷新列表
},
),
],

8
lib/modules/translation/views/translation_view.dart

@ -110,7 +110,9 @@ class TranslationView extends GetView<TranslationController> {
// TTS toggle button
Obx(() => IconButton(
icon: Icon(
controller.isRecording.value ? Icons.mic : Icons.mic_off,
controller.isRecording.value
? Icons.mic
: Icons.fiber_manual_record,
color: isDarkMode ? Colors.white : Colors.black,
size: 24.sp,
),
@ -122,7 +124,7 @@ class TranslationView extends GetView<TranslationController> {
// 历史记录按钮
IconButton(
icon: Icon(
Icons.history,
Icons.folder,
color: isDarkMode ? Colors.white : Colors.black,
size: 24.sp,
),
@ -134,7 +136,7 @@ class TranslationView extends GetView<TranslationController> {
// 返回时不自动开始翻译,只需重新设置滚动监听器
controller.setupScrollListener();
},
tooltip: '查看历史记录',
tooltip: '查看录音文件',
),
],
bottom: PreferredSize(

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

@ -147,13 +147,14 @@ class AzureAsrHelper(private val context: Context) {
}
// 录音文件类
recordfile = RecordFile;
//进入界面手机麦克风就被占用,导致其他app无法使用麦克风,在开始录音再去申请
// 创建识别器
val setupSuccess = setupRecognizer()
if (setupSuccess) {
// 优化:初始化完成后进行预热
warmupRecognizer()
}
return setupSuccess
// val setupSuccess = setupRecognizer()
// if (setupSuccess) {
// // 优化:初始化完成后进行预热
// warmupRecognizer()
// }
return true
} catch (e: Exception) {
Log.e(tag, "初始化失败: ${e.message}")
return false

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

@ -502,7 +502,19 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
result.success(true)
}
"setAudioOutputDevice" -> {
azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.SPEAKER)
val type = call.argument<Int>("type")
if (type == 0) {
// 默认(如果有耳机选耳机,否则使用系统扬声器)
azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.DEFAULT)
} else if (type == 1) {
// 强制使用耳机
azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.HEADPHONES)
} else if (type == 2) {
// 强制使用扬声器
azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.SPEAKER)
}
result.success(true)
}

1
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt

@ -213,6 +213,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService {
override fun setAudioOutputDevice(device: AudioOutputDevice) {
// Azure TTS使用系统默认的音频路由
// 音频输出设备的控制需要通过Android的AudioManager实现
FileLogger.e(TAG, "音频输出设备类型device=: ${device}")
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
audioManager?.let { manager ->
when (device) {

110
local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt

@ -15,12 +15,13 @@ import com.deep_voice.speech.tts.AudioDataListener
import com.deep_voice.speech.tts.AudioOutputDevice
/**
* 高性能音频播放器 - 16kHz采样率
* 语音通信音频播放器 - 16kHz采样率
* 设计要点:
* • 永远使用 USAGE_MEDIA + MODE_NORMAL 媒体播放通路
* • 使用 USAGE_VOICE_COMMUNICATION + MODE_IN_COMMUNICATION 通信通路
* • 利用 AudioDeviceCallback 监听设备热插拔
* • API 31+ 使用 setPreferredDevice() 精准路由
* • 有耳机自动从耳机播放,无耳机自动切到扬声器
* • 内置回声消除和噪音抑制功能
*/
class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, AudioDeviceCallback() {
companion object {
@ -35,6 +36,35 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au
private var isFirstData = true
private var totalBytesWritten = 0
private var sessionActive = true
private var audioFocusRequest: Any? = null // 兼容不同API版本
// 音频焦点变化监听器
private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
when (focusChange) {
AudioManager.AUDIOFOCUS_LOSS -> {
// 永久失去焦点,停止播放
Log.d(TAG, "音频焦点丢失")
stop()
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
// 暂时失去焦点,暂停播放
Log.d(TAG, "音频焦点暂时丢失")
audioTrack?.pause()
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
// 暂时失去焦点但可降低音量,这里选择暂停
Log.d(TAG, "音频焦点暂时丢失(可降低音量)")
audioTrack?.pause()
}
AudioManager.AUDIOFOCUS_GAIN -> {
// 重新获得焦点,恢复播放
Log.d(TAG, "音频焦点恢复")
if (sessionActive) {
audioTrack?.play()
}
}
}
}
// 回调
private var onPlayStarted: (() -> Unit)? = null
@ -62,6 +92,8 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au
init {
// 注册设备热插拔监听
audioManager.registerAudioDeviceCallback(this, Handler(Looper.getMainLooper()))
// 请求音频焦点
requestAudioFocus()
buildTrack(selectOutputDevice()) // 首次创建
}
@ -126,7 +158,7 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au
audioTrack = AudioTrack.Builder()
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA) // 统一媒体通路
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) // 语音通信通路
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build()
)
@ -159,8 +191,8 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au
track.play()
}
// 配置音频管理器 - 统一使用 MODE_NORMAL
audioManager.mode = AudioManager.MODE_NORMAL
// 配置音频管理器 - 使用通信模式以启用回声消除和噪音抑制
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
// 切换扬声器开关仅在无耳机时打开
audioManager.isSpeakerphoneOn = (device == null)
@ -168,6 +200,71 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au
Log.d(TAG, "AudioTrack已创建 - 输出设备: $deviceName, 采样率: ${SAMPLE_RATE}Hz")
}
/**
* 请求音频焦点
*/
private fun requestAudioFocus() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Android O及以上使用新的API
val focusRequest = android.media.AudioFocusRequest.Builder(
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
)
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build()
)
.setAcceptsDelayedFocusGain(true)
.setOnAudioFocusChangeListener(audioFocusChangeListener)
.build()
val result = audioManager.requestAudioFocus(focusRequest)
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
audioFocusRequest = focusRequest
Log.d(TAG, "音频焦点请求成功")
} else {
Log.w(TAG, "音频焦点请求失败")
}
} else {
// Android O以下使用旧的API
@Suppress("DEPRECATION")
val result = audioManager.requestAudioFocus(
audioFocusChangeListener,
AudioManager.STREAM_VOICE_CALL,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
)
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.d(TAG, "音频焦点请求成功(旧API)")
} else {
Log.w(TAG, "音频焦点请求失败(旧API)")
}
}
} catch (e: Exception) {
Log.e(TAG, "请求音频焦点异常: ${e.message}")
}
}
/**
* 释放音频焦点
*/
private fun releaseAudioFocus() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
audioFocusRequest?.let {
audioManager.abandonAudioFocusRequest(it as android.media.AudioFocusRequest)
}
} else {
@Suppress("DEPRECATION")
audioManager.abandonAudioFocus(audioFocusChangeListener)
}
Log.d(TAG, "音频焦点已释放")
} catch (e: Exception) {
Log.e(TAG, "释放音频焦点异常: ${e.message}")
}
}
/**
* 开始新的播放会话
*/
@ -262,7 +359,10 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au
stop()
audioTrack?.release()
audioTrack = null
releaseAudioFocus()
audioManager.unregisterAudioDeviceCallback(this)
// 恢复音频模式
audioManager.mode = AudioManager.MODE_NORMAL
}
/**

2
local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt

@ -116,7 +116,7 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope {
// TTS配置参数
private var appId: String = ""
private var accessToken: String = ""
private var speaker: String = "zh_female_shuangkuaisisi_emo_v2_mars_bigtts" // 默认语音,使用Java示例的音色
private var speaker: String = "zh_female_yuanqinvyou_moon_bigtts" // 默认语音,使用Java示例的音色
private var format: String = "pcm" // pcm 格式
private var sampleRate: Int = 16000 // 使用16kHz采样率
private var enableTimestamp: Boolean = false

Loading…
Cancel
Save