/* * Copyright 2008-2013, ETH Zürich, Samuel Welten, Michael Kuhn, Tobias Langner, * Sandro Affentranger, Lukas Bossard, Michael Grob, Rahul Jain, * Dominic Langenegger, Sonia Mayor Alonso, Roger Odermatt, Tobias Schlueter, * Yannick Stucki, Sebastian Wendland, Samuel Zehnder, Samuel Zihlmann, * Samuel Zweifel * * This file is part of Jukefox. * * Jukefox 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 any later version. Jukefox 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 * Jukefox. If not, see <http://www.gnu.org/licenses/>. */ package ch.ethz.dcg.jukefox.playmode; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Random; import ch.ethz.dcg.jukefox.commons.DataUnavailableException; import ch.ethz.dcg.jukefox.commons.utils.Log; import ch.ethz.dcg.jukefox.commons.utils.RandomProvider; import ch.ethz.dcg.jukefox.commons.utils.Utils; import ch.ethz.dcg.jukefox.data.db.PcaCoordinatesUnavailableException; import ch.ethz.dcg.jukefox.model.AbstractCollectionModelManager; import ch.ethz.dcg.jukefox.model.AbstractPlayerModelManager; import ch.ethz.dcg.jukefox.model.collection.BaseAlbum; import ch.ethz.dcg.jukefox.model.collection.BaseArtist; import ch.ethz.dcg.jukefox.model.collection.PlaylistSong; import ch.ethz.dcg.jukefox.model.collection.PlaylistSong.SongSource; import ch.ethz.dcg.jukefox.model.collection.SongCoords; /** * Handles all the smart shuffling logic. */ @Deprecated public class SmartShuffleManager { protected final static String TAG = SmartShuffleManager.class.getSimpleName(); protected final static int INIT_SAMPLE_SIZE = 300; protected final AbstractCollectionModelManager collectionModel; protected AbstractPlayerModelManager playerModel; protected final Random random; protected LinkedHashSet<Integer> played = new LinkedHashSet<Integer>(); protected ArrayList<ProcessedSong> processed = new ArrayList<ProcessedSong>(); protected double weightThreshold = 0.01; protected int resetCnt = -1; // account for first call to reset... protected int minPlayableSize = 80; protected int sampleSize; protected int minSampleSize = 200; protected int maxPlayedHistorySize = 50; protected int numCandiates = 5; public SmartShuffleManager(AbstractCollectionModelManager collectionModel, AbstractPlayerModelManager playerModel) { this.collectionModel = collectionModel; this.playerModel = playerModel; this.random = RandomProvider.getRandom(); } /** * Get a new song chosen by smart shuffle. * * @param skipped */ public PlaylistSong<BaseArtist, BaseAlbum> getSong(int currentlyPlaying, boolean skipped) throws DataUnavailableException { if (currentlyPlaying == -1 && processed.isEmpty()) { int randomSongId; randomSongId = getRandomNonPlayedId(); return new PlaylistSong<BaseArtist, BaseAlbum>(collectionModel.getSongProvider().getBaseSong(randomSongId), SongSource.RANDOM_SONG); } long start = System.currentTimeMillis(); int songId = getNextSongId(currentlyPlaying, skipped); Log.i(TAG, "returning song id " + songId); long end = System.currentTimeMillis(); Log.i(TAG, "time for smart shuffle: " + (end - start)); PlaylistSong<BaseArtist, BaseAlbum> song = new PlaylistSong<BaseArtist, BaseAlbum>(collectionModel .getSongProvider().getBaseSong(songId), SongSource.RANDOM_SONG); return song; } // TODO: Do we need this or can we not just every song to played? public void addToPlayed(int songId) { played.add(songId); if (played.size() > maxPlayedHistorySize) { // remove oldest int id = played.iterator().next(); played.remove(id); } } public void processSong(int songId, float rating) { Log.v(TAG, "addSongToProcessed(): id: " + songId + ", rating: " + rating); ProcessedSong ps = createProcessedSong(songId, rating); if (ps == null) { return; } processed.add(ps); adjustWeights(); } public float getRatingForSignal(boolean skipped) { return skipped ? -1 : 1; } private ProcessedSong createProcessedSong(int songId, float rating) { if (songId <= 0) { return null; } float[] pcaCoords = null; try { pcaCoords = collectionModel.getOtherDataProvider().getSongPcaCoords(songId); } catch (DataUnavailableException e) { Log.w(TAG, e); } if (pcaCoords == null) { Log.v(TAG, "no pcaCoords available for song => don't add it to processed."); return null; } ProcessedSong ps = new ProcessedSong(songId, Math.abs(rating), rating <= 0, pcaCoords); return ps; } public void reset() { resetCnt++; this.sampleSize = INIT_SAMPLE_SIZE; played.clear(); processed.clear(); } protected int getNextSongId(int currentlyPlaying, boolean skipped) throws DataUnavailableException { // ArrayList<Integer> songIds = // preloadedDataManager.getData().getIdsOfSongsWithCoords(); setSampleSize(sampleSize); // make sure the bounds are still valid... // ArrayList<Integer> sampleIndices = // Utils.getRandomNumbers(songIds.size(), sampleSize, random); List<SongCoords> candidateSongs = collectionModel.getSongCoordinatesProvider().getRandomSongsWithCoords( sampleSize); Log.i(TAG, "sampleSize(): " + sampleSize); Log.i(TAG, "candidateSongs.size(): " + candidateSongs.size()); if (onlySkippedSongs() && (currentlyPlaying == -1 || skipped)) { Log.v(TAG, "returning far to bad song"); return getFarToBadSong(currentlyPlaying, skipped, candidateSongs); } else { Log.v(TAG, "returning close to good song"); return getCloseToGoodSong(currentlyPlaying, skipped, candidateSongs); } } protected int getFarToBadSong(int currentSongId, boolean skipped, List<SongCoords> candidateSongs) throws DataUnavailableException { ArrayList<Float> distribution = new ArrayList<Float>(); ArrayList<Integer> playable = new ArrayList<Integer>(); float sum = 0; for (SongCoords sc : candidateSongs) { int id = sc.getId(); sum = processIdForCloseToBadSong(id, currentSongId, skipped, sum, distribution, playable); // if (played.contains(id)) { // continue; // } // float[] pos; // try { // pos = model.getSongPcaCoords(id); // } catch (DataUnavailableException e) { // continue; // } // ProcessedSong ps = getClosestProcessedSong(pos); // // // songs that have the closest processed song far away get high // // probability to be chosen. // float weightedDist = Utils.distance(pos, ps.pos) * ps.weight; // sum += weightedDist; // distribution.add(sum); // playable.add(id); } if (playable.size() == 0) { return getRandomNonPlayedId(); } if (playable.size() < minPlayableSize) { setSampleSize(sampleSize * 2); } else { setSampleSize((int) Math.round(sampleSize * 0.9)); } Log.i(TAG, "new sample size: " + sampleSize + ", playableCnt: " + playable.size()); return getCandidate(distribution, playable, sum); } protected Float processIdForCloseToBadSong(int id, int currentSongId, boolean skipped, float sum, ArrayList<Float> distribution, ArrayList<Integer> playable) { if (played.contains(id) || id == currentSongId) { return sum; } float[] pos; try { pos = collectionModel.getOtherDataProvider().getSongPcaCoords(id); } catch (PcaCoordinatesUnavailableException e) { // TODO Controller should be noticed that something is probably // strange (coords without pca coords). return sum; } catch (DataUnavailableException e) { return sum; } ProcessedSong ps = getClosestProcessedSong(pos, currentSongId, skipped); // songs that have the closest processed song far away get high // probability to be chosen. float weightedDist = Utils.distance(pos, ps.pos) * ps.weight; sum += weightedDist; distribution.add(sum); playable.add(id); return sum; } protected int getCloseToGoodSong(int currentSongId, boolean skipped, List<SongCoords> candidateSongs) throws DataUnavailableException { ArrayList<Float> distribution = new ArrayList<Float>(); ArrayList<Integer> playable = new ArrayList<Integer>(); float sum = 0; for (SongCoords sc : candidateSongs) { int id = sc.getId(); sum = processIdForCloseToGoodSong(id, currentSongId, skipped, sum, distribution, playable); // if (played.contains(id)) { // continue; // } // float[] pos; // try { // pos = model.getSongPcaCoords(id); // } catch (DataUnavailableException e) { // continue; // } // ProcessedSong ps = getClosestProcessedSong(pos); // if (!ps.skipped) { // double p = ps.weight; // sum += p; // distribution.add(sum); // playable.add(id); // } } if (playable.size() == 0) { return getRandomNonPlayedId(); } if (playable.size() < minPlayableSize) { setSampleSize(sampleSize * 2); } else { setSampleSize((int) Math.round(sampleSize * 0.9)); } Log.i(TAG, "new sample size: " + sampleSize + ", playableCnt: " + playable.size()); return getBestCandidate(currentSongId, skipped, distribution, playable, sum); } protected float processIdForCloseToGoodSong(int id, int currentSongId, boolean skipped, float sum, ArrayList<Float> distribution, ArrayList<Integer> playable) { if (played.contains(id) || id == currentSongId) { return sum; } float[] pos; try { pos = collectionModel.getOtherDataProvider().getSongPcaCoords(id); } catch (PcaCoordinatesUnavailableException e) { // TODO Controller should be notified that something is probably // strange (coords without pca coords). return sum; } catch (DataUnavailableException e) { return sum; } ProcessedSong ps = getClosestProcessedSong(pos, currentSongId, skipped); if (ps != null && !ps.skipped) { double p = ps.weight; sum += p; distribution.add(sum); playable.add(id); } return sum; } protected int getBestCandidate(int currentSongId, boolean skipped, ArrayList<Float> distribution, ArrayList<Integer> ids, float max) throws DataUnavailableException { float minDist = Float.MAX_VALUE; int minId = -1; for (int i = 0; i < numCandiates; i++) { int songId = getCandidate(distribution, ids, max); float[] pos; try { pos = collectionModel.getOtherDataProvider().getSongPcaCoords(songId); } catch (DataUnavailableException e) { continue; } ProcessedSong ps = getClosestProcessedSong(pos, currentSongId, skipped); float weightedDist = Utils.distance(pos, ps.pos) / ps.weight; if (weightedDist < minDist) { minId = songId; minDist = weightedDist; } } if (minId == -1) { String msg = "getBestCandidate: Should never happen! " + "=> returning random song"; Log.i(TAG, msg); return getRandomNonPlayedId(); } return minId; } protected int getCandidate(ArrayList<Float> distribution, ArrayList<Integer> ids, float max) { float f = random.nextFloat() * max; int idx = Collections.binarySearch(distribution, f); if (idx < 0) { idx = -idx - 1; } return ids.get(idx); } protected int getRandomId() throws DataUnavailableException { try { List<PlaylistSong<BaseArtist, BaseAlbum>> songs = collectionModel.getSongProvider() .getRandomSongWithCoordinates(1); if (songs != null && songs.size() > 0) { return songs.get(0).getId(); } } catch (DataUnavailableException e) { Log.w(TAG, e); } Log.v(TAG, "no songs with coordinates available => selecting random song from db."); return collectionModel.getOtherDataProvider().getRandomSongId(); } protected int getRandomNonPlayedId() throws DataUnavailableException { int id = getRandomId(); int cnt = 0; while (played.contains(id) && cnt < 8) { id = getRandomId(); cnt++; } return id; } protected ProcessedSong getClosestProcessedSong(float[] pos, int currentSongId, boolean skipped) { float minDist = Float.MAX_VALUE; ProcessedSong closest = null; for (ProcessedSong ps : processed) { float dist = Utils.distance(pos, ps.pos); if (dist < minDist) { minDist = dist; closest = ps; } } ProcessedSong currentSong = createProcessedSong(currentSongId, getRatingForSignal(skipped)); if (currentSong != null) { float dist = Utils.distance(pos, currentSong.pos); if (dist < minDist) { minDist = dist; closest = currentSong; } } return closest; } protected boolean onlySkippedSongs() { for (ProcessedSong ps : processed) { if (!ps.skipped) { return false; } } return true; } protected void adjustWeights() { // quickly remove existing centroids if the user stops liking some area // consecutiveSkipCnt = skipped ? consecutiveSkipCnt + 1 : 0; int n = processed.size(); for (int i = n - 1; i >= 0; i--) { ProcessedSong ps = processed.get(i); // ps.weight -= 0.1 * (consecutiveSkipCnt + 1); // don't specially treat consecutive skips at the moment ps.weight -= 0.1; if (ps.weight <= weightThreshold) { processed.remove(i); } } } protected void setSampleSize(int size) { // keep this order (if collectionSize < minSampleSize it still works) size = Math.max(minSampleSize, size); int collectionSize = 0; try { collectionSize = collectionModel.getOtherDataProvider().getNumberOfSongsWithCoordinates(); } catch (DataUnavailableException e) { Log.w(TAG, e); } size = Math.min(collectionSize, size); sampleSize = size; } }