feat: loudnorm (#1358)

* feat: loudnorm

* fix

* fix: android only

* fix: toString
This commit is contained in:
My-Responsitories
2025-09-28 22:16:33 +08:00
committed by GitHub
parent 046412b709
commit 22c57bf468
14 changed files with 307 additions and 199 deletions

View File

@@ -23,6 +23,7 @@ import 'package:PiliPlus/pages/setting/models/model.dart';
import 'package:PiliPlus/pages/setting/widgets/select_dialog.dart';
import 'package:PiliPlus/pages/setting/widgets/slide_dialog.dart';
import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart';
import 'package:PiliPlus/plugin/pl_player/controller.dart';
import 'package:PiliPlus/utils/accounts.dart';
import 'package:PiliPlus/utils/cache_manage.dart';
import 'package:PiliPlus/utils/feed_back.dart';
@@ -32,6 +33,7 @@ import 'package:PiliPlus/utils/storage_key.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:PiliPlus/utils/update.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@@ -453,94 +455,27 @@ List<SettingsModel> get extraSettings => [
} catch (_) {}
},
),
SettingsModel(
settingsType: SettingsType.normal,
title: '音量均衡',
setKey: SettingBoxKey.audioNormalization,
leading: const Icon(Icons.multitrack_audio),
getSubtitle: () {
String audioNormalization = Pref.audioNormalization;
// TODO: remove next version
if (audioNormalization == '2') {
GStorage.setting.put(SettingBoxKey.audioNormalization, '1');
audioNormalization = '1';
}
audioNormalization = switch (audioNormalization) {
'0' => AudioNormalization.disable.title,
'1' => AudioNormalization.dynaudnorm.title,
_ => audioNormalization,
};
return '当前:「$audioNormalization';
},
onTap: (setState) async {
String? result = await showDialog(
context: Get.context!,
builder: (context) {
String audioNormalization = Pref.audioNormalization;
final values = {'0', '1', audioNormalization, '2'};
return SelectDialog<String>(
title: '音量均衡',
value: audioNormalization,
values: values
.map(
(e) => (
e,
switch (e) {
'0' => AudioNormalization.disable.title,
'1' => AudioNormalization.dynaudnorm.title,
'2' => AudioNormalization.custom.title,
_ => e,
},
),
)
.toList(),
);
},
);
if (result != null) {
if (result == '2') {
String param = '';
showDialog(
context: Get.context!,
builder: (context) {
return AlertDialog(
title: const Text('自定义参数'),
content: TextField(
autofocus: true,
onChanged: (value) => param = value,
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
onPressed: () async {
Get.back();
await GStorage.setting.put(
SettingBoxKey.audioNormalization,
param,
);
setState();
},
child: const Text('确定'),
),
],
);
},
);
if (kDebugMode || Platform.isAndroid)
SettingsModel(
settingsType: SettingsType.normal,
title: '音量均衡',
setKey: SettingBoxKey.audioNormalization,
leading: const Icon(Icons.multitrack_audio),
getSubtitle: () {
final audioNormalization = AudioNormalization.getTitleFromConfig(
Pref.audioNormalization,
);
String fallback = Pref.fallbackNormalization;
if (fallback == '0') {
fallback = '';
} else {
await GStorage.setting.put(SettingBoxKey.audioNormalization, result);
setState();
fallback =
',无参数时:「${AudioNormalization.getTitleFromConfig(fallback)}';
}
}
},
),
return '当前:「$audioNormalization$fallback';
},
onTap: audioNormalization,
),
SettingsModel(
settingsType: SettingsType.normal,
title: '超分辨率',
@@ -1188,3 +1123,99 @@ List<SettingsModel> get extraSettings => [
},
),
];
Future<void> audioNormalization(
VoidCallback setState, {
bool fallback = false,
}) async {
final key = fallback
? SettingBoxKey.fallbackNormalization
: SettingBoxKey.audioNormalization;
final result = await showDialog<String>(
context: Get.context!,
builder: (context) {
String audioNormalization = fallback
? Pref.fallbackNormalization
: Pref.audioNormalization;
Set<String> values = {
'0',
'1',
if (!fallback) '2',
audioNormalization,
'3',
};
return SelectDialog<String>(
title: fallback ? '服务器无loudnorm配置时使用' : '音量均衡',
toggleable: true,
value: audioNormalization,
values: values
.map(
(e) => (
e,
switch (e) {
'0' => AudioNormalization.disable.title,
'1' => AudioNormalization.dynaudnorm.title,
'2' => AudioNormalization.loudnorm.title,
'3' => AudioNormalization.custom.title,
_ => e,
},
),
)
.toList(),
);
},
);
if (result != null) {
if (result == '3') {
String param = '';
await showDialog(
context: Get.context!,
builder: (context) {
return AlertDialog(
title: const Text('自定义参数'),
content: Column(
mainAxisSize: MainAxisSize.min,
spacing: 16,
children: [
const Text('等同于 --lavfi-complex="[aid1] 参数 [ao]"'),
TextField(
autofocus: true,
onChanged: (value) => param = value,
),
],
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: ColorScheme.of(context).outline,
),
),
),
TextButton(
onPressed: () async {
Get.back();
await GStorage.setting.put(key, param);
if (!fallback &&
PlPlayerController.loudnormRegExp.hasMatch(param)) {
audioNormalization(setState, fallback: true);
}
setState();
},
child: const Text('确定'),
),
],
);
},
);
} else {
await GStorage.setting.put(key, result);
if (result == '2') {
audioNormalization(setState, fallback: true);
}
setState();
}
}
}

