////////////////////////////////////////////////////////////////////////////////
// Copyright 2013 Michael Schmalle - Teoti Graphix, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License
//
// Author: Michael Schmalle, Principal Architect
// mschmalle at teotigraphix dot com
////////////////////////////////////////////////////////////////////////////////
package com.teotigraphix.caustk.library;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.UUID;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.IOFileFilter;
import com.teotigraphix.caustk.controller.ICaustkController;
import com.teotigraphix.caustk.controller.SubControllerBase;
import com.teotigraphix.caustk.controller.SubControllerModel;
import com.teotigraphix.caustk.core.CausticException;
import com.teotigraphix.caustk.core.CtkDebug;
import com.teotigraphix.caustk.core.osc.OutputPanelMessage;
import com.teotigraphix.caustk.core.osc.PatternSequencerMessage;
import com.teotigraphix.caustk.library.LibraryPattern.ToneSet;
import com.teotigraphix.caustk.pattern.PatternUtils;
import com.teotigraphix.caustk.project.Project;
import com.teotigraphix.caustk.sound.ISoundSource;
import com.teotigraphix.caustk.tone.Tone;
import com.teotigraphix.caustk.tone.ToneDescriptor;
import com.teotigraphix.caustk.tone.ToneType;
import com.teotigraphix.caustk.tone.components.SynthComponent;
import com.teotigraphix.caustk.tone.components.PatternSequencerComponent.Resolution;
import com.teotigraphix.caustk.utils.RuntimeUtils;
/*
*/
public class LibraryManager extends SubControllerBase implements ILibraryManager {
@Override
protected Class<? extends SubControllerModel> getModelType() {
return LibraryManagerModel.class;
}
LibraryManagerModel getModel() {
return (LibraryManagerModel)getInternalModel();
}
//--------------------------------------------------------------------------
// API
//--------------------------------------------------------------------------
//----------------------------------
// selectedLibrary
//----------------------------------
private File librariesDirectory;
@Override
public Library getSelectedLibrary() {
return getModel().getSelectedLibrary();
}
@Override
public void setSelectedLibrary(Library value) {
getModel().setSelectedLibrary(value);
getController().getDispatcher().trigger(new OnLibraryManagerSelectedLibraryChange(value));
}
public LibraryManager(ICaustkController controller) {
super(controller);
File root = controller.getConfiguration().getApplicationRoot();
if (!root.exists())
throw new RuntimeException("Application root not specified");
librariesDirectory = new File(root, "libraries");
if (!librariesDirectory.exists())
librariesDirectory.mkdirs();
}
@Override
protected void loadState(Project project) {
super.loadState(project);
load();
String id = getController().getProjectManager().getSessionPreferences()
.getString("selectedLibrary");
if (id != null) {
Library library = getModel().getLibraries().get(UUID.fromString(id));
if (library != null) {
setSelectedLibrary(library);
} else {
CtkDebug.err("LibraryManager; Library null " + id);
}
}
}
@Override
protected void saveState(Project project) {
super.saveState(project);
// if the project has selected a library, save it
if (getSelectedLibrary() != null) {
getController().getProjectManager().getSessionPreferences()
.put("selectedLibrary", getSelectedLibrary().getId());
}
}
/**
* Loads the entire <code>libraries</code> directory into the manager.
* <p>
* Each sub directory located within the <code>libraries</code> directory
* will be created as a {@link Library} instance.
*/
@Override
public void load() {
if (!librariesDirectory.exists())
return;
Collection<File> dirs = FileUtils.listFilesAndDirs(librariesDirectory, new IOFileFilter() {
@Override
public boolean accept(File arg0, String arg1) {
return false;
}
@Override
public boolean accept(File arg0) {
return false;
}
}, new IOFileFilter() {
@Override
public boolean accept(File file, String name) {
if (file.getParentFile().getName().equals("libraries"))
return true;
return false;
}
@Override
public boolean accept(File file) {
if (file.getParentFile().getName().equals("libraries"))
return true;
return false;
}
});
for (File directory : dirs) {
if (directory.equals(librariesDirectory))
continue;
loadLibrary(directory.getName());
}
}
@Override
public Library createLibrary(String name) throws IOException {
File newDirectory = new File(librariesDirectory, name);
//if (newDirectory.exists())
// throw new CausticException("Library already exists " + newDirectory.getAbsolutePath());
newDirectory.mkdir();
// create a default scene for every new Library
LibraryScene defaultScene = null;
try {
defaultScene = createDefaultScene();
} catch (CausticException e) {
e.printStackTrace();
}
Library library = new Library();
library.setId(UUID.randomUUID());
library.setMetadataInfo(new MetadataInfo());
library.setDirectory(newDirectory);
library.mkdirs();
getModel().getLibraries().put(library.getId(), library);
library.addScene(defaultScene);
saveLibrary(library);
return library;
}
private LibraryScene createDefaultScene() throws CausticException {
getController().getSoundSource().clearAndReset();
getController().getSoundSource().createTone("SubSynth", ToneType.SubSynth);
getController().getSoundSource().createTone("PCMSynth1", ToneType.PCMSynth);
getController().getSoundSource().createTone("PCMSynth2", ToneType.PCMSynth);
getController().getSoundSource().createTone("Bassline1", ToneType.Bassline);
getController().getSoundSource().createTone("Bassline2", ToneType.Bassline);
getController().getSoundSource().createTone("Beatbox", ToneType.Beatbox);
for (Tone tone : getController().getSoundSource().getTones()) {
tone.restore();
}
getController().getSoundMixer().restore();
LibraryScene libraryScene = new LibraryScene();
libraryScene.setId(UUID.randomUUID());
MetadataInfo metadataInfo = new MetadataInfo();
metadataInfo.addTag("DefaultScene");
libraryScene.setMetadataInfo(metadataInfo);
libraryScene.setSoundSourceState(new SoundSourceState());
libraryScene.setSoundMixerState(new SoundMixerState());
libraryScene.setEffectMixerState(new EffectMixerState());
getController().getSoundSource().clearAndReset();
return libraryScene;
}
@Override
public Library loadLibrary(File directory) {
if (!directory.exists()) {
CtkDebug.err("Library not found; " + directory);
return null;
}
File file = new File(directory, "library.ctk");
Library library = getController().getSerializeService().fromFile(file, Library.class);
getModel().getLibraries().put(library.getId(), library);
//getController().getDispatcher().trigger(new OnLibraryManagerLoadComplete(library));
return library;
}
@Override
public Library loadLibrary(String name) {
File directory = new File(librariesDirectory, name);
File file = new File(directory, "library.ctk");
if (!file.exists()) {
CtkDebug.err("Library not found; " + file);
return null;
}
return loadLibrary(directory);
}
@Override
public void saveLibrary(Library library) throws IOException {
String data = getController().getSerializeService().toString(library);
File file = new File(library.getDirectory(), "library.ctk");
FileUtils.writeStringToFile(file, data);
}
@Override
public void delete() throws IOException {
for (Library library : getModel().getLibraries().values()) {
library.delete();
}
clear();
}
@Override
public void clear() {
resetModel();
}
@Override
public void importSong(Library library, File causticFile) throws IOException, CausticException {
// Load the song, this automatically resets the sound source
getController().getSoundSource().loadSong(causticFile);
loadLibraryScene(library, causticFile, getController().getSoundSource());
loadLibraryPhrases(library, getController().getSoundSource());
getController().getSoundSource().clearAndReset();
getController().getDispatcher().trigger(new OnLibraryManagerImportComplete());
}
@Override
public void importPatterns(Library library, File causticFile) throws IOException,
CausticException {
// Load the song, this automatically resets the sound source
getController().getSoundSource().loadSong(causticFile);
loadLibraryScene(library, causticFile, getController().getSoundSource());
loadLibraryPhrases(library, getController().getSoundSource());
loadLibraryPatterns(library, getController().getSoundSource());
getController().getSoundSource().clearAndReset();
getController().getDispatcher().trigger(new OnLibraryManagerImportComplete());
}
private void loadLibraryPatterns(Library library, ISoundSource soundSource) {
Map<String, List<LibraryPatch>> map = new TreeMap<String, List<LibraryPatch>>();
// the numbers get merged together
// PART1A, PART1B, PART1C etc
// first sort the parts from the patch name
for (LibraryPatch patch : library.getPatches()) {
// System.out.println(patch.getName());
String machineName = patch.getName();
String post = machineName.substring(4);
String index = post.substring(0, 1);
List<LibraryPatch> list = map.get(index);
if (list == null) {
list = new ArrayList<LibraryPatch>();
map.put(index, list);
}
list.add(patch);
}
// Create a PatternLibrary for EVERY defined phrase
// how many parts [3]
//int numParts = map.size();
// all sets have to be the same size, test the first one to see how many
// sets are contained IE A, B, C etc [2]
int numPartSets = map.get("1").size();
// Number of LibraryPatterns, the pattern holds the numParts
int numPatterns = 64 * numPartSets;
List<ToneDescriptor> descriptors = new ArrayList<ToneDescriptor>();
for (Entry<String, List<LibraryPatch>> entry : map.entrySet()) {
int index = Integer.parseInt(entry.getKey()) - 1; // parts are 1 based
LibraryPatch firstPatch = entry.getValue().get(0);
ToneDescriptor descriptor = new ToneDescriptor(index, "PART" + (index + 1),
firstPatch.getToneType());
descriptor.setPatchId(firstPatch.getId());
//for (LibraryPatch patch : entry.getValue()) {
// List<LibraryPhrase> phrases = library.findPhrasesByTag(patch.getName());
//
//}
descriptors.add(descriptor);
}
ToneSet set = new ToneSet(descriptors);
List<LibraryPattern> patterns = new ArrayList<LibraryPattern>();
// create the LibraryPatterns
for (int i = 0; i < numPatterns; i++) {
LibraryPattern pattern = new LibraryPattern();
pattern.setId(UUID.randomUUID());
pattern.setIndex(i);
pattern.setMetadataInfo(new MetadataInfo());
pattern.setToneSet(set);
patterns.add(pattern);
for (ToneDescriptor descriptor : set.getDescriptors()) {
int partIndex = descriptor.getIndex();
String name = descriptor.getName();
int patternBank = i / (64);
name = name + alpha[patternBank];
System.out.println(" " + name);
List<LibraryPhrase> list = library.findPhrasesByTagStartsWith(name);
LibraryPhrase phrase = getPhraseFor(list, i);
pattern.putPhrase(partIndex, phrase);
}
// get the phrase id of the pattern
}
library.setPatterns(patterns);
}
private String[] alpha = {
"A", "B", "C", "D"
};
private LibraryPhrase getPhraseFor(List<LibraryPhrase> phrases, int index) {
int bank = PatternUtils.getBank(index);
int pattern = PatternUtils.getPattern(index);
// Find the Phrase that has the same bank and pattern
for (LibraryPhrase phrase : phrases) {
if (phrase.getBankIndex() == bank && phrase.getPatternIndex() == pattern)
return phrase;
}
return null;
}
private void loadLibraryScene(Library library, File causticFile, ISoundSource soundSource)
throws IOException {
String name = causticFile.getName().replace(".caustic", "");
LibraryScene scene = new LibraryScene();
scene.setMetadataInfo(new MetadataInfo());
scene.setId(UUID.randomUUID());
library.addScene(scene);
//--------------------------------------
SoundSourceState soundSourceState = new SoundSourceState();
for (int i = 0; i < 6; i++) {
Tone tone = soundSource.getTone(i);
LibraryPatch patch = null;
if (tone != null) {
patch = new LibraryPatch();
patch.setName(tone.getName());
patch.setToneType(tone.getToneType());
patch.setMetadataInfo(new MetadataInfo());
patch.setId(UUID.randomUUID());
TagUtils.addDefaultTags(tone, patch);
relocatePresetFile(tone, library, patch);
library.addPatch(patch);
tone.setDefaultPatchId(patch.getId());
soundSourceState.addTone(tone);
}
}
scene.setSoundSourceState(soundSourceState);
SoundMixerState soundMixerState = new SoundMixerState();
soundMixerState.setData(getController().getSoundMixer().serialize());
scene.setSoundMixerState(soundMixerState);
EffectMixerState effectMixerState = new EffectMixerState();
scene.setEffectMixerState(effectMixerState);
TagUtils.addDefaultTags(name, getController(), scene);
}
private void loadLibraryPhrases(Library library, ISoundSource soundSource) {
for (int i = 0; i < 6; i++) {
Tone tone = soundSource.getTone(i);
if (tone != null) {
String result = PatternSequencerMessage.QUERY_PATTERNS_WITH_DATA.queryString(
getController(), i);
if (result == null)
continue;
for (String patternName : result.split(" ")) {
int bankIndex = PatternUtils.toBank(patternName);
int patternIndex = PatternUtils.toPattern(patternName);
// set the current bank and pattern of the machine to query
// the string pattern data
PatternSequencerMessage.BANK.send(getController(), i, bankIndex);
PatternSequencerMessage.PATTERN.send(getController(), i, patternIndex);
//----------------------------------------------------------------
// Load Pattern
//----------------------------------------------------------------
// load one phrase per pattern; load ALL patterns
// as caustic machine patterns
int length = (int)PatternSequencerMessage.NUM_MEASURES
.query(getController(), i);
float tempo = OutputPanelMessage.BPM.query(getController());
String noteData = PatternSequencerMessage.QUERY_NOTE_DATA.queryString(
getController(), i);
LibraryPhrase phrase = new LibraryPhrase();
phrase.setBankIndex(bankIndex);
phrase.setPatternIndex(patternIndex);
phrase.setMachineName(tone.getName());
phrase.setMetadataInfo(new MetadataInfo());
phrase.setId(UUID.randomUUID());
phrase.setLength(length);
phrase.setTempo(tempo);
phrase.setToneType(tone.getToneType());
phrase.setNoteData(noteData);
phrase.setResolution(calculateResolution(noteData));
TagUtils.addDefaultTags(phrase);
library.addPhrase(phrase);
}
}
}
}
protected void relocatePresetFile(Tone tone, Library library, LibraryPatch patch)
throws IOException {
String id = patch.getId().toString();
tone.getComponent(SynthComponent.class).savePreset(id);
File presetFile = RuntimeUtils.getCausticPresetsFile(tone.getToneType().getValue(), id);
if (!presetFile.exists()) {
throw new IOException("Preset file does not exist");
}
File presetsDirectory = library.getPresetsDirectory();
File destFile = new File(presetsDirectory, presetFile.getName());
FileUtils.copyFile(presetFile, destFile);
FileUtils.deleteQuietly(presetFile);
}
private Resolution calculateResolution(String data) {
// TODO This is totally inefficient, needs to be lazy loaded
// push the notes into the machines sequencer
float smallestGate = 1f;
String[] notes = data.split("\\|");
for (String noteData : notes) {
String[] split = noteData.split(" ");
float start = Float.parseFloat(split[0]);
float end = Float.parseFloat(split[3]);
float gate = end - start;
smallestGate = Math.min(smallestGate, gate);
}
Resolution result = Resolution.SIXTEENTH;
if (smallestGate <= Resolution.SIXTYFOURTH.getValue() * 4)
result = Resolution.SIXTYFOURTH;
else if (smallestGate <= Resolution.THIRTYSECOND.getValue() * 4)
result = Resolution.THIRTYSECOND;
else if (smallestGate <= Resolution.SIXTEENTH.getValue() * 4)
result = Resolution.SIXTEENTH;
return result;
}
@Override
public boolean isLibrary(File reletiveFile) {
return new File(librariesDirectory, reletiveFile.getPath()).exists();
}
@Override
public void deleteLibrary(File reletivePath) throws IOException {
Library library = getModel().getLibrary(reletivePath);
getModel().removeLibrary(library);
}
}