#include "playback.h" #include "SDL_mixer.h" #include extern "C" { #include #include #include } #include #include #include #include #include #ifdef __linux__ #include #endif #include "log.hpp" #include #include "dbus.hpp" #include #include "util.hpp" using namespace std::chrono; int NotSDL_ConvertAudioSamples(const SDL_AudioSpec *src_spec, const Uint8 *src_data, int src_len, const SDL_AudioSpec *dst_spec, Uint8 **dst_data, int *dst_len) { if (dst_data) { *dst_data = NULL; } if (dst_len) { *dst_len = 0; } if (!src_data) { return 0; } else if (src_len < 0) { return 0; } else if (!dst_data) { return 0; } else if (!dst_len) { return 0; } int retval = -1; Uint8 *dst = NULL; int dstlen = 0; SDL_AudioStream *stream = SDL_NewAudioStream(src_spec->format, src_spec->channels, src_spec->freq, dst_spec->format, dst_spec->channels, dst_spec->freq); if (stream) { if ((SDL_AudioStreamPut(stream, src_data, src_len) == 0) && (SDL_AudioStreamFlush(stream) == 0)) { dstlen = SDL_AudioStreamAvailable(stream); if (dstlen >= 0) { dst = (Uint8 *)SDL_malloc(dstlen); if (dst) { retval = (SDL_AudioStreamGet(stream, dst, dstlen) >= 0) ? 0 : -1; } } } } if (retval == -1) { SDL_free(dst); } else { *dst_data = dst; *dst_len = dstlen; } SDL_FreeAudioStream(stream); return retval; } 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 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 && stream == nullptr) { return; } Uint8 *new_buf; int new_bufsize; new_buf = buf; new_bufsize = bytes_per_iter; if (this->stream != nullptr) { size_t samples = (int)bytes_per_iter / sizeof(SAMPLETYPE) / this->stream->channels; #ifdef SOUNDTOUCH_INTEGER_SAMPLES size_t new_samples = render_vgmstream((sample_t*)(buf), (int)samples, this->stream); #else size_t new_samples = render_vgmstream((sample_t*)(buf), (int)samples, this->stream); vgmstream_spec.samples = new_samples; SDL_AudioStreamPut(sdl_stream, buf, new_samples * sizeof(sample_t) * this->stream->channels); new_bufsize = SDL_AudioStreamGet(sdl_stream, buf, bufsize); #endif if (samples > new_samples) { reset_vgmstream(this->stream); position = 0.0; } else { position += samples / this->stream->sample_rate; } samples = new_samples; for (int i = 0; i < new_bufsize / sizeof(SAMPLETYPE); i++) { ((SAMPLETYPE*)new_buf)[i] *= real_volume; } st->putSamples((SAMPLETYPE*)new_buf, new_bufsize / unit); } else { general_mixer(nullptr, buf, (int)bytes_per_iter); st->putSamples((SAMPLETYPE*)new_buf, new_bufsize / unit); } } st->receiveSamples((SAMPLETYPE*)stream, len / unit); } #ifdef __ANDROID__ oboe::DataCallbackResult PlaybackInstance::onAudioReady( oboe::AudioStream *audioStream, void *audioData, int32_t numFrames) { SDLCallbackInner((Uint8*)audioData, numFrames * audioStream->getBytesPerFrame()); return oboe::DataCallbackResult::Continue; } #endif void PlaybackInstance::SDLCallback(void *userdata, Uint8 *stream, int len) { ((PlaybackInstance*)userdata)->SDLCallbackInner(stream, len); } Mix_Music *PlaybackInstance::LoadMix(const char *file) { stream_list_mutex.lock(); streams.clear(); stream_list_mutex.unlock(); { 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()); set_error("Error loading music!"); 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(); load_finished.store(true); set_signal(PlaybackSignalFileChanged); return output; } VGMSTREAM *PlaybackInstance::LoadVgm(const char *file, int idx) { SDL_LockAudioDevice(device); STREAMFILE *sf = open_stdio_streamfile(file); if (!sf) { ERROR.writefln("Could not find file '%s'", file); SDL_UnlockAudioDevice(device); return nullptr; } auto *output = init_vgmstream_from_STREAMFILE(sf); if (!output) { DEBUG.writeln("VGMStream init failed."); SDL_UnlockAudioDevice(device); return nullptr; } int stream_count = output->num_streams; close_vgmstream(output); stream_list_mutex.lock(); streams.clear(); //PlaybackStream defaultStream; //defaultStream.id = 0; //defaultStream.name = "Default"; //streams.push_back(defaultStream); for (int i = 0; i <= stream_count; i++) { PlaybackStream stream; stream.id = i; stream.length = 0; stream.name = ""; if (!sf) { streams.push_back(stream); continue; } sf->stream_index = i; auto *tmp = init_vgmstream_from_STREAMFILE(sf); if (!tmp) { streams.push_back(stream); continue; } reset_vgmstream(tmp); stream.length = (double)tmp->num_samples / (double)tmp->sample_rate; char *buf = (char*)malloc(STREAM_NAME_SIZE + 1); memset(buf, 0, STREAM_NAME_SIZE + 1); strncpy(buf, tmp->stream_name, STREAM_NAME_SIZE); if (buf[0] == '\0') { free(buf); buf = strdup("Unknown"); } if (i == 0) { char *buf2 = NULL; #define DEFAULT_FORMAT_STR "Default (%s)" size_t buflen = snprintf(NULL, 0, DEFAULT_FORMAT_STR, buf) + 1; buf2= (char*)malloc(buflen); memset(buf2, 0, buflen); snprintf(buf2, buflen, DEFAULT_FORMAT_STR, buf); stream.name = buf2; free(buf); } else { stream.name = buf; } DEBUG.writefln("Stream %d: '%s' (Length: %s)", stream.id, stream.name.c_str(), TimeToString(stream.length).c_str()); streams.push_back(stream); free(buf); close_vgmstream(tmp); } sf->stream_index = idx; output = init_vgmstream_from_STREAMFILE(sf); close_streamfile(sf); if (!output) { DEBUG.writeln("VGMStream init failed."); SDL_UnlockAudioDevice(device); return nullptr; } stream_list_mutex.unlock(); //vgmstream_set_loop_target(stream, -1); vgmstream_cfg_t cfg = {0}; cfg.allow_play_forever = 1; cfg.play_forever = 1; //cfg.disable_config_override = 1; vgmstream_apply_config(output, &cfg); vgmstream_spec.channels = output->channels; vgmstream_spec.freq = output->sample_rate; length = streams[idx].length; update.store(true); current_file_mutex.lock(); current_file = std::string(file); const char *title_tag = output->stream_name; // 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); } sdl_stream = SDL_NewAudioStream(vgmstream_spec.format, vgmstream_spec.channels, vgmstream_spec.freq, spec.format, spec.channels, spec.freq); current_file_mutex.unlock(); set_signal(PlaybackSignalFileChanged); SDL_UnlockAudioDevice(device); load_finished.store(true); return output; } void PlaybackInstance::UnloadMix(Mix_Music *music) { stream_list_mutex.lock(); streams.clear(); stream_list_mutex.unlock(); Mix_HaltMusicStream(music); Mix_FreeMusic(music); current_file_mutex.lock(); current_file = {}; current_title = {}; current_file_mutex.unlock(); } void PlaybackInstance::UnloadVgm(VGMSTREAM *stream) { stream_list_mutex.lock(); streams.clear(); stream_list_mutex.unlock(); SDL_LockAudioDevice(device); close_vgmstream(stream); SDL_FreeAudioStream(sdl_stream); this->stream = nullptr; sdl_stream = nullptr; SDL_UnlockAudioDevice(device); 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::InitLoopFunction() { bool reload = false; speed_changed.store(true); tempo_changed.store(true); pitch_changed.store(true); 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()); set_error("Failed to initialize SDL!"); 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); #ifndef __ANDROID__ 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()); set_error("Failed to open audio device!"); running = false; loop_started = false; return; } #else oboe::AudioStreamBuilder builder; builder.setDirection(oboe::Direction::Output); builder.setSharingMode(oboe::SharingMode::Shared); builder.setPerformanceMode(oboe::PerformanceMode::None); builder.setFormat( #ifdef SOUNDTOUCH_INTEGER_SAMPLES oboe::AudioFormat::I16 #else oboe::AudioFormat::Float #endif ); builder.setDataCallback(this); auto res = builder.openStream(ostream); if (res != oboe::Result::OK) { ERROR.writefln("Error opening audio device."); set_error("Failed to open audio device!"); running = false; loop_started = false; return; } obtained = desired; obtained.channels = ostream->getChannelCount(); obtained.freq = ostream->getSampleRate(); auto bufferFrames = ostream->getBufferSizeInFrames(); auto bytesPerFrame = ostream->getBytesPerFrame(); auto bytesPerSample = ostream->getBytesPerSample(); obtained.size = bufferFrames * bytesPerFrame; obtained.samples = obtained.size / bytesPerSample; oboe::AudioFormat format = ostream->getFormat(); DEBUG.writefln("About to start audio stream.\n\ Format: %s\n\ Channel count: %u\n\ Sample rate: %u\n\ Buffer size (frames): %u\n\ Bytes per frame: %u\n\ Total bytes: %u\n\ Bytes per sample: %u\n\ Total samples: %u" , oboe::convertToText(format) , obtained.channels , obtained.freq , bufferFrames , bytesPerFrame , obtained.size , bytesPerSample , obtained.samples ); ostream->requestStart(); #endif spec = obtained; st->setSampleRate(spec.freq); st->setChannels(spec.channels); UpdateST(); bufsize = 0; fakespec = spec; double maxSeconds = GetMaxSeconds(); fakespec.size *= maxSeconds; fakespec.samples *= maxSeconds; vgmstream_spec = fakespec; vgmstream_spec.format = AUDIO_S16; size_t new_bufsize = CalculateBufSize(&spec, GetMaxSeconds(), MaxSeconds); buf = (Uint8*)malloc(new_bufsize); if (buf == nullptr) { ERROR.writeln("Failed to allocate memory for playback!"); set_error("Failed to allocate memory for playback!"); set_signal(PlaybackSignalErrorOccurred); running = false; loop_started = false; return; } bufsize = new_bufsize; general_mixer = Mix_GetGeneralMixer(); Mix_InitMixer(&fakespec, SDL_FALSE); SDL_PauseAudioDevice(device, 0); music = LoadMix(filePath.c_str()); stream = nullptr; if (music == nullptr) { stream = LoadVgm(filePath.c_str(), 0); } reload = false; if (music || stream) { playback_ready.store(true); } else { playback_ready.store(false); } load_finished.store(true); set_signal(PlaybackSignalStarted); } void PlaybackInstance::LoopFunction() { if (file_changed.exchange(false) || load_requested.exchange(false)) { if (stream != nullptr) { UnloadVgm(stream); } if (music != nullptr) { UnloadMix(music); } music = LoadMix(filePath.c_str()); stream = nullptr; if (music == nullptr) { stream = LoadVgm(filePath.c_str(), 0); } if (music || stream) { playback_ready.store(true); } else { playback_ready.store(false); } } if (stream_changed.exchange(false)) { current_file_mutex.lock(); if (current_file.has_value()) { std::string file = current_file.value(); if (current_stream >= streams.size() || current_stream < 0 || streams[current_stream].name == "" || streams[current_stream].length <= 0) { if (stream != nullptr) { current_stream = stream->stream_index; } else { current_stream = 0; } } else { if (stream != nullptr) { UnloadVgm(stream); stream = LoadVgm(file.c_str(), current_stream); } else if (music != nullptr) { UnloadMix(music); music = LoadMix(file.c_str()); } } if (music || stream) { playback_ready.store(true); } else { playback_ready.store(false); } } current_file_mutex.unlock(); } if (flag_mutex.try_lock()) { if (seeking.exchange(false)) { if (stream != nullptr) { SDL_LockAudioDevice(device); seek_vgmstream(stream, (int32_t)((double)stream->sample_rate * position)); st->flush(); SDL_UnlockAudioDevice(device); } else { Mix_SetMusicPositionStream(music, position); } set_signal(PlaybackSignalSeeked); } if (pause_changed.exchange(false)) { if (stream != nullptr) { SDL_PauseAudioDevice(device, paused ? 1 : 0); } if (paused) { if (music != nullptr) { Mix_PauseMusicStream(music); } set_signal(PlaybackSignalPaused); } else { if (music != nullptr) { Mix_ResumeMusicStream(music); } set_signal(PlaybackSignalResumed); } } if (update.exchange(false)) { SDL_LockAudioDevice(device); real_volume = volume / 100.0; if (stream == nullptr) { Mix_VolumeMusicStream(music, (int)(volume / 100.0 * MIX_MAX_VOLUME)); } 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!"); set_error("Failed to allocate memory for playback!"); set_signal(PlaybackSignalErrorOccurred); running = false; stop_loop(); return; } bufsize = correct_buf_size; } SDL_UnlockAudioDevice(device); } flag_mutex.unlock(); } if (music != nullptr) { position = Mix_GetMusicPosition(music); } } void PlaybackInstance::DeinitLoopFunction() { playback_ready.store(false); // ==== if (music != nullptr) { UnloadMix(music); } if (stream != nullptr) { UnloadVgm(stream); } #ifndef __ANDROID__ SDL_CloseAudioDevice(device); #else if (ostream && ostream->getState() != oboe::StreamState::Closed) { ostream->stop(); ostream->close(); } ostream.reset(); #endif 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); } void PlaybackInstance::ThreadFunc() { #ifdef __linux__ pthread_setname_np(pthread_self(), "Playback control thread"); #endif start_loop(); while (running && loop_started) { LoopHook(); std::this_thread::sleep_for(20ms); } stop_loop(); } 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::Load(std::string filePath) { this->filePath = filePath; INFO.writefln("Loading %s...", filePath.c_str()); if (running.exchange(true)) { load_requested.store(true); } else { #if defined(__EMSCRIPTEN__)||defined(__ANDROID__) start_loop(); #else thread = std::thread(&PlaybackInstance::ThreadFunc, this); loop_started = true; #endif } flag_mutex.lock(); this->position = 0.0; seeking.store(true); paused = true; Update(); flag_mutex.unlock(); } void PlaybackInstance::Start(std::string filePath, int streamIdx) { Load(filePath); while (loop_started && !load_finished.exchange(false)) { #if defined(__EMSCRIPTEN__)||defined(__ANDROID__) LoopHook(); #endif std::this_thread::sleep_for(20ms); } if (!loop_started) { return; } INFO.writefln("Playing %s...", filePath.c_str()); flag_mutex.lock(); this->position = 0.0; seeking.store(true); paused = false; current_stream = streamIdx; stream_changed.store(true); Update(); flag_mutex.unlock(); } void PlaybackInstance::play_stream(int idx) { flag_mutex.lock(); this->position = 0.0; seeking.store(true); paused = false; current_stream = idx; stream_changed.store(true); Update(); 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)) { #if defined(__EMSCRIPTEN__)||defined(__ANDROID__) stop_loop(); #else thread.join(); #endif } } 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; } void Playback::set_signal(uint16_t signal) { signal_mutex.lock(); for (auto &kv : signals_occurred) { kv.second |= signal; } signal_mutex.unlock(); } uint16_t Playback::handle_signals(uint16_t signals, void *handle) { if (signal_mutex.try_lock()) { if (!signals_occurred.contains(handle)) { signals_occurred[handle] = PlaybackSignalNone; } uint16_t output = signals_occurred[handle]; signals_occurred[handle] &= ~output; signal_mutex.unlock(); return output; } else { return PlaybackSignalNone; } } void Playback::register_handle(void *handle) { signal_mutex.lock(); if (!signals_occurred.contains(handle)) { signals_occurred[handle] = PlaybackSignalNone; } signal_mutex.unlock(); error_mutex.lock(); if (!errors_occurred.contains(handle)) { std::deque new_value; for (size_t i = 0; i < errors.size(); i++) { new_value.push_back(errors[i]); } errors_occurred[handle] = new_value; } error_mutex.unlock(); } void Playback::unregister_handle(void *handle) { signal_mutex.lock(); if (signals_occurred.contains(handle)) { signals_occurred.erase(handle); } signal_mutex.unlock(); error_mutex.lock(); if (errors_occurred.contains(handle)) { errors_occurred.erase(handle); } error_mutex.unlock(); } 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; } void Playback::set_error(std::string desc) { error_mutex.lock(); errors.push_back(desc); for (auto &kv : errors_occurred) { kv.second.push_front(desc); } error_mutex.unlock(); set_signal(PlaybackSignalErrorOccurred); } Playback *Playback::Create(bool *daemon_found, bool daemon) { #ifdef DBUS_ENABLED auto *dbus_proxy = DBusAPISender::Create(); if (dbus_proxy != nullptr) { if (daemon_found != nullptr) { *daemon_found = dbus_proxy->IsDaemon(); } DEBUG.writefln("DBus proxy daemon found: %s", *daemon_found ? "true" : "false"); if (daemon) { delete dbus_proxy; return nullptr; } else { return dbus_proxy; } } if (daemon_found != nullptr) { *daemon_found = false; } #endif DEBUG.writeln("Creating new playback instance."); return new PlaybackInstance(); } std::vector PlaybackInstance::get_streams() { std::vector output; for (auto stream : streams) { output.push_back(stream); } return output; } int PlaybackInstance::get_current_stream() { return current_stream; }