From 91b2c5a56d9eea64f89e800503066f913fe9ca3b Mon Sep 17 00:00:00 2001 From: Zachary Hall Date: Wed, 10 Apr 2024 18:00:19 -0700 Subject: [PATCH] Add vgmstream support --- CMakeLists.txt | 6 +- playback.cpp | 202 ++++++++++++++++++++++++++++++++++++++++++++----- playback.h | 9 +++ 3 files changed, 194 insertions(+), 23 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f5d4a0..da020a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,7 +90,7 @@ macro(target_pkgconfig) endforeach() pop_fnstack() endmacro() -set(INC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR}) +set(INC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} subprojects/vgmstream/src subprojects/vgmstream/src/base) option(USE_PORTALS "Enable libportal support if available" ON) set(UI_BACKENDS "") list(POP_FRONT UI_BACKENDS) @@ -105,7 +105,7 @@ endmacro() prefix_all(LIBRARY_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/ backend.cpp options.cpp playback.cpp util.cpp log.cpp dbus.cpp) add_library(liblooper STATIC ${LIBRARY_SOURCES}) target_include_directories(liblooper PUBLIC ${INC}) -target_link_libraries(liblooper PUBLIC SDL2::SDL2 ${SDL_MIXER_X_TARGET} PkgConfig::SoundTouch SDBusCpp::sdbus-c++) +target_link_libraries(liblooper PUBLIC SDL2::SDL2 ${SDL_MIXER_X_TARGET} PkgConfig::SoundTouch SDBusCpp::sdbus-c++ libvgmstream libvgmstream_shared) macro(add_ui_backend) set(ARGS ${ARGV}) list(POP_FRONT ARGS target) @@ -147,7 +147,7 @@ if(${ASCLI_EXE} STREQUAL "ASCLIEXE-NOTFOUND") else() add_test(NAME "verify appstream metadata" COMMAND ${ASCLI_EXE} validate --no-net --pedantic "assets/com.complecwaft.Looper.metainfo.xml") endif() -target_link_libraries(looper PUBLIC liblooper libvgmstream PkgConfig::jsoncpp ${UI_BACKENDS}) +target_link_libraries(looper PUBLIC liblooper PkgConfig::jsoncpp ${UI_BACKENDS}) install(TARGETS looper ${EXTRA_LIBS}) install(FILES assets/icon.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps/) install(FILES assets/com.complecwaft.Looper.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) diff --git a/playback.cpp b/playback.cpp index 47776e0..32d7028 100644 --- a/playback.cpp +++ b/playback.cpp @@ -1,6 +1,12 @@ #include "playback.h" #include "SDL_mixer.h" #include +extern "C" { +#include +#include +#include +} +#include #include #include #include @@ -12,6 +18,55 @@ #include #include "dbus.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; } @@ -23,21 +78,33 @@ void PlaybackInstance::SDLCallbackInner(Uint8 *stream, int len) { 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) { + if (general_mixer == nullptr && stream == 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; + 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 + samples = render_vgmstream((sample_t*)(buf), (int)samples, this->stream); + #else + samples = render_vgmstream((sample_t*)(buf), (int)samples, this->stream); + vgmstream_spec.samples = samples; + SDL_AudioStreamPut(sdl_stream, buf, samples * sizeof(sample_t) * this->stream->channels); + new_bufsize = SDL_AudioStreamGet(sdl_stream, buf, bufsize); + #endif + 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); @@ -90,6 +157,52 @@ Mix_Music *PlaybackInstance::Load(const char *file) { set_signal(PlaybackSignalFileChanged); return output; } +VGMSTREAM *PlaybackInstance::Load2(const char *file) { + 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); + sf->stream_index = 0; + close_streamfile(sf); + if (!output) { + DEBUG.writeln("VGMStream init failed."); + SDL_UnlockAudioDevice(device); + return nullptr; + } + + //vgmstream_set_loop_target(stream, -1); + vgmstream_cfg_t cfg = {0}; + cfg.allow_play_forever = 1; + cfg.play_forever = 1; + cfg.force_loop = 1; + cfg.really_force_loop = 1; + cfg.disable_config_override = 1; + vgmstream_apply_config(output, &cfg); + vgmstream_spec.channels = output->channels; + vgmstream_spec.freq = output->sample_rate; + + length = (double)output->loop_end_sample / (double)output->sample_rate; + 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); + return output; +} void PlaybackInstance::Unload(Mix_Music *music) { Mix_HaltMusicStream(music); Mix_FreeMusic(music); @@ -98,6 +211,14 @@ void PlaybackInstance::Unload(Mix_Music *music) { current_title = {}; current_file_mutex.unlock(); } +void PlaybackInstance::Unload2(VGMSTREAM *stream) { + SDL_LockAudioDevice(device); + close_vgmstream(stream); + SDL_FreeAudioStream(sdl_stream); + stream = nullptr; + sdl_stream = nullptr; + SDL_UnlockAudioDevice(device); +} void PlaybackInstance::UpdateST() { if (speed > 0.0f && speed_changed.exchange(false)) { st->setRate(speed); @@ -162,6 +283,8 @@ void PlaybackInstance::ThreadFunc() { 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) { @@ -175,9 +298,14 @@ void PlaybackInstance::ThreadFunc() { general_mixer = Mix_GetGeneralMixer(); Mix_InitMixer(&fakespec, SDL_FALSE); SDL_PauseAudioDevice(device, 0); - Mix_Music *music = Load(filePath.c_str()); + stream = Load2(filePath.c_str()); + Mix_Music *music = nullptr; + if (stream == nullptr) { + music = Load(filePath.c_str()); + } + reload = false; - if (music) { + if (music || stream) { playback_ready.store(true); } else { playback_ready.store(false); @@ -185,9 +313,17 @@ void PlaybackInstance::ThreadFunc() { set_signal(PlaybackSignalStarted); while (running) { if (file_changed.exchange(false)) { - Unload(music); - music = Load(filePath.c_str()); - if (music) { + if (stream != nullptr) { + Unload2(stream); + } + if (music == nullptr) { + Unload(music); + } + stream = Load2(filePath.c_str()); + if (stream == nullptr) { + music = Load(filePath.c_str()); + } + if (music || stream) { playback_ready.store(true); } else { playback_ready.store(false); @@ -195,21 +331,39 @@ void PlaybackInstance::ThreadFunc() { } if (flag_mutex.try_lock()) { if (seeking.exchange(false)) { - Mix_SetMusicPositionStream(music, position); + 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) { - Mix_PauseMusicStream(music); + if (music != nullptr) { + Mix_PauseMusicStream(music); + } set_signal(PlaybackSignalPaused); } else { - Mix_ResumeMusicStream(music); + if (music != nullptr) { + Mix_ResumeMusicStream(music); + } set_signal(PlaybackSignalResumed); } } if (update.exchange(false)) { - Mix_VolumeMusicStream(music, (int)(volume / 100.0 * MIX_MAX_VOLUME)); 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; @@ -238,7 +392,15 @@ void PlaybackInstance::ThreadFunc() { } flag_mutex.unlock(); } - position = Mix_GetMusicPosition(music); + if (stream != nullptr) { + double maybe_new_position = (double)stream->current_sample / stream->sample_rate; + if (position > maybe_new_position) { + position = maybe_new_position; + } + position += 0.02 * (speed * tempo); + } else if (music != nullptr) { + position = Mix_GetMusicPosition(music); + } std::this_thread::sleep_for(20ms); } playback_ready.store(false); diff --git a/playback.h b/playback.h index b87400c..8e4bea9 100644 --- a/playback.h +++ b/playback.h @@ -1,5 +1,8 @@ #pragma once #include +extern "C" { +#include +} #include #include #include @@ -196,12 +199,18 @@ private: SDL_AudioDeviceID device; SoundTouch *st; SDL_AudioSpec spec; + SDL_AudioStream *sdl_stream; /// @brief A fake SDL_AudioSpec used to trick SDL Mixer X into allocating a bigger buffer. SDL_AudioSpec fakespec; + SDL_AudioSpec vgmstream_spec; void SDLCallbackInner(Uint8 *stream, int len); static void SDLCallback(void *userdata, Uint8 *stream, int len); Mix_Music *Load(const char* file); + VGMSTREAM *Load2(const char *file); void Unload(Mix_Music* music); + void Unload2(VGMSTREAM *stream); + VGMSTREAM *stream; + double real_volume = 1.0; void ThreadFunc(); void UpdateST(); double GetMaxSeconds();