392 lines
14 KiB
C++
392 lines
14 KiB
C++
#pragma once
|
|
#ifdef __ANDROID__
|
|
#include <oboe/Oboe.h>
|
|
#endif
|
|
#include <thread>
|
|
#include <SDL.h>
|
|
#include <SDL_audio.h>
|
|
#include <string>
|
|
#include <atomic>
|
|
#include <mutex>
|
|
#include <SoundTouch.h>
|
|
#include <span>
|
|
#include <optional>
|
|
#include <vector>
|
|
#include <queue>
|
|
#include <deque>
|
|
#include <map>
|
|
#include "file_backend.hpp"
|
|
#include "playback_backend.hpp"
|
|
#include "config.hpp"
|
|
#include "playback_process.hpp"
|
|
#include <google/protobuf/any.h>
|
|
#include <ipc/internal.pb.h>
|
|
#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<void*, uint16_t> signals_occurred;
|
|
/// @brief The global error state for initializing handle-specific state.
|
|
std::vector<std::string> errors;
|
|
/// @brief The handle-specific error state.
|
|
std::map<void*, std::deque<std::string>> 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<std::string> 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<std::string> get_current_title() {
|
|
return {};
|
|
}
|
|
inline virtual int get_current_stream() {
|
|
return 0;
|
|
}
|
|
inline virtual std::vector<PlaybackStream> get_streams() {
|
|
std::vector<PlaybackStream> 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<std::string> GetError(void *handle) {
|
|
std::optional<std::string> 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<google::protobuf::Any> get_property(std::string path) {
|
|
return {};
|
|
}
|
|
virtual std::optional<google::protobuf::Any> 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<Property> 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<oboe::AudioStream> 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<playback_data> 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<PlaybackStream> streams;
|
|
std::mutex stream_list_mutex;
|
|
double real_volume = 1.0;
|
|
void ThreadFunc();
|
|
void UpdateST();
|
|
double GetMaxSeconds();
|
|
queue<std::string> errors;
|
|
std::mutex current_file_mutex;
|
|
int current_stream;
|
|
std::optional<std::string> current_file;
|
|
std::optional<std::string> current_title;
|
|
float prev_pitch, prev_speed, prev_tempo;
|
|
FILE_TYPE *file;
|
|
bool initial_render = false;
|
|
std::mutex output_file_mutex;
|
|
std::optional<std::string> output_file;
|
|
inline void wait(std::function<bool()> fn) {
|
|
while (!fn()) {
|
|
#ifdef NO_THREADS
|
|
LoopHook();
|
|
#endif
|
|
std::this_thread::sleep_for(20ms);
|
|
}
|
|
}
|
|
public:
|
|
PlaybackInstance();
|
|
~PlaybackInstance() override;
|
|
std::optional<std::string> get_current_file() override;
|
|
std::optional<std::string> get_current_title() override;
|
|
int get_current_stream() override;
|
|
inline bool is_proxy() override {
|
|
return false;
|
|
}
|
|
std::vector<PlaybackStream> 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<google::protobuf::Any> get_property(std::string path) override;
|
|
std::optional<google::protobuf::Any> reset_property(std::string path) override;
|
|
std::vector<Property> 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;
|
|
};
|