package ch.retorte.intervalmusiccompositor.playlist;
import static ch.retorte.intervalmusiccompositor.commons.Utf8Bundle.getBundle;
import static ch.retorte.intervalmusiccompositor.compilation.CompilationParameters.*;
import static ch.retorte.intervalmusiccompositor.list.BlendMode.CROSS;
import static ch.retorte.intervalmusiccompositor.list.EnumerationMode.CONTINUOUS;
import static ch.retorte.intervalmusiccompositor.list.EnumerationMode.SINGLE_EXTRACT;
import static ch.retorte.intervalmusiccompositor.list.ListSortMode.SHUFFLE;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Lists.newArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import ch.retorte.intervalmusiccompositor.audiofile.IAudioFile;
import ch.retorte.intervalmusiccompositor.commons.MessageFormatBundle;
import ch.retorte.intervalmusiccompositor.compilation.CompilationParameters;
import ch.retorte.intervalmusiccompositor.list.BlendMode;
import ch.retorte.intervalmusiccompositor.list.EnumerationMode;
import ch.retorte.intervalmusiccompositor.list.ListSortMode;
import ch.retorte.intervalmusiccompositor.messagebus.ErrorMessage;
import ch.retorte.intervalmusiccompositor.spi.messagebus.MessageProducer;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
/**
* @author nw
*/
public class Playlist implements Iterable<PlaylistItem> {
private MessageFormatBundle bundle = getBundle("core_imc");
private List<PlaylistItem> playlistItems = newArrayList();
private Long startCutOffInMilliseconds = Long.parseLong(bundle.getString("imc.audio.cutoff.start"));
private Long endCutOffInMilliseconds = Long.parseLong(bundle.getString("imc.audio.cutoff.end"));
private Random random = new Random();
private BlendMode blendMode = DEFAULT_BLEND_MODE;
private Double blendTime = DEFAULT_BLEND_DURATION;
private EnumerationMode enumerationMode = DEFAULT_ENUMERATION_MODE;
private ListSortMode listSortMode = DEFAULT_LIST_SORT_MODE;
private Map<IAudioFile, Long> currentProgress = Maps.newConcurrentMap();
private MessageProducer messageProducer;
@VisibleForTesting
Playlist(MessageProducer messageProducer) {
this.messageProducer = messageProducer;
}
@VisibleForTesting
Playlist(BlendMode blendMode, Double blendTime, EnumerationMode enumerationMode, ListSortMode listSortMode, MessageProducer messageProducer) {
this.blendMode = blendMode;
this.blendTime = blendTime;
this.enumerationMode = enumerationMode;
this.listSortMode = listSortMode;
this.messageProducer = messageProducer;
}
public Playlist(CompilationParameters p, MessageProducer messageProducer) {
this(p.getBlendMode(), p.getBlendDuration(), p.getEnumerationMode(), p.getListSortMode(), messageProducer);
}
public void generatePlaylist(List<IAudioFile> musicFiles,
List<Integer> musicPattern,
List<IAudioFile> breakFiles,
List<Integer> breakPattern,
Integer iterations) {
playlistItems = generatePlaylistFrom(musicFiles, musicPattern, breakFiles, breakPattern, iterations);
}
@VisibleForTesting
List<PlaylistItem> generatePlaylistFrom(List<IAudioFile> musicFiles,
List<Integer> musicPattern,
List<IAudioFile> breakFiles,
List<Integer> breakPattern,
Integer iterations) {
checkNotNull(musicFiles);
checkNotNull(musicPattern);
checkNotNull(breakFiles);
checkNotNull(breakPattern);
if (iterations == 0 || musicFiles.isEmpty()) {
return newArrayList();
}
List<PlaylistItem> musicTracks = createMusicPlaylist(musicFiles, musicPattern, iterations);
List<PlaylistItem> breakTracks = createBreakPlaylist(breakFiles, breakPattern, iterations, musicPattern.size());
if (breakPattern.isEmpty()) {
return musicTracks;
}
return interleaveTracks(musicTracks, breakTracks);
}
private List<PlaylistItem> interleaveTracks(List<PlaylistItem> musicTracks, List<PlaylistItem> breakTracks) {
List<PlaylistItem> result = Lists.newArrayList();
for (int i = 0; i < musicTracks.size(); i++) {
result.add(musicTracks.get(i));
result.add(breakTracks.get(i));
}
return result;
}
private List<PlaylistItem> createMusicPlaylist(List<IAudioFile> musicFiles, List<Integer> musicPattern, Integer iterations) {
List<PlaylistItem> musicPlaylist = Lists.newArrayList();
int desiredPlaylistSize = musicPattern.size() * iterations;
int musicTrackCounter = 0;
int lastShuffleHappened = 0;
int musicPatternCounter = 0;
int skippedTracks = 0;
while (musicPlaylist.size() < desiredPlaylistSize) {
IAudioFile currentAudioFile = musicFiles.get((musicTrackCounter) % musicFiles.size());
int currentSoundPattern = musicPattern.get(musicPatternCounter % musicPattern.size());
PlaylistItem newMusicTrack = createPlaylistItemFrom(currentAudioFile, currentSoundPattern * 1000);
if (newMusicTrack != null) {
musicPlaylist.add(newMusicTrack);
musicPatternCounter++;
}
else {
skippedTracks++;
}
if (isSingleExtractEnumeration() || newMusicTrack == null) {
musicTrackCounter++;
}
if (isShuffleMode() && musicTrackCounter != lastShuffleHappened && (musicTrackCounter % musicFiles.size() == 0)) {
shuffle(musicFiles);
lastShuffleHappened = musicTrackCounter;
skippedTracks = 0;
}
if (musicFiles.size() < skippedTracks) {
messageProducer.send(new ErrorMessage("Too few usable tracks."));
throw new IllegalStateException("Too few usable tracks.");
}
}
return musicPlaylist;
}
private void shuffle(List<IAudioFile> audioFiles) {
Collections.shuffle(audioFiles, random);
}
private List<PlaylistItem> createBreakPlaylist(List<IAudioFile> breakFiles, List<Integer> breakPattern, Integer iterations, int musicPatternSize) {
List<PlaylistItem> breakPlaylist = Lists.newArrayList();
if (breakPattern.isEmpty()) {
return breakPlaylist;
}
int desiredBreakListSize = musicPatternSize * iterations;
int breakTrackCounter = 0;
int skippedTracks = 0;
while (breakPlaylist.size() < desiredBreakListSize) {
int currentBreakPattern = breakPattern.get((breakTrackCounter % musicPatternSize) % breakPattern.size());
if (!breakFiles.isEmpty()) {
IAudioFile currentBreakFile = breakFiles.get((breakTrackCounter % musicPatternSize) % breakFiles.size());
PlaylistItem newBreakTrack = createPlaylistItemFrom(currentBreakFile, currentBreakPattern * 1000);
if (newBreakTrack != null) {
breakPlaylist.add(new BreakPlaylistItem(newBreakTrack));
}
else {
skippedTracks++;
}
}
else {
breakPlaylist.add(new BreakPlaylistItem(createPlaylistItem(null, 0L, (long) currentBreakPattern * 1000)));
}
breakTrackCounter++;
if (breakFiles.size() < skippedTracks) {
messageProducer.send(new ErrorMessage("Too few usable tracks."));
throw new IllegalStateException("Too few usable tracks.");
}
}
return breakPlaylist;
}
private PlaylistItem createPlaylistItemFrom(IAudioFile audioFile, long extractLengthInMilliseconds) {
long maximalRangeForDuration;
long trackStart = startCutOffInMilliseconds;
if (isCrossFadingMode()) {
extractLengthInMilliseconds += (long) (blendTime * 1000);
}
if (isContinuousExtractEnumerator()) {
if (!currentProgress.containsKey(audioFile)) {
currentProgress.put(audioFile, startCutOffInMilliseconds);
}
trackStart = currentProgress.get(audioFile);
maximalRangeForDuration = (audioFile.getDuration() - trackStart - endCutOffInMilliseconds - extractLengthInMilliseconds);
}
else {
maximalRangeForDuration = (audioFile.getDuration() - startCutOffInMilliseconds - endCutOffInMilliseconds - extractLengthInMilliseconds);
if (0 < maximalRangeForDuration) {
trackStart += random.nextInt((int) maximalRangeForDuration);
}
}
if (maximalRangeForDuration < 0) {
if (isContinuousExtractEnumerator()) {
currentProgress.remove(audioFile);
}
return null;
}
if (isContinuousExtractEnumerator()) {
currentProgress.put(audioFile, trackStart + extractLengthInMilliseconds);
}
return createPlaylistItem(audioFile, trackStart, trackStart + extractLengthInMilliseconds);
}
private PlaylistItem createPlaylistItem(IAudioFile audioFile, Long startInMilliseconds, Long endInMilliseconds) {
return new PlaylistItem(audioFile, startInMilliseconds, endInMilliseconds);
}
long getTotalLength(List<PlaylistItem> playlistItems) {
long result = 0L;
for (PlaylistItem playlistItem : playlistItems) {
result += playlistItem.getExtractDurationInMilliseconds();
if (isCrossFadingMode()) {
// Removing 1/2 of the blend time due to overlapping between the tracks.
result -= (blendTime * 500);
}
}
if (isCrossFadingMode()) {
result += (blendTime * 1000);
}
return result;
}
private boolean isCrossFadingMode() {
return blendMode == CROSS;
}
private boolean isSingleExtractEnumeration() {
return enumerationMode == SINGLE_EXTRACT;
}
private boolean isContinuousExtractEnumerator() {
return enumerationMode == CONTINUOUS;
}
private boolean isShuffleMode() {
return listSortMode == SHUFFLE;
}
private long getTotalLength() {
return getTotalLength(playlistItems);
}
public int getTotalLengthInSeconds() {
return (int) (getTotalLength() / 1000);
}
void setCutOff(long startCutOffInMilliseconds, long endCutOffInMilliseconds) {
this.startCutOffInMilliseconds = startCutOffInMilliseconds;
this.endCutOffInMilliseconds = endCutOffInMilliseconds;
}
public BlendMode getBlendMode() {
return blendMode;
}
public Double getBlendTime() {
return blendTime;
}
@Override
public Iterator<PlaylistItem> iterator() {
return playlistItems.iterator();
}
}