mucke/lib/system/datasources/audio_player_data_source_impl.dart
2021-04-11 20:42:50 +02:00

428 lines
13 KiB
Dart

import 'dart:math';
import 'package:just_audio/just_audio.dart' as ja;
import 'package:logging/logging.dart';
import 'package:rxdart/rxdart.dart';
import '../../domain/entities/loop_mode.dart';
import '../models/loop_mode_model.dart';
import '../models/playback_event_model.dart';
import '../models/song_model.dart';
import 'audio_player_data_source.dart';
const int LOAD_INTERVAL = 2;
const int LOAD_MAX = 6;
class AudioPlayerDataSourceImpl implements AudioPlayerDataSource {
AudioPlayerDataSourceImpl(this._audioPlayer) {
_audioPlayer.currentIndexStream.listen((index) async {
_log.info('currentIndexSteam.listen: $index');
if (!_lockUpdate) {
if (!await _updateLoadedQueue(index)) {
_updateCurrentIndex(index);
}
}
});
_audioPlayer.playingStream.listen((event) => _playingSubject.add(event));
_audioPlayer.positionStream.listen((event) => _positionSubject.add(event));
_playbackEventModelStream = Rx.combineLatest2<ja.PlaybackEvent, bool, PlaybackEventModel>(
_audioPlayer.playbackEventStream,
_audioPlayer.playingStream,
(a, b) => PlaybackEventModel.fromJAPlaybackEvent(a, b),
).distinct();
}
final ja.AudioPlayer _audioPlayer;
ja.ConcatenatingAudioSource _audioSource;
static final _log = Logger('AudioPlayer');
bool _lockUpdate = false;
final BehaviorSubject<int> _currentIndexSubject = BehaviorSubject();
final BehaviorSubject<PlaybackEventModel> _playbackEventSubject = BehaviorSubject();
final BehaviorSubject<bool> _playingSubject = BehaviorSubject();
final BehaviorSubject<Duration> _positionSubject = BehaviorSubject();
Stream<PlaybackEventModel> _playbackEventModelStream;
List<SongModel> _queue;
int _loadStartIndex;
int _loadEndIndex;
@override
ValueStream<int> get currentIndexStream => _currentIndexSubject.stream;
@override
Stream<PlaybackEventModel> get playbackEventStream => _playbackEventModelStream;
@override
ValueStream<Duration> get positionStream => _positionSubject.stream;
@override
ValueStream<bool> get playingStream => _playingSubject.stream;
@override
Future<void> dispose() async {
await _currentIndexSubject.close();
await _playbackEventSubject.close();
await _positionSubject.close();
await _audioPlayer.dispose();
}
@override
Future<void> loadQueue({List<SongModel> queue, int initialIndex = 0}) async {
if (queue == null || initialIndex == null || initialIndex >= queue.length) {
return;
}
_queue = queue;
final queueToLoad = _getQueueToLoad(queue, initialIndex);
_audioSource = _songModelsToAudioSource(queueToLoad);
await _audioPlayer.setAudioSource(_audioSource, initialIndex: _calcSourceIndex(initialIndex));
_currentIndexSubject.add(initialIndex);
}
@override
Future<void> pause() async {
await _audioPlayer.pause();
}
@override
Future<void> play() async {
_audioPlayer.play();
}
@override
Future<bool> seekToNext() async {
final result = _audioPlayer.hasNext;
await _audioPlayer.seekToNext();
return result;
}
@override
Future<void> seekToPrevious() async {
if (_audioPlayer.position > const Duration(seconds: 3) || !_audioPlayer.hasPrevious) {
await _audioPlayer.seek(const Duration(seconds: 0));
} else {
await _audioPlayer.seekToPrevious();
}
}
@override
Future<void> seekToIndex(int index) async {
if (_isQueueIndexInSaveInterval(index)) {
await _audioPlayer.seek(const Duration(seconds: 0), index: _calcSourceIndex(index));
} else {
await loadQueue(queue: _queue, initialIndex: index);
}
}
@override
Future<void> stop() async {
_audioPlayer.stop();
}
@override
Future<void> addToQueue(SongModel song) async {
_queue.add(song);
if (_loadStartIndex < _loadEndIndex) {
if (_loadEndIndex == _queue.length) {
_loadEndIndex++;
await _audioSource.add(ja.AudioSource.uri(Uri.file(song.path)));
}
} else {
await _audioSource.add(ja.AudioSource.uri(Uri.file(song.path)));
}
}
@override
Future<void> moveQueueItem(int oldIndex, int newIndex) async {
final oldCurrentIndex = currentIndexStream.value;
int newCurrentIndex = oldCurrentIndex;
if (oldIndex == oldCurrentIndex) {
newCurrentIndex = newIndex;
} else {
if (oldIndex < oldCurrentIndex) {
newCurrentIndex--;
}
if (newIndex < oldCurrentIndex) {
newCurrentIndex++;
}
}
final song = _queue[oldIndex];
final newQueue = List<SongModel>.from(_queue);
newQueue.removeAt(oldIndex);
newQueue.insert(newIndex, song);
final before = newQueue.sublist(0, newCurrentIndex);
final after = newQueue.sublist(min(newCurrentIndex + 1, newQueue.length));
replaceQueueAroundCurrentIndex(
before: before,
after: after,
);
}
@override
Future<void> playNext(SongModel song) async {
final index = currentIndexStream.value + 1;
_queue.insert(index, song);
if (index < _loadEndIndex) {
_loadEndIndex++;
}
await _audioSource.insert(
_audioPlayer.currentIndex + 1, ja.AudioSource.uri(Uri.file(song.path)));
}
@override
Future<void> removeQueueIndex(int index) async {
_queue.removeAt(index);
if (_isQueueIndexInLoadInterval(index)) {
if (index < _loadStartIndex) _loadStartIndex--;
if (index < _loadEndIndex) _loadEndIndex--;
await _audioSource.removeAt(_calcSourceIndex(index));
}
}
// TODO: maybe remove this
@override
Future<void> replaceQueueAroundIndex({
List<SongModel> before,
List<SongModel> after,
int index,
}) async {
_log.info('REPLACE QUEUE AROUND INDEX: $index');
_queue = before + [_queue[index]] + after;
final newIndex = before.length;
final oldSourceIndex = _calcSourceIndex(index);
final queueToLoad = _getQueueToLoad(_queue, newIndex);
final newSourceIndex = _calcSourceIndex(newIndex);
final _before = _songModelsToAudioSource(queueToLoad.sublist(0, newSourceIndex));
final _after = _songModelsToAudioSource(
queueToLoad.sublist(min(newSourceIndex + 1, queueToLoad.length)),
);
_lockUpdate = true;
await _audioSource.removeRange(0, oldSourceIndex);
await _audioSource.removeRange(1, _audioSource.length);
await _audioSource.insertAll(0, _before.children);
await _audioSource.addAll(_after.children);
_lockUpdate = false;
_updateCurrentIndex(newSourceIndex);
}
Future<void> replaceQueueAroundCurrentIndex({
List<SongModel> before,
List<SongModel> after,
}) async {
_log.info('REPLACE QUEUE AROUND CURRENT INDEX');
final currIndex = currentIndexStream.value;
final newIndex = before.length;
_queue = before + [_queue[currIndex]] + after;
final oldSourceIndex = _calcSourceIndex(currIndex);
final queueToLoad = _getQueueToLoad(_queue, newIndex);
final newSourceIndex = _calcSourceIndex(newIndex);
final _before = _songModelsToAudioSource(queueToLoad.sublist(0, newSourceIndex));
final _after = _songModelsToAudioSource(
queueToLoad.sublist(min(newSourceIndex + 1, queueToLoad.length)),
);
_lockUpdate = true;
await _audioSource.removeRange(0, oldSourceIndex);
await _audioSource.removeRange(1, _audioSource.length);
await _audioSource.insertAll(0, _before.children);
await _audioSource.addAll(_after.children);
_lockUpdate = false;
_updateCurrentIndex(newSourceIndex);
}
@override
Future<void> setLoopMode(LoopMode loopMode) async {
if (loopMode == null) return;
await _audioPlayer.setLoopMode(loopMode.toJA());
}
ja.ConcatenatingAudioSource _songModelsToAudioSource(List<SongModel> songModels) {
return ja.ConcatenatingAudioSource(
children: songModels.map((SongModel m) => ja.AudioSource.uri(Uri.file(m.path))).toList(),
);
}
/// Determine the songs to load and set loadStartIndex/loadEndIndex accordingly.
List<SongModel> _getQueueToLoad(List<SongModel> queue, int initialIndex) {
if (queue.length > LOAD_MAX) {
_loadStartIndex = (initialIndex - LOAD_INTERVAL) % queue.length;
_loadEndIndex = (initialIndex + LOAD_INTERVAL + 1) % queue.length;
List<SongModel> smallQueue;
if (_loadStartIndex < _loadEndIndex) {
smallQueue = queue.sublist(_loadStartIndex, _loadEndIndex);
} else {
smallQueue = queue.sublist(0, _loadEndIndex) + queue.sublist(_loadStartIndex);
}
return smallQueue;
} else {
_loadStartIndex = 0;
_loadEndIndex = queue.length;
return queue;
}
}
/// extend the loaded audiosource, when seeking to previous/next
Future<bool> _updateLoadedQueue(int newIndex) async {
_log.info('updateLoadedQueue: $newIndex');
_log.info('[$_loadStartIndex, $_loadEndIndex]');
if (_loadStartIndex == null || _loadEndIndex == null || newIndex == null) return false;
if (_loadStartIndex == _loadEndIndex ||
(_loadStartIndex == 0 && _loadEndIndex == _queue.length)) return false;
if (_loadStartIndex < _loadEndIndex) {
_log.info('base case');
return await _updateLoadedQueueBaseCase(newIndex);
} else {
_log.info('inverted case');
return await _updateLoadedQueueInverted(newIndex);
}
}
Future<bool> _updateLoadedQueueBaseCase(int newIndex) async {
if (newIndex < LOAD_INTERVAL) {
// nearing the start of the loaded songs
if (_loadStartIndex > 0) {
// load the song previous to the already loaded songs
_log.info('loadStartIndex--');
_loadStartIndex--;
await _audioSource.insert(0, ja.AudioSource.uri(Uri.file(_queue[_loadStartIndex].path)));
return true;
} else if (_loadEndIndex < _queue.length) {
// load the last song, if it isn't already loaded
_log.info('loadStartIndex = ${_queue.length - 1}');
_loadStartIndex = _queue.length - 1;
await _audioSource.add(ja.AudioSource.uri(Uri.file(_queue.last.path)));
return false;
}
} else if (newIndex > _audioSource.length - LOAD_INTERVAL - 1) {
// need to load next song
if (_loadEndIndex < _queue.length) {
// we ARE NOT at the end of the queue -> load next song
_log.info('loadEndIndex++');
_loadEndIndex++;
await _audioSource.add(ja.AudioSource.uri(Uri.file(_queue[_loadEndIndex - 1].path)));
return false;
} else if (_loadStartIndex > 0) {
// we ARE at the end of the queue AND the first song has not been loaded yet
// -> load first song
_log.info('loadEndIndex = 1');
_loadEndIndex = 1;
await _audioSource.insert(0, ja.AudioSource.uri(Uri.file(_queue[0].path)));
return true;
}
}
return false;
}
Future<bool> _updateLoadedQueueInverted(int newIndex) async {
final rightOfLoadEnd = newIndex >= _loadEndIndex;
int leftBorder = newIndex - LOAD_INTERVAL;
if (newIndex < _loadEndIndex) {
leftBorder += _audioSource.length;
}
int rightBorder = newIndex + LOAD_INTERVAL;
if (newIndex > _loadEndIndex) {
rightBorder -= _audioSource.length;
}
if (leftBorder < _loadEndIndex) {
// nearing the start of the loaded songs
// load the song previous to the already loaded songs
_log.info('inv: loadStartIndex--');
_loadStartIndex--;
await _audioSource.insert(
_loadEndIndex, ja.AudioSource.uri(Uri.file(_queue[_loadStartIndex].path)));
return rightOfLoadEnd;
} else if (rightBorder >= _loadEndIndex) {
// need to load next song
// we ARE NOT at the end of the queue -> load next song
_log.info('inv: loadEndIndex++');
_loadEndIndex++;
await _audioSource.insert(
_loadEndIndex - 1, ja.AudioSource.uri(Uri.file(_queue[_loadEndIndex - 1].path)));
return rightOfLoadEnd;
}
return false;
}
void _updateCurrentIndex(int apIndex) {
if (_loadStartIndex == null || _loadEndIndex == null) {
_currentIndexSubject.add(apIndex);
return;
}
int result;
if (_audioSource != null && _audioSource.length == _queue.length) {
_log.info('EVERYTHING LOADED');
result = apIndex;
} else if (_loadStartIndex < _loadEndIndex) {
// base case
result = apIndex != null ? (apIndex + (_loadStartIndex ?? 0)) : null;
} else {
// inverted case
if (apIndex < _loadEndIndex) {
result = apIndex;
} else {
result = apIndex + (_loadStartIndex - _loadEndIndex);
}
}
_currentIndexSubject.add(result);
_log.info('updateCurrentIndex: $result');
}
int _calcSourceIndex(int queueIndex) {
if (_loadStartIndex < _loadEndIndex) {
return queueIndex - _loadStartIndex;
} else {
if (queueIndex < _loadEndIndex) {
return queueIndex;
} else {
return queueIndex - _loadStartIndex + _loadEndIndex;
}
}
}
bool _isQueueIndexInLoadInterval(int index) {
if (_loadStartIndex < _loadEndIndex) {
return _loadStartIndex <= index && index < _loadEndIndex;
} else {
return _loadStartIndex <= index || index < _loadEndIndex;
}
}
bool _isQueueIndexInSaveInterval(int index) {
if (_audioSource.length == _queue.length) return index < _queue.length;
final int leftBorder = (_loadStartIndex + LOAD_INTERVAL - 1) % _queue.length;
final int rightBorder = (_loadEndIndex - LOAD_INTERVAL + 1) % _queue.length;
if (leftBorder < rightBorder) {
return leftBorder <= index && index < rightBorder;
} else {
return leftBorder <= index || index < rightBorder;
}
}
}