#include "playback.h" #include "SDL_mixer.h" #include #include #include #include #include #ifdef __linux__ #include #endif #include "log.hpp" #include using namespace std::chrono; size_t CalculateBufSize(SDL_AudioSpec *obtained, double seconds, double max_seconds, size_t samples_override = 0) { return ((((samples_override == 0) ? obtained->samples : samples_override) * std::min(seconds, max_seconds)) + 1) * sizeof(SAMPLETYPE) * obtained->channels; } void PlaybackInstance::SDLCallbackInner(Uint8 *stream, int len) { SDL_memset((void*)stream, 0, len); if (!playback_ready.load()) { return; } if (st == nullptr) { return; } size_t i = 0; size_t max = 0; size_t unit = sizeof(SAMPLETYPE) * spec.channels; size_t bytes_per_iter = std::min(((bufsize / unit)) * unit, (size_t)fakespec.size); while (st->numSamples() < (size_t)len) { if (general_mixer == nullptr) { return; } general_mixer(nullptr, buf + i, (int)bytes_per_iter); i += bytes_per_iter; max = i + bytes_per_iter; if (max >= bufsize) { st->putSamples((SAMPLETYPE*)buf, i/unit); i = 0; max = i + bytes_per_iter; } } st->receiveSamples((SAMPLETYPE*)stream, len / unit); } void PlaybackInstance::SDLCallback(void *userdata, Uint8 *stream, int len) { ((PlaybackInstance*)userdata)->SDLCallbackInner(stream, len); } Mix_Music *PlaybackInstance::Load(const char *file) { { std::filesystem::path fpath = std::string(file); fpath = fpath.parent_path() / fpath.stem(); std::string fpath_str = fpath.string(); std::vector soundfonts = {fpath_str + ".sf2", fpath_str + ".dls"}; std::string sf_path_str = ""; bool any_path_exists = false; for (auto sf_path : soundfonts) { if (std::filesystem::exists(sf_path)) { any_path_exists = true; sf_path_str += ";" + sf_path; } } if (any_path_exists) { sf_path_str = sf_path_str.substr(1); Mix_SetSoundFonts(sf_path_str.c_str()); } else { Mix_SetSoundFonts(NULL); } } Mix_Music *output = Mix_LoadMUS(file); if (output == nullptr) { ERROR.writefln("Error loading music '%s': %s", file, Mix_GetError()); error_mutex.lock(); errors.emplace("Error loading music!"); error_mutex.unlock(); return nullptr; } Mix_PlayMusicStream(output, -1); length = Mix_MusicDuration(output); update.store(true); current_file_mutex.lock(); current_file = std::string(file); const char *title_tag = Mix_GetMusicTitleTag(output); // Check for an empty string, which indicates there's no title tag. if (title_tag[0] == '\0') { std::filesystem::path path(current_file.value()); current_title = path.stem().string(); } else { current_title = std::string(title_tag); } current_file_mutex.unlock(); set_signal(PlaybackSignalFileChanged); return output; } void PlaybackInstance::Unload(Mix_Music *music) { Mix_HaltMusicStream(music); Mix_FreeMusic(music); current_file_mutex.lock(); current_file = {}; current_title = {}; current_file_mutex.unlock(); } void PlaybackInstance::UpdateST() { if (speed > 0.0f && speed_changed.exchange(false)) { st->setRate(speed); set_signal(PlaybackSignalSpeedChanged); } if (tempo > 0.0f && tempo_changed.exchange(false)) { st->setTempo(tempo); set_signal(PlaybackSignalTempoChanged); } if (pitch > 0.0f && pitch_changed.exchange(false)) { st->setPitch(pitch); set_signal(PlaybackSignalPitchChanged); } } double PlaybackInstance::GetMaxSeconds() { return std::max((double)(MaxSpeed * MaxTempo), st->getInputOutputSampleRatio()); } void PlaybackInstance::ThreadFunc() { #ifdef __linux__ pthread_setname_np(pthread_self(), "Playback control thread"); #endif bool reload = false; speed_changed.store(true); tempo_changed.store(true); pitch_changed.store(true); while (running) { playback_ready.store(false); if (!SDL_WasInit(SDL_INIT_AUDIO)) { if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { ERROR.writefln("Error initializing SDL: '%s'", SDL_GetError()); error_mutex.lock(); errors.emplace("Failed to initialize SDL!"); error_mutex.unlock(); return; } } SDL_AudioSpec obtained; SDL_AudioSpec desired; desired.format = #ifdef SOUNDTOUCH_INTEGER_SAMPLES AUDIO_S16SYS; #else AUDIO_F32SYS; #endif desired.freq = 48000; desired.samples = 1024; desired.channels = 2; desired.callback = PlaybackInstance::SDLCallback; desired.userdata = this; st = new SoundTouch(); Mix_Init(MIX_INIT_FLAC|MIX_INIT_MID|MIX_INIT_MOD|MIX_INIT_MP3|MIX_INIT_OGG|MIX_INIT_OPUS|MIX_INIT_WAVPACK); if ((device = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, SDL_AUDIO_ALLOW_CHANNELS_CHANGE|SDL_AUDIO_ALLOW_FREQUENCY_CHANGE|SDL_AUDIO_ALLOW_SAMPLES_CHANGE)) == 0) { ERROR.writefln("Error opening audio device: '%s'", SDL_GetError()); error_mutex.lock(); errors.emplace("Failed to open audio device!"); error_mutex.unlock(); running = false; break; } spec = obtained; st->setSampleRate(spec.freq); st->setChannels(spec.channels); UpdateST(); bufsize = 0; fakespec = spec; double maxSeconds = GetMaxSeconds(); fakespec.size *= maxSeconds; fakespec.samples *= maxSeconds; size_t new_bufsize = CalculateBufSize(&spec, GetMaxSeconds(), MaxSeconds); buf = (Uint8*)malloc(new_bufsize); if (buf == nullptr) { ERROR.writeln("Failed to allocate memory for playback!"); error_mutex.lock(); errors.emplace("Failed to allocate memory for playback!"); error_mutex.unlock(); set_signal(PlaybackSignalErrorOccurred); running = false; break; } bufsize = new_bufsize; general_mixer = Mix_GetGeneralMixer(); Mix_InitMixer(&fakespec, SDL_FALSE); SDL_PauseAudioDevice(device, 0); Mix_Music *music = Load(filePath.c_str()); reload = false; if (music) { playback_ready.store(true); } else { playback_ready.store(false); } set_signal(PlaybackSignalStarted); while (running) { if (file_changed.exchange(false)) { Unload(music); music = Load(filePath.c_str()); if (music) { playback_ready.store(true); } else { playback_ready.store(false); } } if (flag_mutex.try_lock()) { if (seeking.exchange(false)) { Mix_SetMusicPositionStream(music, position); set_signal(PlaybackSignalSeeked); } if (pause_changed.exchange(false)) { if (paused) { Mix_PauseMusicStream(music); set_signal(PlaybackSignalPaused); } else { Mix_ResumeMusicStream(music); set_signal(PlaybackSignalResumed); } } if (update.exchange(false)) { Mix_VolumeMusicStream(music, (int)(volume / 100.0 * MIX_MAX_VOLUME)); SDL_LockAudioDevice(device); UpdateST(); size_t correct_buf_size = CalculateBufSize(&spec, GetMaxSeconds(), MaxSeconds); size_t max_buf_size = correct_buf_size * 10; bool too_large = max_buf_size < bufsize; bool too_small = correct_buf_size > bufsize; if (too_large) { ERROR.writes("Bufsize is too large - "); } else if (too_small) { ERROR.writes("Bufsize is too small - "); } if (too_large || too_small) { ERROR.writeln("Resizing buffer..."); general_mixer = nullptr; bufsize = 0; buf = (Uint8*)realloc((void*)buf, correct_buf_size); if (buf == nullptr) { ERROR.writes("Failed to allocate memory for playback!"); error_mutex.lock(); errors.emplace("Failed to allocate memory for playback!"); error_mutex.unlock(); set_signal(PlaybackSignalErrorOccurred); running = false; break; } bufsize = correct_buf_size; } SDL_UnlockAudioDevice(device); } flag_mutex.unlock(); } position = Mix_GetMusicPosition(music); std::this_thread::sleep_for(20ms); } playback_ready.store(false); // ==== Unload(music); SDL_CloseAudioDevice(device); Mix_CloseAudio(); Mix_Quit(); SDL_QuitSubSystem(SDL_INIT_AUDIO); delete st; free(buf); } current_file_mutex.lock(); current_file = {}; current_file_mutex.unlock(); set_signal(PlaybackSignalStopped); } PlaybackInstance::PlaybackInstance() { running = false; paused = true; position = 0; length = 0; volume = 100.0; speed = 1.0; pitch = 1.0; tempo = 1.0; tempo_changed.store(true); speed_changed.store(true); pitch_changed.store(true); current_file = {}; playback_ready = false; bufsize = 0; } std::optional PlaybackInstance::get_current_file() { current_file_mutex.lock(); std::optional output = current_file; current_file_mutex.unlock(); return output; } std::optional PlaybackInstance::get_current_title() { current_file_mutex.lock(); std::optional output = current_title; current_file_mutex.unlock(); return output; } PlaybackInstance::~PlaybackInstance() { Stop(); } void PlaybackInstance::Start(std::string filePath) { this->filePath = filePath; INFO.writefln("Playing %s...", filePath.c_str()); flag_mutex.lock(); this->position = 0.0; seeking.store(true); paused = false; Update(); if (running.exchange(true)) { file_changed.store(true); } else { thread = std::thread(&PlaybackInstance::ThreadFunc, this); } flag_mutex.unlock(); } double PlaybackInstance::GetPosition() { return position; } double PlaybackInstance::GetLength() { return length; } void PlaybackInstance::Seek(double position) { flag_mutex.lock(); this->position = position; seeking.store(true); flag_mutex.unlock(); } void PlaybackInstance::Pause() { flag_mutex.lock(); paused = !paused; pause_changed.store(true); flag_mutex.unlock(); } bool PlaybackInstance::IsPaused() { return paused; } void PlaybackInstance::Stop() { if (running.exchange(false)) { thread.join(); } } void PlaybackInstance::Update() { if (prev_pitch != pitch) { pitch_changed.store(true); } if (prev_speed != speed) { speed_changed.store(true); } if (prev_tempo != tempo) { tempo_changed.store(true); } update.store(true); } bool PlaybackInstance::IsStopped() { return !running; } optional PlaybackInstance::GetError() { if (ErrorExists()) { error_mutex.lock(); std::string error = errors.back(); errors.pop(); error_mutex.unlock(); return error; } else { return {}; } } bool PlaybackInstance::ErrorExists() { error_mutex.lock(); bool output = !errors.empty(); error_mutex.unlock(); return output; } void PlaybackInstance::set_signal(uint16_t signal) { signal_mutex.lock(); signals_occurred |= signal; signal_mutex.unlock(); } uint16_t PlaybackInstance::handle_signals(uint16_t signals) { if (signal_mutex.try_lock()) { uint16_t output = signals_occurred & signals; signals_occurred &= ~output; signal_mutex.unlock(); return output; } else { return PlaybackSignalNone; } } void PlaybackInstance::SetTempo(float tempo) { this->tempo = tempo; Update(); } void PlaybackInstance::SetPitch(float pitch) { this->pitch = pitch; Update(); } void PlaybackInstance::SetSpeed(float speed) { this->speed = speed; Update(); } void PlaybackInstance::SetVolume(float volume) { this->volume = volume; Update(); } float PlaybackInstance::GetTempo() { return tempo; } float PlaybackInstance::GetPitch() { return pitch; } float PlaybackInstance::GetSpeed() { return speed; } float PlaybackInstance::GetVolume() { return volume; }