Permalink
Browse files

peek song list cache

  • Loading branch information...
1 parent 382a50e commit e4303c3689d640dad8b7b0bb8e9215b5ab41105f @albertz committed Dec 27, 2012
Showing with 170 additions and 24 deletions.
  1. +1 −0 ffmpeg.c
  2. +5 −0 ffmpeg.h
  3. +51 −15 ffmpeg_player.cpp
  4. +101 −7 ffmpeg_player_decoding.cpp
  5. +9 −2 player.py
  6. +3 −0 queue.py
View
@@ -17,6 +17,7 @@
// Pyton interface:
// createPlayer() -> player object with:
// queue: song generator
+// peekQueue(n): list of upcoming songs. this should not change the queue. this also might not be accurate but that doesn't matter. it might also return less. it is used for caching and gapless playback. if the queue returns other songs later, it will just ignore these peeked songs. otherwise, it will use these caches.
// playing: True or False, initially False
// volume: 1.0 is norm; this is just a factor to each sample. default is 0.9
// volumeSmoothClip: smooth clipping, see below. set to (1,1) to disable. default is (0.95,10)
View
@@ -131,6 +131,7 @@ struct PlayerObject {
// public
PyObject* queue;
+ PyObject* peekQueue;
PyObject* curSong;
bool playing;
int setPlaying(bool playing);
@@ -158,7 +159,11 @@ struct PlayerObject {
struct InStream;
boost::shared_ptr<InStream> inStream;
+ typedef std::list<boost::shared_ptr<InStream> > PeekInStreams;
+ PeekInStreams peekInStreams;
bool openInStream();
+ bool tryOvertakePeekInStream();
+ void openPeekInStreams();
bool isInStreamOpened() const; // in case we hit EOF, it is still opened
Buffer* inStreamBuffer();
void resetBuffers();
View
@@ -42,7 +42,11 @@ bool PlayerObject::getNextSong(bool skipped) {
if(!player->curSong || PyErr_Occurred())
goto final;
- if(!player->openInStream()) {
+ if(tryOvertakePeekInStream()) {
+ // nothing needs to be done anymore!
+ ret = true;
+ }
+ else if(!player->openInStream()) {
// This is not fatal, so don't make a Python exception.
// When we are in playing state, we will just skip to the next song.
// This can happen if we don't support the format or whatever.
@@ -84,6 +88,8 @@ bool PlayerObject::getNextSong(bool skipped) {
}
}
+ openPeekInStreams();
+
final:
Py_XDECREF(oldSong);
PyGILState_Release(gstate);
@@ -107,6 +113,20 @@ static int player_setqueue(PlayerObject* player, PyObject* queue) {
return 0;
}
+static int player_setpeekqueue(PlayerObject* player, PyObject* queue) {
+ Py_XDECREF(player->queue);
+ Py_INCREF((PyObject*)player);
+ Py_BEGIN_ALLOW_THREADS
+ {
+ PyScopedLock lock(player->lock);
+ player->peekQueue = queue;
+ }
+ Py_END_ALLOW_THREADS
+ Py_DECREF((PyObject*)player);
+ Py_XINCREF(queue);
+ return 0;
+}
+
static
PyObject* player_new(PyTypeObject *subtype, PyObject *args, PyObject *kwds) {
PlayerObject* player = (PlayerObject*) subtype->tp_alloc(subtype, 0);
@@ -178,6 +198,9 @@ void player_dealloc(PyObject* obj) {
Py_XDECREF(player->queue);
player->queue = NULL;
+ Py_XDECREF(player->peekQueue);
+ player->peekQueue = NULL;
+
player->outStream.reset();
player->inStream.reset();
@@ -301,21 +324,22 @@ PyObject* player_getattr(PyObject* obj, char* key) {
}
if(strcmp(key, "__members__") == 0) {
- PyObject* mlist = PyList_New(14);
+ PyObject* mlist = PyList_New(15);
PyList_SetItem(mlist, 0, PyString_FromString("queue"));
- PyList_SetItem(mlist, 1, PyString_FromString("playing"));
- PyList_SetItem(mlist, 2, PyString_FromString("curSong"));
- PyList_SetItem(mlist, 3, PyString_FromString("curSongPos"));
- PyList_SetItem(mlist, 4, PyString_FromString("curSongLen"));
- PyList_SetItem(mlist, 5, PyString_FromString("curSongMetadata"));
- PyList_SetItem(mlist, 6, PyString_FromString("curSongGainFactor"));
- PyList_SetItem(mlist, 7, PyString_FromString("seekAbs"));
- PyList_SetItem(mlist, 8, PyString_FromString("seekRel"));
- PyList_SetItem(mlist, 9, PyString_FromString("nextSong"));
- PyList_SetItem(mlist, 10, PyString_FromString("volume"));
- PyList_SetItem(mlist, 11, PyString_FromString("volumeSmoothClip"));
- PyList_SetItem(mlist, 12, PyString_FromString("outSamplerate"));
- PyList_SetItem(mlist, 13, PyString_FromString("outNumChannels"));
+ PyList_SetItem(mlist, 1, PyString_FromString("peekQueue"));
+ PyList_SetItem(mlist, 2, PyString_FromString("playing"));
+ PyList_SetItem(mlist, 3, PyString_FromString("curSong"));
+ PyList_SetItem(mlist, 4, PyString_FromString("curSongPos"));
+ PyList_SetItem(mlist, 5, PyString_FromString("curSongLen"));
+ PyList_SetItem(mlist, 6, PyString_FromString("curSongMetadata"));
+ PyList_SetItem(mlist, 7, PyString_FromString("curSongGainFactor"));
+ PyList_SetItem(mlist, 8, PyString_FromString("seekAbs"));
+ PyList_SetItem(mlist, 9, PyString_FromString("seekRel"));
+ PyList_SetItem(mlist, 10, PyString_FromString("nextSong"));
+ PyList_SetItem(mlist, 11, PyString_FromString("volume"));
+ PyList_SetItem(mlist, 12, PyString_FromString("volumeSmoothClip"));
+ PyList_SetItem(mlist, 13, PyString_FromString("outSamplerate"));
+ PyList_SetItem(mlist, 14, PyString_FromString("outNumChannels"));
return mlist;
}
@@ -326,6 +350,14 @@ PyObject* player_getattr(PyObject* obj, char* key) {
}
goto returnNone;
}
+
+ if(strcmp(key, "peekQueue") == 0) {
+ if(player->peekQueue) {
+ Py_INCREF(player->peekQueue);
+ return player->peekQueue;
+ }
+ goto returnNone;
+ }
if(strcmp(key, "playing") == 0) {
return PyBool_FromLong(player->playing);
@@ -426,6 +458,10 @@ int player_setattr(PyObject* obj, char* key, PyObject* value) {
if(strcmp(key, "queue") == 0) {
return player_setqueue(player, value);
}
+
+ if(strcmp(key, "peekQueue") == 0) {
+ return player_setpeekqueue(player, value);
+ }
if(strcmp(key, "playing") == 0) {
return player->setPlaying(PyObject_IsTrue(value));
View
@@ -17,6 +17,7 @@ extern "C" {
#define PROCESS_SIZE (BUFFER_CHUNK_SIZE * 10) // how much data to proceed in processInStream()
#define BUFFER_FILL_SIZE (48000 * 2 * 2 * 10) // 10secs for 48kHz,stereo - around 2MB
+#define PEEKSTREAM_NUM 3
int initPlayerDecoder() {
av_log_set_level(0);
@@ -846,9 +847,7 @@ static long audio_decode_frame(PlayerObject* player, PlayerObject::InStream *is,
}
}
-bool PlayerObject::buffersFullEnough() const {
- PlayerObject::InStream* is = this->inStream.get();
- if(!is) return true;
+static bool _buffersFullEnough(PlayerObject::InStream* is) {
if(is->playerHitEnd) return true;
size_t c = 0;
for(auto& it : is->outBuffer.chunks) {
@@ -858,12 +857,20 @@ bool PlayerObject::buffersFullEnough() const {
return false;
}
+bool PlayerObject::buffersFullEnough() const {
+ PlayerObject::InStream* is = this->inStream.get();
+ if(!is) return true;
+ return _buffersFullEnough(is);
+}
+
+static bool _processInStream(PlayerObject* player, PlayerObject::InStream* is) {
+ return audio_decode_frame(player, is, PROCESS_SIZE) >= 0;
+}
+
bool PlayerObject::processInStream() {
PlayerObject::InStream* is = this->inStream.get();
if(!is) return false;
- if(audio_decode_frame(this, is, PROCESS_SIZE) < 0)
- return false;
- return true;
+ return _processInStream(this, is);
}
bool PlayerObject::isInStreamOpened() const {
@@ -877,6 +884,78 @@ Buffer* PlayerObject::inStreamBuffer() {
return NULL;
}
+static boost::shared_ptr<PlayerObject::InStream> takePeekInStream(PlayerObject::PeekInStreams& list, PyObject* song) {
+ for(PlayerObject::PeekInStreams::iterator it = list.begin(); it != list.end(); ++it) {
+ assert(it->get() != NULL);
+ assert(it->get()->song != NULL);
+ if(PyObject_RichCompareBool(song, it->get()->song, Py_EQ) == 1) {
+ boost::shared_ptr<PlayerObject::InStream> inStream = *it;
+ list.erase(it);
+ return inStream;
+ }
+ }
+ return boost::shared_ptr<PlayerObject::InStream>();
+}
+
+void PlayerObject::openPeekInStreams() {
+ PlayerObject* player = this;
+ if(player->peekQueue == NULL) return;
+
+ PyObject* args = NULL;
+ PyObject* peekList = NULL;
+ PyObject* peekListIter = NULL;
+ PyObject* song = NULL;
+ PeekInStreams oldPeekList;
+
+ args = PyTuple_New(1);
+ if(!args) goto final;
+ PyTuple_SetItem(args, 0, PyInt_FromLong(PEEKSTREAM_NUM));
+ peekList = PyObject_CallObject(player->peekQueue, args);
+ if(!peekList) goto final;
+
+ peekListIter = PyObject_GetIter(peekList);
+ if(!peekListIter) goto final;
+
+ std::swap(oldPeekList, player->peekInStreams);
+ while((song = PyIter_Next(peekListIter)) != NULL) {
+ boost::shared_ptr<PlayerObject::InStream> s = takePeekInStream(oldPeekList, song);
+ if(!s.get()) {
+ s.reset(new PlayerObject::InStream);
+ if(!s->open(player, song))
+ s.reset();
+ }
+ if(s.get())
+ player->peekInStreams.push_front(s);
+ Py_DECREF(song);
+ }
+ oldPeekList.clear();
+
+final:
+ // pass through any Python errors
+ if(PyErr_Occurred())
+ PyErr_Print();
+
+ Py_XDECREF(song);
+ Py_XDECREF(peekListIter);
+ Py_XDECREF(peekList);
+ Py_XDECREF(args);
+}
+
+bool PlayerObject::tryOvertakePeekInStream() {
+ assert(curSong != NULL);
+ boost::shared_ptr<InStream> s = takePeekInStream(this->peekInStreams, curSong);
+ if(s.get()) {
+ inStream = s;
+ // take the new Song object. it might be a different one.
+ Py_XDECREF(curSong);
+ curSong = s->song;
+ Py_INCREF(curSong);
+ return true;
+ }
+ return false;
+}
+
+
static void loopFrame(PlayerObject* player) {
// We must not hold the PyGIL here!
@@ -943,6 +1022,14 @@ static void loopFrame(PlayerObject* player) {
PyGILState_Release(gstate);
}
}
+
+ {
+ PyScopedLock lock(player->lock);
+ for(auto& it : player->peekInStreams) {
+ if(!_buffersFullEnough(it.get()))
+ _processInStream(player, it.get());
+ }
+ }
}
void PlayerObject::workerProc(PyMutex& lock, bool& stopSignal) {
@@ -953,7 +1040,14 @@ void PlayerObject::workerProc(PyMutex& lock, bool& stopSignal) {
}
loopFrame(this);
- usleep(1000);
+
+ {
+ PyScopedLock l(lock);
+ if(buffersFullEnough()) {
+ PyScopedUnlock ul(lock);
+ usleep(1000);
+ }
+ }
}
}
View
@@ -13,7 +13,7 @@ class PlayerEventCallbacks:
onSongFinished = None
onPlayingStateChange = None
-def songs(state):
+def songsQueue(state):
if state.curSong:
# We just started the player and we have a current song from persistent storage.
# Yield it now so that we begin playing with this song.
@@ -30,6 +30,12 @@ def songs(state):
song.openFile()
yield song
+def songsPeekQueue():
+ def openSong(song):
+ song.openFile()
+ return song
+ return lambda n: filter(openSong, queue.peekNextSongs(n))
+
# This is an special extra callback.
# This is called very first. We do this so that
# we always have state.curSong right.
@@ -47,7 +53,8 @@ def loadPlayer(state):
cb.extraCall = onSongChange
setattr(PlayerEventCallbacks, e, cb)
setattr(player, e, cb)
- player.queue = songs(state)
+ player.queue = songsQueue(state)
+ player.peekQueue = songsPeekQueue()
player.volume = state.volume
return player
View
@@ -223,6 +223,9 @@ def shuffle(self):
def getNextSong():
return queue.getNextSong()
+def peekNextSongs(n=10):
+ return queue.peekNextN(n=n)
+
def queueMain():
queue.fillUpTo() # add some right away if empty...
for ev, args, kwargs in state.updates.read():

0 comments on commit e4303c3

Please sign in to comment.