From 5fba8d773c3f6ea35a2934baa91372806f1c9940 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Sat, 15 Aug 2015 15:55:46 +0200 Subject: PlayerThread, ...: move to src/player/ --- Makefile.am | 8 +- src/CrossFade.cxx | 140 ----- src/CrossFade.hxx | 72 --- src/Main.cxx | 2 +- src/Partition.hxx | 4 +- src/PlayerControl.cxx | 267 --------- src/PlayerControl.hxx | 463 --------------- src/PlayerListener.hxx | 36 -- src/PlayerThread.cxx | 1208 ---------------------------------------- src/PlayerThread.hxx | 45 -- src/Stats.cxx | 2 +- src/output/MultipleOutputs.cxx | 2 +- src/output/OutputCommand.cxx | 2 +- src/output/OutputThread.cxx | 2 +- src/player/Control.cxx | 267 +++++++++ src/player/Control.hxx | 463 +++++++++++++++ src/player/CrossFade.cxx | 140 +++++ src/player/CrossFade.hxx | 72 +++ src/player/Listener.hxx | 36 ++ src/player/Thread.cxx | 1208 ++++++++++++++++++++++++++++++++++++++++ src/player/Thread.hxx | 45 ++ src/queue/Playlist.cxx | 2 +- src/queue/PlaylistControl.cxx | 2 +- src/queue/PlaylistEdit.cxx | 2 +- src/queue/PlaylistState.cxx | 2 +- test/run_output.cxx | 2 +- test/test_mixramp.cxx | 2 +- 27 files changed, 2248 insertions(+), 2248 deletions(-) delete mode 100644 src/CrossFade.cxx delete mode 100644 src/CrossFade.hxx delete mode 100644 src/PlayerControl.cxx delete mode 100644 src/PlayerControl.hxx delete mode 100644 src/PlayerListener.hxx delete mode 100644 src/PlayerThread.cxx delete mode 100644 src/PlayerThread.hxx create mode 100644 src/player/Control.cxx create mode 100644 src/player/Control.hxx create mode 100644 src/player/CrossFade.cxx create mode 100644 src/player/CrossFade.hxx create mode 100644 src/player/Listener.hxx create mode 100644 src/player/Thread.cxx create mode 100644 src/player/Thread.hxx diff --git a/Makefile.am b/Makefile.am index 6e56b0a6b..d4f5e5090 100644 --- a/Makefile.am +++ b/Makefile.am @@ -95,7 +95,6 @@ libmpd_a_SOURCES = \ src/command/OtherCommands.cxx src/command/OtherCommands.hxx \ src/command/CommandListBuilder.cxx src/command/CommandListBuilder.hxx \ src/Idle.cxx src/Idle.hxx \ - src/CrossFade.cxx src/CrossFade.hxx \ src/decoder/DecoderError.cxx src/decoder/DecoderError.hxx \ src/decoder/DecoderThread.cxx src/decoder/DecoderThread.hxx \ src/decoder/DecoderCommand.hxx \ @@ -140,9 +139,10 @@ libmpd_a_SOURCES = \ src/Mapper.cxx src/Mapper.hxx \ src/Partition.cxx src/Partition.hxx \ src/Permission.cxx src/Permission.hxx \ - src/PlayerThread.cxx src/PlayerThread.hxx \ - src/PlayerControl.cxx src/PlayerControl.hxx \ - src/PlayerListener.hxx \ + src/player/CrossFade.cxx src/player/CrossFade.hxx \ + src/player/Thread.cxx src/player/Thread.hxx \ + src/player/Control.cxx src/player/Control.hxx \ + src/player/Listener.hxx \ src/PlaylistError.cxx src/PlaylistError.hxx \ src/PlaylistGlobal.cxx src/PlaylistGlobal.hxx \ src/PlaylistPrint.cxx src/PlaylistPrint.hxx \ diff --git a/src/CrossFade.cxx b/src/CrossFade.cxx deleted file mode 100644 index 6d7b41440..000000000 --- a/src/CrossFade.cxx +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (C) 2003-2015 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program 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 2 of the License, or - * (at your option) any later version. - * - * This program 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 this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "config.h" -#include "CrossFade.hxx" -#include "Chrono.hxx" -#include "MusicChunk.hxx" -#include "AudioFormat.hxx" -#include "util/NumberParser.hxx" -#include "util/Domain.hxx" -#include "Log.hxx" - -#include - -static constexpr Domain cross_fade_domain("cross_fade"); - -gcc_pure -static float -mixramp_interpolate(const char *ramp_list, float required_db) -{ - float last_db = 0, last_secs = 0; - bool have_last = false; - - /* ramp_list is a string of pairs of dBs and seconds that describe the - * volume profile. Delimiters are semi-colons between pairs and spaces - * between the dB and seconds of a pair. - * The dB values must be monotonically increasing for this to work. */ - - while (1) { - /* Parse the dB value. */ - char *endptr; - const float db = ParseFloat(ramp_list, &endptr); - if (endptr == ramp_list || *endptr != ' ') - break; - - ramp_list = endptr + 1; - - /* Parse the time. */ - float secs = ParseFloat(ramp_list, &endptr); - if (endptr == ramp_list || (*endptr != ';' && *endptr != 0)) - break; - - ramp_list = endptr; - if (*ramp_list == ';') - ++ramp_list; - - /* Check for exact match. */ - if (db == required_db) { - return secs; - } - - /* Save if too quiet. */ - if (db < required_db) { - last_db = db; - last_secs = secs; - have_last = true; - continue; - } - - /* If required db < any stored value, use the least. */ - if (!have_last) - return secs; - - /* Finally, interpolate linearly. */ - secs = last_secs + (required_db - last_db) * (secs - last_secs) / (db - last_db); - return secs; - } - - return -1; -} - -unsigned -CrossFadeSettings::Calculate(SignedSongTime total_time, - float replay_gain_db, float replay_gain_prev_db, - const char *mixramp_start, const char *mixramp_prev_end, - const AudioFormat af, - const AudioFormat old_format, - unsigned max_chunks) const -{ - unsigned int chunks = 0; - float chunks_f; - - if (total_time.IsNegative() || - duration < 0 || duration >= total_time.ToDoubleS() || - /* we can't crossfade when the audio formats are different */ - af != old_format) - return 0; - - assert(duration >= 0); - assert(af.IsValid()); - - chunks_f = (float)af.GetTimeToSize() / (float)CHUNK_SIZE; - - if (mixramp_delay <= 0 || !mixramp_start || !mixramp_prev_end) { - chunks = (chunks_f * duration + 0.5); - } else { - /* Calculate mixramp overlap. */ - const float mixramp_overlap_current = - mixramp_interpolate(mixramp_start, - mixramp_db - replay_gain_db); - const float mixramp_overlap_prev = - mixramp_interpolate(mixramp_prev_end, - mixramp_db - replay_gain_prev_db); - const float mixramp_overlap = - mixramp_overlap_current + mixramp_overlap_prev; - - if (mixramp_overlap_current >= 0 && - mixramp_overlap_prev >= 0 && - mixramp_delay <= mixramp_overlap) { - chunks = (chunks_f * (mixramp_overlap - mixramp_delay)); - FormatDebug(cross_fade_domain, - "will overlap %d chunks, %fs", chunks, - mixramp_overlap - mixramp_delay); - } - } - - if (chunks > max_chunks) { - chunks = max_chunks; - LogWarning(cross_fade_domain, - "audio_buffer_size too small for computed MixRamp overlap"); - } - - return chunks; -} diff --git a/src/CrossFade.hxx b/src/CrossFade.hxx deleted file mode 100644 index 672abb718..000000000 --- a/src/CrossFade.hxx +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2003-2015 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program 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 2 of the License, or - * (at your option) any later version. - * - * This program 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 this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_CROSSFADE_HXX -#define MPD_CROSSFADE_HXX - -#include "Compiler.h" - -struct AudioFormat; -class SignedSongTime; - -struct CrossFadeSettings { - /** - * The configured cross fade duration [s]. - */ - float duration; - - float mixramp_db; - - /** - * The configured MixRapm delay [s]. A non-positive value - * disables MixRamp. - */ - float mixramp_delay; - - CrossFadeSettings() - :duration(0), - mixramp_db(0), - mixramp_delay(-1) - {} - - - /** - * Calculate how many music pipe chunks should be used for crossfading. - * - * @param total_time total_time the duration of the new song - * @param replay_gain_db the ReplayGain adjustment used for this song - * @param replay_gain_prev_db the ReplayGain adjustment used on the last song - * @param mixramp_start the next songs mixramp_start tag - * @param mixramp_prev_end the last songs mixramp_end setting - * @param af the audio format of the new song - * @param old_format the audio format of the current song - * @param max_chunks the maximum number of chunks - * @return the number of chunks for crossfading, or 0 if cross fading - * should be disabled for this song change - */ - gcc_pure - unsigned Calculate(SignedSongTime total_time, - float replay_gain_db, float replay_gain_prev_db, - const char *mixramp_start, - const char *mixramp_prev_end, - AudioFormat af, AudioFormat old_format, - unsigned max_chunks) const; -}; - -#endif diff --git a/src/Main.cxx b/src/Main.cxx index ba47856a0..6e646da39 100644 --- a/src/Main.cxx +++ b/src/Main.cxx @@ -25,7 +25,7 @@ #include "PlaylistGlobal.hxx" #include "MusicChunk.hxx" #include "StateFile.hxx" -#include "PlayerThread.hxx" +#include "player/Thread.hxx" #include "Mapper.hxx" #include "Permission.hxx" #include "Listen.hxx" diff --git a/src/Partition.hxx b/src/Partition.hxx index dfc4ac19a..cd7b3c66e 100644 --- a/src/Partition.hxx +++ b/src/Partition.hxx @@ -23,8 +23,8 @@ #include "queue/Playlist.hxx" #include "output/MultipleOutputs.hxx" #include "mixer/Listener.hxx" -#include "PlayerControl.hxx" -#include "PlayerListener.hxx" +#include "player/Control.hxx" +#include "player/Listener.hxx" #include "Chrono.hxx" #include "Compiler.h" diff --git a/src/PlayerControl.cxx b/src/PlayerControl.cxx deleted file mode 100644 index 972b33361..000000000 --- a/src/PlayerControl.cxx +++ /dev/null @@ -1,267 +0,0 @@ -/* - * Copyright (C) 2003-2015 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program 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 2 of the License, or - * (at your option) any later version. - * - * This program 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 this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "config.h" -#include "PlayerControl.hxx" -#include "Idle.hxx" -#include "DetachedSong.hxx" - -#include - -#include - -PlayerControl::PlayerControl(PlayerListener &_listener, - MultipleOutputs &_outputs, - unsigned _buffer_chunks, - unsigned _buffered_before_play) - :listener(_listener), outputs(_outputs), - buffer_chunks(_buffer_chunks), - buffered_before_play(_buffered_before_play), - command(PlayerCommand::NONE), - state(PlayerState::STOP), - error_type(PlayerError::NONE), - tagged_song(nullptr), - next_song(nullptr), - total_play_time(0), - border_pause(false) -{ -} - -PlayerControl::~PlayerControl() -{ - delete next_song; - delete tagged_song; -} - -void -PlayerControl::Play(DetachedSong *song) -{ - assert(song != nullptr); - - Lock(); - - if (state != PlayerState::STOP) - SynchronousCommand(PlayerCommand::STOP); - - assert(next_song == nullptr); - - EnqueueSongLocked(song); - - assert(next_song == nullptr); - - Unlock(); -} - -void -PlayerControl::Cancel() -{ - LockSynchronousCommand(PlayerCommand::CANCEL); - assert(next_song == nullptr); -} - -void -PlayerControl::Stop() -{ - LockSynchronousCommand(PlayerCommand::CLOSE_AUDIO); - assert(next_song == nullptr); - - idle_add(IDLE_PLAYER); -} - -void -PlayerControl::UpdateAudio() -{ - LockSynchronousCommand(PlayerCommand::UPDATE_AUDIO); -} - -void -PlayerControl::Kill() -{ - assert(thread.IsDefined()); - - LockSynchronousCommand(PlayerCommand::EXIT); - thread.Join(); - - idle_add(IDLE_PLAYER); -} - -void -PlayerControl::PauseLocked() -{ - if (state != PlayerState::STOP) { - SynchronousCommand(PlayerCommand::PAUSE); - idle_add(IDLE_PLAYER); - } -} - -void -PlayerControl::Pause() -{ - Lock(); - PauseLocked(); - Unlock(); -} - -void -PlayerControl::SetPause(bool pause_flag) -{ - Lock(); - - switch (state) { - case PlayerState::STOP: - break; - - case PlayerState::PLAY: - if (pause_flag) - PauseLocked(); - break; - - case PlayerState::PAUSE: - if (!pause_flag) - PauseLocked(); - break; - } - - Unlock(); -} - -void -PlayerControl::SetBorderPause(bool _border_pause) -{ - Lock(); - border_pause = _border_pause; - Unlock(); -} - -player_status -PlayerControl::GetStatus() -{ - player_status status; - - Lock(); - SynchronousCommand(PlayerCommand::REFRESH); - - status.state = state; - - if (state != PlayerState::STOP) { - status.bit_rate = bit_rate; - status.audio_format = audio_format; - status.total_time = total_time; - status.elapsed_time = elapsed_time; - } - - Unlock(); - - return status; -} - -void -PlayerControl::SetError(PlayerError type, Error &&_error) -{ - assert(type != PlayerError::NONE); - assert(_error.IsDefined()); - - error_type = type; - error = std::move(_error); -} - -void -PlayerControl::ClearError() -{ - Lock(); - - if (error_type != PlayerError::NONE) { - error_type = PlayerError::NONE; - error.Clear(); - } - - Unlock(); -} - -void -PlayerControl::LockSetTaggedSong(const DetachedSong &song) -{ - Lock(); - delete tagged_song; - tagged_song = new DetachedSong(song); - Unlock(); -} - -void -PlayerControl::ClearTaggedSong() -{ - delete tagged_song; - tagged_song = nullptr; -} - -void -PlayerControl::EnqueueSong(DetachedSong *song) -{ - assert(song != nullptr); - - Lock(); - EnqueueSongLocked(song); - Unlock(); -} - -bool -PlayerControl::Seek(DetachedSong *song, SongTime t) -{ - assert(song != nullptr); - - Lock(); - - delete next_song; - next_song = song; - seek_time = t; - SynchronousCommand(PlayerCommand::SEEK); - Unlock(); - - assert(next_song == nullptr); - - idle_add(IDLE_PLAYER); - - return true; -} - -void -PlayerControl::SetCrossFade(float _cross_fade_seconds) -{ - if (_cross_fade_seconds < 0) - _cross_fade_seconds = 0; - cross_fade.duration = _cross_fade_seconds; - - idle_add(IDLE_OPTIONS); -} - -void -PlayerControl::SetMixRampDb(float _mixramp_db) -{ - cross_fade.mixramp_db = _mixramp_db; - - idle_add(IDLE_OPTIONS); -} - -void -PlayerControl::SetMixRampDelay(float _mixramp_delay_seconds) -{ - cross_fade.mixramp_delay = _mixramp_delay_seconds; - - idle_add(IDLE_OPTIONS); -} diff --git a/src/PlayerControl.hxx b/src/PlayerControl.hxx deleted file mode 100644 index a2807a9a1..000000000 --- a/src/PlayerControl.hxx +++ /dev/null @@ -1,463 +0,0 @@ -/* - * Copyright (C) 2003-2015 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program 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 2 of the License, or - * (at your option) any later version. - * - * This program 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 this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_PLAYER_CONTROL_HXX -#define MPD_PLAYER_CONTROL_HXX - -#include "AudioFormat.hxx" -#include "thread/Mutex.hxx" -#include "thread/Cond.hxx" -#include "thread/Thread.hxx" -#include "util/Error.hxx" -#include "CrossFade.hxx" -#include "Chrono.hxx" - -#include - -class PlayerListener; -class MultipleOutputs; -class DetachedSong; - -enum class PlayerState : uint8_t { - STOP, - PAUSE, - PLAY -}; - -enum class PlayerCommand : uint8_t { - NONE, - EXIT, - STOP, - PAUSE, - SEEK, - CLOSE_AUDIO, - - /** - * At least one AudioOutput.enabled flag has been modified; - * commit those changes to the output threads. - */ - UPDATE_AUDIO, - - /** PlayerControl.next_song has been updated */ - QUEUE, - - /** - * cancel pre-decoding PlayerControl.next_song; if the player - * has already started playing this song, it will completely - * stop - */ - CANCEL, - - /** - * Refresh status information in the #PlayerControl struct, - * e.g. elapsed_time. - */ - REFRESH, -}; - -enum class PlayerError : uint8_t { - NONE, - - /** - * The decoder has failed to decode the song. - */ - DECODER, - - /** - * The audio output has failed. - */ - OUTPUT, -}; - -struct player_status { - PlayerState state; - uint16_t bit_rate; - AudioFormat audio_format; - SignedSongTime total_time; - SongTime elapsed_time; -}; - -struct PlayerControl { - PlayerListener &listener; - - MultipleOutputs &outputs; - - const unsigned buffer_chunks; - - const unsigned buffered_before_play; - - /** - * The handle of the player thread. - */ - Thread thread; - - /** - * This lock protects #command, #state, #error, #tagged_song. - */ - mutable Mutex mutex; - - /** - * Trigger this object after you have modified #command. - */ - Cond cond; - - /** - * This object gets signalled when the player thread has - * finished the #command. It wakes up the client that waits - * (i.e. the main thread). - */ - Cond client_cond; - - PlayerCommand command; - PlayerState state; - - PlayerError error_type; - - /** - * The error that occurred in the player thread. This - * attribute is only valid if #error is not - * #PlayerError::NONE. The object must be freed when this - * object transitions back to #PlayerError::NONE. - */ - Error error; - - /** - * A copy of the current #DetachedSong after its tags have - * been updated by the decoder (for example, a radio stream - * that has sent a new tag after switching to the next song). - * This shall be used by PlayerListener::OnPlayerTagModified() - * to update the current #DetachedSong in the queue. - * - * Protected by #mutex. Set by the PlayerThread and consumed - * by the main thread. - */ - DetachedSong *tagged_song; - - uint16_t bit_rate; - AudioFormat audio_format; - SignedSongTime total_time; - SongTime elapsed_time; - - /** - * The next queued song. - * - * This is a duplicate, and must be freed when this attribute - * is cleared. - */ - DetachedSong *next_song; - - SongTime seek_time; - - CrossFadeSettings cross_fade; - - double total_play_time; - - /** - * If this flag is set, then the player will be auto-paused at - * the end of the song, before the next song starts to play. - * - * This is a copy of the queue's "single" flag most of the - * time. - */ - bool border_pause; - - PlayerControl(PlayerListener &_listener, - MultipleOutputs &_outputs, - unsigned buffer_chunks, - unsigned buffered_before_play); - ~PlayerControl(); - - /** - * Locks the object. - */ - void Lock() const { - mutex.lock(); - } - - /** - * Unlocks the object. - */ - void Unlock() const { - mutex.unlock(); - } - - /** - * Signals the object. The object should be locked prior to - * calling this function. - */ - void Signal() { - cond.signal(); - } - - /** - * Signals the object. The object is temporarily locked by - * this function. - */ - void LockSignal() { - Lock(); - Signal(); - Unlock(); - } - - /** - * Waits for a signal on the object. This function is only - * valid in the player thread. The object must be locked - * prior to calling this function. - */ - void Wait() { - assert(thread.IsInside()); - - cond.wait(mutex); - } - - /** - * Wake up the client waiting for command completion. - * - * Caller must lock the object. - */ - void ClientSignal() { - assert(thread.IsInside()); - - client_cond.signal(); - } - - /** - * The client calls this method to wait for command - * completion. - * - * Caller must lock the object. - */ - void ClientWait() { - assert(!thread.IsInside()); - - client_cond.wait(mutex); - } - - /** - * A command has been finished. This method clears the - * command and signals the client. - * - * To be called from the player thread. Caller must lock the - * object. - */ - void CommandFinished() { - assert(command != PlayerCommand::NONE); - - command = PlayerCommand::NONE; - ClientSignal(); - } - -private: - /** - * Wait for the command to be finished by the player thread. - * - * To be called from the main thread. Caller must lock the - * object. - */ - void WaitCommandLocked() { - while (command != PlayerCommand::NONE) - ClientWait(); - } - - /** - * Send a command to the player thread and synchronously wait - * for it to finish. - * - * To be called from the main thread. Caller must lock the - * object. - */ - void SynchronousCommand(PlayerCommand cmd) { - assert(command == PlayerCommand::NONE); - - command = cmd; - Signal(); - WaitCommandLocked(); - } - - /** - * Send a command to the player thread and synchronously wait - * for it to finish. - * - * To be called from the main thread. This method locks the - * object. - */ - void LockSynchronousCommand(PlayerCommand cmd) { - Lock(); - SynchronousCommand(cmd); - Unlock(); - } - -public: - /** - * @param song the song to be queued; the given instance will - * be owned and freed by the player - */ - void Play(DetachedSong *song); - - /** - * see PlayerCommand::CANCEL - */ - void Cancel(); - - void SetPause(bool pause_flag); - -private: - void PauseLocked(); - -public: - void Pause(); - - /** - * Set the player's #border_pause flag. - */ - void SetBorderPause(bool border_pause); - - void Kill(); - - gcc_pure - player_status GetStatus(); - - PlayerState GetState() const { - return state; - } - - /** - * Set the error. Discards any previous error condition. - * - * Caller must lock the object. - * - * @param type the error type; must not be #PlayerError::NONE - * @param error detailed error information; must be defined. - */ - void SetError(PlayerError type, Error &&error); - - /** - * Checks whether an error has occurred, and if so, returns a - * copy of the #Error object. - * - * Caller must lock the object. - */ - gcc_pure - Error GetError() const { - Error result; - if (error_type != PlayerError::NONE) - result.Set(error); - return result; - } - - /** - * Like GetError(), but locks and unlocks the object. - */ - gcc_pure - Error LockGetError() const { - Lock(); - Error result = GetError(); - Unlock(); - return result; - } - - void ClearError(); - - PlayerError GetErrorType() const { - return error_type; - } - - /** - * Set the #tagged_song attribute to a newly allocated copy of - * the given #DetachedSong. Locks and unlocks the object. - */ - void LockSetTaggedSong(const DetachedSong &song); - - void ClearTaggedSong(); - - /** - * Read and clear the #tagged_song attribute. - * - * Caller must lock the object. - */ - DetachedSong *ReadTaggedSong() { - DetachedSong *result = tagged_song; - tagged_song = nullptr; - return result; - } - - /** - * Like ReadTaggedSong(), but locks and unlocks the object. - */ - DetachedSong *LockReadTaggedSong() { - Lock(); - DetachedSong *result = ReadTaggedSong(); - Unlock(); - return result; - } - - void Stop(); - - void UpdateAudio(); - -private: - void EnqueueSongLocked(DetachedSong *song) { - assert(song != nullptr); - assert(next_song == nullptr); - - next_song = song; - SynchronousCommand(PlayerCommand::QUEUE); - } - -public: - /** - * @param song the song to be queued; the given instance will be owned - * and freed by the player - */ - void EnqueueSong(DetachedSong *song); - - /** - * Makes the player thread seek the specified song to a position. - * - * @param song the song to be queued; the given instance will be owned - * and freed by the player - * @return true on success, false on failure (e.g. if MPD isn't - * playing currently) - */ - bool Seek(DetachedSong *song, SongTime t); - - void SetCrossFade(float cross_fade_seconds); - - float GetCrossFade() const { - return cross_fade.duration; - } - - void SetMixRampDb(float mixramp_db); - - float GetMixRampDb() const { - return cross_fade.mixramp_db; - } - - void SetMixRampDelay(float mixramp_delay_seconds); - - float GetMixRampDelay() const { - return cross_fade.mixramp_delay; - } - - double GetTotalPlayTime() const { - return total_play_time; - } -}; - -#endif diff --git a/src/PlayerListener.hxx b/src/PlayerListener.hxx deleted file mode 100644 index e10f2547b..000000000 --- a/src/PlayerListener.hxx +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2003-2015 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program 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 2 of the License, or - * (at your option) any later version. - * - * This program 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 this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#ifndef MPD_PLAYER_LISTENER_HXX -#define MPD_PLAYER_LISTENER_HXX - -class PlayerListener { -public: - /** - * Must call playlist_sync(). - */ - virtual void OnPlayerSync() = 0; - - /** - * The current song's tag has changed. - */ - virtual void OnPlayerTagModified() = 0; -}; - -#endif diff --git a/src/PlayerThread.cxx b/src/PlayerThread.cxx deleted file mode 100644 index a1fd24bbe..000000000 --- a/src/PlayerThread.cxx +++ /dev/null @@ -1,1208 +0,0 @@ -/* - * Copyright (C) 2003-2015 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program 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 2 of the License, or - * (at your option) any later version. - * - * This program 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 this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -#include "config.h" -#include "PlayerThread.hxx" -#include "PlayerListener.hxx" -#include "decoder/DecoderThread.hxx" -#include "decoder/DecoderControl.hxx" -#include "MusicPipe.hxx" -#include "MusicBuffer.hxx" -#include "MusicChunk.hxx" -#include "DetachedSong.hxx" -#include "system/FatalError.hxx" -#include "CrossFade.hxx" -#include "PlayerControl.hxx" -#include "output/MultipleOutputs.hxx" -#include "tag/Tag.hxx" -#include "Idle.hxx" -#include "util/Domain.hxx" -#include "thread/Name.hxx" -#include "Log.hxx" - -#include - -static constexpr Domain player_domain("player"); - -enum class CrossFadeState : int8_t { - DISABLED = -1, - UNKNOWN = 0, - ENABLED = 1 -}; - -class Player { - PlayerControl &pc; - - DecoderControl &dc; - - MusicBuffer &buffer; - - MusicPipe *pipe; - - /** - * are we waiting for buffered_before_play? - */ - bool buffering; - - /** - * true if the decoder is starting and did not provide data - * yet - */ - bool decoder_starting; - - /** - * Did we wake up the DecoderThread recently? This avoids - * duplicate wakeup calls. - */ - bool decoder_woken; - - /** - * is the player paused? - */ - bool paused; - - /** - * is there a new song in pc.next_song? - */ - bool queued; - - /** - * Was any audio output opened successfully? It might have - * failed meanwhile, but was not explicitly closed by the - * player thread. When this flag is unset, some output - * methods must not be called. - */ - bool output_open; - - /** - * the song currently being played - */ - DetachedSong *song; - - /** - * is cross fading enabled? - */ - CrossFadeState xfade_state; - - /** - * has cross-fading begun? - */ - bool cross_fading; - - /** - * The number of chunks used for crossfading. - */ - unsigned cross_fade_chunks; - - /** - * The tag of the "next" song during cross-fade. It is - * postponed, and sent to the output thread when the new song - * really begins. - */ - Tag *cross_fade_tag; - - /** - * The current audio format for the audio outputs. - */ - AudioFormat play_audio_format; - - /** - * The time stamp of the chunk most recently sent to the - * output thread. This attribute is only used if - * MultipleOutputs::GetElapsedTime() didn't return a usable - * value; the output thread can estimate the elapsed time more - * precisely. - */ - SongTime elapsed_time; - -public: - Player(PlayerControl &_pc, DecoderControl &_dc, - MusicBuffer &_buffer) - :pc(_pc), dc(_dc), buffer(_buffer), - buffering(true), - decoder_starting(false), - decoder_woken(false), - paused(false), - queued(true), - output_open(false), - song(nullptr), - xfade_state(CrossFadeState::UNKNOWN), - cross_fading(false), - cross_fade_chunks(0), - cross_fade_tag(nullptr), - elapsed_time(SongTime::zero()) {} - -private: - void ClearAndDeletePipe() { - pipe->Clear(buffer); - delete pipe; - } - - void ClearAndReplacePipe(MusicPipe *_pipe) { - ClearAndDeletePipe(); - pipe = _pipe; - } - - void ReplacePipe(MusicPipe *_pipe) { - delete pipe; - pipe = _pipe; - } - - /** - * Start the decoder. - * - * Player lock is not held. - */ - void StartDecoder(MusicPipe &pipe); - - /** - * The decoder has acknowledged the "START" command (see - * player::WaitForDecoder()). This function checks if the decoder - * initialization has completed yet. - * - * The player lock is not held. - */ - bool CheckDecoderStartup(); - - /** - * Stop the decoder and clears (and frees) its music pipe. - * - * Player lock is not held. - */ - void StopDecoder(); - - /** - * Is the decoder still busy on the same song as the player? - * - * Note: this function does not check if the decoder is already - * finished. - */ - gcc_pure - bool IsDecoderAtCurrentSong() const { - assert(pipe != nullptr); - - return dc.pipe == pipe; - } - - /** - * Returns true if the decoder is decoding the next song (or has begun - * decoding it, or has finished doing it), and the player hasn't - * switched to that song yet. - */ - gcc_pure - bool IsDecoderAtNextSong() const { - return dc.pipe != nullptr && !IsDecoderAtCurrentSong(); - } - - /** - * This is the handler for the #PlayerCommand::SEEK command. - * - * The player lock is not held. - */ - bool SeekDecoder(); - - /** - * After the decoder has been started asynchronously, wait for - * the "START" command to finish. The decoder may not be - * initialized yet, i.e. there is no audio_format information - * yet. - * - * The player lock is not held. - */ - bool WaitForDecoder(); - - /** - * Wrapper for MultipleOutputs::Open(). Upon failure, it - * pauses the player. - * - * @return true on success - */ - bool OpenOutput(); - - /** - * Obtains the next chunk from the music pipe, optionally applies - * cross-fading, and sends it to all audio outputs. - * - * @return true on success, false on error (playback will be stopped) - */ - bool PlayNextChunk(); - - /** - * Sends a chunk of silence to the audio outputs. This is - * called when there is not enough decoded data in the pipe - * yet, to prevent underruns in the hardware buffers. - * - * The player lock is not held. - */ - bool SendSilence(); - - /** - * Player lock must be held before calling. - */ - void ProcessCommand(); - - /** - * This is called at the border between two songs: the audio output - * has consumed all chunks of the current song, and we should start - * sending chunks from the next one. - * - * The player lock is not held. - * - * @return true on success, false on error (playback will be stopped) - */ - bool SongBorder(); - -public: - /* - * The main loop of the player thread, during playback. This - * is basically a state machine, which multiplexes data - * between the decoder thread and the output threads. - */ - void Run(); -}; - -static void -player_command_finished(PlayerControl &pc) -{ - pc.Lock(); - pc.CommandFinished(); - pc.Unlock(); -} - -void -Player::StartDecoder(MusicPipe &_pipe) -{ - assert(queued || pc.command == PlayerCommand::SEEK); - assert(pc.next_song != nullptr); - - SongTime start_time = pc.next_song->GetStartTime(); - if (pc.command == PlayerCommand::SEEK) - start_time += pc.seek_time; - - dc.Start(new DetachedSong(*pc.next_song), - start_time, pc.next_song->GetEndTime(), - buffer, _pipe); -} - -void -Player::StopDecoder() -{ - dc.Stop(); - - if (dc.pipe != nullptr) { - /* clear and free the decoder pipe */ - - dc.pipe->Clear(buffer); - - if (dc.pipe != pipe) - delete dc.pipe; - - dc.pipe = nullptr; - } -} - -bool -Player::WaitForDecoder() -{ - assert(queued || pc.command == PlayerCommand::SEEK); - assert(pc.next_song != nullptr); - - queued = false; - - pc.Lock(); - Error error = dc.GetError(); - if (error.IsDefined()) { - pc.SetError(PlayerError::DECODER, std::move(error)); - - delete pc.next_song; - pc.next_song = nullptr; - - pc.Unlock(); - - return false; - } - - pc.ClearTaggedSong(); - - delete song; - song = pc.next_song; - elapsed_time = SongTime::zero(); - - /* set the "starting" flag, which will be cleared by - player_check_decoder_startup() */ - decoder_starting = true; - - /* update PlayerControl's song information */ - pc.total_time = pc.next_song->GetDuration(); - pc.bit_rate = 0; - pc.audio_format.Clear(); - - /* clear the queued song */ - pc.next_song = nullptr; - - pc.Unlock(); - - /* call syncPlaylistWithQueue() in the main thread */ - pc.listener.OnPlayerSync(); - - return true; -} - -/** - * Returns the real duration of the song, comprising the duration - * indicated by the decoder plugin. - */ -static SignedSongTime -real_song_duration(const DetachedSong &song, SignedSongTime decoder_duration) -{ - if (decoder_duration.IsNegative()) - /* the decoder plugin didn't provide information; fall - back to Song::GetDuration() */ - return song.GetDuration(); - - const SongTime start_time = song.GetStartTime(); - const SongTime end_time = song.GetEndTime(); - - if (end_time.IsPositive() && end_time < SongTime(decoder_duration)) - return SignedSongTime(end_time - start_time); - - return SignedSongTime(SongTime(decoder_duration) - start_time); -} - -bool -Player::OpenOutput() -{ - assert(play_audio_format.IsDefined()); - assert(pc.state == PlayerState::PLAY || - pc.state == PlayerState::PAUSE); - - Error error; - if (pc.outputs.Open(play_audio_format, buffer, error)) { - output_open = true; - paused = false; - - pc.Lock(); - pc.state = PlayerState::PLAY; - pc.Unlock(); - - idle_add(IDLE_PLAYER); - - return true; - } else { - LogError(error); - - output_open = false; - - /* pause: the user may resume playback as soon as an - audio output becomes available */ - paused = true; - - pc.Lock(); - pc.SetError(PlayerError::OUTPUT, std::move(error)); - pc.state = PlayerState::PAUSE; - pc.Unlock(); - - idle_add(IDLE_PLAYER); - - return false; - } -} - -bool -Player::CheckDecoderStartup() -{ - assert(decoder_starting); - - pc.Lock(); - - Error error = dc.GetError(); - if (error.IsDefined()) { - /* the decoder failed */ - pc.SetError(PlayerError::DECODER, std::move(error)); - pc.Unlock(); - - return false; - } else if (!dc.IsStarting()) { - /* the decoder is ready and ok */ - - pc.Unlock(); - - if (output_open && - !pc.outputs.Wait(pc, 1)) - /* the output devices havn't finished playing - all chunks yet - wait for that */ - return true; - - pc.Lock(); - pc.total_time = real_song_duration(*dc.song, dc.total_time); - pc.audio_format = dc.in_audio_format; - pc.Unlock(); - - idle_add(IDLE_PLAYER); - - play_audio_format = dc.out_audio_format; - decoder_starting = false; - - if (!paused && !OpenOutput()) { - FormatError(player_domain, - "problems opening audio device " - "while playing \"%s\"", - dc.song->GetURI()); - return true; - } - - return true; - } else { - /* the decoder is not yet ready; wait - some more */ - dc.WaitForDecoder(); - pc.Unlock(); - - return true; - } -} - -bool -Player::SendSilence() -{ - assert(output_open); - assert(play_audio_format.IsDefined()); - - MusicChunk *chunk = buffer.Allocate(); - if (chunk == nullptr) { - LogError(player_domain, "Failed to allocate silence buffer"); - return false; - } - -#ifndef NDEBUG - chunk->audio_format = play_audio_format; -#endif - - const size_t frame_size = play_audio_format.GetFrameSize(); - /* this formula ensures that we don't send - partial frames */ - unsigned num_frames = sizeof(chunk->data) / frame_size; - - chunk->time = SignedSongTime::Negative(); /* undefined time stamp */ - chunk->length = num_frames * frame_size; - memset(chunk->data, 0, chunk->length); - - Error error; - if (!pc.outputs.Play(chunk, error)) { - LogError(error); - buffer.Return(chunk); - return false; - } - - return true; -} - -inline bool -Player::SeekDecoder() -{ - assert(pc.next_song != nullptr); - - const SongTime start_time = pc.next_song->GetStartTime(); - - if (!dc.LockIsCurrentSong(*pc.next_song)) { - /* the decoder is already decoding the "next" song - - stop it and start the previous song again */ - - StopDecoder(); - - /* clear music chunks which might still reside in the - pipe */ - pipe->Clear(buffer); - - /* re-start the decoder */ - StartDecoder(*pipe); - if (!WaitForDecoder()) { - /* decoder failure */ - player_command_finished(pc); - return false; - } - } else { - if (!IsDecoderAtCurrentSong()) { - /* the decoder is already decoding the "next" song, - but it is the same song file; exchange the pipe */ - ClearAndReplacePipe(dc.pipe); - } - - delete pc.next_song; - pc.next_song = nullptr; - queued = false; - } - - /* wait for the decoder to complete initialization */ - - while (decoder_starting) { - if (!CheckDecoderStartup()) { - /* decoder failure */ - player_command_finished(pc); - return false; - } - } - - /* send the SEEK command */ - - SongTime where = pc.seek_time; - if (!pc.total_time.IsNegative()) { - const SongTime total_time(pc.total_time); - if (where > total_time) - where = total_time; - } - - if (!dc.Seek(where + start_time)) { - /* decoder failure */ - player_command_finished(pc); - return false; - } - - elapsed_time = where; - - player_command_finished(pc); - - xfade_state = CrossFadeState::UNKNOWN; - - /* re-fill the buffer after seeking */ - buffering = true; - - pc.outputs.Cancel(); - - return true; -} - -inline void -Player::ProcessCommand() -{ - switch (pc.command) { - case PlayerCommand::NONE: - case PlayerCommand::STOP: - case PlayerCommand::EXIT: - case PlayerCommand::CLOSE_AUDIO: - break; - - case PlayerCommand::UPDATE_AUDIO: - pc.Unlock(); - pc.outputs.EnableDisable(); - pc.Lock(); - pc.CommandFinished(); - break; - - case PlayerCommand::QUEUE: - assert(pc.next_song != nullptr); - assert(!queued); - assert(!IsDecoderAtNextSong()); - - queued = true; - pc.CommandFinished(); - - pc.Unlock(); - if (dc.LockIsIdle()) - StartDecoder(*new MusicPipe()); - pc.Lock(); - - break; - - case PlayerCommand::PAUSE: - pc.Unlock(); - - paused = !paused; - if (paused) { - pc.outputs.Pause(); - pc.Lock(); - - pc.state = PlayerState::PAUSE; - } else if (!play_audio_format.IsDefined()) { - /* the decoder hasn't provided an audio format - yet - don't open the audio device yet */ - pc.Lock(); - - pc.state = PlayerState::PLAY; - } else { - OpenOutput(); - - pc.Lock(); - } - - pc.CommandFinished(); - break; - - case PlayerCommand::SEEK: - pc.Unlock(); - SeekDecoder(); - pc.Lock(); - break; - - case PlayerCommand::CANCEL: - if (pc.next_song == nullptr) { - /* the cancel request arrived too late, we're - already playing the queued song... stop - everything now */ - pc.command = PlayerCommand::STOP; - return; - } - - if (IsDecoderAtNextSong()) { - /* the decoder is already decoding the song - - stop it and reset the position */ - pc.Unlock(); - StopDecoder(); - pc.Lock(); - } - - delete pc.next_song; - pc.next_song = nullptr; - queued = false; - pc.CommandFinished(); - break; - - case PlayerCommand::REFRESH: - if (output_open && !paused) { - pc.Unlock(); - pc.outputs.Check(); - pc.Lock(); - } - - pc.elapsed_time = !pc.outputs.GetElapsedTime().IsNegative() - ? SongTime(pc.outputs.GetElapsedTime()) - : elapsed_time; - - pc.CommandFinished(); - break; - } -} - -static void -update_song_tag(PlayerControl &pc, DetachedSong &song, const Tag &new_tag) -{ - if (song.IsFile()) - /* don't update tags of local files, only remote - streams may change tags dynamically */ - return; - - song.SetTag(new_tag); - - pc.LockSetTaggedSong(song); - - /* the main thread will update the playlist version when he - receives this event */ - pc.listener.OnPlayerTagModified(); - - /* notify all clients that the tag of the current song has - changed */ - idle_add(IDLE_PLAYER); -} - -/** - * Plays a #MusicChunk object (after applying software volume). If - * it contains a (stream) tag, copy it to the current song, so MPD's - * playlist reflects the new stream tag. - * - * Player lock is not held. - */ -static bool -play_chunk(PlayerControl &pc, - DetachedSong &song, MusicChunk *chunk, - MusicBuffer &buffer, - const AudioFormat format, - Error &error) -{ - assert(chunk->CheckFormat(format)); - - if (chunk->tag != nullptr) - update_song_tag(pc, song, *chunk->tag); - - if (chunk->IsEmpty()) { - buffer.Return(chunk); - return true; - } - - pc.Lock(); - pc.bit_rate = chunk->bit_rate; - pc.Unlock(); - - /* send the chunk to the audio outputs */ - - if (!pc.outputs.Play(chunk, error)) - return false; - - pc.total_play_time += (double)chunk->length / - format.GetTimeToSize(); - return true; -} - -inline bool -Player::PlayNextChunk() -{ - if (!pc.outputs.Wait(pc, 64)) - /* the output pipe is still large enough, don't send - another chunk */ - return true; - - unsigned cross_fade_position; - MusicChunk *chunk = nullptr; - if (xfade_state == CrossFadeState::ENABLED && IsDecoderAtNextSong() && - (cross_fade_position = pipe->GetSize()) <= cross_fade_chunks) { - /* perform cross fade */ - MusicChunk *other_chunk = dc.pipe->Shift(); - - if (!cross_fading) { - /* beginning of the cross fade - adjust - crossFadeChunks which might be bigger than - the remaining number of chunks in the old - song */ - cross_fade_chunks = cross_fade_position; - cross_fading = true; - } - - if (other_chunk != nullptr) { - chunk = pipe->Shift(); - assert(chunk != nullptr); - assert(chunk->other == nullptr); - - /* don't send the tags of the new song (which - is being faded in) yet; postpone it until - the current song is faded out */ - cross_fade_tag = - Tag::MergeReplace(cross_fade_tag, - other_chunk->tag); - other_chunk->tag = nullptr; - - if (pc.cross_fade.mixramp_delay <= 0) { - chunk->mix_ratio = ((float)cross_fade_position) - / cross_fade_chunks; - } else { - chunk->mix_ratio = -1; - } - - if (other_chunk->IsEmpty()) { - /* the "other" chunk was a MusicChunk - which had only a tag, but no music - data - we cannot cross-fade that; - but since this happens only at the - beginning of the new song, we can - easily recover by throwing it away - now */ - buffer.Return(other_chunk); - other_chunk = nullptr; - } - - chunk->other = other_chunk; - } else { - /* there are not enough decoded chunks yet */ - - pc.Lock(); - - if (dc.IsIdle()) { - /* the decoder isn't running, abort - cross fading */ - pc.Unlock(); - - xfade_state = CrossFadeState::DISABLED; - } else { - /* wait for the decoder */ - dc.Signal(); - dc.WaitForDecoder(); - pc.Unlock(); - - return true; - } - } - } - - if (chunk == nullptr) - chunk = pipe->Shift(); - - assert(chunk != nullptr); - - /* insert the postponed tag if cross-fading is finished */ - - if (xfade_state != CrossFadeState::ENABLED && cross_fade_tag != nullptr) { - chunk->tag = Tag::MergeReplace(chunk->tag, cross_fade_tag); - cross_fade_tag = nullptr; - } - - /* play the current chunk */ - - Error error; - if (!play_chunk(pc, *song, chunk, buffer, play_audio_format, error)) { - LogError(error); - - buffer.Return(chunk); - - pc.Lock(); - - pc.SetError(PlayerError::OUTPUT, std::move(error)); - - /* pause: the user may resume playback as soon as an - audio output becomes available */ - pc.state = PlayerState::PAUSE; - paused = true; - - pc.Unlock(); - - idle_add(IDLE_PLAYER); - - return false; - } - - /* this formula should prevent that the decoder gets woken up - with each chunk; it is more efficient to make it decode a - larger block at a time */ - pc.Lock(); - if (!dc.IsIdle() && - dc.pipe->GetSize() <= (pc.buffered_before_play + - buffer.GetSize() * 3) / 4) { - if (!decoder_woken) { - decoder_woken = true; - dc.Signal(); - } - } else - decoder_woken = false; - pc.Unlock(); - - return true; -} - -inline bool -Player::SongBorder() -{ - xfade_state = CrossFadeState::UNKNOWN; - - FormatDefault(player_domain, "played \"%s\"", song->GetURI()); - - ReplacePipe(dc.pipe); - - pc.outputs.SongBorder(); - - if (!WaitForDecoder()) - return false; - - pc.Lock(); - - const bool border_pause = pc.border_pause; - if (border_pause) { - paused = true; - pc.state = PlayerState::PAUSE; - } - - pc.Unlock(); - - if (border_pause) - idle_add(IDLE_PLAYER); - - return true; -} - -inline void -Player::Run() -{ - pipe = new MusicPipe(); - - StartDecoder(*pipe); - if (!WaitForDecoder()) { - assert(song == nullptr); - - StopDecoder(); - player_command_finished(pc); - delete pipe; - return; - } - - pc.Lock(); - pc.state = PlayerState::PLAY; - - if (pc.command == PlayerCommand::SEEK) - elapsed_time = pc.seek_time; - - pc.CommandFinished(); - - while (true) { - ProcessCommand(); - if (pc.command == PlayerCommand::STOP || - pc.command == PlayerCommand::EXIT || - pc.command == PlayerCommand::CLOSE_AUDIO) { - pc.Unlock(); - pc.outputs.Cancel(); - break; - } - - pc.Unlock(); - - if (buffering) { - /* buffering at the start of the song - wait - until the buffer is large enough, to - prevent stuttering on slow machines */ - - if (pipe->GetSize() < pc.buffered_before_play && - !dc.LockIsIdle()) { - /* not enough decoded buffer space yet */ - - if (!paused && output_open && - pc.outputs.Check() < 4 && - !SendSilence()) - break; - - pc.Lock(); - /* XXX race condition: check decoder again */ - dc.WaitForDecoder(); - continue; - } else { - /* buffering is complete */ - buffering = false; - } - } - - if (decoder_starting) { - /* wait until the decoder is initialized completely */ - - if (!CheckDecoderStartup()) - break; - - pc.Lock(); - continue; - } - -#ifndef NDEBUG - /* - music_pipe_check_format(&play_audio_format, - next_song_chunk, - &dc.out_audio_format); - */ -#endif - - if (dc.LockIsIdle() && queued && dc.pipe == pipe) { - /* the decoder has finished the current song; - make it decode the next song */ - - assert(dc.pipe == nullptr || dc.pipe == pipe); - - StartDecoder(*new MusicPipe()); - } - - if (/* no cross-fading if MPD is going to pause at the - end of the current song */ - !pc.border_pause && - IsDecoderAtNextSong() && - xfade_state == CrossFadeState::UNKNOWN && - !dc.LockIsStarting()) { - /* enable cross fading in this song? if yes, - calculate how many chunks will be required - for it */ - cross_fade_chunks = - pc.cross_fade.Calculate(dc.total_time, - dc.replay_gain_db, - dc.replay_gain_prev_db, - dc.GetMixRampStart(), - dc.GetMixRampPreviousEnd(), - dc.out_audio_format, - play_audio_format, - buffer.GetSize() - - pc.buffered_before_play); - if (cross_fade_chunks > 0) { - xfade_state = CrossFadeState::ENABLED; - cross_fading = false; - } else - /* cross fading is disabled or the - next song is too short */ - xfade_state = CrossFadeState::DISABLED; - } - - if (paused) { - pc.Lock(); - - if (pc.command == PlayerCommand::NONE) - pc.Wait(); - continue; - } else if (!pipe->IsEmpty()) { - /* at least one music chunk is ready - send it - to the audio output */ - - PlayNextChunk(); - } else if (pc.outputs.Check() > 0) { - /* not enough data from decoder, but the - output thread is still busy, so it's - okay */ - - pc.Lock(); - - /* wake up the decoder (just in case it's - waiting for space in the MusicBuffer) and - wait for it */ - dc.Signal(); - dc.WaitForDecoder(); - continue; - } else if (IsDecoderAtNextSong()) { - /* at the beginning of a new song */ - - if (!SongBorder()) - break; - } else if (dc.LockIsIdle()) { - /* check the size of the pipe again, because - the decoder thread may have added something - since we last checked */ - if (pipe->IsEmpty()) { - /* wait for the hardware to finish - playback */ - pc.outputs.Drain(); - break; - } - } else if (output_open) { - /* the decoder is too busy and hasn't provided - new PCM data in time: send silence (if the - output pipe is empty) */ - if (!SendSilence()) - break; - } - - pc.Lock(); - } - - StopDecoder(); - - ClearAndDeletePipe(); - - delete cross_fade_tag; - - if (song != nullptr) { - FormatDefault(player_domain, "played \"%s\"", song->GetURI()); - delete song; - } - - pc.Lock(); - - pc.ClearTaggedSong(); - - if (queued) { - assert(pc.next_song != nullptr); - delete pc.next_song; - pc.next_song = nullptr; - } - - pc.state = PlayerState::STOP; - - pc.Unlock(); -} - -static void -do_play(PlayerControl &pc, DecoderControl &dc, - MusicBuffer &buffer) -{ - Player player(pc, dc, buffer); - player.Run(); -} - -static void -player_task(void *arg) -{ - PlayerControl &pc = *(PlayerControl *)arg; - - SetThreadName("player"); - - DecoderControl dc(pc.mutex, pc.cond); - decoder_thread_start(dc); - - MusicBuffer buffer(pc.buffer_chunks); - - pc.Lock(); - - while (1) { - switch (pc.command) { - case PlayerCommand::SEEK: - case PlayerCommand::QUEUE: - assert(pc.next_song != nullptr); - - pc.Unlock(); - do_play(pc, dc, buffer); - pc.listener.OnPlayerSync(); - pc.Lock(); - break; - - case PlayerCommand::STOP: - pc.Unlock(); - pc.outputs.Cancel(); - pc.Lock(); - - /* fall through */ - - case PlayerCommand::PAUSE: - delete pc.next_song; - pc.next_song = nullptr; - - pc.CommandFinished(); - break; - - case PlayerCommand::CLOSE_AUDIO: - pc.Unlock(); - - pc.outputs.Release(); - - pc.Lock(); - pc.CommandFinished(); - - assert(buffer.IsEmptyUnsafe()); - - break; - - case PlayerCommand::UPDATE_AUDIO: - pc.Unlock(); - pc.outputs.EnableDisable(); - pc.Lock(); - pc.CommandFinished(); - break; - - case PlayerCommand::EXIT: - pc.Unlock(); - - dc.Quit(); - - pc.outputs.Close(); - - player_command_finished(pc); - return; - - case PlayerCommand::CANCEL: - delete pc.next_song; - pc.next_song = nullptr; - - pc.CommandFinished(); - break; - - case PlayerCommand::REFRESH: - /* no-op when not playing */ - pc.CommandFinished(); - break; - - case PlayerCommand::NONE: - pc.Wait(); - break; - } - } -} - -void -StartPlayerThread(PlayerControl &pc) -{ - assert(!pc.thread.IsDefined()); - - Error error; - if (!pc.thread.Start(player_task, &pc, error)) - FatalError(error); -} diff --git a/src/PlayerThread.hxx b/src/PlayerThread.hxx deleted file mode 100644 index 128273aa7..000000000 --- a/src/PlayerThread.hxx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2003-2015 The Music Player Daemon Project - * http://www.musicpd.org - * - * This program 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 2 of the License, or - * (at your option) any later version. - * - * This program 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 this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - */ - -/* \file - * - * The player thread controls the playback. It acts as a bridge - * between the decoder thread and the output thread(s): it receives - * #MusicChunk objects from the decoder, optionally mixes them - * (cross-fading), applies software volume, and sends them to the - * audio outputs via audio_output_all_play(). - * - * It is controlled by the main thread (the playlist code), see - * PlayerControl.hxx. The playlist enqueues new songs into the player - * thread and sends it commands. - * - * The player thread itself does not do any I/O. It synchronizes with - * other threads via #GMutex and #GCond objects, and passes - * #MusicChunk instances around in #MusicPipe objects. - */ - -#ifndef MPD_PLAYER_THREAD_HXX -#define MPD_PLAYER_THREAD_HXX - -struct PlayerControl; - -void -StartPlayerThread(PlayerControl &pc); - -#endif diff --git a/src/Stats.cxx b/src/Stats.cxx index 9ed3a25dd..0f92b4f47 100644 --- a/src/Stats.cxx +++ b/src/Stats.cxx @@ -19,7 +19,7 @@ #include "config.h" #include "Stats.hxx" -#include "PlayerControl.hxx" +#include "player/Control.hxx" #include "client/Response.hxx" #include "Partition.hxx" #include "Instance.hxx" diff --git a/src/output/MultipleOutputs.cxx b/src/output/MultipleOutputs.cxx index a2260f19c..fc8b3888b 100644 --- a/src/output/MultipleOutputs.cxx +++ b/src/output/MultipleOutputs.cxx @@ -19,7 +19,7 @@ #include "config.h" #include "MultipleOutputs.hxx" -#include "PlayerControl.hxx" +#include "player/Control.hxx" #include "Internal.hxx" #include "Domain.hxx" #include "MusicBuffer.hxx" diff --git a/src/output/OutputCommand.cxx b/src/output/OutputCommand.cxx index 0297ebbcd..83abcf2ca 100644 --- a/src/output/OutputCommand.cxx +++ b/src/output/OutputCommand.cxx @@ -28,7 +28,7 @@ #include "OutputCommand.hxx" #include "MultipleOutputs.hxx" #include "Internal.hxx" -#include "PlayerControl.hxx" +#include "player/Control.hxx" #include "mixer/MixerControl.hxx" #include "Idle.hxx" diff --git a/src/output/OutputThread.cxx b/src/output/OutputThread.cxx index badf8f6c9..67205aa4c 100644 --- a/src/output/OutputThread.cxx +++ b/src/output/OutputThread.cxx @@ -27,7 +27,7 @@ #include "filter/FilterInternal.hxx" #include "filter/plugins/ConvertFilterPlugin.hxx" #include "filter/plugins/ReplayGainFilterPlugin.hxx" -#include "PlayerControl.hxx" +#include "player/Control.hxx" #include "MusicPipe.hxx" #include "MusicChunk.hxx" #include "thread/Util.hxx" diff --git a/src/player/Control.cxx b/src/player/Control.cxx new file mode 100644 index 000000000..d7352ad57 --- /dev/null +++ b/src/player/Control.cxx @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2003-2015 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "Control.hxx" +#include "Idle.hxx" +#include "DetachedSong.hxx" + +#include + +#include + +PlayerControl::PlayerControl(PlayerListener &_listener, + MultipleOutputs &_outputs, + unsigned _buffer_chunks, + unsigned _buffered_before_play) + :listener(_listener), outputs(_outputs), + buffer_chunks(_buffer_chunks), + buffered_before_play(_buffered_before_play), + command(PlayerCommand::NONE), + state(PlayerState::STOP), + error_type(PlayerError::NONE), + tagged_song(nullptr), + next_song(nullptr), + total_play_time(0), + border_pause(false) +{ +} + +PlayerControl::~PlayerControl() +{ + delete next_song; + delete tagged_song; +} + +void +PlayerControl::Play(DetachedSong *song) +{ + assert(song != nullptr); + + Lock(); + + if (state != PlayerState::STOP) + SynchronousCommand(PlayerCommand::STOP); + + assert(next_song == nullptr); + + EnqueueSongLocked(song); + + assert(next_song == nullptr); + + Unlock(); +} + +void +PlayerControl::Cancel() +{ + LockSynchronousCommand(PlayerCommand::CANCEL); + assert(next_song == nullptr); +} + +void +PlayerControl::Stop() +{ + LockSynchronousCommand(PlayerCommand::CLOSE_AUDIO); + assert(next_song == nullptr); + + idle_add(IDLE_PLAYER); +} + +void +PlayerControl::UpdateAudio() +{ + LockSynchronousCommand(PlayerCommand::UPDATE_AUDIO); +} + +void +PlayerControl::Kill() +{ + assert(thread.IsDefined()); + + LockSynchronousCommand(PlayerCommand::EXIT); + thread.Join(); + + idle_add(IDLE_PLAYER); +} + +void +PlayerControl::PauseLocked() +{ + if (state != PlayerState::STOP) { + SynchronousCommand(PlayerCommand::PAUSE); + idle_add(IDLE_PLAYER); + } +} + +void +PlayerControl::Pause() +{ + Lock(); + PauseLocked(); + Unlock(); +} + +void +PlayerControl::SetPause(bool pause_flag) +{ + Lock(); + + switch (state) { + case PlayerState::STOP: + break; + + case PlayerState::PLAY: + if (pause_flag) + PauseLocked(); + break; + + case PlayerState::PAUSE: + if (!pause_flag) + PauseLocked(); + break; + } + + Unlock(); +} + +void +PlayerControl::SetBorderPause(bool _border_pause) +{ + Lock(); + border_pause = _border_pause; + Unlock(); +} + +player_status +PlayerControl::GetStatus() +{ + player_status status; + + Lock(); + SynchronousCommand(PlayerCommand::REFRESH); + + status.state = state; + + if (state != PlayerState::STOP) { + status.bit_rate = bit_rate; + status.audio_format = audio_format; + status.total_time = total_time; + status.elapsed_time = elapsed_time; + } + + Unlock(); + + return status; +} + +void +PlayerControl::SetError(PlayerError type, Error &&_error) +{ + assert(type != PlayerError::NONE); + assert(_error.IsDefined()); + + error_type = type; + error = std::move(_error); +} + +void +PlayerControl::ClearError() +{ + Lock(); + + if (error_type != PlayerError::NONE) { + error_type = PlayerError::NONE; + error.Clear(); + } + + Unlock(); +} + +void +PlayerControl::LockSetTaggedSong(const DetachedSong &song) +{ + Lock(); + delete tagged_song; + tagged_song = new DetachedSong(song); + Unlock(); +} + +void +PlayerControl::ClearTaggedSong() +{ + delete tagged_song; + tagged_song = nullptr; +} + +void +PlayerControl::EnqueueSong(DetachedSong *song) +{ + assert(song != nullptr); + + Lock(); + EnqueueSongLocked(song); + Unlock(); +} + +bool +PlayerControl::Seek(DetachedSong *song, SongTime t) +{ + assert(song != nullptr); + + Lock(); + + delete next_song; + next_song = song; + seek_time = t; + SynchronousCommand(PlayerCommand::SEEK); + Unlock(); + + assert(next_song == nullptr); + + idle_add(IDLE_PLAYER); + + return true; +} + +void +PlayerControl::SetCrossFade(float _cross_fade_seconds) +{ + if (_cross_fade_seconds < 0) + _cross_fade_seconds = 0; + cross_fade.duration = _cross_fade_seconds; + + idle_add(IDLE_OPTIONS); +} + +void +PlayerControl::SetMixRampDb(float _mixramp_db) +{ + cross_fade.mixramp_db = _mixramp_db; + + idle_add(IDLE_OPTIONS); +} + +void +PlayerControl::SetMixRampDelay(float _mixramp_delay_seconds) +{ + cross_fade.mixramp_delay = _mixramp_delay_seconds; + + idle_add(IDLE_OPTIONS); +} diff --git a/src/player/Control.hxx b/src/player/Control.hxx new file mode 100644 index 000000000..a2807a9a1 --- /dev/null +++ b/src/player/Control.hxx @@ -0,0 +1,463 @@ +/* + * Copyright (C) 2003-2015 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_PLAYER_CONTROL_HXX +#define MPD_PLAYER_CONTROL_HXX + +#include "AudioFormat.hxx" +#include "thread/Mutex.hxx" +#include "thread/Cond.hxx" +#include "thread/Thread.hxx" +#include "util/Error.hxx" +#include "CrossFade.hxx" +#include "Chrono.hxx" + +#include + +class PlayerListener; +class MultipleOutputs; +class DetachedSong; + +enum class PlayerState : uint8_t { + STOP, + PAUSE, + PLAY +}; + +enum class PlayerCommand : uint8_t { + NONE, + EXIT, + STOP, + PAUSE, + SEEK, + CLOSE_AUDIO, + + /** + * At least one AudioOutput.enabled flag has been modified; + * commit those changes to the output threads. + */ + UPDATE_AUDIO, + + /** PlayerControl.next_song has been updated */ + QUEUE, + + /** + * cancel pre-decoding PlayerControl.next_song; if the player + * has already started playing this song, it will completely + * stop + */ + CANCEL, + + /** + * Refresh status information in the #PlayerControl struct, + * e.g. elapsed_time. + */ + REFRESH, +}; + +enum class PlayerError : uint8_t { + NONE, + + /** + * The decoder has failed to decode the song. + */ + DECODER, + + /** + * The audio output has failed. + */ + OUTPUT, +}; + +struct player_status { + PlayerState state; + uint16_t bit_rate; + AudioFormat audio_format; + SignedSongTime total_time; + SongTime elapsed_time; +}; + +struct PlayerControl { + PlayerListener &listener; + + MultipleOutputs &outputs; + + const unsigned buffer_chunks; + + const unsigned buffered_before_play; + + /** + * The handle of the player thread. + */ + Thread thread; + + /** + * This lock protects #command, #state, #error, #tagged_song. + */ + mutable Mutex mutex; + + /** + * Trigger this object after you have modified #command. + */ + Cond cond; + + /** + * This object gets signalled when the player thread has + * finished the #command. It wakes up the client that waits + * (i.e. the main thread). + */ + Cond client_cond; + + PlayerCommand command; + PlayerState state; + + PlayerError error_type; + + /** + * The error that occurred in the player thread. This + * attribute is only valid if #error is not + * #PlayerError::NONE. The object must be freed when this + * object transitions back to #PlayerError::NONE. + */ + Error error; + + /** + * A copy of the current #DetachedSong after its tags have + * been updated by the decoder (for example, a radio stream + * that has sent a new tag after switching to the next song). + * This shall be used by PlayerListener::OnPlayerTagModified() + * to update the current #DetachedSong in the queue. + * + * Protected by #mutex. Set by the PlayerThread and consumed + * by the main thread. + */ + DetachedSong *tagged_song; + + uint16_t bit_rate; + AudioFormat audio_format; + SignedSongTime total_time; + SongTime elapsed_time; + + /** + * The next queued song. + * + * This is a duplicate, and must be freed when this attribute + * is cleared. + */ + DetachedSong *next_song; + + SongTime seek_time; + + CrossFadeSettings cross_fade; + + double total_play_time; + + /** + * If this flag is set, then the player will be auto-paused at + * the end of the song, before the next song starts to play. + * + * This is a copy of the queue's "single" flag most of the + * time. + */ + bool border_pause; + + PlayerControl(PlayerListener &_listener, + MultipleOutputs &_outputs, + unsigned buffer_chunks, + unsigned buffered_before_play); + ~PlayerControl(); + + /** + * Locks the object. + */ + void Lock() const { + mutex.lock(); + } + + /** + * Unlocks the object. + */ + void Unlock() const { + mutex.unlock(); + } + + /** + * Signals the object. The object should be locked prior to + * calling this function. + */ + void Signal() { + cond.signal(); + } + + /** + * Signals the object. The object is temporarily locked by + * this function. + */ + void LockSignal() { + Lock(); + Signal(); + Unlock(); + } + + /** + * Waits for a signal on the object. This function is only + * valid in the player thread. The object must be locked + * prior to calling this function. + */ + void Wait() { + assert(thread.IsInside()); + + cond.wait(mutex); + } + + /** + * Wake up the client waiting for command completion. + * + * Caller must lock the object. + */ + void ClientSignal() { + assert(thread.IsInside()); + + client_cond.signal(); + } + + /** + * The client calls this method to wait for command + * completion. + * + * Caller must lock the object. + */ + void ClientWait() { + assert(!thread.IsInside()); + + client_cond.wait(mutex); + } + + /** + * A command has been finished. This method clears the + * command and signals the client. + * + * To be called from the player thread. Caller must lock the + * object. + */ + void CommandFinished() { + assert(command != PlayerCommand::NONE); + + command = PlayerCommand::NONE; + ClientSignal(); + } + +private: + /** + * Wait for the command to be finished by the player thread. + * + * To be called from the main thread. Caller must lock the + * object. + */ + void WaitCommandLocked() { + while (command != PlayerCommand::NONE) + ClientWait(); + } + + /** + * Send a command to the player thread and synchronously wait + * for it to finish. + * + * To be called from the main thread. Caller must lock the + * object. + */ + void SynchronousCommand(PlayerCommand cmd) { + assert(command == PlayerCommand::NONE); + + command = cmd; + Signal(); + WaitCommandLocked(); + } + + /** + * Send a command to the player thread and synchronously wait + * for it to finish. + * + * To be called from the main thread. This method locks the + * object. + */ + void LockSynchronousCommand(PlayerCommand cmd) { + Lock(); + SynchronousCommand(cmd); + Unlock(); + } + +public: + /** + * @param song the song to be queued; the given instance will + * be owned and freed by the player + */ + void Play(DetachedSong *song); + + /** + * see PlayerCommand::CANCEL + */ + void Cancel(); + + void SetPause(bool pause_flag); + +private: + void PauseLocked(); + +public: + void Pause(); + + /** + * Set the player's #border_pause flag. + */ + void SetBorderPause(bool border_pause); + + void Kill(); + + gcc_pure + player_status GetStatus(); + + PlayerState GetState() const { + return state; + } + + /** + * Set the error. Discards any previous error condition. + * + * Caller must lock the object. + * + * @param type the error type; must not be #PlayerError::NONE + * @param error detailed error information; must be defined. + */ + void SetError(PlayerError type, Error &&error); + + /** + * Checks whether an error has occurred, and if so, returns a + * copy of the #Error object. + * + * Caller must lock the object. + */ + gcc_pure + Error GetError() const { + Error result; + if (error_type != PlayerError::NONE) + result.Set(error); + return result; + } + + /** + * Like GetError(), but locks and unlocks the object. + */ + gcc_pure + Error LockGetError() const { + Lock(); + Error result = GetError(); + Unlock(); + return result; + } + + void ClearError(); + + PlayerError GetErrorType() const { + return error_type; + } + + /** + * Set the #tagged_song attribute to a newly allocated copy of + * the given #DetachedSong. Locks and unlocks the object. + */ + void LockSetTaggedSong(const DetachedSong &song); + + void ClearTaggedSong(); + + /** + * Read and clear the #tagged_song attribute. + * + * Caller must lock the object. + */ + DetachedSong *ReadTaggedSong() { + DetachedSong *result = tagged_song; + tagged_song = nullptr; + return result; + } + + /** + * Like ReadTaggedSong(), but locks and unlocks the object. + */ + DetachedSong *LockReadTaggedSong() { + Lock(); + DetachedSong *result = ReadTaggedSong(); + Unlock(); + return result; + } + + void Stop(); + + void UpdateAudio(); + +private: + void EnqueueSongLocked(DetachedSong *song) { + assert(song != nullptr); + assert(next_song == nullptr); + + next_song = song; + SynchronousCommand(PlayerCommand::QUEUE); + } + +public: + /** + * @param song the song to be queued; the given instance will be owned + * and freed by the player + */ + void EnqueueSong(DetachedSong *song); + + /** + * Makes the player thread seek the specified song to a position. + * + * @param song the song to be queued; the given instance will be owned + * and freed by the player + * @return true on success, false on failure (e.g. if MPD isn't + * playing currently) + */ + bool Seek(DetachedSong *song, SongTime t); + + void SetCrossFade(float cross_fade_seconds); + + float GetCrossFade() const { + return cross_fade.duration; + } + + void SetMixRampDb(float mixramp_db); + + float GetMixRampDb() const { + return cross_fade.mixramp_db; + } + + void SetMixRampDelay(float mixramp_delay_seconds); + + float GetMixRampDelay() const { + return cross_fade.mixramp_delay; + } + + double GetTotalPlayTime() const { + return total_play_time; + } +}; + +#endif diff --git a/src/player/CrossFade.cxx b/src/player/CrossFade.cxx new file mode 100644 index 000000000..6d7b41440 --- /dev/null +++ b/src/player/CrossFade.cxx @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2003-2015 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "CrossFade.hxx" +#include "Chrono.hxx" +#include "MusicChunk.hxx" +#include "AudioFormat.hxx" +#include "util/NumberParser.hxx" +#include "util/Domain.hxx" +#include "Log.hxx" + +#include + +static constexpr Domain cross_fade_domain("cross_fade"); + +gcc_pure +static float +mixramp_interpolate(const char *ramp_list, float required_db) +{ + float last_db = 0, last_secs = 0; + bool have_last = false; + + /* ramp_list is a string of pairs of dBs and seconds that describe the + * volume profile. Delimiters are semi-colons between pairs and spaces + * between the dB and seconds of a pair. + * The dB values must be monotonically increasing for this to work. */ + + while (1) { + /* Parse the dB value. */ + char *endptr; + const float db = ParseFloat(ramp_list, &endptr); + if (endptr == ramp_list || *endptr != ' ') + break; + + ramp_list = endptr + 1; + + /* Parse the time. */ + float secs = ParseFloat(ramp_list, &endptr); + if (endptr == ramp_list || (*endptr != ';' && *endptr != 0)) + break; + + ramp_list = endptr; + if (*ramp_list == ';') + ++ramp_list; + + /* Check for exact match. */ + if (db == required_db) { + return secs; + } + + /* Save if too quiet. */ + if (db < required_db) { + last_db = db; + last_secs = secs; + have_last = true; + continue; + } + + /* If required db < any stored value, use the least. */ + if (!have_last) + return secs; + + /* Finally, interpolate linearly. */ + secs = last_secs + (required_db - last_db) * (secs - last_secs) / (db - last_db); + return secs; + } + + return -1; +} + +unsigned +CrossFadeSettings::Calculate(SignedSongTime total_time, + float replay_gain_db, float replay_gain_prev_db, + const char *mixramp_start, const char *mixramp_prev_end, + const AudioFormat af, + const AudioFormat old_format, + unsigned max_chunks) const +{ + unsigned int chunks = 0; + float chunks_f; + + if (total_time.IsNegative() || + duration < 0 || duration >= total_time.ToDoubleS() || + /* we can't crossfade when the audio formats are different */ + af != old_format) + return 0; + + assert(duration >= 0); + assert(af.IsValid()); + + chunks_f = (float)af.GetTimeToSize() / (float)CHUNK_SIZE; + + if (mixramp_delay <= 0 || !mixramp_start || !mixramp_prev_end) { + chunks = (chunks_f * duration + 0.5); + } else { + /* Calculate mixramp overlap. */ + const float mixramp_overlap_current = + mixramp_interpolate(mixramp_start, + mixramp_db - replay_gain_db); + const float mixramp_overlap_prev = + mixramp_interpolate(mixramp_prev_end, + mixramp_db - replay_gain_prev_db); + const float mixramp_overlap = + mixramp_overlap_current + mixramp_overlap_prev; + + if (mixramp_overlap_current >= 0 && + mixramp_overlap_prev >= 0 && + mixramp_delay <= mixramp_overlap) { + chunks = (chunks_f * (mixramp_overlap - mixramp_delay)); + FormatDebug(cross_fade_domain, + "will overlap %d chunks, %fs", chunks, + mixramp_overlap - mixramp_delay); + } + } + + if (chunks > max_chunks) { + chunks = max_chunks; + LogWarning(cross_fade_domain, + "audio_buffer_size too small for computed MixRamp overlap"); + } + + return chunks; +} diff --git a/src/player/CrossFade.hxx b/src/player/CrossFade.hxx new file mode 100644 index 000000000..672abb718 --- /dev/null +++ b/src/player/CrossFade.hxx @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2003-2015 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_CROSSFADE_HXX +#define MPD_CROSSFADE_HXX + +#include "Compiler.h" + +struct AudioFormat; +class SignedSongTime; + +struct CrossFadeSettings { + /** + * The configured cross fade duration [s]. + */ + float duration; + + float mixramp_db; + + /** + * The configured MixRapm delay [s]. A non-positive value + * disables MixRamp. + */ + float mixramp_delay; + + CrossFadeSettings() + :duration(0), + mixramp_db(0), + mixramp_delay(-1) + {} + + + /** + * Calculate how many music pipe chunks should be used for crossfading. + * + * @param total_time total_time the duration of the new song + * @param replay_gain_db the ReplayGain adjustment used for this song + * @param replay_gain_prev_db the ReplayGain adjustment used on the last song + * @param mixramp_start the next songs mixramp_start tag + * @param mixramp_prev_end the last songs mixramp_end setting + * @param af the audio format of the new song + * @param old_format the audio format of the current song + * @param max_chunks the maximum number of chunks + * @return the number of chunks for crossfading, or 0 if cross fading + * should be disabled for this song change + */ + gcc_pure + unsigned Calculate(SignedSongTime total_time, + float replay_gain_db, float replay_gain_prev_db, + const char *mixramp_start, + const char *mixramp_prev_end, + AudioFormat af, AudioFormat old_format, + unsigned max_chunks) const; +}; + +#endif diff --git a/src/player/Listener.hxx b/src/player/Listener.hxx new file mode 100644 index 000000000..e10f2547b --- /dev/null +++ b/src/player/Listener.hxx @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2003-2015 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_PLAYER_LISTENER_HXX +#define MPD_PLAYER_LISTENER_HXX + +class PlayerListener { +public: + /** + * Must call playlist_sync(). + */ + virtual void OnPlayerSync() = 0; + + /** + * The current song's tag has changed. + */ + virtual void OnPlayerTagModified() = 0; +}; + +#endif diff --git a/src/player/Thread.cxx b/src/player/Thread.cxx new file mode 100644 index 000000000..60e253f4c --- /dev/null +++ b/src/player/Thread.cxx @@ -0,0 +1,1208 @@ +/* + * Copyright (C) 2003-2015 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "Thread.hxx" +#include "Listener.hxx" +#include "decoder/DecoderThread.hxx" +#include "decoder/DecoderControl.hxx" +#include "MusicPipe.hxx" +#include "MusicBuffer.hxx" +#include "MusicChunk.hxx" +#include "DetachedSong.hxx" +#include "system/FatalError.hxx" +#include "CrossFade.hxx" +#include "Control.hxx" +#include "output/MultipleOutputs.hxx" +#include "tag/Tag.hxx" +#include "Idle.hxx" +#include "util/Domain.hxx" +#include "thread/Name.hxx" +#include "Log.hxx" + +#include + +static constexpr Domain player_domain("player"); + +enum class CrossFadeState : int8_t { + DISABLED = -1, + UNKNOWN = 0, + ENABLED = 1 +}; + +class Player { + PlayerControl &pc; + + DecoderControl &dc; + + MusicBuffer &buffer; + + MusicPipe *pipe; + + /** + * are we waiting for buffered_before_play? + */ + bool buffering; + + /** + * true if the decoder is starting and did not provide data + * yet + */ + bool decoder_starting; + + /** + * Did we wake up the DecoderThread recently? This avoids + * duplicate wakeup calls. + */ + bool decoder_woken; + + /** + * is the player paused? + */ + bool paused; + + /** + * is there a new song in pc.next_song? + */ + bool queued; + + /** + * Was any audio output opened successfully? It might have + * failed meanwhile, but was not explicitly closed by the + * player thread. When this flag is unset, some output + * methods must not be called. + */ + bool output_open; + + /** + * the song currently being played + */ + DetachedSong *song; + + /** + * is cross fading enabled? + */ + CrossFadeState xfade_state; + + /** + * has cross-fading begun? + */ + bool cross_fading; + + /** + * The number of chunks used for crossfading. + */ + unsigned cross_fade_chunks; + + /** + * The tag of the "next" song during cross-fade. It is + * postponed, and sent to the output thread when the new song + * really begins. + */ + Tag *cross_fade_tag; + + /** + * The current audio format for the audio outputs. + */ + AudioFormat play_audio_format; + + /** + * The time stamp of the chunk most recently sent to the + * output thread. This attribute is only used if + * MultipleOutputs::GetElapsedTime() didn't return a usable + * value; the output thread can estimate the elapsed time more + * precisely. + */ + SongTime elapsed_time; + +public: + Player(PlayerControl &_pc, DecoderControl &_dc, + MusicBuffer &_buffer) + :pc(_pc), dc(_dc), buffer(_buffer), + buffering(true), + decoder_starting(false), + decoder_woken(false), + paused(false), + queued(true), + output_open(false), + song(nullptr), + xfade_state(CrossFadeState::UNKNOWN), + cross_fading(false), + cross_fade_chunks(0), + cross_fade_tag(nullptr), + elapsed_time(SongTime::zero()) {} + +private: + void ClearAndDeletePipe() { + pipe->Clear(buffer); + delete pipe; + } + + void ClearAndReplacePipe(MusicPipe *_pipe) { + ClearAndDeletePipe(); + pipe = _pipe; + } + + void ReplacePipe(MusicPipe *_pipe) { + delete pipe; + pipe = _pipe; + } + + /** + * Start the decoder. + * + * Player lock is not held. + */ + void StartDecoder(MusicPipe &pipe); + + /** + * The decoder has acknowledged the "START" command (see + * player::WaitForDecoder()). This function checks if the decoder + * initialization has completed yet. + * + * The player lock is not held. + */ + bool CheckDecoderStartup(); + + /** + * Stop the decoder and clears (and frees) its music pipe. + * + * Player lock is not held. + */ + void StopDecoder(); + + /** + * Is the decoder still busy on the same song as the player? + * + * Note: this function does not check if the decoder is already + * finished. + */ + gcc_pure + bool IsDecoderAtCurrentSong() const { + assert(pipe != nullptr); + + return dc.pipe == pipe; + } + + /** + * Returns true if the decoder is decoding the next song (or has begun + * decoding it, or has finished doing it), and the player hasn't + * switched to that song yet. + */ + gcc_pure + bool IsDecoderAtNextSong() const { + return dc.pipe != nullptr && !IsDecoderAtCurrentSong(); + } + + /** + * This is the handler for the #PlayerCommand::SEEK command. + * + * The player lock is not held. + */ + bool SeekDecoder(); + + /** + * After the decoder has been started asynchronously, wait for + * the "START" command to finish. The decoder may not be + * initialized yet, i.e. there is no audio_format information + * yet. + * + * The player lock is not held. + */ + bool WaitForDecoder(); + + /** + * Wrapper for MultipleOutputs::Open(). Upon failure, it + * pauses the player. + * + * @return true on success + */ + bool OpenOutput(); + + /** + * Obtains the next chunk from the music pipe, optionally applies + * cross-fading, and sends it to all audio outputs. + * + * @return true on success, false on error (playback will be stopped) + */ + bool PlayNextChunk(); + + /** + * Sends a chunk of silence to the audio outputs. This is + * called when there is not enough decoded data in the pipe + * yet, to prevent underruns in the hardware buffers. + * + * The player lock is not held. + */ + bool SendSilence(); + + /** + * Player lock must be held before calling. + */ + void ProcessCommand(); + + /** + * This is called at the border between two songs: the audio output + * has consumed all chunks of the current song, and we should start + * sending chunks from the next one. + * + * The player lock is not held. + * + * @return true on success, false on error (playback will be stopped) + */ + bool SongBorder(); + +public: + /* + * The main loop of the player thread, during playback. This + * is basically a state machine, which multiplexes data + * between the decoder thread and the output threads. + */ + void Run(); +}; + +static void +player_command_finished(PlayerControl &pc) +{ + pc.Lock(); + pc.CommandFinished(); + pc.Unlock(); +} + +void +Player::StartDecoder(MusicPipe &_pipe) +{ + assert(queued || pc.command == PlayerCommand::SEEK); + assert(pc.next_song != nullptr); + + SongTime start_time = pc.next_song->GetStartTime(); + if (pc.command == PlayerCommand::SEEK) + start_time += pc.seek_time; + + dc.Start(new DetachedSong(*pc.next_song), + start_time, pc.next_song->GetEndTime(), + buffer, _pipe); +} + +void +Player::StopDecoder() +{ + dc.Stop(); + + if (dc.pipe != nullptr) { + /* clear and free the decoder pipe */ + + dc.pipe->Clear(buffer); + + if (dc.pipe != pipe) + delete dc.pipe; + + dc.pipe = nullptr; + } +} + +bool +Player::WaitForDecoder() +{ + assert(queued || pc.command == PlayerCommand::SEEK); + assert(pc.next_song != nullptr); + + queued = false; + + pc.Lock(); + Error error = dc.GetError(); + if (error.IsDefined()) { + pc.SetError(PlayerError::DECODER, std::move(error)); + + delete pc.next_song; + pc.next_song = nullptr; + + pc.Unlock(); + + return false; + } + + pc.ClearTaggedSong(); + + delete song; + song = pc.next_song; + elapsed_time = SongTime::zero(); + + /* set the "starting" flag, which will be cleared by + player_check_decoder_startup() */ + decoder_starting = true; + + /* update PlayerControl's song information */ + pc.total_time = pc.next_song->GetDuration(); + pc.bit_rate = 0; + pc.audio_format.Clear(); + + /* clear the queued song */ + pc.next_song = nullptr; + + pc.Unlock(); + + /* call syncPlaylistWithQueue() in the main thread */ + pc.listener.OnPlayerSync(); + + return true; +} + +/** + * Returns the real duration of the song, comprising the duration + * indicated by the decoder plugin. + */ +static SignedSongTime +real_song_duration(const DetachedSong &song, SignedSongTime decoder_duration) +{ + if (decoder_duration.IsNegative()) + /* the decoder plugin didn't provide information; fall + back to Song::GetDuration() */ + return song.GetDuration(); + + const SongTime start_time = song.GetStartTime(); + const SongTime end_time = song.GetEndTime(); + + if (end_time.IsPositive() && end_time < SongTime(decoder_duration)) + return SignedSongTime(end_time - start_time); + + return SignedSongTime(SongTime(decoder_duration) - start_time); +} + +bool +Player::OpenOutput() +{ + assert(play_audio_format.IsDefined()); + assert(pc.state == PlayerState::PLAY || + pc.state == PlayerState::PAUSE); + + Error error; + if (pc.outputs.Open(play_audio_format, buffer, error)) { + output_open = true; + paused = false; + + pc.Lock(); + pc.state = PlayerState::PLAY; + pc.Unlock(); + + idle_add(IDLE_PLAYER); + + return true; + } else { + LogError(error); + + output_open = false; + + /* pause: the user may resume playback as soon as an + audio output becomes available */ + paused = true; + + pc.Lock(); + pc.SetError(PlayerError::OUTPUT, std::move(error)); + pc.state = PlayerState::PAUSE; + pc.Unlock(); + + idle_add(IDLE_PLAYER); + + return false; + } +} + +bool +Player::CheckDecoderStartup() +{ + assert(decoder_starting); + + pc.Lock(); + + Error error = dc.GetError(); + if (error.IsDefined()) { + /* the decoder failed */ + pc.SetError(PlayerError::DECODER, std::move(error)); + pc.Unlock(); + + return false; + } else if (!dc.IsStarting()) { + /* the decoder is ready and ok */ + + pc.Unlock(); + + if (output_open && + !pc.outputs.Wait(pc, 1)) + /* the output devices havn't finished playing + all chunks yet - wait for that */ + return true; + + pc.Lock(); + pc.total_time = real_song_duration(*dc.song, dc.total_time); + pc.audio_format = dc.in_audio_format; + pc.Unlock(); + + idle_add(IDLE_PLAYER); + + play_audio_format = dc.out_audio_format; + decoder_starting = false; + + if (!paused && !OpenOutput()) { + FormatError(player_domain, + "problems opening audio device " + "while playing \"%s\"", + dc.song->GetURI()); + return true; + } + + return true; + } else { + /* the decoder is not yet ready; wait + some more */ + dc.WaitForDecoder(); + pc.Unlock(); + + return true; + } +} + +bool +Player::SendSilence() +{ + assert(output_open); + assert(play_audio_format.IsDefined()); + + MusicChunk *chunk = buffer.Allocate(); + if (chunk == nullptr) { + LogError(player_domain, "Failed to allocate silence buffer"); + return false; + } + +#ifndef NDEBUG + chunk->audio_format = play_audio_format; +#endif + + const size_t frame_size = play_audio_format.GetFrameSize(); + /* this formula ensures that we don't send + partial frames */ + unsigned num_frames = sizeof(chunk->data) / frame_size; + + chunk->time = SignedSongTime::Negative(); /* undefined time stamp */ + chunk->length = num_frames * frame_size; + memset(chunk->data, 0, chunk->length); + + Error error; + if (!pc.outputs.Play(chunk, error)) { + LogError(error); + buffer.Return(chunk); + return false; + } + + return true; +} + +inline bool +Player::SeekDecoder() +{ + assert(pc.next_song != nullptr); + + const SongTime start_time = pc.next_song->GetStartTime(); + + if (!dc.LockIsCurrentSong(*pc.next_song)) { + /* the decoder is already decoding the "next" song - + stop it and start the previous song again */ + + StopDecoder(); + + /* clear music chunks which might still reside in the + pipe */ + pipe->Clear(buffer); + + /* re-start the decoder */ + StartDecoder(*pipe); + if (!WaitForDecoder()) { + /* decoder failure */ + player_command_finished(pc); + return false; + } + } else { + if (!IsDecoderAtCurrentSong()) { + /* the decoder is already decoding the "next" song, + but it is the same song file; exchange the pipe */ + ClearAndReplacePipe(dc.pipe); + } + + delete pc.next_song; + pc.next_song = nullptr; + queued = false; + } + + /* wait for the decoder to complete initialization */ + + while (decoder_starting) { + if (!CheckDecoderStartup()) { + /* decoder failure */ + player_command_finished(pc); + return false; + } + } + + /* send the SEEK command */ + + SongTime where = pc.seek_time; + if (!pc.total_time.IsNegative()) { + const SongTime total_time(pc.total_time); + if (where > total_time) + where = total_time; + } + + if (!dc.Seek(where + start_time)) { + /* decoder failure */ + player_command_finished(pc); + return false; + } + + elapsed_time = where; + + player_command_finished(pc); + + xfade_state = CrossFadeState::UNKNOWN; + + /* re-fill the buffer after seeking */ + buffering = true; + + pc.outputs.Cancel(); + + return true; +} + +inline void +Player::ProcessCommand() +{ + switch (pc.command) { + case PlayerCommand::NONE: + case PlayerCommand::STOP: + case PlayerCommand::EXIT: + case PlayerCommand::CLOSE_AUDIO: + break; + + case PlayerCommand::UPDATE_AUDIO: + pc.Unlock(); + pc.outputs.EnableDisable(); + pc.Lock(); + pc.CommandFinished(); + break; + + case PlayerCommand::QUEUE: + assert(pc.next_song != nullptr); + assert(!queued); + assert(!IsDecoderAtNextSong()); + + queued = true; + pc.CommandFinished(); + + pc.Unlock(); + if (dc.LockIsIdle()) + StartDecoder(*new MusicPipe()); + pc.Lock(); + + break; + + case PlayerCommand::PAUSE: + pc.Unlock(); + + paused = !paused; + if (paused) { + pc.outputs.Pause(); + pc.Lock(); + + pc.state = PlayerState::PAUSE; + } else if (!play_audio_format.IsDefined()) { + /* the decoder hasn't provided an audio format + yet - don't open the audio device yet */ + pc.Lock(); + + pc.state = PlayerState::PLAY; + } else { + OpenOutput(); + + pc.Lock(); + } + + pc.CommandFinished(); + break; + + case PlayerCommand::SEEK: + pc.Unlock(); + SeekDecoder(); + pc.Lock(); + break; + + case PlayerCommand::CANCEL: + if (pc.next_song == nullptr) { + /* the cancel request arrived too late, we're + already playing the queued song... stop + everything now */ + pc.command = PlayerCommand::STOP; + return; + } + + if (IsDecoderAtNextSong()) { + /* the decoder is already decoding the song - + stop it and reset the position */ + pc.Unlock(); + StopDecoder(); + pc.Lock(); + } + + delete pc.next_song; + pc.next_song = nullptr; + queued = false; + pc.CommandFinished(); + break; + + case PlayerCommand::REFRESH: + if (output_open && !paused) { + pc.Unlock(); + pc.outputs.Check(); + pc.Lock(); + } + + pc.elapsed_time = !pc.outputs.GetElapsedTime().IsNegative() + ? SongTime(pc.outputs.GetElapsedTime()) + : elapsed_time; + + pc.CommandFinished(); + break; + } +} + +static void +update_song_tag(PlayerControl &pc, DetachedSong &song, const Tag &new_tag) +{ + if (song.IsFile()) + /* don't update tags of local files, only remote + streams may change tags dynamically */ + return; + + song.SetTag(new_tag); + + pc.LockSetTaggedSong(song); + + /* the main thread will update the playlist version when he + receives this event */ + pc.listener.OnPlayerTagModified(); + + /* notify all clients that the tag of the current song has + changed */ + idle_add(IDLE_PLAYER); +} + +/** + * Plays a #MusicChunk object (after applying software volume). If + * it contains a (stream) tag, copy it to the current song, so MPD's + * playlist reflects the new stream tag. + * + * Player lock is not held. + */ +static bool +play_chunk(PlayerControl &pc, + DetachedSong &song, MusicChunk *chunk, + MusicBuffer &buffer, + const AudioFormat format, + Error &error) +{ + assert(chunk->CheckFormat(format)); + + if (chunk->tag != nullptr) + update_song_tag(pc, song, *chunk->tag); + + if (chunk->IsEmpty()) { + buffer.Return(chunk); + return true; + } + + pc.Lock(); + pc.bit_rate = chunk->bit_rate; + pc.Unlock(); + + /* send the chunk to the audio outputs */ + + if (!pc.outputs.Play(chunk, error)) + return false; + + pc.total_play_time += (double)chunk->length / + format.GetTimeToSize(); + return true; +} + +inline bool +Player::PlayNextChunk() +{ + if (!pc.outputs.Wait(pc, 64)) + /* the output pipe is still large enough, don't send + another chunk */ + return true; + + unsigned cross_fade_position; + MusicChunk *chunk = nullptr; + if (xfade_state == CrossFadeState::ENABLED && IsDecoderAtNextSong() && + (cross_fade_position = pipe->GetSize()) <= cross_fade_chunks) { + /* perform cross fade */ + MusicChunk *other_chunk = dc.pipe->Shift(); + + if (!cross_fading) { + /* beginning of the cross fade - adjust + crossFadeChunks which might be bigger than + the remaining number of chunks in the old + song */ + cross_fade_chunks = cross_fade_position; + cross_fading = true; + } + + if (other_chunk != nullptr) { + chunk = pipe->Shift(); + assert(chunk != nullptr); + assert(chunk->other == nullptr); + + /* don't send the tags of the new song (which + is being faded in) yet; postpone it until + the current song is faded out */ + cross_fade_tag = + Tag::MergeReplace(cross_fade_tag, + other_chunk->tag); + other_chunk->tag = nullptr; + + if (pc.cross_fade.mixramp_delay <= 0) { + chunk->mix_ratio = ((float)cross_fade_position) + / cross_fade_chunks; + } else { + chunk->mix_ratio = -1; + } + + if (other_chunk->IsEmpty()) { + /* the "other" chunk was a MusicChunk + which had only a tag, but no music + data - we cannot cross-fade that; + but since this happens only at the + beginning of the new song, we can + easily recover by throwing it away + now */ + buffer.Return(other_chunk); + other_chunk = nullptr; + } + + chunk->other = other_chunk; + } else { + /* there are not enough decoded chunks yet */ + + pc.Lock(); + + if (dc.IsIdle()) { + /* the decoder isn't running, abort + cross fading */ + pc.Unlock(); + + xfade_state = CrossFadeState::DISABLED; + } else { + /* wait for the decoder */ + dc.Signal(); + dc.WaitForDecoder(); + pc.Unlock(); + + return true; + } + } + } + + if (chunk == nullptr) + chunk = pipe->Shift(); + + assert(chunk != nullptr); + + /* insert the postponed tag if cross-fading is finished */ + + if (xfade_state != CrossFadeState::ENABLED && cross_fade_tag != nullptr) { + chunk->tag = Tag::MergeReplace(chunk->tag, cross_fade_tag); + cross_fade_tag = nullptr; + } + + /* play the current chunk */ + + Error error; + if (!play_chunk(pc, *song, chunk, buffer, play_audio_format, error)) { + LogError(error); + + buffer.Return(chunk); + + pc.Lock(); + + pc.SetError(PlayerError::OUTPUT, std::move(error)); + + /* pause: the user may resume playback as soon as an + audio output becomes available */ + pc.state = PlayerState::PAUSE; + paused = true; + + pc.Unlock(); + + idle_add(IDLE_PLAYER); + + return false; + } + + /* this formula should prevent that the decoder gets woken up + with each chunk; it is more efficient to make it decode a + larger block at a time */ + pc.Lock(); + if (!dc.IsIdle() && + dc.pipe->GetSize() <= (pc.buffered_before_play + + buffer.GetSize() * 3) / 4) { + if (!decoder_woken) { + decoder_woken = true; + dc.Signal(); + } + } else + decoder_woken = false; + pc.Unlock(); + + return true; +} + +inline bool +Player::SongBorder() +{ + xfade_state = CrossFadeState::UNKNOWN; + + FormatDefault(player_domain, "played \"%s\"", song->GetURI()); + + ReplacePipe(dc.pipe); + + pc.outputs.SongBorder(); + + if (!WaitForDecoder()) + return false; + + pc.Lock(); + + const bool border_pause = pc.border_pause; + if (border_pause) { + paused = true; + pc.state = PlayerState::PAUSE; + } + + pc.Unlock(); + + if (border_pause) + idle_add(IDLE_PLAYER); + + return true; +} + +inline void +Player::Run() +{ + pipe = new MusicPipe(); + + StartDecoder(*pipe); + if (!WaitForDecoder()) { + assert(song == nullptr); + + StopDecoder(); + player_command_finished(pc); + delete pipe; + return; + } + + pc.Lock(); + pc.state = PlayerState::PLAY; + + if (pc.command == PlayerCommand::SEEK) + elapsed_time = pc.seek_time; + + pc.CommandFinished(); + + while (true) { + ProcessCommand(); + if (pc.command == PlayerCommand::STOP || + pc.command == PlayerCommand::EXIT || + pc.command == PlayerCommand::CLOSE_AUDIO) { + pc.Unlock(); + pc.outputs.Cancel(); + break; + } + + pc.Unlock(); + + if (buffering) { + /* buffering at the start of the song - wait + until the buffer is large enough, to + prevent stuttering on slow machines */ + + if (pipe->GetSize() < pc.buffered_before_play && + !dc.LockIsIdle()) { + /* not enough decoded buffer space yet */ + + if (!paused && output_open && + pc.outputs.Check() < 4 && + !SendSilence()) + break; + + pc.Lock(); + /* XXX race condition: check decoder again */ + dc.WaitForDecoder(); + continue; + } else { + /* buffering is complete */ + buffering = false; + } + } + + if (decoder_starting) { + /* wait until the decoder is initialized completely */ + + if (!CheckDecoderStartup()) + break; + + pc.Lock(); + continue; + } + +#ifndef NDEBUG + /* + music_pipe_check_format(&play_audio_format, + next_song_chunk, + &dc.out_audio_format); + */ +#endif + + if (dc.LockIsIdle() && queued && dc.pipe == pipe) { + /* the decoder has finished the current song; + make it decode the next song */ + + assert(dc.pipe == nullptr || dc.pipe == pipe); + + StartDecoder(*new MusicPipe()); + } + + if (/* no cross-fading if MPD is going to pause at the + end of the current song */ + !pc.border_pause && + IsDecoderAtNextSong() && + xfade_state == CrossFadeState::UNKNOWN && + !dc.LockIsStarting()) { + /* enable cross fading in this song? if yes, + calculate how many chunks will be required + for it */ + cross_fade_chunks = + pc.cross_fade.Calculate(dc.total_time, + dc.replay_gain_db, + dc.replay_gain_prev_db, + dc.GetMixRampStart(), + dc.GetMixRampPreviousEnd(), + dc.out_audio_format, + play_audio_format, + buffer.GetSize() - + pc.buffered_before_play); + if (cross_fade_chunks > 0) { + xfade_state = CrossFadeState::ENABLED; + cross_fading = false; + } else + /* cross fading is disabled or the + next song is too short */ + xfade_state = CrossFadeState::DISABLED; + } + + if (paused) { + pc.Lock(); + + if (pc.command == PlayerCommand::NONE) + pc.Wait(); + continue; + } else if (!pipe->IsEmpty()) { + /* at least one music chunk is ready - send it + to the audio output */ + + PlayNextChunk(); + } else if (pc.outputs.Check() > 0) { + /* not enough data from decoder, but the + output thread is still busy, so it's + okay */ + + pc.Lock(); + + /* wake up the decoder (just in case it's + waiting for space in the MusicBuffer) and + wait for it */ + dc.Signal(); + dc.WaitForDecoder(); + continue; + } else if (IsDecoderAtNextSong()) { + /* at the beginning of a new song */ + + if (!SongBorder()) + break; + } else if (dc.LockIsIdle()) { + /* check the size of the pipe again, because + the decoder thread may have added something + since we last checked */ + if (pipe->IsEmpty()) { + /* wait for the hardware to finish + playback */ + pc.outputs.Drain(); + break; + } + } else if (output_open) { + /* the decoder is too busy and hasn't provided + new PCM data in time: send silence (if the + output pipe is empty) */ + if (!SendSilence()) + break; + } + + pc.Lock(); + } + + StopDecoder(); + + ClearAndDeletePipe(); + + delete cross_fade_tag; + + if (song != nullptr) { + FormatDefault(player_domain, "played \"%s\"", song->GetURI()); + delete song; + } + + pc.Lock(); + + pc.ClearTaggedSong(); + + if (queued) { + assert(pc.next_song != nullptr); + delete pc.next_song; + pc.next_song = nullptr; + } + + pc.state = PlayerState::STOP; + + pc.Unlock(); +} + +static void +do_play(PlayerControl &pc, DecoderControl &dc, + MusicBuffer &buffer) +{ + Player player(pc, dc, buffer); + player.Run(); +} + +static void +player_task(void *arg) +{ + PlayerControl &pc = *(PlayerControl *)arg; + + SetThreadName("player"); + + DecoderControl dc(pc.mutex, pc.cond); + decoder_thread_start(dc); + + MusicBuffer buffer(pc.buffer_chunks); + + pc.Lock(); + + while (1) { + switch (pc.command) { + case PlayerCommand::SEEK: + case PlayerCommand::QUEUE: + assert(pc.next_song != nullptr); + + pc.Unlock(); + do_play(pc, dc, buffer); + pc.listener.OnPlayerSync(); + pc.Lock(); + break; + + case PlayerCommand::STOP: + pc.Unlock(); + pc.outputs.Cancel(); + pc.Lock(); + + /* fall through */ + + case PlayerCommand::PAUSE: + delete pc.next_song; + pc.next_song = nullptr; + + pc.CommandFinished(); + break; + + case PlayerCommand::CLOSE_AUDIO: + pc.Unlock(); + + pc.outputs.Release(); + + pc.Lock(); + pc.CommandFinished(); + + assert(buffer.IsEmptyUnsafe()); + + break; + + case PlayerCommand::UPDATE_AUDIO: + pc.Unlock(); + pc.outputs.EnableDisable(); + pc.Lock(); + pc.CommandFinished(); + break; + + case PlayerCommand::EXIT: + pc.Unlock(); + + dc.Quit(); + + pc.outputs.Close(); + + player_command_finished(pc); + return; + + case PlayerCommand::CANCEL: + delete pc.next_song; + pc.next_song = nullptr; + + pc.CommandFinished(); + break; + + case PlayerCommand::REFRESH: + /* no-op when not playing */ + pc.CommandFinished(); + break; + + case PlayerCommand::NONE: + pc.Wait(); + break; + } + } +} + +void +StartPlayerThread(PlayerControl &pc) +{ + assert(!pc.thread.IsDefined()); + + Error error; + if (!pc.thread.Start(player_task, &pc, error)) + FatalError(error); +} diff --git a/src/player/Thread.hxx b/src/player/Thread.hxx new file mode 100644 index 000000000..fc6ea4364 --- /dev/null +++ b/src/player/Thread.hxx @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2003-2015 The Music Player Daemon Project + * http://www.musicpd.org + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/* \file + * + * The player thread controls the playback. It acts as a bridge + * between the decoder thread and the output thread(s): it receives + * #MusicChunk objects from the decoder, optionally mixes them + * (cross-fading), applies software volume, and sends them to the + * audio outputs via audio_output_all_play(). + * + * It is controlled by the main thread (the playlist code), see + * Control.hxx. The playlist enqueues new songs into the player + * thread and sends it commands. + * + * The player thread itself does not do any I/O. It synchronizes with + * other threads via #GMutex and #GCond objects, and passes + * #MusicChunk instances around in #MusicPipe objects. + */ + +#ifndef MPD_PLAYER_THREAD_HXX +#define MPD_PLAYER_THREAD_HXX + +struct PlayerControl; + +void +StartPlayerThread(PlayerControl &pc); + +#endif diff --git a/src/queue/Playlist.cxx b/src/queue/Playlist.cxx index 1da89469c..841684272 100644 --- a/src/queue/Playlist.cxx +++ b/src/queue/Playlist.cxx @@ -20,7 +20,7 @@ #include "config.h" #include "Playlist.hxx" #include "PlaylistError.hxx" -#include "PlayerControl.hxx" +#include "player/Control.hxx" #include "DetachedSong.hxx" #include "Idle.hxx" #include "Log.hxx" diff --git a/src/queue/PlaylistControl.cxx b/src/queue/PlaylistControl.cxx index ed4a77e36..f7f0a4225 100644 --- a/src/queue/PlaylistControl.cxx +++ b/src/queue/PlaylistControl.cxx @@ -25,7 +25,7 @@ #include "config.h" #include "Playlist.hxx" #include "PlaylistError.hxx" -#include "PlayerControl.hxx" +#include "player/Control.hxx" #include "DetachedSong.hxx" #include "Log.hxx" diff --git a/src/queue/PlaylistEdit.cxx b/src/queue/PlaylistEdit.cxx index c5aad092f..0d15f6a04 100644 --- a/src/queue/PlaylistEdit.cxx +++ b/src/queue/PlaylistEdit.cxx @@ -26,7 +26,7 @@ #include "config.h" #include "Playlist.hxx" #include "PlaylistError.hxx" -#include "PlayerControl.hxx" +#include "player/Control.hxx" #include "util/UriUtil.hxx" #include "util/Error.hxx" #include "DetachedSong.hxx" diff --git a/src/queue/PlaylistState.cxx b/src/queue/PlaylistState.cxx index 56687b6d6..fa51b1519 100644 --- a/src/queue/PlaylistState.cxx +++ b/src/queue/PlaylistState.cxx @@ -29,7 +29,7 @@ #include "queue/QueueSave.hxx" #include "fs/io/TextFile.hxx" #include "fs/io/BufferedOutputStream.hxx" -#include "PlayerControl.hxx" +#include "player/Control.hxx" #include "config/ConfigGlobal.hxx" #include "config/ConfigOption.hxx" #include "util/CharUtil.hxx" diff --git a/test/run_output.cxx b/test/run_output.cxx index 88d9491dd..0b8f0b55a 100644 --- a/test/run_output.cxx +++ b/test/run_output.cxx @@ -31,7 +31,7 @@ #include "AudioParser.hxx" #include "pcm/PcmConvert.hxx" #include "filter/FilterRegistry.hxx" -#include "PlayerControl.hxx" +#include "player/Control.hxx" #include "stdbin.h" #include "util/Error.hxx" #include "Log.hxx" diff --git a/test/test_mixramp.cxx b/test/test_mixramp.cxx index 954aade87..ea245f72f 100644 --- a/test/test_mixramp.cxx +++ b/test/test_mixramp.cxx @@ -3,7 +3,7 @@ */ #include "config.h" -#include "CrossFade.cxx" +#include "player/CrossFade.cxx" #include #include -- cgit v1.2.3