nkbmusic/lib/audio/sound_settings.dart
metatroncubeswdev 178d2d00ab Nanga Kathai: Flutter app, self-hosted backend and song tools
- Flutter app (Android/iOS): home, explore, albums, search, player with
  equalizer, mini player, sign up/sign in with email codes, profile,
  encrypted offline downloads, About page
- server/: one-command deploy of self-hosted Supabase behind HestiaCP/nginx
  (or Caddy), migrations, backups, email templates
- supabase/migrations: schema, row-level security, profiles
- tools/: song scanner and importer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 10:39:26 -04:00

138 lines
4.5 KiB
Dart

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<int, double> customGains;
/// Loudness boost for quiet old recordings, 0 to 10 dB.
final double boostDb;
SoundState copyWith({bool? eqEnabled, EqPreset? preset, Map<int, double>? customGains, double? boostDb}) =>
SoundState(
eqEnabled: eqEnabled ?? this.eqEnabled,
preset: preset ?? this.preset,
customGains: customGains ?? this.customGains,
boostDb: boostDb ?? this.boostDb,
);
Map<String, Object> toJson() => {
'eq': eqEnabled,
'preset': preset.name,
'custom': {for (final e in customGains.entries) '${e.key}': e.value},
'boost': boostDb,
};
static SoundState fromJson(Map<String, dynamic> 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<SoundState> {
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<String, dynamic>);
} 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<void> 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<void> _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, SoundState>(SoundSettings.new);