package ch.retorte.intervalmusiccompositor; import static ch.retorte.intervalmusiccompositor.commons.Utf8Bundle.getBundle; import static ch.retorte.intervalmusiccompositor.list.ListSortMode.MANUAL; import static ch.retorte.intervalmusiccompositor.list.ListSortMode.SHUFFLE; import static ch.retorte.intervalmusiccompositor.list.ListSortMode.SORT; import static ch.retorte.intervalmusiccompositor.list.ListSortMode.SORT_REV; import static com.google.common.collect.Lists.newArrayList; import static java.lang.Integer.valueOf; import java.io.File; import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import ch.retorte.intervalmusiccompositor.audiofile.AudioFileComparator; import ch.retorte.intervalmusiccompositor.audiofile.AudioFileFactory; import ch.retorte.intervalmusiccompositor.audiofile.IAudioFile; import ch.retorte.intervalmusiccompositor.cache.CreateCacheJob; import ch.retorte.intervalmusiccompositor.cache.CreateCacheJobManager; import ch.retorte.intervalmusiccompositor.commons.ArrayHelper; import ch.retorte.intervalmusiccompositor.commons.MessageFormatBundle; import ch.retorte.intervalmusiccompositor.commons.platform.Platform; import ch.retorte.intervalmusiccompositor.commons.platform.PlatformFactory; import ch.retorte.intervalmusiccompositor.compilation.CompilationException; import ch.retorte.intervalmusiccompositor.compilation.CompilationGenerator; import ch.retorte.intervalmusiccompositor.compilation.CompilationParameters; import ch.retorte.intervalmusiccompositor.list.ListSortMode; import ch.retorte.intervalmusiccompositor.messagebus.*; import ch.retorte.intervalmusiccompositor.spi.ApplicationData; import ch.retorte.intervalmusiccompositor.spi.MusicCompilationControl; import ch.retorte.intervalmusiccompositor.spi.MusicListControl; import ch.retorte.intervalmusiccompositor.spi.ProgramControl; import ch.retorte.intervalmusiccompositor.spi.Ui; import ch.retorte.intervalmusiccompositor.spi.audio.MusicPlayer; import ch.retorte.intervalmusiccompositor.spi.decoder.AudioFileDecoder; import ch.retorte.intervalmusiccompositor.spi.encoder.AudioFileEncoder; import ch.retorte.intervalmusiccompositor.util.AudioFilesLoader; import javafx.collections.FXCollections; import javafx.collections.ObservableList; /** * Main controller of the software; collects data and reacts to ui events. Implements a lot of control interfaces. * * @author nw */ class MainControl implements MusicListControl, MusicCompilationControl, ProgramControl, ApplicationData { private static final int ONE_DAY_IN_MILLISECONDS = 86400000; private ListSortMode musicListSortMode = null; private int maxListEntries; private ObservableList<IAudioFile> musicList = FXCollections.observableArrayList(); private ObservableList<IAudioFile> breakList = FXCollections.observableArrayList(); private CreateCacheJobManager createCacheJobManager; private CompilationGenerator compilationGenerator; private AudioFileFactory audioFileFactory; private MusicPlayer musicPlayer; private MessageBus messageBus; private Ui ui; private Platform platform = new PlatformFactory().getPlatform(); private String programName; private Version programVersion; private String temporaryFileSuffix; private int maximumImportWorkerThreads; MainControl(CompilationGenerator compilationGenerator, AudioFileFactory audioFileFactory, MusicPlayer musicPlayer, MessageBus messageBus) { this.compilationGenerator = compilationGenerator; this.audioFileFactory = audioFileFactory; this.musicPlayer = musicPlayer; this.messageBus = messageBus; this.compilationGenerator.setMusicListControl(this); this.compilationGenerator.setApplicationData(this); setConfigurationProperties(); createCreateCacheJobManager(); } private void setConfigurationProperties() { MessageFormatBundle bundle = getBundle("imc"); programName = bundle.getString("imc.name"); programVersion = new Version(bundle.getString("imc.version")); MessageFormatBundle coreBundle = getBundle("core_imc"); temporaryFileSuffix = coreBundle.getString("imc.temporaryFile.suffix"); maxListEntries = valueOf(coreBundle.getString("imc.musicList.max_entries")); maximumImportWorkerThreads = valueOf(coreBundle.getString("imc.import.maximumWorkerThreads")); } void setUi(Ui ui) { this.ui = ui; } private void createCreateCacheJobManager() { createCacheJobManager = new CreateCacheJobManager(messageBus, getThreadLimit()); } /** * If we process too much tracks in parallel, it produces IO and may cause memory issues if the tracks are very big. */ private int getThreadLimit() { return Math.min(maximumImportWorkerThreads, platform.getCores()); } public void startCompilation(CompilationParameters compilationParameters) { if (!compilationParameters.hasUsableData()) { addDebugMessage("Compilation must not be of 0 length; aborting."); return; } addDebugMessage("Started compilation with: " + compilationParameters.getMusicPattern() + " " + compilationParameters.getBreakPattern() + " " + compilationParameters.getIterations()); if (!hasUsableTracksWith(compilationParameters)) { addDebugMessage("Not enough usable tracks; aborting."); return; } compilationGenerator.clearListeners(); compilationGenerator.addListener(() -> { ui.setEnvelopeImage(compilationGenerator.getEnvelope()); ui.setActive(); }); ui.setInactive(); try { compilationGenerator.createCompilationWith(compilationParameters); } catch (CompilationException e) { messageBus.send(new ErrorMessage(e.getMessage())); addDebugMessage(e); ui.setActive(); } } private boolean hasUsableTracksWith(CompilationParameters compilationParameters) { return 0 < getUsableTracks(compilationParameters.getMusicPattern()); } @Override public List<AudioFileDecoder> getAvailableDecoders() { return newArrayList(audioFileFactory.getDecoders()); } @Override public List<AudioFileEncoder> getAvailableEncoders() { return compilationGenerator.getEncoders().stream().filter(AudioFileEncoder::isAbleToEncode).collect(Collectors.toList()); } public IAudioFile addMusicTrack(int i, File file) { if (musicList.size() < maxListEntries) { return addTrack(musicList, i, file); } else { messageBus.send(new ErrorMessage("Too much tracks.")); } return null; } public IAudioFile appendMusicTrack(File file) { return addMusicTrack(getMusicList().size(), file); } public IAudioFile addBreakTrack(int i, File file) { if (breakList.size() < 1) { return addTrack(breakList, i, file); } return null; } public IAudioFile appendBreakTrack(File file) { return addBreakTrack(getBreakList().size(), file); } private synchronized IAudioFile addTrack(List<IAudioFile> audioFileList, int i, File f) { addDebugMessage("Added new file: " + f); IAudioFile audioFile = audioFileFactory.createAudioFileFrom(f); CreateCacheJob job = new CreateCacheJob(audioFile, messageBus); createCacheJobManager.addNewJob(job); if (i < 0 || audioFileList.size() < i) { i = audioFileList.size(); } audioFileList.add(i, audioFile); musicListSortMode = MANUAL; return audioFile; } public void removeMusicTracks(int[] list) { List<Integer> preparedList = ArrayHelper.prepareListForRemoval(list); preparedList.forEach(this::removeMusicTrack); } public void removeMusicTrack(int i) { IAudioFile audioFile = musicList.get(i); musicList.remove(audioFile); removeTrackCache(audioFile); } public void removeBreakTracks(int[] list) { List<Integer> preparedList = ArrayHelper.prepareListForRemoval(list); preparedList.forEach(this::removeBreakTrack); } public void removeBreakTrack(int i) { IAudioFile audioFile = breakList.get(i); breakList.remove(audioFile); removeTrackCache(audioFile); } private synchronized void removeTrackCache(IAudioFile audioFile) { addDebugMessage("Removed audio file: " + audioFile); // Check if there is another object with the same cache around // TODO: Does this still work? int sameInstances = 0; for (IAudioFile musicListFile : musicList) { if (audioFile.equals(musicListFile)) { sameInstances++; } } // Only delete cache if this is the last of its kind if (sameInstances < 2) { try { audioFile.removeCache(); } catch (IOException e) { addDebugMessage(e.getMessage()); } } } void loadAudioFiles() { AudioFilesLoader audioFilesLoader = new AudioFilesLoader(this, audioFileFactory, messageBus); audioFilesLoader.addListener(() -> musicListSortMode = SORT); new Thread(audioFilesLoader).start(); } public void quit() { removeCachedFiles(); messageBus.send(new InfoMessage("Gracefully shutting down ...")); } @Override public LogBuffer getLogBuffer() { return messageBus.getLogBuffer(); } private void removeCachedFiles() { addDebugMessage("Removing cached files."); for (IAudioFile audioFile : musicList) { try { audioFile.removeCache(); } catch (IOException e) { addDebugMessage(e.getMessage()); } } musicList.clear(); for (IAudioFile audioFile : breakList) { try { audioFile.removeCache(); } catch (IOException e) { addDebugMessage(e.getMessage()); } } breakList.clear(); } void tidyOldTemporaryFiles() { File tmpFile = null; try { tmpFile = File.createTempFile("imc", ".imc"); File directory = new File(tmpFile.getParent()); File[] fileList = directory.listFiles((dir, name) -> name.toLowerCase().endsWith(temporaryFileSuffix)); if (fileList != null) { for (File f : fileList) { if (f.lastModified() < System.currentTimeMillis() - ONE_DAY_IN_MILLISECONDS) { messageBus.send(new InfoMessage("Purging old temporary file: " + f)); boolean deleted = f.delete(); if (!deleted) { addDebugMessage("Was not able to delete temporary file: " + f); } } } } } catch (Exception e) { addDebugMessage(e.getMessage()); } finally { if (tmpFile != null) { tmpFile.delete(); } } } /** * Returns the number of tracks which are to be used for the compilation with the currently selected compilation properties. */ public int getUsableTracks(List<Integer> pattern) { int usableTracks = 0; int i = 0; for (IAudioFile audioFile : musicList) { if (audioFile.isOK() && audioFile.isLongEnoughFor(getIthPatternOf(pattern, i))) { usableTracks++; } i++; } return usableTracks; } private int getIthPatternOf(List<Integer> pattern, int i) { if (pattern == null || pattern.isEmpty()) { return 0; } return pattern.get(i % pattern.size()); } @Override public int getOkTracks() { return (int) musicList.stream().filter(IAudioFile::isOK).count(); } /** * Returns the number of music and break tracks currently showing the in progress status. */ private int getIdleTracks() { int idleTracks = 0; for (IAudioFile musicFile : musicList) { if (musicFile.isLoading()) { idleTracks++; } } for (IAudioFile breakFile : breakList) { if (breakFile.isLoading()) { idleTracks++; } } return idleTracks; } public boolean isTrackListReady() { return getIdleTracks() == 0; } @Override public ListSortMode getSortMode() { return musicListSortMode; } @Override public ObservableList<IAudioFile> getMusicList() { return musicList; } @Override public ObservableList<IAudioFile> getBreakList() { return breakList; } public void shuffleMusicList() { musicListSortMode = SHUFFLE; Collections.shuffle(musicList); } public void sortMusicList() { Collections.sort(musicList, new AudioFileComparator()); if (musicListSortMode == SORT) { musicListSortMode = SORT_REV; Collections.reverse(musicList); } else { musicListSortMode = SORT; } } @Override public void playMusicTrack(int index) { if (0 <= index && index < musicList.size()) { musicPlayer.play(musicList.get(index)); } } @Override public void playBreakTrack(int index) { if (0 <= index && index < breakList.size()) { musicPlayer.play(breakList.get(index)); } } @Override public void stopMusic() { musicPlayer.stop(); } public void writeMusicBpm(int[] list) { for (int i : list) { writeMusicBpm(i); } } public void writeBreakBpm(int[] list) { for (int i : list) { writeBreakBpm(i); } } public void writeMusicBpm(int index) { if (0 <= index && index < musicList.size()) { writeBpm(musicList.get(index)); } } @Override public int getMusicBPM(int index) { if (0 <= index && index < musicList.size()) { return musicList.get(index).getBpm(); } return -1; } @Override public void setMusicBpm(int index, int bpm) { if (0 <= index && index < musicList.size()) { musicList.get(index).setBpm(bpm); } } public void writeBreakBpm(int index) { if (0 <= index && index < breakList.size()) { writeBpm(breakList.get(index)); } } @Override public int getBreakBpm(int index) { if (0 <= index && index < breakList.size()) { return breakList.get(index).getBpm(); } return -1; } @Override public void setBreakBpm(int index, int bpm) { if (0 <= index && index < breakList.size()) { breakList.get(index).setBpm(bpm); } } private void writeBpm(IAudioFile audioFile) { try { audioFile.writeBpm(audioFile.getBpm()); addDebugMessage("Wrote BPM value '" + audioFile.getBpm() + "' into file: " + audioFile.getDisplayName()); } catch (IOException e) { messageBus.send(new ErrorMessage("Not able to write BPM information: " + e.getMessage())); addDebugMessage(e); } } private void addDebugMessage(String message) { messageBus.send(new DebugMessage(this, message)); } private void addDebugMessage(Throwable throwable) { messageBus.send(new DebugMessage(this, throwable)); } @Override public void moveTrack(int sourceIndex, int destinationIndex) { if (0 <= sourceIndex && sourceIndex < musicList.size()) { if (destinationIndex < 0 || musicList.size() < destinationIndex) { destinationIndex = musicList.size(); } IAudioFile movedFile = musicList.remove(sourceIndex); musicList.add(destinationIndex, movedFile); musicListSortMode = MANUAL; } } @Override public void moveMusicToBreak(int sourceIndex, int destinationIndex) { breakList.add(destinationIndex, musicList.remove(sourceIndex)); } @Override public void moveBreakToMusic(int sourceIndex, int destinationIndex) { musicList.add(destinationIndex, breakList.remove(sourceIndex)); musicListSortMode = MANUAL; } @Override public String getProgramName() { return programName; } @Override public Version getProgramVersion() { return programVersion; } }