diff --git a/lib/injection_container.dart b/lib/injection_container.dart index 2762cb6..6fdf6a9 100644 --- a/lib/injection_container.dart +++ b/lib/injection_container.dart @@ -10,8 +10,8 @@ import 'domain/repositories/music_data_repository.dart'; import 'presentation/state/audio_store.dart'; import 'presentation/state/music_data_store.dart'; import 'presentation/state/navigation_store.dart'; -import 'system/datasources/audio_manager.dart'; -import 'system/datasources/audio_manager_contract.dart'; +import 'system/audio/audio_manager.dart'; +import 'system/audio/audio_manager_contract.dart'; import 'system/datasources/local_music_fetcher.dart'; import 'system/datasources/local_music_fetcher_contract.dart'; import 'system/datasources/moor_music_data_source.dart'; diff --git a/lib/presentation/widgets/audio_service_widget.dart b/lib/presentation/widgets/audio_service_widget.dart index 02ebb9b..fc7585e 100644 --- a/lib/presentation/widgets/audio_service_widget.dart +++ b/lib/presentation/widgets/audio_service_widget.dart @@ -1,7 +1,7 @@ import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; -import '../../system/datasources/audio_player_task.dart'; +import '../../system/audio/audio_player_task.dart'; class AudioServiceWidget extends StatefulWidget { const AudioServiceWidget({@required this.child}); diff --git a/lib/system/datasources/audio_manager.dart b/lib/system/audio/audio_manager.dart similarity index 100% rename from lib/system/datasources/audio_manager.dart rename to lib/system/audio/audio_manager.dart diff --git a/lib/system/datasources/audio_manager_contract.dart b/lib/system/audio/audio_manager_contract.dart similarity index 100% rename from lib/system/datasources/audio_manager_contract.dart rename to lib/system/audio/audio_manager_contract.dart diff --git a/lib/system/datasources/audio_player_task.dart b/lib/system/audio/audio_player_task.dart similarity index 64% rename from lib/system/datasources/audio_player_task.dart rename to lib/system/audio/audio_player_task.dart index a29d796..60372bc 100644 --- a/lib/system/datasources/audio_player_task.dart +++ b/lib/system/audio/audio_player_task.dart @@ -4,13 +4,15 @@ import 'dart:ui'; import 'package:audio_service/audio_service.dart'; import 'package:just_audio/just_audio.dart'; +import 'package:logging/logging.dart'; import 'package:moor/isolate.dart'; import 'package:moor/moor.dart'; import '../../domain/entities/shuffle_mode.dart'; +import '../datasources/moor_music_data_source.dart'; +import '../models/queue_item.dart'; import '../models/song_model.dart'; -import 'moor_music_data_source.dart'; -import 'queue_manager.dart'; +import 'queue_generator.dart'; const String INIT = 'INIT'; const String PLAY_WITH_CONTEXT = 'PLAY_WITH_CONTEXT'; @@ -22,13 +24,15 @@ const String KEY_INDEX = 'INDEX'; class AudioPlayerTask extends BackgroundAudioTask { final audioPlayer = AudioPlayer(); MoorMusicDataSource moorMusicDataSource; - QueueManager qm; + QueueGenerator queueGenerator; + // TODO: confusing naming List originalPlaybackContext = []; - List playbackContext = []; + List playbackContext = []; + // TODO: this is not trivial: queue is loaded by audioplayer + // this reference enables direct manipulation of the loaded queue ConcatenatingAudioSource queue; - List permutation; ShuffleMode _shuffleMode = ShuffleMode.none; ShuffleMode get shuffleMode => _shuffleMode; @@ -40,10 +44,9 @@ class AudioPlayerTask extends BackgroundAudioTask { int _playbackIndex = -1; int get playbackIndex => _playbackIndex; set playbackIndex(int i) { - print(i); if (i != null) { _playbackIndex = i; - AudioServiceBackground.setMediaItem(playbackContext[i]); + AudioServiceBackground.setMediaItem(playbackContext[i].mediaItem); AudioServiceBackground.sendCustomEvent({KEY_INDEX: i}); AudioServiceBackground.setState( @@ -61,6 +64,12 @@ class AudioPlayerTask extends BackgroundAudioTask { } } + static final _log = Logger('AudioPlayerTask') + ..onRecord.listen((record) { + print( + '${record.time} [${record.level.name}] ${record.loggerName}: ${record.message}'); + }); + @override Future onStop() async { await audioPlayer.stop(); @@ -112,19 +121,20 @@ class AudioPlayerTask extends BackgroundAudioTask { Future init() async { print('AudioPlayerTask.init'); audioPlayer.playerStateStream.listen((event) => handlePlayerState(event)); + audioPlayer.currentIndexStream.listen((event) => playbackIndex = event); audioPlayer.sequenceStateStream - .listen((event) => playbackIndex = event?.currentIndex); + .listen((event) => handleSequenceState(event)); final connectPort = IsolateNameServer.lookupPortByName(MOOR_ISOLATE); final MoorIsolate moorIsolate = MoorIsolate.fromConnectPort(connectPort); final DatabaseConnection databaseConnection = await moorIsolate.connect(); moorMusicDataSource = MoorMusicDataSource.connect(databaseConnection); - qm = QueueManager(moorMusicDataSource); + queueGenerator = QueueGenerator(moorMusicDataSource); } Future playWithContext(List context, int index) async { - final mediaItems = await qm.getMediaItemsFromPaths(context); + final mediaItems = await queueGenerator.getMediaItemsFromPaths(context); playPlaylist(mediaItems, index); } @@ -137,22 +147,44 @@ class AudioPlayerTask extends BackgroundAudioTask { Future setShuffleMode(ShuffleMode mode) async { shuffleMode = mode; - final index = permutation[playbackIndex]; - permutation = - qm.generatePermutation(shuffleMode, originalPlaybackContext, index); + final QueueItem currentQueueItem = playbackContext[playbackIndex]; + final int index = currentQueueItem.originalIndex; playbackContext = - qm.getPermutatedSongs(originalPlaybackContext, permutation); + await queueGenerator.generateQueue(shuffleMode, originalPlaybackContext, index); + final queuedMediaItems = playbackContext.map((e) => e.mediaItem).toList(); - AudioServiceBackground.setQueue(playbackContext); + // FIXME: this does not react correctly when inserted track is currently played + AudioServiceBackground.setQueue(queuedMediaItems); + + final newQueue = queueGenerator.mediaItemsToAudioSource(queuedMediaItems); + _updateQueue(newQueue, currentQueueItem); + } + + void _updateQueue(ConcatenatingAudioSource newQueue, QueueItem currentQueueItem) { + final int index = currentQueueItem.originalIndex; - final newQueue = qm.mediaItemsToAudioSource(playbackContext); queue.removeRange(0, playbackIndex); queue.removeRange(1, queue.length); if (shuffleMode == ShuffleMode.none) { - queue.insertAll(0, newQueue.children.sublist(0, index)); - queue.addAll(newQueue.children.sublist(index + 1)); - playbackIndex = index; + switch (currentQueueItem.type) { + case QueueItemType.standard: + queue.insertAll(0, newQueue.children.sublist(0, index)); + queue.addAll(newQueue.children.sublist(index + 1)); + playbackIndex = index; + break; + case QueueItemType.predecessor: + queue.insertAll(0, newQueue.children.sublist(0, index)); + queue.addAll(newQueue.children.sublist(index)); + playbackIndex = index; + break; + case QueueItemType.successor: + queue.insertAll(0, newQueue.children.sublist(0, index + 1)); + queue.addAll(newQueue.children.sublist(index + 1)); + playbackIndex = index; + break; + } + } else { queue.addAll(newQueue.children.sublist(1)); } @@ -171,18 +203,20 @@ class AudioPlayerTask extends BackgroundAudioTask { } Future playPlaylist(List mediaItems, int index) async { - permutation = qm.generatePermutation(shuffleMode, mediaItems, index); - playbackContext = qm.getPermutatedSongs(mediaItems, permutation); originalPlaybackContext = mediaItems; - AudioServiceBackground.setQueue(playbackContext); - queue = qm.mediaItemsToAudioSource(playbackContext); + playbackContext = await queueGenerator.generateQueue(shuffleMode, mediaItems, index); + final queuedMediaItems = playbackContext.map((e) => e.mediaItem).toList(); + + AudioServiceBackground.setQueue(queuedMediaItems); + queue = queueGenerator.mediaItemsToAudioSource(queuedMediaItems); audioPlayer.play(); final int startIndex = shuffleMode == ShuffleMode.none ? index : 0; await audioPlayer.load(queue, initialIndex: startIndex); } void handlePlayerState(PlayerState ps) { + _log.info('handlePlayerState called'); if (ps.processingState == ProcessingState.ready && ps.playing) { AudioServiceBackground.setState( controls: [ @@ -211,4 +245,14 @@ class AudioPlayerTask extends BackgroundAudioTask { ); } } + + // TODO: this can only be a temporary solution! gets called too often. + void handleSequenceState(SequenceState st) { + _log.info('handleSequenceState called'); + if (0 <= playbackIndex && playbackIndex < playbackContext.length) { + _log.info('handleSequenceState: setting MediaItem'); + AudioServiceBackground.setMediaItem( + playbackContext[playbackIndex].mediaItem); + } + } } diff --git a/lib/system/audio/queue_generator.dart b/lib/system/audio/queue_generator.dart new file mode 100644 index 0000000..4fbdd72 --- /dev/null +++ b/lib/system/audio/queue_generator.dart @@ -0,0 +1,176 @@ +import 'package:audio_service/audio_service.dart'; +import 'package:just_audio/just_audio.dart'; + +import '../../domain/entities/shuffle_mode.dart'; +import '../datasources/music_data_source_contract.dart'; +import '../models/queue_item.dart'; +import '../models/song_model.dart'; + +class QueueGenerator { + QueueGenerator(this._musicDataSource); + + final MusicDataSource _musicDataSource; + + // TODO: test + // TODO: optimize -> too slow for whole library + // fetching all songs together and preparing playback takes ~500ms compared to ~10.000ms individually + Future> getMediaItemsFromPaths(List paths) async { + final mediaItems = []; + for (final path in paths) { + final song = await _musicDataSource.getSongByPath(path); + mediaItems.add(song.toMediaItem()); + } + + return mediaItems; + } + + Future> generateQueue( + ShuffleMode shuffleMode, + List mediaItems, + int startIndex, + ) async { + List queue; + + switch (shuffleMode) { + case ShuffleMode.none: + queue = _generateNormalQueue(mediaItems); + break; + case ShuffleMode.standard: + queue = _generateShuffleQueue(mediaItems, startIndex); + break; + case ShuffleMode.plus: + queue = await _generateShufflePlusQueue(mediaItems, startIndex); + } + + return queue; + } + + ConcatenatingAudioSource mediaItemsToAudioSource(List mediaItems) { + return ConcatenatingAudioSource( + children: mediaItems + .map((MediaItem m) => AudioSource.uri(Uri.file(m.id))) + .toList()); + } + + List _generateNormalQueue(List mediaItems) { + return List.generate( + mediaItems.length, + (i) => QueueItem( + mediaItems[i], + originalIndex: i, + ), + ); + } + + List _generateShuffleQueue( + List mediaItems, + int startIndex, + ) { + final List queue = List.generate( + mediaItems.length, + (i) => QueueItem( + mediaItems[i], + originalIndex: i, + ), + ); + queue.removeAt(startIndex); + queue.shuffle(); + final first = QueueItem( + mediaItems[startIndex], + originalIndex: startIndex, + ); + return [first] + queue; + } + + Future> _generateShufflePlusQueue( + List mediaItems, + int startIndex, + ) async { + final List queue = await _getQueueItemWithLinks( + mediaItems[startIndex], + startIndex, + ); + final List indices = []; + + // filter mediaitem list + // TODO: multiply higher rated songs + for (var i = 0; i < mediaItems.length; i++) { + if (i != startIndex && !(mediaItems[i].extras['blocked'] as bool)) { + indices.add(i); + } + } + + indices.shuffle(); + + for (var i = 0; i < indices.length; i++) { + final int index = indices[i]; + final MediaItem mediaItem = mediaItems[index]; + + queue.addAll(await _getQueueItemWithLinks(mediaItem, index)); + } + + return queue; + } + + // TODO: naming things is hard + Future> _getQueueItemWithLinks( + MediaItem mediaItem, + int index, + ) async { + final List queueItems = []; + + final predecessors = await _getPredecessors(mediaItem); + final successors = await _getSuccessors(mediaItem); + + for (final p in predecessors) { + queueItems.add(QueueItem( + p, + originalIndex: index, + type: QueueItemType.predecessor, + )); + } + + queueItems.add(QueueItem( + mediaItem, + originalIndex: index, + )); + + for (final p in successors) { + queueItems.add(QueueItem( + p, + originalIndex: index, + type: QueueItemType.successor, + )); + } + + return queueItems; + } + + Future> _getPredecessors(MediaItem mediaItem) async { + final List mediaItems = []; + MediaItem currentMediaItem = mediaItem; + + while (currentMediaItem.previous != null) { + currentMediaItem = + (await _musicDataSource.getSongByPath(currentMediaItem.previous)) + .toMediaItem(); + mediaItems.add(currentMediaItem); + } + + return mediaItems.reversed.toList(); + } + + Future> _getSuccessors(MediaItem mediaItem) async { + final List mediaItems = []; + MediaItem currentMediaItem = mediaItem; + + while (currentMediaItem.next != null) { + currentMediaItem = + (await _musicDataSource.getSongByPath(currentMediaItem.next)) + .toMediaItem(); + mediaItems.add(currentMediaItem); + } + + return mediaItems.toList(); + } +} diff --git a/lib/system/datasources/queue_manager.dart b/lib/system/datasources/queue_manager.dart deleted file mode 100644 index 305aca0..0000000 --- a/lib/system/datasources/queue_manager.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:audio_service/audio_service.dart'; -import 'package:just_audio/just_audio.dart'; - -import '../../domain/entities/shuffle_mode.dart'; -import 'music_data_source_contract.dart'; - -class QueueManager { - QueueManager(this._musicDataSource); - - final MusicDataSource _musicDataSource; - - // TODO: test - // TODO: optimize -> too slow for whole library - // fetching all songs together and preparing playback takes ~500ms compared to ~10.000ms individually - Future> getMediaItemsFromPaths(List paths) async { - final mediaItems = []; - for (final path in paths) { - final song = await _musicDataSource.getSongByPath(path); - mediaItems.add(song.toMediaItem()); - } - - return mediaItems; - } - - // TODO: test - List generatePermutation( - ShuffleMode shuffleMode, List mediaItems, int startIndex) { - // permutation[i] = j; => song j is on the i-th position in the permutated list - List permutation; - final int length = mediaItems.length; - - switch (shuffleMode) { - case ShuffleMode.none: - permutation = List.generate(length, (i) => i); - break; - case ShuffleMode.standard: - final tmp = List.generate(length, (i) => i) - ..removeAt(startIndex) - ..shuffle(); - permutation = [startIndex] + tmp; - break; - case ShuffleMode.plus: - permutation = generatePlusPermutation(mediaItems, startIndex); - } - - return permutation; - } - - List getPermutatedSongs( - List songs, List permutation) { - return List.generate( - permutation.length, (index) => songs[permutation[index]]); - } - - ConcatenatingAudioSource mediaItemsToAudioSource(List mediaItems) { - return ConcatenatingAudioSource( - children: mediaItems - .map((MediaItem m) => AudioSource.uri(Uri.file(m.id))) - .toList()); - } - - List generatePlusPermutation( - List mediaItems, int startIndex) { - final List indices = []; - for (var i = 0; i < mediaItems.length; i++) { - if (i != startIndex && mediaItems[i].extras['blocked'] == 'false') { - indices.add(i); - } - } - indices.shuffle(); - return [startIndex] + indices; - } -} diff --git a/lib/system/models/queue_item.dart b/lib/system/models/queue_item.dart new file mode 100644 index 0000000..3f96502 --- /dev/null +++ b/lib/system/models/queue_item.dart @@ -0,0 +1,11 @@ +import 'package:audio_service/audio_service.dart'; + +class QueueItem { + QueueItem(this.mediaItem, {this.originalIndex, this.type = QueueItemType.standard}); + + final MediaItem mediaItem; + final int originalIndex; + final QueueItemType type; +} + +enum QueueItemType { standard, predecessor, successor } diff --git a/lib/system/models/song_model.dart b/lib/system/models/song_model.dart index c32013a..e402ade 100644 --- a/lib/system/models/song_model.dart +++ b/lib/system/models/song_model.dart @@ -209,3 +209,9 @@ class SongModel extends Song { return [discNumber, trackNumber]; } } + +// TODO: maybe move to another file +extension SongModelExtension on MediaItem { + String get previous => extras['previous'] as String; + String get next => extras['next'] as String; +} diff --git a/lib/system/repositories/audio_repository_impl.dart b/lib/system/repositories/audio_repository_impl.dart index 3a2105b..531a2ba 100644 --- a/lib/system/repositories/audio_repository_impl.dart +++ b/lib/system/repositories/audio_repository_impl.dart @@ -5,7 +5,7 @@ import '../../domain/entities/playback_state.dart'; import '../../domain/entities/shuffle_mode.dart'; import '../../domain/entities/song.dart'; import '../../domain/repositories/audio_repository.dart'; -import '../datasources/audio_manager_contract.dart'; +import '../audio/audio_manager_contract.dart'; import '../models/song_model.dart'; class AudioRepositoryImpl implements AudioRepository { diff --git a/test/coverage_helper_test.dart b/test/coverage_helper_test.dart index d0e360e..54f58df 100644 --- a/test/coverage_helper_test.dart +++ b/test/coverage_helper_test.dart @@ -3,13 +3,13 @@ import 'package:mucke/injection_container.dart'; import 'package:mucke/system/repositories/audio_repository_impl.dart'; import 'package:mucke/system/repositories/music_data_repository_impl.dart'; -import 'package:mucke/system/datasources/audio_manager_contract.dart'; +import 'package:mucke/system/audio/audio_manager_contract.dart'; import 'package:mucke/system/datasources/local_music_fetcher.dart'; -import 'package:mucke/system/datasources/audio_player_task.dart'; +import 'package:mucke/system/audio/audio_player_task.dart'; import 'package:mucke/system/datasources/local_music_fetcher_contract.dart'; -import 'package:mucke/system/datasources/audio_manager.dart'; +import 'package:mucke/system/audio/audio_manager.dart'; import 'package:mucke/system/datasources/moor_music_data_source.dart'; -import 'package:mucke/system/datasources/queue_manager.dart'; +import 'package:mucke/system/audio/queue_generator.dart'; import 'package:mucke/system/datasources/music_data_source_contract.dart'; import 'package:mucke/system/models/artist_model.dart'; import 'package:mucke/system/models/album_model.dart'; diff --git a/test/system/datasources/queue_manager_test.dart b/test/system/datasources/queue_manager_test.dart new file mode 100644 index 0000000..e48e9ba --- /dev/null +++ b/test/system/datasources/queue_manager_test.dart @@ -0,0 +1,37 @@ +import 'package:audio_service/audio_service.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:mucke/system/datasources/music_data_source_contract.dart'; +import 'package:mucke/system/audio/queue_generator.dart'; +import 'package:mucke/system/models/album_model.dart'; +import 'package:mucke/system/models/song_model.dart'; + +import '../../test_constants.dart'; + +class MockMusicDataSource extends Mock implements MusicDataSource {} + +void main() { + QueueGenerator queueManager; + MusicDataSource musicDataSource; + List mediaItems; + + group('generatePlusPermutation', () { + + setUp(() { + musicDataSource = MockMusicDataSource(); + queueManager = QueueGenerator(musicDataSource); + }); + + test( + 'should exclude blocked songs', + () async { + // arrange + + // act + + // assert + + }, + ); + }); +} diff --git a/test/system/repositories/audio_repository_impl_test.dart b/test/system/repositories/audio_repository_impl_test.dart index 635f446..7aa463c 100644 --- a/test/system/repositories/audio_repository_impl_test.dart +++ b/test/system/repositories/audio_repository_impl_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; -import 'package:mucke/system/datasources/audio_manager_contract.dart'; +import 'package:mucke/system/audio/audio_manager_contract.dart'; import 'package:mucke/system/models/song_model.dart'; import 'package:mucke/system/repositories/audio_repository_impl.dart';