/*
* The GPLv3 licence :
* -----------------
* Copyright (c) 2009 Ricardo Dias
*
* This file is part of MuVis.
*
* MuVis is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* MuVis is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MuVis. If not, see <http://www.gnu.org/licenses/>.
*/
package muvis.analyser.processor;
import comirva.audio.extraction.AudioFeatureExtractionThread;
import comirva.audio.extraction.FluctuationPatternExtractionThread;
import comirva.data.DataMatrix;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JButton;
import javax.swing.JFrame;
import muvis.Elements;
import muvis.NBTreeManager;
import muvis.Environment;
import muvis.Messages;
import muvis.analyser.loader.Loader;
import muvis.audio.AudioMetadata;
import muvis.audio.AudioMetadataExtractor;
import muvis.audio.MP3AudioMetadataExtractor;
import muvis.audio.MP3AudioSnippetExtractor;
import muvis.database.MusicLibraryDatabaseManager;
import muvis.exceptions.CannotRetrieveMP3TagException;
import muvis.util.MP3AudioFile;
import muvis.util.Observable;
import muvis.util.Observer;
import muvis.util.Util;
import muvis.view.loader.LoadingLibraryViewUI;
import nbtree.NBPoint;
import nbtree.NBTree;
import nbtree.exceptions.NBTreeException;
public class ContentProcessor implements Observer, Observable {
private ArrayList<Observer> observers;
private ExecutorService threadPool;
private int nextFileToProcess = -1;
private MusicLibraryDatabaseManager dbManager;
private NBTree tracksNBTree, albumsTree, artistsTree;
private File[] filesToProcess;
private LoadingLibraryViewUI loadingLibraryUI;
private boolean libraryLoadingFinished;
private State processorState;
enum State{
RUN, PAUSE, STOP
}
public ContentProcessor() {
observers = new ArrayList<Observer>();
threadPool = Executors.newFixedThreadPool(5);
dbManager = Environment.getEnvironmentInstance().getDatabaseManager();
NBTreeManager nbtreeManager = Environment.getEnvironmentInstance().getNbtreesManager();
tracksNBTree = nbtreeManager.getNBTree(Elements.TRACKS_NBTREE);
albumsTree = nbtreeManager.getNBTree(Elements.ALBUMS_NBTREE);
artistsTree = nbtreeManager.getNBTree(Elements.ARTISTS_NBTREE);
libraryLoadingFinished = false;
}
/**
* Thread that extracts the main track tags
*/
class PreliminarContentProcessorThread extends Thread {
private AudioMetadataExtractor metadataExtractor;
private int numFilesProcessed = 0;
final private Object parent;
private int fileToProc = -1;
Random rnd = new Random();
Random rnd2 = new Random();
public PreliminarContentProcessorThread(Object parent) {
metadataExtractor = new MP3AudioMetadataExtractor();
this.parent = parent;
}
@Override
public void run() {
File file;
while (true) {
if (processorState.equals(ContentProcessor.State.PAUSE)){
try {
sleep(2000);
continue;
} catch (InterruptedException ex) {
ex.printStackTrace();
}
} else if (processorState.equals(ContentProcessor.State.STOP)){
//ends the processing abruptly
return;
}
synchronized (parent) {
if (nextFileToProcess < (filesToProcess.length - 1)) {
nextFileToProcess++;
fileToProc = nextFileToProcess;
file = filesToProcess[fileToProc];
loadingLibraryUI.loadingTracksLabel.setText("Loading track " + fileToProc + " of " + filesToProcess.length);
loadingLibraryUI.trackPathNameLabel.setText(file.getAbsolutePath());
loadingLibraryUI.loadingLibraryProgressBar.setValue(fileToProc);
} else {
return;
}
if (numFilesProcessed == 100) {
try {
sleep(2000);
} catch (InterruptedException ex) {
System.out.print("Couldn't sleep this thread in PreliminarContentProcessor");
}
numFilesProcessed = 0;
}
}
try {
AudioMetadata metadata =
metadataExtractor.getAudioMetadata(file.getAbsolutePath());
String artistName = metadata.getAuthor();
String albumName = metadata.getAlbum();
String filename = file.getAbsolutePath();
//add to the database to make the main informations available in the interface
dbManager.addNewSong(filename, artistName, albumName, metadata);
dbManager.setTrackMood(filename, Util.mood[rnd.nextInt(4)]);
dbManager.setTrackBeat(filename, Util.beat[rnd2.nextInt(4)]);
numFilesProcessed++;
} catch (SQLException ex) {
ex.printStackTrace();
continue;
} catch (CannotRetrieveMP3TagException ex) {
ex.printStackTrace();
continue;
}
}
}
}
/**
* Class that extracts the main features and saves them in the database and
* in the nbtree structure
*/
class ContentProcessorTrackThread extends Thread {
final private Object lock;
private int id, numTries = 0, numFilesProcessed = 0;
private int fileToProc = -1;
public ContentProcessorTrackThread(Object lock, int threadId) {
this.lock = lock;
id = threadId;
}
@Override
public void run() {
File file = null;
System.out.println("Starting thread with id: " + id);
while (true) {
if (processorState.equals(ContentProcessor.State.PAUSE)){
try {
sleep(2000);
continue;
} catch (InterruptedException ex) {
ex.printStackTrace();
}
} else if (processorState.equals(ContentProcessor.State.STOP)){
//ends the processing abruptly
return;
}
if (numTries == 0) {
synchronized (lock) {
if (nextFileToProcess < (filesToProcess.length - 1)) {
nextFileToProcess++;
fileToProc = nextFileToProcess;
file = filesToProcess[fileToProc];
loadingLibraryUI.loadingTracksLabel.setText("Loading track " + fileToProc + " of " + filesToProcess.length);
loadingLibraryUI.trackPathNameLabel.setText(file.getAbsolutePath());
loadingLibraryUI.loadingLibraryProgressBar.setValue(fileToProc);
} else {
System.out.println("Finishing thread with id: " + id);
return;
}
}
}
//process now the file
try {
if (numFilesProcessed == 100) {
sleep(5000);
numFilesProcessed = 0;
}
File[] filesInQueue = new File[1];
filesInQueue[0] = file;
byte[] snippet;
AudioFeatureExtractionThread thread = null;
if (numTries == 0) {
try {
snippet = MP3AudioSnippetExtractor.extractAudioSnippet(file.getAbsolutePath());
ByteArrayInputStream inputStream = new ByteArrayInputStream(snippet);
thread = new FluctuationPatternExtractionThread(inputStream);
thread.run();
} catch (Exception e) {
System.out.println("Couldn't extract with snippet, trying with file: " + file.getAbsolutePath());
//numTries++;
numTries = 0;
continue;
}
}
//obtaining the processed datamatrix
DataMatrix featuresMatrix = thread.getMatrix();
//updating the track with their descriptor
double[] descriptor = featuresMatrix.toDoubleArray()[0];
//normalizing the descriptor
descriptor = Util.normalize(descriptor);
double key = -1;
key = dbManager.getTrackKey(file.getAbsolutePath());
if (key != -1) {
tracksNBTree.removePoint(key);
}
//insert track in the respective NBTree
key = tracksNBTree.insertPoint(new NBPoint(descriptor));
//update the database with the new key for the track
dbManager.setTrackKey(file.getAbsolutePath(), key);
numFilesProcessed++;
System.out.println("Track processed: " + file.getAbsolutePath());
} catch (SQLException ex) {
ex.printStackTrace();
} catch (NBTreeException ex) {
ex.printStackTrace();
} catch (Exception ex) {
ex.printStackTrace();
continue;
}
}
}
}
class ContentProcessorAlbumsArtistsThread extends Thread {
@Override
public void run() {
//updating the artist and albums descriptors
ArrayList<String> artistNames = dbManager.getAllArtistNames();
ArrayList<double[]> albumsDescriptors = new ArrayList<double[]>();
int i = 0, total = artistNames.size();
loadingLibraryUI.loadingLibraryProgressBar.setMinimum(0);
loadingLibraryUI.loadingLibraryProgressBar.setMaximum(total);
loadingLibraryUI.loadingTracksLabel.setText(Messages.EMPTY_STRING);
loadingLibraryUI.trackPathNameLabel.setText(Messages.EMPTY_STRING);
for (String artist : artistNames) {
while (processorState.equals(ContentProcessor.State.PAUSE)){
try {
sleep(2000);
if (processorState.equals(ContentProcessor.State.STOP)){
//ends the processing abruptly
return;
}
continue;
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
if (processorState.equals(ContentProcessor.State.STOP)){
//ends the processing abruptly
return;
}
i++;
loadingLibraryUI.loadingTracksLabel.setText("Processing artist " + i + " of " + total);
loadingLibraryUI.trackPathNameLabel.setText("");
ArrayList<String> artistAlbums = dbManager.getArtistAlbums(artist);
for (String album : artistAlbums) {
ArrayList<String> albumTracks = dbManager.getAlbumTracks(artist, album);
int numAlbumTracks = 1;
double[] albumDescriptor = new double[1200];
for (String track : albumTracks) {
double key = dbManager.getTrackKey(track);
try {
NBPoint point = tracksNBTree.lookupPoint(key);
if (point != null) {
double[] trackDescriptor = tracksNBTree.lookupPoint(key).toArray();
albumDescriptor = Util.sum(albumDescriptor, trackDescriptor);
numAlbumTracks++;
} else {
continue;
}
} catch (NBTreeException ex) {
ex.printStackTrace();
}
}
//calculating the new descriptor
for (int j = 0; j < 1200; j++) {
albumDescriptor[j] /= numAlbumTracks;
}
try {
//normalize audio descriptor
albumDescriptor = Util.normalize(albumDescriptor);
double albumKey = dbManager.getAlbumKey(artist, album);
if (albumKey != -1) {
//must remove the key to update it again
albumsTree.removePoint(albumKey);
}
//updating the nbTree with the new point
albumKey = albumsTree.insertPoint(new NBPoint(albumDescriptor));
//updates the database with the new album key
dbManager.setAlbumKey(artist, album, albumKey);
} catch (SQLException ex) {
ex.printStackTrace();
} catch (NBTreeException ex) {
ex.printStackTrace();
}
albumsDescriptors.add(albumDescriptor);
}
double[] artistDescriptor = new double[1200];
for (double[] albumDescriptor : albumsDescriptors) {
artistDescriptor = Util.sum(artistDescriptor, albumDescriptor);
}
//calculating the new descriptor
for (int j = 0; j < 1200; j++) {
artistDescriptor[j] /= albumsDescriptors.size();
}
try {
//normalize artist descriptor
artistDescriptor = Util.normalize(artistDescriptor);
double artistKey = dbManager.getArtistKey(artist);
if (artistKey != -1) {
//remove the artist key so we can update it again
artistsTree.removePoint(artistKey);
}
//insert the new artist key
artistKey = artistsTree.insertPoint(new NBPoint(artistDescriptor));
//updates the key in the artist
dbManager.setArtistKey(artist, artistKey);
} catch (SQLException ex) {
ex.printStackTrace();
} catch (NBTreeException ex) {
ex.printStackTrace();
}
albumsDescriptors.clear();
}
}
}
class ContentProcessorGUIThread extends Thread {
ArrayList<Thread> threads;
private void buildGUI() {
final JFrame frame = new JFrame(Messages.LOAD_LIBRARY_FRAME_LABEL);
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
loadingLibraryUI = new LoadingLibraryViewUI();
loadingLibraryUI.skipLoadingLibraryButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
//library processing must be stopped before disposing the frame
if (!libraryLoadingFinished) {
int skip = Util.displayConfirmationMessage(frame, Messages.LOAD_LIBRARY_CONFIRMATION_SCREEN, Messages.CONFIRMATION_LABEL);
if (skip == 0) { //must stop all the library processing
processorState = ContentProcessor.State.STOP;
frame.dispose();
}
//else do nothing
} else {
frame.dispose();
}
}
});
loadingLibraryUI.pauseLibraryLoadingButton.addActionListener(new ActionListener() {
boolean paused = false;
@Override
@SuppressWarnings("static-access")
public void actionPerformed(ActionEvent e) {
JButton button = (JButton) e.getSource();
if (!paused) {
button.setText(Messages.RESUME_LABEL);
paused = true;
button.setIcon(new javax.swing.ImageIcon(getClass().getResource("/icons/music/media-playback-start.png")));
processorState = ContentProcessor.State.PAUSE;
} else {
button.setText(Messages.PAUSE_LABEL);
paused = false;
button.setIcon(new javax.swing.ImageIcon(getClass().getResource("/icons/music/media-playback-pause.png")));
processorState = ContentProcessor.State.RUN;
}
}
});
loadingLibraryUI.loadingLibraryProgressBar.setMinimum(0);
loadingLibraryUI.loadingLibraryProgressBar.setMaximum(filesToProcess.length);
loadingLibraryUI.processingStageLabel.setText(Messages.LOAD_LIBRARY_INITIALIZE_STAGES);
loadingLibraryUI.loadingTracksLabel.setText(Messages.EMPTY_STRING);
loadingLibraryUI.trackPathNameLabel.setText(Messages.EMPTY_STRING);
frame.add(loadingLibraryUI);
frame.pack();
frame.setVisible(true);
}
@Override
public void run() {
buildGUI();
Executors.newFixedThreadPool(1).execute(new Thread() {
@Override
public void run() {
processorState = ContentProcessor.State.RUN;
int threadNum = Integer.parseInt(Environment.getEnvironmentInstance().getProperty("loader.threads_tags_extraction").toString());
threads = new ArrayList<Thread>(threadNum);
loadingLibraryUI.processingStageLabel.setText(Messages.LOAD_LIBRARY_TAGS_EXTRACTION);
for (int i = 0; i < threadNum; i++) {
Thread th = new PreliminarContentProcessorThread(this);
th.setPriority(Thread.MIN_PRIORITY);
threads.add(th);
threadPool.execute(th);
try {
th.join();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
//shutdown the threadPool and wait for it to finish
threadPool.shutdown();
while (!threadPool.isTerminated()) {
try {
sleep(3000);
} catch (InterruptedException ex) {
ex.printStackTrace();
continue;
}
}
//dispose this threads
threads.clear();
threadNum = Integer.parseInt(Environment.getEnvironmentInstance().getProperty("loader.threads_content_extraction").toString());
threadPool = Executors.newFixedThreadPool(threadNum);
loadingLibraryUI.processingStageLabel.setText(Messages.LOAD_LIBRARY_CONTENT_TRACKS_EXTRACTION);
loadingLibraryUI.loadingLibraryProgressBar.setValue(0);
loadingLibraryUI.loadingTracksLabel.setText(Messages.EMPTY_STRING);
loadingLibraryUI.trackPathNameLabel.setText(Messages.EMPTY_STRING);
nextFileToProcess = -1;
for (int i = 0; i < threadNum; i++) {
Thread t = new ContentProcessorTrackThread(this, i);
t.setPriority(Thread.MIN_PRIORITY);
threads.add(t);
threadPool.execute(t);
try {
t.join();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
threadPool.shutdown();
while (!threadPool.isTerminated()) {
try {
sleep(3000);
} catch (InterruptedException ex) {
ex.printStackTrace();
continue;
}
}
//dispose some more threads
threads.clear();
try {
tracksNBTree.save();
} catch (IOException ex) {
ex.printStackTrace();
}
loadingLibraryUI.processingStageLabel.setText(Messages.LOAD_LIBRARY_FULL_CONTENT_EXTRACTION);
loadingLibraryUI.loadingLibraryProgressBar.setValue(0);
loadingLibraryUI.loadingTracksLabel.setText(Messages.EMPTY_STRING);
loadingLibraryUI.trackPathNameLabel.setText(Messages.EMPTY_STRING);
threadPool = Executors.newFixedThreadPool(1);
Thread th = new ContentProcessorAlbumsArtistsThread();
th.setPriority(Thread.MIN_PRIORITY);
threadPool.execute(th);
threadPool.shutdown();
while (!threadPool.isTerminated()) {
try {
sleep(3000);
} catch (InterruptedException ex) {
ex.printStackTrace();
continue;
}
}
try {
albumsTree.save();
artistsTree.save();
} catch (IOException ex) {
ex.printStackTrace();
}
loadingLibraryUI.processingStageLabel.setText(Messages.LOAD_LIBRARY_FINISH_LABEL);
loadingLibraryUI.loadingTracksLabel.setText(Messages.EMPTY_STRING);
loadingLibraryUI.trackPathNameLabel.setText(Messages.EMPTY_STRING);
loadingLibraryUI.pauseLibraryLoadingButton.setEnabled(false);
loadingLibraryUI.pauseLibraryLoadingButton.setVisible(false);
loadingLibraryUI.skipLoadingLibraryButton.setText(Messages.CLOSE_LABEL);
loadingLibraryUI.skipLoadingLibraryButton.setIcon(null);
libraryLoadingFinished = true;
}
});
}
}
/**
* ###############################
* Observable and Observer methods
* ###############################
*/
@Override
public void update(Observable obs, Object arg) {
if (obs instanceof Loader) {
ArrayList<MP3AudioFile> files = (ArrayList<MP3AudioFile>) arg;
filesToProcess = new File[files.size()];
int i = 0;
for (MP3AudioFile file : files) {
filesToProcess[i] = file.getAudioFile();
i++;
}
Executors.newFixedThreadPool(1).execute(new ContentProcessorGUIThread());
}
}
@Override
public void registerObserver(Observer obs) {
observers.add(obs);
}
@Override
public void unregisterObserver(Observer obs) {
observers.remove(obs);
}
@Override
public void updateObservers() {
for (Observer obs : observers) {
obs.update(this, null);
}
}
}