import 'package:audioplayers/audioplayers.dart'; /// Looping background music, independent of the SFX pool in [AudioService]. /// One track plays at a time from `assets/audio/bgm/.mp3`. A track change /// (e.g. switching seasons) restarts; toggling sound off or backgrounding the /// app pauses/resumes the same track. Every player call is swallowed — music /// must never break the app, and a missing track asset is silently ignored. class MusicService { MusicService({bool enabled = true}) : _enabled = enabled; /// Background music sits well under the SFX so it never competes. static const double _volume = 0.45; final AudioPlayer _player = AudioPlayer(); bool _enabled; bool _backgrounded = false; String? _currentKey; bool get enabled => _enabled; bool get _shouldPlay => _enabled && !_backgrounded && _currentKey != null; /// Request a looping track by theme key (e.g. 'menu', 'season_001'). No-op /// if it is already the current track (avoids restarting on every rebuild). Future playKey(String key) async { if (key == _currentKey) { await _reconcile(); return; } _currentKey = key; await _restart(); } set enabled(bool value) { if (_enabled == value) return; _enabled = value; _reconcile(); } /// App lifecycle: pause when backgrounded, resume on return. void setBackgrounded(bool value) { if (_backgrounded == value) return; _backgrounded = value; _reconcile(); } Future _restart() async { try { await _player.stop(); if (!_shouldPlay) return; await _player.setReleaseMode(ReleaseMode.loop); await _player.play( AssetSource('audio/bgm/$_currentKey.mp3'), volume: _volume, ); } catch (_) { // Missing asset or platform hiccup: stay silent. } } /// Pause/resume the CURRENT track to match [_shouldPlay] without restarting. Future _reconcile() async { try { if (_shouldPlay) { if (_player.state == PlayerState.paused) { await _player.resume(); } else if (_player.state != PlayerState.playing) { await _restart(); } } else if (_player.state == PlayerState.playing) { await _player.pause(); } } catch (_) { // Never throw from lifecycle/toggle changes. } } void dispose() => _player.dispose(); }