目前iOS无法修改播放音频的音调,增加提示和判断

修复部分我的音频和喜欢数据变化列表未刷新问题
This commit is contained in:
fengshengxiong 2024-06-12 11:47:32 +08:00
parent 422a3f8802
commit 564f4b7b18
16 changed files with 145 additions and 95 deletions

View File

@ -519,12 +519,15 @@
baseConfigurationReference = 3D79BFA926F2692E951E423D /* Pods-RunnerTests.debug.xcconfig */; baseConfigurationReference = 3D79BFA926F2692E951E423D /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.fl.musicPlayer.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.fl.musicPlayer.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;

View File

@ -51,7 +51,6 @@ class RenameDialogState extends State<RenameDialog> {
canPop: true, canPop: true,
child: Center( child: Center(
child: SingleChildScrollView( child: SingleChildScrollView(
primary: true,
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Container( child: Container(
width: 0.8.sw, width: 0.8.sw,
@ -81,8 +80,6 @@ class RenameDialogState extends State<RenameDialog> {
color: const Color(0xFFf9f9fc), color: const Color(0xFFf9f9fc),
borderRadius: BorderRadius.circular(8).r, borderRadius: BorderRadius.circular(8).r,
), ),
child: FocusScope(
canRequestFocus: true,
child: TextField( child: TextField(
maxLines: 1, maxLines: 1,
controller: _textEditingController, controller: _textEditingController,
@ -106,7 +103,6 @@ class RenameDialogState extends State<RenameDialog> {
}, },
), ),
), ),
),
const DividerWidget(), const DividerWidget(),
SizedBox( SizedBox(
height: 52.h, height: 52.h,

View File

@ -140,7 +140,7 @@ class PlayerController extends GetxController {
isPlaying.value = false; isPlaying.value = false;
isCompleted.value = true; isCompleted.value = true;
positionValue.value = 0.0; positionValue.value = 0.0;
duration.value = Duration.zero; // duration.value = Duration.zero;
positionDuration.value = Duration.zero; positionDuration.value = Duration.zero;
} }

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:ffmpeg_kit_flutter_audio/ffmpeg_kit.dart'; import 'package:ffmpeg_kit_flutter_audio/ffmpeg_kit.dart';
import 'package:ffmpeg_kit_flutter_audio/ffmpeg_session.dart'; import 'package:ffmpeg_kit_flutter_audio/ffmpeg_session.dart';
import 'package:ffmpeg_kit_flutter_audio/return_code.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/local_path_util.dart';
import 'package:tone_snap/utils/log_print.dart'; import 'package:tone_snap/utils/log_print.dart';
import 'package:tone_snap/utils/num_util.dart'; import 'package:tone_snap/utils/num_util.dart';
import 'package:tone_snap/utils/obj_util.dart';
class ChangeVoiceController extends GetxController { class ChangeVoiceController extends GetxController {
var timberList = [ var timberList = [
@ -41,9 +44,9 @@ class ChangeVoiceController extends GetxController {
} }
@override @override
void onClose() { void onClose() async {
playerController.setSpeed(1.0); if (Platform.isAndroid) await playerController.setPitch(1.0);
playerController.setPitch(1.0); await playerController.setSpeed(1.0);
playerController.stopPlay(); playerController.stopPlay();
super.onClose(); super.onClose();
} }
@ -54,7 +57,7 @@ class ChangeVoiceController extends GetxController {
} }
toneValue.value = item.check ? item.tone : 1.0; toneValue.value = item.check ? item.tone : 1.0;
soundSpeedValue.value = item.check ? item.soundSpeed : 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); playerController.setSpeed(soundSpeedValue.value);
timberList.refresh(); timberList.refresh();
} }
@ -72,7 +75,7 @@ class ChangeVoiceController extends GetxController {
var item = timberList.firstWhereOrNull((e) => e.check); var item = timberList.firstWhereOrNull((e) => e.check);
if (i == 0) { if (i == 0) {
toneValue.value = value; toneValue.value = value;
playerController.setPitch(toneValue.value); if (Platform.isAndroid) playerController.setPitch(toneValue.value);
if (item?.tone.toString() != NumUtil.formatNum(value)) { if (item?.tone.toString() != NumUtil.formatNum(value)) {
item?.check = false; item?.check = false;
timberList.refresh(); timberList.refresh();
@ -110,18 +113,20 @@ class ChangeVoiceController extends GetxController {
int index = timberList.indexOf(timber); int index = timberList.indexOf(timber);
// Rapper // Rapper
// filter = ",aecho=0.8:0.88:60:0.4,areverb=50:50:100:100:0.5:0.5"; // filter = ",aecho=0.8:0.88:60:0.4,areverb=50:50:100:100:0.5:0.5";
if (index == 4) filter = ",afftdn=nf=-30";
filter = index == 4 ? ",afftdn=nf=-30" : ",aresample=44100";
} }
//
String sampleRate = await _getSampleRate() ?? '24000';
// FFmpeg // 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 // FFmpeg
FFmpegSession session = await FFmpegKit.execute(command); FFmpegSession session = await FFmpegKit.execute(command);
// //
ReturnCode? returnCode = await session.getReturnCode(); final returnCode = await session.getReturnCode();
if (ReturnCode.isSuccess(returnCode)) { if (ReturnCode.isSuccess(returnCode)) {
LogPrint.d('Audio processing successful'); LogPrint.d('Audio processing successful');
try { try {
@ -142,6 +147,28 @@ class ChangeVoiceController extends GetxController {
BaseEasyLoading.toast('Audio processing failed'); BaseEasyLoading.toast('Audio processing failed');
} }
} }
///
Future<String?> _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 { class Timber {

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -150,6 +152,8 @@ class ChangeVoiceView extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 24).w, padding: const EdgeInsets.symmetric(horizontal: 24).w,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [ children: [
Text( Text(
i == 0 ? 'Tone' : 'Sound speed', i == 0 ? 'Tone' : 'Sound speed',
@ -159,6 +163,22 @@ class ChangeVoiceView extends StatelessWidget {
fontWeight: FontWeight.w700, 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), SizedBox(height: 16.h),
Row( Row(
children: [ children: [
@ -188,15 +208,18 @@ class ChangeVoiceView extends StatelessWidget {
}), }),
), ),
), ),
SizedBox(width: 16.w), SizedBox(width: 20.w),
Obx(() { Obx(() {
return Text( return SizedBox(
width: 40.w,
child: Text(
i == 0 ? NumUtil.formatNum(controller.toneValue.value) : NumUtil.formatNum(controller.soundSpeedValue.value), i == 0 ? NumUtil.formatNum(controller.toneValue.value) : NumUtil.formatNum(controller.soundSpeedValue.value),
style: TextStyle( style: TextStyle(
color: const Color(0x73000000), color: const Color(0x73000000),
fontSize: 16.sp, fontSize: 16.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
),
); );
}), }),
], ],

View File

@ -23,6 +23,10 @@ class FavouriteController extends GetxController {
void getData() { void getData() {
voiceList.value = FavoriteData().getList().reversed.toList(); voiceList.value = FavoriteData().getList().reversed.toList();
_refreshList();
}
void _refreshList() {
viewState.value = voiceList.isNotEmpty ? ViewState.normal : ViewState.empty; viewState.value = voiceList.isNotEmpty ? ViewState.normal : ViewState.empty;
voiceList.refresh(); voiceList.refresh();
} }
@ -43,7 +47,7 @@ class FavouriteController extends GetxController {
voiceList.refresh(); voiceList.refresh();
// item item // 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); InitialController.to.currentPlayVoiceModel.update((e) => e?.name = value);
} }
}, },
@ -60,13 +64,13 @@ class FavouriteController extends GetxController {
BaseEasyLoading.loading(); BaseEasyLoading.loading();
await item.delete(); await item.delete();
voiceList.remove(item); voiceList.remove(item);
voiceList.refresh();
BaseEasyLoading.toast('Removed'); BaseEasyLoading.toast('Removed');
// item item // item item
if (identical(item, InitialController.to.currentPlayVoiceModel.value)) { if (item.path == InitialController.to.currentPlayVoiceModel.value?.path) {
InitialController.to.isFavourite.value = false; InitialController.to.isFavourite.value = false;
} }
_refreshList();
}, },
), ),
); );

View File

@ -10,7 +10,7 @@ class FavouriteView extends GetView<FavouriteController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Get.find<FavouriteController>(); Get.put(FavouriteController());
return Obx(() { return Obx(() {
return ViewStateWidget( return ViewStateWidget(
viewState: controller.viewState.value, viewState: controller.viewState.value,

View File

@ -40,8 +40,9 @@ class InitialController extends GetxController {
super.onClose(); super.onClose();
} }
void onBottomAppBarItemChanged(int index) { Future<void> onBottomAppBarItemChanged(int index) async {
if (index == 1) { if (index == 1) {
await PlayerController.to.stopPlay();
Get.toNamed(AppRoutes.uploadMethod); Get.toNamed(AppRoutes.uploadMethod);
} else { } else {
if (index == 2) _refreshMe(); if (index == 2) _refreshMe();
@ -66,25 +67,23 @@ class InitialController extends GetxController {
} }
Future<void> onTapFavourite() async { Future<void> onTapFavourite() async {
if (currentPlayVoiceModel.value != null) {
if (isFavourite.value) { if (isFavourite.value) {
getIsFavouriteModel()?.delete(); getIsFavouriteModel()?.delete();
isFavourite.value = false; isFavourite.value = false;
} else { } else {
if (currentPlayVoiceModel.value != null) {
await FavoriteData().addData(currentPlayVoiceModel.value!.copyWith()); await FavoriteData().addData(currentPlayVoiceModel.value!.copyWith());
isFavourite.value = true; isFavourite.value = true;
} }
_refreshMe();
} }
_refreshMe();
} }
/// ///
void _refreshMe() { void _refreshMe() {
if (Get.isRegistered<MeController>()) {
if (Get.isRegistered<MyVoiceController>()) MyVoiceController.to.getData(); if (Get.isRegistered<MyVoiceController>()) MyVoiceController.to.getData();
if (Get.isRegistered<FavouriteController>()) FavouriteController.to.getData(); if (Get.isRegistered<FavouriteController>()) FavouriteController.to.getData();
} }
}
} }
class PageItem { class PageItem {

View File

@ -23,6 +23,10 @@ class MyVoiceController extends GetxController {
void getData() { void getData() {
voiceList.value = MyVoiceData().getList().reversed.toList(); voiceList.value = MyVoiceData().getList().reversed.toList();
_refreshList();
}
void _refreshList() {
viewState.value = voiceList.isNotEmpty ? ViewState.normal : ViewState.empty; viewState.value = voiceList.isNotEmpty ? ViewState.normal : ViewState.empty;
voiceList.refresh(); voiceList.refresh();
} }
@ -60,8 +64,8 @@ class MyVoiceController extends GetxController {
BaseEasyLoading.loading(); BaseEasyLoading.loading();
await item.delete(); await item.delete();
voiceList.remove(item); voiceList.remove(item);
voiceList.refresh();
BaseEasyLoading.toast('Removed'); BaseEasyLoading.toast('Removed');
_refreshList();
}, },
), ),
); );

View File

@ -10,7 +10,7 @@ class MyVoiceView extends GetView<MyVoiceController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Get.find<MyVoiceController>(); Get.put(MyVoiceController());
return Obx(() { return Obx(() {
return ViewStateWidget( return ViewStateWidget(
viewState: controller.viewState.value, viewState: controller.viewState.value,

View File

@ -33,6 +33,7 @@ class PlaySoundController extends GetxController {
} }
void goChangeVoice() async { void goChangeVoice() async {
await PlayerController.to.stopPlay();
Get.toNamed(AppRoutes.changeVoice, arguments: voiceModel.path); Get.toNamed(AppRoutes.changeVoice, arguments: voiceModel.path);
} }
} }

View File

@ -5,14 +5,12 @@ import 'package:file_picker/file_picker.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:tone_snap/components/base_easyloading.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/routes/app_routes.dart';
import 'package:tone_snap/utils/permission_util.dart'; import 'package:tone_snap/utils/permission_util.dart';
class UploadMethodController extends GetxController { class UploadMethodController extends GetxController {
Future<void> goRecordSound() async { Future<void> goRecordSound() async {
await PlayerController.to.pausePlay();
Get.toNamed(AppRoutes.recordSound); Get.toNamed(AppRoutes.recordSound);
} }

View File

@ -32,7 +32,7 @@ class AppPages {
GetPage( GetPage(
name: AppRoutes.initial, name: AppRoutes.initial,
page: () => InitialView(), page: () => InitialView(),
bindings: [InitialBinding(), HomeBinding(), MeBinding(), SettingsBinding(), MyVoiceBinding(), FavouriteBinding()], bindings: [InitialBinding(), HomeBinding(), MeBinding(), SettingsBinding()],
), ),
GetPage( GetPage(
name: AppRoutes.uploadMethod, name: AppRoutes.uploadMethod,

View File

@ -29,7 +29,7 @@ class LocalPathUtil {
/// ///
static Future<Directory> getRecordingsDir() async { static Future<Directory> getRecordingsDir() async {
Directory cacheDir = await getTemporaryDirectory(); Directory cacheDir = await getTemporaryPath();
Directory recordingsDir = Directory('${cacheDir.path}/recordings'); Directory recordingsDir = Directory('${cacheDir.path}/recordings');
bool exist = await recordingsDir.exists(); bool exist = await recordingsDir.exists();
if (!exist) { if (!exist) {
@ -41,7 +41,7 @@ class LocalPathUtil {
/// ///
static Future<Directory> getAssetsDir() async { static Future<Directory> getAssetsDir() async {
Directory cacheDir = await getTemporaryDirectory(); Directory cacheDir = await getTemporaryPath();
Directory recordingsDir = Directory('${cacheDir.path}/assets'); Directory recordingsDir = Directory('${cacheDir.path}/assets');
bool exist = await recordingsDir.exists(); bool exist = await recordingsDir.exists();
if (!exist) { if (!exist) {
@ -53,7 +53,7 @@ class LocalPathUtil {
/// ///
static Future<Directory> getVoiceChangeOutputDir() async { static Future<Directory> getVoiceChangeOutputDir() async {
Directory cacheDir = await getTemporaryDirectory(); Directory cacheDir = await getDocumentsPath();
Directory recordingsDir = Directory('${cacheDir.path}/change_voice'); Directory recordingsDir = Directory('${cacheDir.path}/change_voice');
bool exist = await recordingsDir.exists(); bool exist = await recordingsDir.exists();
if (!exist) { if (!exist) {

View File

@ -2,6 +2,8 @@
// Date: 2024/5/10 // Date: 2024/5/10
// Description: // Description:
import 'dart:io';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:tone_snap/components/dialog/remind_dialog.dart'; import 'package:tone_snap/components/dialog/remind_dialog.dart';
@ -32,9 +34,9 @@ class PermissionUtil {
// //
case PermissionStatus.granted: case PermissionStatus.granted:
case PermissionStatus.limited: case PermissionStatus.limited:
case PermissionStatus.provisional:
return true; return true;
// //
case PermissionStatus.provisional:
case PermissionStatus.restricted: case PermissionStatus.restricted:
case PermissionStatus.permanentlyDenied: case PermissionStatus.permanentlyDenied:
_showFailedDialog(newPermissionList, isPermanentlyDenied: true); _showFailedDialog(newPermissionList, isPermanentlyDenied: true);
@ -59,45 +61,42 @@ class PermissionUtil {
return currentPermissionStatus; return currentPermissionStatus;
} }
///
static Future<bool> checkLocationAlways() async { static Future<bool> checkLocationAlways() async {
// //
// Android没有这一步 ios会先访问这个再访问其他的 // Android没有这一步 ios会先访问这个再访问其他的
PermissionStatus status = PermissionStatus.granted; PermissionStatus status1 = PermissionStatus.granted;
status = await _checkSinglePermission(Permission.locationWhenInUse); status1 = await _checkSinglePermission(Permission.locationWhenInUse);
// //
PermissionStatus status2 = PermissionStatus.denied; PermissionStatus status2 = PermissionStatus.denied;
// //
if (status.isGranted) { if (status1.isGranted) {
status2 = await _checkSinglePermission(Permission.locationAlways); status2 = await _checkSinglePermission(Permission.locationAlways);
} }
// //
if (status.isGranted && status2.isGranted) { if (status1.isGranted && status2.isGranted) {
return true; return true;
//
} else if (status.isDenied || status2.isDenied) {
_showFailedDialog([Permission.locationWhenInUse, Permission.locationAlways]);
} else { } else {
//
_showFailedDialog([Permission.locationWhenInUse, Permission.locationAlways], _showFailedDialog([Permission.locationWhenInUse, Permission.locationAlways],
isPermanentlyDenied: true, isPermanentlyDenied: Platform.isIOS ? true : false,
); );
} }
return false; return false;
} }
///
static _checkSinglePermission(Permission permission) async { static _checkSinglePermission(Permission permission) async {
// //
PermissionStatus status = await permission.status; PermissionStatus status = await permission.status;
PermissionStatus currentPermissionStatus = PermissionStatus.granted; PermissionStatus currentPermissionStatus = PermissionStatus.granted;
// //
if (!status.isGranted) { if (!status.isGranted) {
currentPermissionStatus = await _requestPermission([permission]); currentPermissionStatus = await _requestPermission([permission]);
} }
// //
return currentPermissionStatus; return currentPermissionStatus;
} }
@ -107,7 +106,7 @@ class PermissionUtil {
Get.dialog( Get.dialog(
barrierDismissible: false, barrierDismissible: false,
RemindDialog( RemindDialog(
content: await _getInstructions(permissionList), content: await _getDescription(permissionList),
confirmText: isPermanentlyDenied ? 'Go open' : 'Confirm', confirmText: isPermanentlyDenied ? 'Go open' : 'Confirm',
confirmOnTap: () { confirmOnTap: () {
if (isPermanentlyDenied) { if (isPermanentlyDenied) {
@ -120,20 +119,16 @@ class PermissionUtil {
); );
} }
/// 使 ///
static Future<String> _getInstructions(List<Permission> permissionList) async { static Future<String> _getDescription(List<Permission> permissionList) async {
late Permission failedPermission; late Permission failedPermission;
//
for (Permission permission in permissionList) { for (Permission permission in permissionList) {
PermissionStatus status = await permission.status; if (!await permission.status.isGranted) {
//
if (!status.isGranted || !status.isLimited) {
failedPermission = permission; failedPermission = permission;
break; break;
} }
} }
String description = ''; String description = '';
if (failedPermission == Permission.microphone) { if (failedPermission == Permission.microphone) {
description = 'We need to access the microphone to record or select audio files.'; description = 'We need to access the microphone to record or select audio files.';

View File

@ -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 # 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 # 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. # 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: environment:
sdk: '>=3.4.1 <4.0.0' sdk: '>=3.4.1 <4.0.0'