package ch.retorte.intervalmusiccompositor.compilation;
import static ch.retorte.intervalmusiccompositor.commons.Utf8Bundle.getBundle;
import static ch.retorte.intervalmusiccompositor.list.BlendMode.SEPARATE;
import static com.google.common.collect.Lists.newArrayList;
import static java.lang.String.valueOf;
import java.io.*;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import ch.retorte.intervalmusiccompositor.audiofile.IAudioFile;
import ch.retorte.intervalmusiccompositor.commons.MessageFormatBundle;
import ch.retorte.intervalmusiccompositor.list.BlendMode;
import ch.retorte.intervalmusiccompositor.messagebus.DebugMessage;
import ch.retorte.intervalmusiccompositor.messagebus.ProgressMessage;
import ch.retorte.intervalmusiccompositor.messagebus.SubProcessProgressMessage;
import ch.retorte.intervalmusiccompositor.output.OutputGenerator;
import ch.retorte.intervalmusiccompositor.playlist.Playlist;
import ch.retorte.intervalmusiccompositor.playlist.PlaylistReport;
import ch.retorte.intervalmusiccompositor.spi.ApplicationData;
import ch.retorte.intervalmusiccompositor.spi.MusicListControl;
import ch.retorte.intervalmusiccompositor.spi.TaskFinishListener;
import ch.retorte.intervalmusiccompositor.spi.encoder.AudioFileEncoder;
import ch.retorte.intervalmusiccompositor.spi.messagebus.MessageProducer;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import javafx.scene.image.WritableImage;
/**
* @author nw
*/
public class CompilationGenerator implements Runnable {
private static final int MAXIMUM_BLEND_TIME = 20;
private static final String FILENAME_PART_DELIMITER = ".";
private static final String FILENAME_PREFIX_SOUND_MARKER = "s";
private static final String FILENAME_PREFIX_BREAK_MARKER = "b";
private static final String FILENAME_PREFIX_ITERATION_MARKER = "x";
private static final String FILENAME_PREFIX_PART_DELIMITER = "_";
private static final String FILENAME_PREFIX_ITERATION_DELIMITER = "-";
private MessageFormatBundle bundle = getBundle("core_imc");
private ArrayList<TaskFinishListener> listeners = newArrayList();
private MusicListControl musicListControl;
private Compilation compilation;
private MessageProducer messageProducer;
private CompilationParameters compilationParameters;
private ArrayList<IAudioFile> musicPlaylistCandidates = newArrayList();
private ArrayList<IAudioFile> breakPlaylistCandidates = newArrayList();
private ArrayList<IAudioFile> badSoundFiles = newArrayList();
private String playlist_outfile;
private String correctedOutputPath;
private String outfile_prefix;
private Playlist playlist;
private File compilationDataFile;
private long compilationDataSize;
private OutputGenerator outputGenerator;
private WritableImage envelope;
private ApplicationData applicationData;
public CompilationGenerator(Compilation compilation, OutputGenerator outputGenerator, MessageProducer messageProducer) {
this.compilation = compilation;
this.outputGenerator = outputGenerator;
this.messageProducer = messageProducer;
}
@Override
public void run() {
updateProgress(0, bundle.getString("compilation.status.reading"));
adjustBlendTimeIfTooLong();
clearOldData();
compileOutputParameters();
updateProgress(10, bundle.getString("compilation.status.loadingMusic"));
collectMusicTracks();
updateProgress(20, bundle.getString("compilation.status.loadingBreak"));
collectBreakTracks();
updateProgress(30, bundle.getString("compilation.status.creatingPlaylist"));
createPlaylist();
createPlaylistReport();
updateProgress(40, bundle.getString("compilation.status.creatingCompilation"));
createCompilation();
updateProgress(60, bundle.getString("compilation.status.convertingOutput"));
writeOutputFile();
updateProgress(90, bundle.getString("compilation.status.creatingEnvelope"));
createEnvelope();
cleanUp();
notifyListeners();
updateProgress(100, bundle.getString("compilation.status.finished"));
}
public void createCompilationWith(CompilationParameters compilationParameters) {
this.compilationParameters = compilationParameters;
new Thread(this).start();
}
public WritableImage getEnvelope() {
return envelope;
}
public void addListener(TaskFinishListener l) {
listeners.add(l);
}
public void clearListeners() {
listeners.clear();
}
private void notifyListeners() {
listeners.forEach(TaskFinishListener::onTaskFinished);
}
private void adjustBlendTimeIfTooLong() {
int shortestSoundPattern = MAXIMUM_BLEND_TIME;
for (int s : compilationParameters.getMusicPattern()) {
shortestSoundPattern = Math.min(shortestSoundPattern, s);
}
if (compilationParameters.getBlendMode().equals(SEPARATE) && shortestSoundPattern / 2 < compilationParameters.getBlendDuration()) {
compilationParameters.setBlendDuration(Math.floor(shortestSoundPattern / 2.0));
}
else if (compilationParameters.getBlendMode().equals(BlendMode.CROSS) && shortestSoundPattern < compilationParameters.getBlendDuration()) {
compilationParameters.setBlendDuration((double) Math.min(shortestSoundPattern, 10));
}
}
private void clearOldData() {
musicPlaylistCandidates.clear();
breakPlaylistCandidates.clear();
badSoundFiles.clear();
}
private void compileOutputParameters() {
correctedOutputPath = compilationParameters.getOutputPath();
if (correctedOutputPath == null || correctedOutputPath.equals("")) {
correctedOutputPath = bundle.getString("imc.workPath");
}
String identification_prefix = createMusicAndBreakPatternPrefixWith(compilationParameters.getMusicPattern(), compilationParameters.getBreakPattern(), compilationParameters.getIterations());
playlist_outfile = identification_prefix + FILENAME_PART_DELIMITER + bundle.getString("imc.outfile.playlist.suffix");
outfile_prefix = identification_prefix + FILENAME_PART_DELIMITER + bundle.getString("imc.outfile.sound.infix");
}
@VisibleForTesting
String createMusicAndBreakPatternPrefixWith(List<Integer> musicPattern, List<Integer> breakPattern, int iterations) {
List<String> parts = Lists.newArrayList();
for (int i = 0; i < musicPattern.size(); i++) {
parts.add(valueOf(musicPattern.get(i)) + FILENAME_PREFIX_SOUND_MARKER);
if (hasAtLeastOneNonNullElement(breakPattern)) {
parts.add(valueOf(breakPattern.get(i % breakPattern.size())) + FILENAME_PREFIX_BREAK_MARKER);
}
}
return Joiner.on(FILENAME_PREFIX_PART_DELIMITER).join(parts) + FILENAME_PREFIX_ITERATION_DELIMITER + FILENAME_PREFIX_ITERATION_MARKER + valueOf(iterations);
}
private boolean hasAtLeastOneNonNullElement(List<Integer> breakPattern) {
for (int element : breakPattern) {
if (element != 0) {
return true;
}
}
return false;
}
private void collectMusicTracks() {
int i = 0;
for (IAudioFile audioFile : musicListControl.getMusicList()) {
if (audioFile.isOK() && audioFile.isLongEnoughFor(compilationParameters.getMusicPattern().get(i % compilationParameters.getMusicPattern().size()))) {
musicPlaylistCandidates.add(audioFile);
}
else {
badSoundFiles.add(audioFile);
}
i++;
}
}
private void collectBreakTracks() {
int i = 0;
for (IAudioFile audioFile : musicListControl.getBreakList()) {
if (audioFile.isOK() && audioFile.isLongEnoughFor(compilationParameters.getBreakPattern().get(i % compilationParameters.getBreakPattern().size()))) {
breakPlaylistCandidates.add(audioFile);
}
else {
badSoundFiles.add(audioFile);
}
i++;
}
}
private void createPlaylist() {
playlist = new Playlist(compilationParameters, messageProducer);
playlist.generatePlaylist(musicPlaylistCandidates, compilationParameters.getMusicPattern(), breakPlaylistCandidates, compilationParameters.getBreakPattern(), compilationParameters.getIterations());
}
private void createPlaylistReport() {
File reportFile = new File(correctedOutputPath + "/" + playlist_outfile);
String playlistReport = new PlaylistReport(applicationData).generateReportFor(playlist, badSoundFiles);
try {
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(reportFile), Charsets.UTF_8);
outputStreamWriter.write(playlistReport);
outputStreamWriter.close();
}
catch (IOException e) {
String message = bundle.getString("ui.error.introduction");
message += e.getMessage();
throw new CompilationException(message);
}
}
private void createCompilation() {
try {
byte[] compilationBytes = compilation.generateCompilation(playlist);
compilationDataFile = File.createTempFile("compilation", bundle.getString("imc.temporaryFile.suffix"));
compilationDataSize = compilationBytes.length;
writeToCompilationDataFile(compilationBytes);
}
catch (IOException e) {
String message = bundle.getString("ui.error.sound.introduction");
message += e.getMessage();
throw new CompilationException(message);
}
catch (OutOfMemoryError e) {
String message = bundle.getString("ui.error.memory.introduction");
message += e.getMessage();
throw new CompilationException(message);
}
}
private void writeToCompilationDataFile(byte[] compilationBytes) throws IOException {
FileOutputStream fos = null;
try {
fos = new FileOutputStream(compilationDataFile);
fos.write(compilationBytes);
} finally {
try {
if (fos != null) {
fos.close();
}
} catch (Exception e) {
// nop
}
}
}
private void writeOutputFile() {
try {
outputGenerator.generateOutput(readCompilationDataFile(), compilationDataSize, correctedOutputPath, outfile_prefix, compilationParameters.getEncoderIdentifier(), new InertProgressListener() {
@Override
protected void onProgressChange(int percent) {
updateSubProgress(percent);
}
});
} catch (Exception e) {
String message = bundle.getString("ui.error.introduction");
message += e.getMessage();
throw new CompilationException(message);
}
}
private FileInputStream readCompilationDataFile() throws FileNotFoundException {
return new FileInputStream(compilationDataFile);
}
private byte[] readCompilationDataFileIntoByteArray() throws IOException {
return Files.readAllBytes(compilationDataFile.toPath());
}
private void createEnvelope() {
Integer width = Integer.valueOf(bundle.getString("ui.envelope.width"));
Integer height = Integer.valueOf(bundle.getString("ui.envelope.height"));
EnvelopeImage envelopeImage = new EnvelopeImage(width, height);
try {
envelopeImage.generateEnvelope(readCompilationDataFileIntoByteArray(), compilationParameters.getMusicPattern(), compilationParameters.getBreakPattern(), compilationParameters.getIterations());
}
catch (IOException e) {
String message = bundle.getString("ui.error.introduction");
message += e.getMessage();
throw new CompilationException(message);
}
envelope = envelopeImage.getBufferedImage();
}
private void cleanUp() {
/* We want unused objects to be reclaimed. */
if (!compilationDataFile.delete()) {
addDebugMessage("Was not able to delete compilation file " + compilationDataFile.getAbsolutePath());
}
System.gc();
}
private void updateProgress(Integer progressInPercent, String currentActivity) {
messageProducer.send(new ProgressMessage(progressInPercent, currentActivity));
addDebugMessage("Progress: " + progressInPercent + ", " + currentActivity);
}
private void updateSubProgress(Integer progressInPercent) {
messageProducer.send(new SubProcessProgressMessage(progressInPercent));
addDebugMessage("Progress of sub process: " + progressInPercent);
}
private void addDebugMessage(String message) {
messageProducer.send(new DebugMessage(this, message));
}
public void setMusicListControl(MusicListControl musicListControl) {
this.musicListControl = musicListControl;
}
public void setApplicationData(ApplicationData applicationData) {
this.applicationData = applicationData;
}
public List<AudioFileEncoder> getEncoders() {
return outputGenerator.getEncoders();
}
}