import 'dart:convert'; import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:just_audio/just_audio.dart'; import '../data/user_library.dart'; import 'player_providers.dart'; /// Equalizer presets, described as a gain curve over frequency so they fit /// whatever bands the phone's equalizer has (usually 5). enum EqPreset { flat('Flat'), bass('Bass boost'), vocal('Vocal'), festival('Festival'), treble('Treble'), custom('Custom'); const EqPreset(this.label); final String label; /// Gain in dB for a band centred on [hz]; null for [custom]. double? gainAt(double hz) => switch (this) { flat => 0, bass => hz < 120 ? 6 : hz < 400 ? 3.5 : 0, vocal => hz < 200 ? -2 : hz < 600 ? 1 : hz < 5000 ? 4 : 1, festival => hz < 200 ? 5 : hz < 1500 ? 0 : hz < 5000 ? 2 : 4, treble => hz < 1000 ? 0 : hz < 5000 ? 2.5 : 5, custom => null, }; } @immutable class SoundState { const SoundState({ this.eqEnabled = false, this.preset = EqPreset.flat, this.customGains = const {}, this.boostDb = 0, }); final bool eqEnabled; final EqPreset preset; /// Band index -> dB, used when [preset] is custom. final Map customGains; /// Loudness boost for quiet old recordings, 0 to 10 dB. final double boostDb; SoundState copyWith({bool? eqEnabled, EqPreset? preset, Map? customGains, double? boostDb}) => SoundState( eqEnabled: eqEnabled ?? this.eqEnabled, preset: preset ?? this.preset, customGains: customGains ?? this.customGains, boostDb: boostDb ?? this.boostDb, ); Map toJson() => { 'eq': eqEnabled, 'preset': preset.name, 'custom': {for (final e in customGains.entries) '${e.key}': e.value}, 'boost': boostDb, }; static SoundState fromJson(Map j) => SoundState( eqEnabled: j['eq'] as bool? ?? false, preset: EqPreset.values.asNameMap()[j['preset']] ?? EqPreset.flat, customGains: { for (final e in ((j['custom'] as Map?) ?? {}).entries) int.parse(e.key as String): (e.value as num).toDouble(), }, boostDb: (j['boost'] as num?)?.toDouble() ?? 0, ); } /// Whether this platform supports the in-app equalizer and boost. bool get soundEffectsSupported => !kIsWeb && defaultTargetPlatform == TargetPlatform.android; class SoundSettings extends Notifier { static const _key = 'sound_settings'; @override SoundState build() { final raw = ref.read(sharedPrefsProvider).getString(_key); SoundState state; try { state = raw == null ? const SoundState() : SoundState.fromJson(jsonDecode(raw) as Map); } catch (_) { state = const SoundState(); } _apply(state); return state; } AndroidEqualizer get _eq => ref.read(audioHandlerProvider).equalizer; AndroidLoudnessEnhancer get _boost => ref.read(audioHandlerProvider).loudness; void setEnabled(bool on) => _update(state.copyWith(eqEnabled: on)); void setPreset(EqPreset preset) => _update(state.copyWith(preset: preset, eqEnabled: true)); /// Moving a band switches to the custom preset, starting from the current curve. Future setBandGain(int index, double db) async { final params = await _eq.parameters; final gains = {for (final b in params.bands) b.index: b.gain}..[index] = db; _update(state.copyWith(preset: EqPreset.custom, customGains: gains, eqEnabled: true)); } void setBoost(double db) => _update(state.copyWith(boostDb: db)); void reset() => _update(const SoundState()); void _update(SoundState next) { state = next; ref.read(sharedPrefsProvider).setString(_key, jsonEncode(next.toJson())); _apply(next); } Future _apply(SoundState s) async { if (!soundEffectsSupported) return; await _boost.setEnabled(s.boostDb > 0); await _boost.setTargetGain(s.boostDb); await _eq.setEnabled(s.eqEnabled); // Parameters arrive once the player has an audio session (first song loaded). final params = await _eq.parameters; for (final band in params.bands) { final db = s.preset == EqPreset.custom ? (s.customGains[band.index] ?? 0) : s.preset.gainAt(band.centerFrequency)!; await band.setGain(math.max(params.minDecibels, math.min(params.maxDecibels, db))); } } } final soundSettingsProvider = NotifierProvider(SoundSettings.new);