- Native Bruecke AudioEffectsBridge.kt: Equalizer + BassBoost + Virtualizer auf der Audio-Session des Players, inkl. System-Klangeffekt-Panel - Dart-Seite AudioEffects: Presets, Baender, Bass/Immersiv, persistiert - EqualizerScreen nach Vorlage inkl. Drehregler (CustomPainter) - Auto-Wiedergabe bei verbundenen Kopfhoerern (Klinke + Bluetooth) - MODIFY_AUDIO_SETTINGS im Manifest - Einstellungen: neuer Bereich "Ton" Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011snPXPafPC5i87o68H14W7
199 lines
6.6 KiB
Dart
199 lines
6.6 KiB
Dart
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:shared_preferences/shared_preferences.dart';
|
||
|
||
/// Ein Equalizer-Band, wie es das Gerät meldet.
|
||
class EqBand {
|
||
const EqBand({required this.index, required this.centerHz, required this.level});
|
||
final int index;
|
||
|
||
/// Mittenfrequenz in Hz (Android liefert Milli-Hertz).
|
||
final int centerHz;
|
||
|
||
/// Pegel in Millibel.
|
||
final int level;
|
||
|
||
EqBand withLevel(int newLevel) =>
|
||
EqBand(index: index, centerHz: centerHz, level: newLevel);
|
||
|
||
/// "60Hz" bzw. "14.0kHz" — wie in der Vorlage.
|
||
String get label => centerHz >= 1000
|
||
? '${(centerHz / 1000).toStringAsFixed(1)}kHz'
|
||
: '${centerHz}Hz';
|
||
}
|
||
|
||
/// Steuert die nativen Android-Klangeffekte (Equalizer, Bass, Immersives
|
||
/// Audio) und merkt sich die Einstellungen über Neustarts hinweg.
|
||
///
|
||
/// Die Effekte hängen an der Audio-Session des Players. Die meldet just_audio
|
||
/// erst beim Abspielen und bei jedem Wechsel neu — deshalb [attach].
|
||
class AudioEffects extends ChangeNotifier {
|
||
AudioEffects({MethodChannel? channel})
|
||
: _channel = channel ?? const MethodChannel('de.baka.melo/audio_effects');
|
||
|
||
final MethodChannel _channel;
|
||
|
||
static const _keyEnabled = 'eq_aktiv';
|
||
static const _keyLevels = 'eq_pegel';
|
||
static const _keyBass = 'eq_bass';
|
||
static const _keyVirtualizer = 'eq_immersiv';
|
||
|
||
bool _enabled = false;
|
||
bool _available = false;
|
||
List<EqBand> _bands = const [];
|
||
List<String> _presets = const [];
|
||
int _minLevel = -1500;
|
||
int _maxLevel = 1500;
|
||
int _bass = 0;
|
||
int _virtualizer = 0;
|
||
bool _bassSupported = false;
|
||
bool _virtualizerSupported = false;
|
||
String? _error;
|
||
|
||
bool get enabled => _enabled;
|
||
|
||
/// Ob das Gerät die Effekte überhaupt bereitgestellt hat.
|
||
bool get available => _available;
|
||
List<EqBand> get bands => _bands;
|
||
List<String> get presets => _presets;
|
||
int get minLevel => _minLevel;
|
||
int get maxLevel => _maxLevel;
|
||
|
||
/// Bass-Stärke 0–1000 (Android-Skala).
|
||
int get bass => _bass;
|
||
|
||
/// Virtualizer-Stärke 0–1000 ("Immersives Audio").
|
||
int get virtualizer => _virtualizer;
|
||
bool get bassSupported => _bassSupported;
|
||
bool get virtualizerSupported => _virtualizerSupported;
|
||
String? get error => _error;
|
||
|
||
/// Lädt die gespeicherten Werte. Angewendet werden sie, sobald über
|
||
/// [attach] eine Audio-Session bekannt ist.
|
||
Future<void> init() async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
_enabled = prefs.getBool(_keyEnabled) ?? false;
|
||
_bass = prefs.getInt(_keyBass) ?? 0;
|
||
_virtualizer = prefs.getInt(_keyVirtualizer) ?? 0;
|
||
notifyListeners();
|
||
}
|
||
|
||
/// Verbindet die Effekte mit der Audio-Session [sessionId] und stellt die
|
||
/// gespeicherten Werte wieder her.
|
||
Future<void> attach(int sessionId) async {
|
||
try {
|
||
final config = await _channel
|
||
.invokeMapMethod<String, dynamic>('attach', {'sessionId': sessionId});
|
||
if (config == null) return;
|
||
_bands = [
|
||
for (final raw in (config['bands'] as List).cast<Map>())
|
||
EqBand(
|
||
index: raw['index'] as int,
|
||
// Android liefert Milli-Hertz.
|
||
centerHz: (raw['centerFrequencyMilliHz'] as int) ~/ 1000,
|
||
level: raw['level'] as int,
|
||
),
|
||
];
|
||
_presets = (config['presets'] as List).cast<String>();
|
||
_minLevel = config['minLevel'] as int;
|
||
_maxLevel = config['maxLevel'] as int;
|
||
_bassSupported = config['bassSupported'] as bool? ?? false;
|
||
_virtualizerSupported = config['virtualizerSupported'] as bool? ?? false;
|
||
_available = true;
|
||
_error = null;
|
||
await _restoreSavedLevels();
|
||
await setEnabled(_enabled);
|
||
await setBass(_bass);
|
||
await setVirtualizer(_virtualizer);
|
||
} on PlatformException catch (e) {
|
||
_available = false;
|
||
_error = e.message;
|
||
}
|
||
notifyListeners();
|
||
}
|
||
|
||
Future<void> setEnabled(bool value) async {
|
||
_enabled = value;
|
||
notifyListeners();
|
||
await _invoke('setEnabled', {'enabled': value});
|
||
final prefs = await SharedPreferences.getInstance();
|
||
await prefs.setBool(_keyEnabled, value);
|
||
}
|
||
|
||
Future<void> setBandLevel(int index, int level) async {
|
||
final clamped = level.clamp(_minLevel, _maxLevel);
|
||
_bands = [
|
||
for (final band in _bands)
|
||
band.index == index ? band.withLevel(clamped) : band,
|
||
];
|
||
notifyListeners();
|
||
await _invoke('setBandLevel', {'band': index, 'level': clamped});
|
||
await _saveLevels();
|
||
}
|
||
|
||
Future<void> usePreset(int preset) async {
|
||
final levels = await _invoke<List<Object?>>('usePreset', {'preset': preset});
|
||
if (levels != null && levels.length == _bands.length) {
|
||
_bands = [
|
||
for (var i = 0; i < _bands.length; i++)
|
||
_bands[i].withLevel(levels[i] as int),
|
||
];
|
||
notifyListeners();
|
||
await _saveLevels();
|
||
}
|
||
}
|
||
|
||
Future<void> setBass(int strength) async {
|
||
_bass = strength.clamp(0, 1000);
|
||
notifyListeners();
|
||
await _invoke('setBassStrength', {'strength': _bass});
|
||
final prefs = await SharedPreferences.getInstance();
|
||
await prefs.setInt(_keyBass, _bass);
|
||
}
|
||
|
||
Future<void> setVirtualizer(int strength) async {
|
||
_virtualizer = strength.clamp(0, 1000);
|
||
notifyListeners();
|
||
await _invoke('setVirtualizerStrength', {'strength': _virtualizer});
|
||
final prefs = await SharedPreferences.getInstance();
|
||
await prefs.setInt(_keyVirtualizer, _virtualizer);
|
||
}
|
||
|
||
/// Öffnet die Klangeffekte des Systems (auf POCO/Xiaomi Dolby Atmos).
|
||
/// Gibt false zurück, wenn das Gerät keine solche Oberfläche anbietet.
|
||
Future<bool> openSystemPanel() async {
|
||
return await _invoke<bool>('openSystemPanel', null) ?? false;
|
||
}
|
||
|
||
Future<void> _restoreSavedLevels() async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
final saved = prefs.getStringList(_keyLevels);
|
||
if (saved == null || saved.length != _bands.length) return;
|
||
for (var i = 0; i < _bands.length; i++) {
|
||
final level = int.tryParse(saved[i]);
|
||
if (level == null) continue;
|
||
_bands[i] = _bands[i].withLevel(level);
|
||
await _invoke('setBandLevel', {'band': _bands[i].index, 'level': level});
|
||
}
|
||
}
|
||
|
||
Future<void> _saveLevels() async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
await prefs.setStringList(
|
||
_keyLevels, [for (final b in _bands) b.level.toString()]);
|
||
}
|
||
|
||
Future<T?> _invoke<T>(String method, Map<String, dynamic>? args) async {
|
||
try {
|
||
return await _channel.invokeMethod<T>(method, args);
|
||
} on PlatformException catch (e) {
|
||
_error = e.message;
|
||
notifyListeners();
|
||
return null;
|
||
} on MissingPluginException {
|
||
// Kein Android (Tests, Desktop): Effekte gibt es dort nicht.
|
||
return null;
|
||
}
|
||
}
|
||
}
|