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 _bands = const []; List _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 get bands => _bands; List 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 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 attach(int sessionId) async { try { final config = await _channel .invokeMapMethod('attach', {'sessionId': sessionId}); if (config == null) return; _bands = [ for (final raw in (config['bands'] as List).cast()) 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(); _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 setEnabled(bool value) async { _enabled = value; notifyListeners(); await _invoke('setEnabled', {'enabled': value}); final prefs = await SharedPreferences.getInstance(); await prefs.setBool(_keyEnabled, value); } Future 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 usePreset(int preset) async { final levels = await _invoke>('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 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 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 openSystemPanel() async { return await _invoke('openSystemPanel', null) ?? false; } Future _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 _saveLevels() async { final prefs = await SharedPreferences.getInstance(); await prefs.setStringList( _keyLevels, [for (final b in _bands) b.level.toString()]); } Future _invoke(String method, Map? args) async { try { return await _channel.invokeMethod(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; } } }