#pragma once #ifdef __ANDROID__ #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include "file_backend.hpp" #include "playback_backend.hpp" #include "config.hpp" #include "playback_process.hpp" #include #include #include "playlist.hpp" #include "util.hpp" using namespace soundtouch; using std::span; using std::optional; using std::vector; using std::queue; using std::deque; enum { /// @brief No signals have occurred. PlaybackSignalNone = 0, /// @brief The file was changed. Recheck the properties of the file because they are likely different. PlaybackSignalFileChanged = 1 << 0, /// @brief The speed was changed. PlaybackSignalSpeedChanged = 1 << 1, /// @brief The speed was changed. PlaybackSignalTempoChanged = 1 << 2, /// @brief The speed was changed. PlaybackSignalPitchChanged = 1 << 3, /// @brief Playback was paused. If @ref PlaybackSignalResumed has also been sent, you must use @ref Playback::IsPaused to check if playback was paused or resumed. PlaybackSignalPaused = 1 << 4, /// @brief Playback was resumed. If @ref PlaybackSignalPaused has also been sent, you must use @ref Playback::IsPaused to check if playback was paused or resumed. PlaybackSignalResumed = 1 << 5, /// @brief Playback was stopped entirely. If @ref PlaybackSignalStarted has also been signalled, call @ref Playback::IsStopped to find out if playback is currently playing. PlaybackSignalStopped = 1 << 6, /// @brief An error occurred and playback has likely (but not necessarily) stopped. Call @ref Playback::GetError for details. PlaybackSignalErrorOccurred = 1 << 7, /// @brief Playback was seeked by the @ref Playback::Seek function PlaybackSignalSeeked = 1 << 8, /// @brief Playback has started. If @ref PlaybackSignalStopped has also been signalled, call @ref Playback::IsStopped to find out if playback is currently playing. PlaybackSignalStarted = 1 << 9 }; /// @brief Playback handler base class. class Playback { protected: friend class DBusAPI; /// @brief The handle-specific signal state. std::map signals_occurred; /// @brief The global error state for initializing handle-specific state. std::vector errors; /// @brief The handle-specific error state. std::map> errors_occurred; /// @brief The mutex for signals std::mutex signal_mutex; /// @brief The mutex for errors std::mutex error_mutex; /// @brief Signals other code with the specified signal(s). Use with PlaybackSignal* enum variants. void set_signal(uint16_t signal); /// @brief Signals an error, and pushes details to other code. void set_error(std::string desc); public: inline virtual bool is_proxy() { return false; } inline Playback() {}; inline virtual ~Playback() {} /// @brief Gets the current file, if any. /// @returns No value if there is no file playing, or the path to the file if there is a file playing. inline virtual std::optional get_current_file() { return {}; } /// @brief Gets the current file's title, if any, for display to the user. /// @returns No value if there is no file playing. If there is a file playing, a string is returned. The contents of the string depend on whether or not the file has a title tag. If so, the string contains the contents of that tag. Otherwise, the string contains the stem of the file's path. inline virtual std::optional get_current_title() { return {}; } inline virtual int get_current_stream() { return 0; } inline virtual std::vector get_streams() { std::vector output; return output; } inline virtual void play_stream(int idx) {} /// @brief Gets the position of the playing file. inline virtual double GetPosition() { return 0.0; } /// @brief Gets the length of the file. If the length is less than or equal to zero, the file can be considered to have an unknown length. inline virtual double GetLength() { return 0.0; } /// @brief Sets the playback position into the current file. inline virtual void Seek(double position) {} /// @brief Loads a new file. inline virtual void Load(std::string filePath) {} /// @brief Plays a new file. inline virtual void Start(std::string filePath, int streamIdx = 0) {} /// @brief Checks whether or not the file is paused. /// @returns true if playback is paused, false otherwise inline virtual bool IsPaused() { return true; } /// @brief Toggles the pause state of the playback. inline virtual void Pause() {} /// @brief Stops all playback. inline virtual void Stop() {} /// @brief Checks whether or not playback is stopped. /// @returns true if playback is stopped, false otherwise. inline virtual bool IsStopped() { return true; } /// @brief Sets the tempo multiplier of the playback. inline virtual void SetTempo(float tempo) {} /// @brief Sets the speed multiplier (Both speed and pitch, but this is a separate property) of the playback. inline virtual void SetSpeed(float speed) {} /// @brief Sets the pitch multiplier of the playback inline virtual void SetPitch(float pitch) {} /// @brief Sets the volume of the playback, as a percentage inline virtual void SetVolume(float volume) {} /// @brief Gets the tempo multiplier of the playback. inline virtual float GetTempo() { return 1.0; } /// @brief Gets the speed multiplier (Both speed and pitch, but this is a separate property) of the playback. inline virtual float GetSpeed() { return 1.0; } /// @brief Gets the pitch multiplier of the playback. inline virtual float GetPitch() { return 1.0; } /// @brief Gets the volume of the playback, as a percentage inline virtual float GetVolume() { return 1.0; } /// @brief Returns and clears signals, using a registered handle if specified. /// @param signal The signal(s) to check for and clear. /// @param handle The handle, which was specified to @ref Playback::register_handle , or nullptr. uint16_t handle_signals(uint16_t signal, void *handle = nullptr); /// @brief Registers a handle for usage in handle_signals and GetError. void register_handle(void *handle); /// @brief Unregisters a handle that is no longer needed. void unregister_handle(void *handle); /// @brief Gets the last error from the playback engine, or no value if there was none. /// @param handle The handle as registered with @ref Playback::register_handle inline virtual optional GetError(void *handle) { std::optional output = {}; if (ErrorExists(handle)) { error_mutex.lock(); if (errors_occurred.contains(handle)) { output = errors_occurred[handle].back(); errors_occurred[handle].pop_back(); } error_mutex.unlock(); } return output; } /// @brief Checks if an unread error has occurred. /// @param handle The handle as registered with @ref Playback::register_handle inline virtual bool ErrorExists(void *handle) { bool output = false; error_mutex.lock(); if (errors_occurred.contains(handle)) { output = !errors_occurred[handle].empty(); } error_mutex.unlock(); return output; } /// @brief Helper function to set the paused status via the @ref Playback::IsPaused and @ref Playback::Pause APIs. /// @param paused The new pause state. inline virtual void SetPaused(bool paused) { if (IsPaused() != paused) { Pause(); } } virtual void InitLoopFunction() { } virtual void LoopFunction() { } virtual void DeinitLoopFunction() { } virtual void set_property(std::string path, google::protobuf::Any value) { } virtual std::optional get_property(std::string path) { return {}; } virtual std::optional reset_property(std::string path) { return {}; } bool loop_started = false; virtual void start_loop() { InitLoopFunction(); loop_started = true; } virtual void stop_loop() { DeinitLoopFunction(); loop_started = false; } virtual void LoopHook() { if (loop_started) { LoopFunction(); } } virtual std::vector get_property_list() { return {}; } inline virtual void Render(std::string path) { } static Playback *Create(bool *daemon_found, bool daemon = false); }; class DBusAPISender; class PlaybackInstance : public Playback #ifdef __ANDROID__ , public oboe::AudioStreamDataCallback #endif { private: #ifdef __ANDROID__ std::shared_ptr ostream; public: oboe::DataCallbackResult onAudioReady( oboe::AudioStream *audioStream, void *audioData, int32_t numFrames) override; private: #endif PlaybackProcess *process; std::string filePath; #ifndef USE_SDL std::mutex audio_playback_mutex; #endif std::atomic_bool audio_playback_paused; void LockAudioDevice(); void UnlockAudioDevice(); std::atomic_bool running; std::atomic_bool file_changed; std::atomic_bool seeking; std::atomic_bool update; std::atomic_bool restart; std::atomic_bool playback_ready; std::atomic_bool speed_changed; std::atomic_bool tempo_changed; std::atomic_bool pitch_changed; std::atomic_bool pause_changed; std::atomic_bool load_requested; std::atomic_bool load_finished; std::atomic_bool stream_changed; std::atomic_bool stopped; std::atomic_bool just_stopped; std::atomic_bool just_started; std::mutex flag_mutex; std::mutex error_mutex; std::thread thread; double position; double length; bool paused; Uint8* buf; size_t tmpbuf_len; size_t bufsize; SDL_AudioDeviceID device; SDL_AudioSpec spec; struct playback_data { SDL_AudioStream *m_pStream = nullptr; SDL_AudioSpec m_backend_spec = {0}; PlaybackProcess *m_pProcess = nullptr; SoundTouch *m_st = nullptr; bool delete_needed = false; inline bool fading() { return m_fade_time >= 0.0; } double m_fade_time = -1.0; inline bool valid() { return m_st != nullptr && m_pStream != nullptr && m_pProcess != nullptr; } inline playback_data() {} inline playback_data(SDL_AudioSpec backend_spec, SDL_AudioSpec spec, PlaybackProcess *process, double speed = 1.0, double tempo = 1.0, double pitch = 1.0) { m_backend_spec = backend_spec; m_pStream = SDL_NewAudioStream(backend_spec.format, backend_spec.channels, backend_spec.freq, spec.format, spec.channels, spec.freq); if (m_pStream == nullptr) throw CustomException(fmt::format("Could not create stream: {}", SDL_GetError())); m_st = new SoundTouch(); m_st->setChannels(spec.channels); m_st->setSampleRate(spec.freq); m_st->setRate(speed); m_st->setPitch(pitch); m_st->setTempo(tempo); } inline ~playback_data() { if (m_pStream != nullptr) SDL_FreeAudioStream(m_pStream); delete m_pProcess; delete m_st; } }; Playlist playlist; SoundTouch *m_st; std::vector m_playback_data; void CleanPlaybackData(); void SDLCallbackInner(Uint8 *stream, int len); static void SDLCallback(void *userdata, Uint8 *stream, int len); void Load(const char *file, int idx); void Unload(); std::vector streams; std::mutex stream_list_mutex; double real_volume = 1.0; void ThreadFunc(); void UpdateST(); double GetMaxSeconds(); queue errors; std::mutex current_file_mutex; int current_stream; std::optional current_file; std::optional current_title; float prev_pitch, prev_speed, prev_tempo; FILE_TYPE *file; bool initial_render = false; std::mutex output_file_mutex; std::optional output_file; inline void wait(std::function fn) { while (!fn()) { #ifdef NO_THREADS LoopHook(); #endif std::this_thread::sleep_for(20ms); } } public: PlaybackInstance(); ~PlaybackInstance() override; std::optional get_current_file() override; std::optional get_current_title() override; int get_current_stream() override; inline bool is_proxy() override { return false; } std::vector get_streams() override; void play_stream(int idx) override; void Load(std::string filePath) override; double GetPosition() override; double GetLength() override; void Seek(double position) override; void Start(std::string filePath, int streamIdx = 0) override; bool IsPaused() override; void Pause() override; void Stop() override; void Update(); bool IsStopped() override; void SetTempo(float tempo) override; void SetPitch(float pitch) override; void SetSpeed(float speed) override; void SetVolume(float volume) override; float GetTempo() override; float GetPitch() override; float GetSpeed() override; float GetVolume() override; void InitLoopFunction() override; void DeinitLoopFunction() override; void LoopFunction() override; void set_property(std::string path, google::protobuf::Any) override; std::optional get_property(std::string path) override; std::optional reset_property(std::string path) override; std::vector get_property_list() override; void Render(std::string opath) override; float volume; float speed; float tempo; float pitch; float MaxSeconds = 100.0; float MaxSpeed = 4.0; float MaxPitch = 4.0; float MaxTempo = 4.0; float MinSpeed = 0.25; float MinPitch = 0.25; float MinTempo = 0.25; };