View File

@@ -58,7 +58,7 @@ class SettingsModel {
SettingsType.sw1tch => SetSwitchItem(
title: title,
subtitle: subtitle,
setKey: setKey,
setKey: setKey!,
defaultVal: defaultVal,
onChanged: onChanged,
needReboot: needReboot,

View File

@@ -16,6 +16,7 @@ class SelectDialog<T> extends StatelessWidget {
final String title;
final List<(T, String)> values;
final Widget Function(BuildContext, int)? subtitleBuilder;
final bool toggleable;
const SelectDialog({
super.key,
@@ -23,11 +24,12 @@ class SelectDialog<T> extends StatelessWidget {
required this.values,
required this.title,
this.subtitleBuilder,
this.toggleable = false,
});
@override
Widget build(BuildContext context) {
final titleMedium = Theme.of(context).textTheme.titleMedium!;
final titleMedium = TextTheme.of(context).titleMedium!;
return AlertDialog(
clipBehavior: Clip.hardEdge,
title: Text(title),
@@ -37,7 +39,7 @@ class SelectDialog<T> extends StatelessWidget {
contentPadding: const EdgeInsets.symmetric(vertical: 12),
content: SingleChildScrollView(
child: RadioGroup<T>(
onChanged: Navigator.of(context).pop,
onChanged: (v) => Navigator.of(context).pop(v ?? value),
groupValue: value,
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -46,6 +48,7 @@ class SelectDialog<T> extends StatelessWidget {
(index) {
final item = values[index];
return RadioListTile<T>(
toggleable: toggleable,
dense: true,
value: item.$1,
title: Text(

View File

@@ -9,7 +9,7 @@ import 'package:get/get.dart';
class SetSwitchItem extends StatefulWidget {
final String? title;
final String? subtitle;
final String? setKey;
final String setKey;
final bool defaultVal;
final ValueChanged<bool>? onChanged;
final bool needReboot;
@@ -21,7 +21,7 @@ class SetSwitchItem extends StatefulWidget {
const SetSwitchItem({
this.title,
this.subtitle,
this.setKey,
required this.setKey,
this.defaultVal = false,
this.onChanged,
this.needReboot = false,