diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 5754e6e..6ab24fb 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -519,12 +519,15 @@ baseConfigurationReference = 3D79BFA926F2692E951E423D /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.fl.musicPlayer.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; diff --git a/lib/components/dialog/rename_dialog.dart b/lib/components/dialog/rename_dialog.dart index d21d8ac..b1cf3f3 100644 --- a/lib/components/dialog/rename_dialog.dart +++ b/lib/components/dialog/rename_dialog.dart @@ -51,7 +51,6 @@ class RenameDialogState extends State { canPop: true, child: Center( child: SingleChildScrollView( - primary: true, padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), child: Container( width: 0.8.sw, @@ -81,30 +80,27 @@ class RenameDialogState extends State { color: const Color(0xFFf9f9fc), borderRadius: BorderRadius.circular(8).r, ), - child: FocusScope( - canRequestFocus: true, - child: TextField( - maxLines: 1, - controller: _textEditingController, - textInputAction: TextInputAction.done, - textAlign: TextAlign.start, - keyboardType: TextInputType.text, - style: TextStyle(color: Colors.black, fontSize: 16.sp), - textAlignVertical: TextAlignVertical.center, - decoration: InputDecoration( - counterText: '', - hintText: 'please enter', - hintStyle: TextStyle(color: const Color(0xFF999999), fontSize: 16.sp,), - contentPadding: const EdgeInsets.symmetric(horizontal: 10).w, - isCollapsed: true, - border: InputBorder.none, - ), - onChanged: (text) { - _textEditingController.selection = TextSelection.fromPosition( - TextPosition(offset: _cursorPosition), - ); - }, + child: TextField( + maxLines: 1, + controller: _textEditingController, + textInputAction: TextInputAction.done, + textAlign: TextAlign.start, + keyboardType: TextInputType.text, + style: TextStyle(color: Colors.black, fontSize: 16.sp), + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + counterText: '', + hintText: 'please enter', + hintStyle: TextStyle(color: const Color(0xFF999999), fontSize: 16.sp,), + contentPadding: const EdgeInsets.symmetric(horizontal: 10).w, + isCollapsed: true, + border: InputBorder.none, ), + onChanged: (text) { + _textEditingController.selection = TextSelection.fromPosition( + TextPosition(offset: _cursorPosition), + ); + }, ), ), const DividerWidget(), diff --git a/lib/controllers/player_controller.dart b/lib/controllers/player_controller.dart index 788371d..72a1d01 100644 --- a/lib/controllers/player_controller.dart +++ b/lib/controllers/player_controller.dart @@ -140,7 +140,7 @@ class PlayerController extends GetxController { isPlaying.value = false; isCompleted.value = true; positionValue.value = 0.0; - duration.value = Duration.zero; + // duration.value = Duration.zero; positionDuration.value = Duration.zero; } diff --git a/lib/modules/voice/change_voice/change_voice_controller.dart b/lib/modules/voice/change_voice/change_voice_controller.dart index dfe3cfc..6ee2e8e 100644 --- a/lib/modules/voice/change_voice/change_voice_controller.dart +++ b/lib/modules/voice/change_voice/change_voice_controller.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:ffmpeg_kit_flutter_audio/ffmpeg_kit.dart'; import 'package:ffmpeg_kit_flutter_audio/ffmpeg_session.dart'; import 'package:ffmpeg_kit_flutter_audio/return_code.dart'; @@ -14,6 +16,7 @@ import 'package:tone_snap/utils/file_util.dart'; import 'package:tone_snap/utils/local_path_util.dart'; import 'package:tone_snap/utils/log_print.dart'; import 'package:tone_snap/utils/num_util.dart'; +import 'package:tone_snap/utils/obj_util.dart'; class ChangeVoiceController extends GetxController { var timberList = [ @@ -41,9 +44,9 @@ class ChangeVoiceController extends GetxController { } @override - void onClose() { - playerController.setSpeed(1.0); - playerController.setPitch(1.0); + void onClose() async { + if (Platform.isAndroid) await playerController.setPitch(1.0); + await playerController.setSpeed(1.0); playerController.stopPlay(); super.onClose(); } @@ -54,7 +57,7 @@ class ChangeVoiceController extends GetxController { } toneValue.value = item.check ? item.tone : 1.0; soundSpeedValue.value = item.check ? item.soundSpeed : 1.0; - playerController.setPitch(toneValue.value); + if (Platform.isAndroid) playerController.setPitch(toneValue.value); playerController.setSpeed(soundSpeedValue.value); timberList.refresh(); } @@ -72,7 +75,7 @@ class ChangeVoiceController extends GetxController { var item = timberList.firstWhereOrNull((e) => e.check); if (i == 0) { toneValue.value = value; - playerController.setPitch(toneValue.value); + if (Platform.isAndroid) playerController.setPitch(toneValue.value); if (item?.tone.toString() != NumUtil.formatNum(value)) { item?.check = false; timberList.refresh(); @@ -110,18 +113,20 @@ class ChangeVoiceController extends GetxController { int index = timberList.indexOf(timber); // Rapper // filter = ",aecho=0.8:0.88:60:0.4,areverb=50:50:100:100:0.5:0.5"; - - filter = index == 4 ? ",afftdn=nf=-30" : ",aresample=44100"; + if (index == 4) filter = ",afftdn=nf=-30"; } + // 获取采样率 + String sampleRate = await _getSampleRate() ?? '24000'; + // 构建 FFmpeg 命令 - final String command = '-i $filePath -af "asetrate=44100*${toneValue.value},atempo=${soundSpeedValue.value}$filter" $outputPath'; + final String command = '-i $filePath -af "asetrate=$sampleRate*${toneValue.value},atempo=${soundSpeedValue.value}$filter" $outputPath'; // 执行 FFmpeg 命令 FFmpegSession session = await FFmpegKit.execute(command); - // 检查执行结果 - ReturnCode? returnCode = await session.getReturnCode(); + // 获取执行结果 + final returnCode = await session.getReturnCode(); if (ReturnCode.isSuccess(returnCode)) { LogPrint.d('Audio processing successful'); try { @@ -142,6 +147,28 @@ class ChangeVoiceController extends GetxController { BaseEasyLoading.toast('Audio processing failed'); } } + + /// 获取音频的采样率 + Future _getSampleRate() async { + String? sampleRate; + String ffmpegCommand = '-i $filePath'; + FFmpegSession session = await FFmpegKit.execute(ffmpegCommand); + final output = await session.getOutput(); + if (ObjUtil.isNotEmpty(output)) { + // 使用正则表达式提取采样率 + final regex = RegExp(r'(\d+) Hz'); + final match = regex.firstMatch(output!); + if (match != null) { + sampleRate = match.group(1); + LogPrint.d('采样率: $sampleRate Hz'); + } else { + LogPrint.e('未找到采样率'); + } + } else { + LogPrint.e('获取采样率失败'); + } + return sampleRate; + } } class Timber { diff --git a/lib/modules/voice/change_voice/change_voice_view.dart b/lib/modules/voice/change_voice/change_voice_view.dart index 847f831..049565f 100644 --- a/lib/modules/voice/change_voice/change_voice_view.dart +++ b/lib/modules/voice/change_voice/change_voice_view.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; @@ -151,13 +153,31 @@ class ChangeVoiceView extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - i == 0 ? 'Tone' : 'Sound speed', - style: TextStyle( - color: Colors.white, - fontSize: 20.sp, - fontWeight: FontWeight.w700, - ), + Row( + children: [ + Text( + i == 0 ? 'Tone' : 'Sound speed', + style: TextStyle( + color: Colors.white, + fontSize: 20.sp, + fontWeight: FontWeight.w700, + ), + ), + Expanded( + child: Visibility( + visible: i == 0 && Platform.isIOS, + child: Text( + ' (Only take effect after saving)', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + ), + ), + ), + ), + ], ), SizedBox(height: 16.h), Row( @@ -188,14 +208,17 @@ class ChangeVoiceView extends StatelessWidget { }), ), ), - SizedBox(width: 16.w), + SizedBox(width: 20.w), Obx(() { - return Text( - i == 0 ? NumUtil.formatNum(controller.toneValue.value) : NumUtil.formatNum(controller.soundSpeedValue.value), - style: TextStyle( - color: const Color(0x73000000), - fontSize: 16.sp, - fontWeight: FontWeight.w600, + return SizedBox( + width: 40.w, + child: Text( + i == 0 ? NumUtil.formatNum(controller.toneValue.value) : NumUtil.formatNum(controller.soundSpeedValue.value), + style: TextStyle( + color: const Color(0x73000000), + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), ), ); }), diff --git a/lib/modules/voice/favourite/favourite_controller.dart b/lib/modules/voice/favourite/favourite_controller.dart index 8892e06..3747906 100644 --- a/lib/modules/voice/favourite/favourite_controller.dart +++ b/lib/modules/voice/favourite/favourite_controller.dart @@ -23,6 +23,10 @@ class FavouriteController extends GetxController { void getData() { voiceList.value = FavoriteData().getList().reversed.toList(); + _refreshList(); + } + + void _refreshList() { viewState.value = voiceList.isNotEmpty ? ViewState.normal : ViewState.empty; voiceList.refresh(); } @@ -43,7 +47,7 @@ class FavouriteController extends GetxController { voiceList.refresh(); // 若 item 和当前正播放的 item 是同个对象,则需要同步修改 - if (identical(item, InitialController.to.currentPlayVoiceModel.value)) { + if (item.path == InitialController.to.currentPlayVoiceModel.value?.path) { InitialController.to.currentPlayVoiceModel.update((e) => e?.name = value); } }, @@ -60,13 +64,13 @@ class FavouriteController extends GetxController { BaseEasyLoading.loading(); await item.delete(); voiceList.remove(item); - voiceList.refresh(); BaseEasyLoading.toast('Removed'); // 若 item 和当前正播放的 item 是同个对象,则需要同步修改 - if (identical(item, InitialController.to.currentPlayVoiceModel.value)) { + if (item.path == InitialController.to.currentPlayVoiceModel.value?.path) { InitialController.to.isFavourite.value = false; } + _refreshList(); }, ), ); diff --git a/lib/modules/voice/favourite/favourite_view.dart b/lib/modules/voice/favourite/favourite_view.dart index e364fa8..379493b 100644 --- a/lib/modules/voice/favourite/favourite_view.dart +++ b/lib/modules/voice/favourite/favourite_view.dart @@ -10,7 +10,7 @@ class FavouriteView extends GetView { @override Widget build(BuildContext context) { - Get.find(); + Get.put(FavouriteController()); return Obx(() { return ViewStateWidget( viewState: controller.viewState.value, diff --git a/lib/modules/voice/initial/initial_controller.dart b/lib/modules/voice/initial/initial_controller.dart index 3d01cd0..ffb62ea 100644 --- a/lib/modules/voice/initial/initial_controller.dart +++ b/lib/modules/voice/initial/initial_controller.dart @@ -40,8 +40,9 @@ class InitialController extends GetxController { super.onClose(); } - void onBottomAppBarItemChanged(int index) { + Future onBottomAppBarItemChanged(int index) async { if (index == 1) { + await PlayerController.to.stopPlay(); Get.toNamed(AppRoutes.uploadMethod); } else { if (index == 2) _refreshMe(); @@ -66,24 +67,22 @@ class InitialController extends GetxController { } Future onTapFavourite() async { - if (currentPlayVoiceModel.value != null) { - if (isFavourite.value) { - getIsFavouriteModel()?.delete(); - isFavourite.value = false; - } else { + if (isFavourite.value) { + getIsFavouriteModel()?.delete(); + isFavourite.value = false; + } else { + if (currentPlayVoiceModel.value != null) { await FavoriteData().addData(currentPlayVoiceModel.value!.copyWith()); isFavourite.value = true; } - _refreshMe(); } + _refreshMe(); } /// 刷新我的页面 void _refreshMe() { - if (Get.isRegistered()) { - if (Get.isRegistered()) MyVoiceController.to.getData(); - if (Get.isRegistered()) FavouriteController.to.getData(); - } + if (Get.isRegistered()) MyVoiceController.to.getData(); + if (Get.isRegistered()) FavouriteController.to.getData(); } } diff --git a/lib/modules/voice/my_voice/my_voice_controller.dart b/lib/modules/voice/my_voice/my_voice_controller.dart index 4587034..8a7811c 100644 --- a/lib/modules/voice/my_voice/my_voice_controller.dart +++ b/lib/modules/voice/my_voice/my_voice_controller.dart @@ -23,6 +23,10 @@ class MyVoiceController extends GetxController { void getData() { voiceList.value = MyVoiceData().getList().reversed.toList(); + _refreshList(); + } + + void _refreshList() { viewState.value = voiceList.isNotEmpty ? ViewState.normal : ViewState.empty; voiceList.refresh(); } @@ -60,8 +64,8 @@ class MyVoiceController extends GetxController { BaseEasyLoading.loading(); await item.delete(); voiceList.remove(item); - voiceList.refresh(); BaseEasyLoading.toast('Removed'); + _refreshList(); }, ), ); diff --git a/lib/modules/voice/my_voice/my_voice_view.dart b/lib/modules/voice/my_voice/my_voice_view.dart index 63d3362..44d0b2e 100644 --- a/lib/modules/voice/my_voice/my_voice_view.dart +++ b/lib/modules/voice/my_voice/my_voice_view.dart @@ -10,7 +10,7 @@ class MyVoiceView extends GetView { @override Widget build(BuildContext context) { - Get.find(); + Get.put(MyVoiceController()); return Obx(() { return ViewStateWidget( viewState: controller.viewState.value, diff --git a/lib/modules/voice/play_sound/play_sound_controller.dart b/lib/modules/voice/play_sound/play_sound_controller.dart index 9252329..def9b71 100644 --- a/lib/modules/voice/play_sound/play_sound_controller.dart +++ b/lib/modules/voice/play_sound/play_sound_controller.dart @@ -33,6 +33,7 @@ class PlaySoundController extends GetxController { } void goChangeVoice() async { + await PlayerController.to.stopPlay(); Get.toNamed(AppRoutes.changeVoice, arguments: voiceModel.path); } } diff --git a/lib/modules/voice/upload_method/upload_mothod_controller.dart b/lib/modules/voice/upload_method/upload_mothod_controller.dart index f9ccd58..b99b080 100644 --- a/lib/modules/voice/upload_method/upload_mothod_controller.dart +++ b/lib/modules/voice/upload_method/upload_mothod_controller.dart @@ -5,14 +5,12 @@ import 'package:file_picker/file_picker.dart'; import 'package:get/get.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:tone_snap/components/base_easyloading.dart'; -import 'package:tone_snap/controllers/player_controller.dart'; import 'package:tone_snap/routes/app_routes.dart'; import 'package:tone_snap/utils/permission_util.dart'; class UploadMethodController extends GetxController { Future goRecordSound() async { - await PlayerController.to.pausePlay(); Get.toNamed(AppRoutes.recordSound); } diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 22f6cd5..0354eb7 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -32,7 +32,7 @@ class AppPages { GetPage( name: AppRoutes.initial, page: () => InitialView(), - bindings: [InitialBinding(), HomeBinding(), MeBinding(), SettingsBinding(), MyVoiceBinding(), FavouriteBinding()], + bindings: [InitialBinding(), HomeBinding(), MeBinding(), SettingsBinding()], ), GetPage( name: AppRoutes.uploadMethod, diff --git a/lib/utils/local_path_util.dart b/lib/utils/local_path_util.dart index 54560e8..44fa169 100644 --- a/lib/utils/local_path_util.dart +++ b/lib/utils/local_path_util.dart @@ -29,7 +29,7 @@ class LocalPathUtil { /// 获取录音文件保存目录 static Future getRecordingsDir() async { - Directory cacheDir = await getTemporaryDirectory(); + Directory cacheDir = await getTemporaryPath(); Directory recordingsDir = Directory('${cacheDir.path}/recordings'); bool exist = await recordingsDir.exists(); if (!exist) { @@ -41,7 +41,7 @@ class LocalPathUtil { /// 获取录音文件保存目录 static Future getAssetsDir() async { - Directory cacheDir = await getTemporaryDirectory(); + Directory cacheDir = await getTemporaryPath(); Directory recordingsDir = Directory('${cacheDir.path}/assets'); bool exist = await recordingsDir.exists(); if (!exist) { @@ -53,7 +53,7 @@ class LocalPathUtil { /// 获取音频文件变声后输出目录 static Future getVoiceChangeOutputDir() async { - Directory cacheDir = await getTemporaryDirectory(); + Directory cacheDir = await getDocumentsPath(); Directory recordingsDir = Directory('${cacheDir.path}/change_voice'); bool exist = await recordingsDir.exists(); if (!exist) { diff --git a/lib/utils/permission_util.dart b/lib/utils/permission_util.dart index 6f9c1a8..6a67c6f 100644 --- a/lib/utils/permission_util.dart +++ b/lib/utils/permission_util.dart @@ -2,6 +2,8 @@ // Date: 2024/5/10 // Description: 权限处理 +import 'dart:io'; + import 'package:get/get.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:tone_snap/components/dialog/remind_dialog.dart'; @@ -32,9 +34,9 @@ class PermissionUtil { // 允许状态 case PermissionStatus.granted: case PermissionStatus.limited: - case PermissionStatus.provisional: return true; - // 永久拒绝 活动限制 + // 永久拒绝 + case PermissionStatus.provisional: case PermissionStatus.restricted: case PermissionStatus.permanentlyDenied: _showFailedDialog(newPermissionList, isPermanentlyDenied: true); @@ -59,45 +61,42 @@ class PermissionUtil { return currentPermissionStatus; } + /// 请求位置权限 static Future checkLocationAlways() async { // 获取前置状态 // Android没有这一步 ios会先访问这个再访问其他的 - PermissionStatus status = PermissionStatus.granted; - status = await _checkSinglePermission(Permission.locationWhenInUse); + PermissionStatus status1 = PermissionStatus.granted; + status1 = await _checkSinglePermission(Permission.locationWhenInUse); // 获取第二个状态 PermissionStatus status2 = PermissionStatus.denied; // 如果前置状态为成功才能执行获取第二个状态 - if (status.isGranted) { + if (status1.isGranted) { status2 = await _checkSinglePermission(Permission.locationAlways); } // 如果两个都成功那么就返回成功 - if (status.isGranted && status2.isGranted) { + if (status1.isGranted && status2.isGranted) { return true; - - // 如果有一个拒绝那么就失败了 - } else if (status.isDenied || status2.isDenied) { - _showFailedDialog([Permission.locationWhenInUse, Permission.locationAlways]); } else { + // 如果有一个拒绝那么就失败了 _showFailedDialog([Permission.locationWhenInUse, Permission.locationAlways], - isPermanentlyDenied: true, + isPermanentlyDenied: Platform.isIOS ? true : false, ); } return false; } + /// 请求单个权限 static _checkSinglePermission(Permission permission) async { // 获取当前状态 PermissionStatus status = await permission.status; PermissionStatus currentPermissionStatus = PermissionStatus.granted; - // 如果它状态不是允许那么就去获取 if (!status.isGranted) { currentPermissionStatus = await _requestPermission([permission]); } - // 返回最终状态 return currentPermissionStatus; } @@ -107,7 +106,7 @@ class PermissionUtil { Get.dialog( barrierDismissible: false, RemindDialog( - content: await _getInstructions(permissionList), + content: await _getDescription(permissionList), confirmText: isPermanentlyDenied ? 'Go open' : 'Confirm', confirmOnTap: () { if (isPermanentlyDenied) { @@ -120,20 +119,16 @@ class PermissionUtil { ); } - /// 获取权限使用说明 - static Future _getInstructions(List permissionList) async { + /// 获取权限描述 + static Future _getDescription(List permissionList) async { late Permission failedPermission; - - // 遍历当前权限申请列表 for (Permission permission in permissionList) { - PermissionStatus status = await permission.status; - - // 如果不是允许状态就添加到新的申请列表中 - if (!status.isGranted || !status.isLimited) { + if (!await permission.status.isGranted) { failedPermission = permission; break; } } + String description = ''; if (failedPermission == Permission.microphone) { description = 'We need to access the microphone to record or select audio files.'; diff --git a/pubspec.yaml b/pubspec.yaml index 064018f..9f80cbe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.2+4 +version: 1.0.2+5 environment: sdk: '>=3.4.1 <4.0.0'