diff --git a/README.md b/README.md index 18889a8..fe06fe1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ jukebox -a simple .xm music player +a simple library for loading and playing music files + +supported formats: WAV, MP3, FLAC, XM diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 2171386..040e84b 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -1,13 +1,25 @@ project(jukebox-app) -add_executable(jukebox) +add_library(jukebox-app) +add_library(jukebox-app::lib ALIAS jukebox-app) -target_link_libraries(jukebox - PRIVATE - jukebox::deps +target_link_libraries(jukebox-app + PUBLIC juke::lib ) -target_sources(jukebox PRIVATE - src/main.cpp +# Glob all headers +file(GLOB_RECURSE headers "include/*.hpp") + +# Set as FILE_SET, this also sets the include directory +target_sources(jukebox-app PUBLIC FILE_SET HEADERS + BASE_DIRS include + FILES ${headers} +) + +# Setup our include structure +target_include_directories(jukebox-app + PRIVATE "src" ) + +add_subdirectory(src) diff --git a/library/include/juke/core/Application.hpp b/app/include/app/Application.hpp similarity index 93% rename from library/include/juke/core/Application.hpp rename to app/include/app/Application.hpp index cb4b1fa..d778cfb 100644 --- a/library/include/juke/core/Application.hpp +++ b/app/include/app/Application.hpp @@ -1,9 +1,9 @@ #pragma once +#include #include #include -#include #include #include diff --git a/library/include/juke/graphics/Colors.hpp b/app/include/app/Colors.hpp similarity index 100% rename from library/include/juke/graphics/Colors.hpp rename to app/include/app/Colors.hpp diff --git a/app/include/app/MediaPlayer.hpp b/app/include/app/MediaPlayer.hpp new file mode 100644 index 0000000..0f7ea62 --- /dev/null +++ b/app/include/app/MediaPlayer.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace juke { + +class MediaPlayer { + public: + explicit MediaPlayer(capo::IEngine& audio_engine); + bool load_media(std::filesystem::path const& path); + void handle_input(); + + private: + Jukebox m_jukebox; +}; + +} // namespace juke diff --git a/library/src/core/Application.cpp b/app/src/Application.cpp similarity index 79% rename from library/src/core/Application.cpp rename to app/src/Application.cpp index 47dc924..1105fb3 100644 --- a/library/src/core/Application.cpp +++ b/app/src/Application.cpp @@ -1,5 +1,5 @@ -#include +#include #include namespace juke { @@ -19,19 +19,8 @@ Application::Application() { } void Application::run() { - using Clock = std::chrono::steady_clock; - - auto now = Clock::now(); - while (m_context->next_frame()) { - auto const new_now = Clock::now(); - auto const dt = std::chrono::duration_cast>(new_now - now); - now = new_now; - m_player->handle_input(); - - m_player->update(dt); - m_context->render(); } } diff --git a/app/src/CMakeLists.txt b/app/src/CMakeLists.txt new file mode 100644 index 0000000..04e07f0 --- /dev/null +++ b/app/src/CMakeLists.txt @@ -0,0 +1,15 @@ + +target_sources(jukebox-app PRIVATE + MediaPlayer.cpp + Application.cpp +) + +add_executable(jukebox-player) + +target_sources(jukebox-player PRIVATE + main.cpp +) + +target_link_libraries(jukebox-player PRIVATE + jukebox-app::lib +) diff --git a/app/src/MediaPlayer.cpp b/app/src/MediaPlayer.cpp new file mode 100644 index 0000000..7712368 --- /dev/null +++ b/app/src/MediaPlayer.cpp @@ -0,0 +1,43 @@ + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace juke { + +using namespace std::chrono_literals; + +MediaPlayer::MediaPlayer(capo::IEngine& audio_engine) : m_jukebox(audio_engine) {} + +bool MediaPlayer::load_media(std::filesystem::path const& path) { return m_jukebox.load_media(path); } + +void MediaPlayer::handle_input() { + ImGuiWindowFlags flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove; + if (ImGui::Begin("Player", nullptr, flags)) { + + static auto looping{true}; + /* Control Buttons */ + if (ImGui::Button("play")) { m_jukebox.play(looping); } + ImGui::SameLine(); + if (ImGui::Button("pause")) { m_jukebox.pause(); } + ImGui::SameLine(); + if (ImGui::Button("stop")) { m_jukebox.stop(); } + ImGui::SameLine(); + ImGui::Checkbox("loop", &looping); + ImGui::Separator(); + + /* Metadata & Info */ + ImGui::Text("File Name: "); + ImGui::SameLine(); + m_jukebox.has_file() ? ImGui::TextColored(colors::blue, "%s", m_jukebox.get_filename().c_str()) : ImGui::TextColored(colors::grey, ""); + ImGui::Text("Media Status: %s", m_jukebox.is_playing() ? "playing" : "paused"); + } + ImGui::End(); +} + +} // namespace juke diff --git a/app/src/main.cpp b/app/src/main.cpp index 31f9a59..e180d18 100644 --- a/app/src/main.cpp +++ b/app/src/main.cpp @@ -1,5 +1,5 @@ -#include +#include #include int main() { diff --git a/library/include/juke/core/MediaPlayer.hpp b/library/include/juke/core/MediaPlayer.hpp deleted file mode 100644 index 29b44f1..0000000 --- a/library/include/juke/core/MediaPlayer.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -namespace juke { - -enum class MediaStatus { playing, paused, stopped }; - -class MediaPlayer { - public: - explicit MediaPlayer(capo::IEngine& audio_engine); - bool load_media(std::filesystem::path const& path); - void handle_input(); - void update(std::chrono::duration dt); - - [[nodiscard]] auto playing() const -> bool { return m_status == MediaStatus::playing; } - [[nodiscard]] auto paused() const -> bool { return m_status == MediaStatus::paused; } - [[nodiscard]] auto stopped() const -> bool { return m_status == MediaStatus::stopped; } - - private: - MediaStatus m_status{MediaStatus::stopped}; - [[maybe_unused]] bool m_trigger{}; - - std::unique_ptr m_media_file{}; - std::unique_ptr m_source{}; - - std::string m_status_string{"stopped"}; -}; - -} // namespace juke diff --git a/library/include/juke/juke.hpp b/library/include/juke/juke.hpp new file mode 100644 index 0000000..731e1dd --- /dev/null +++ b/library/include/juke/juke.hpp @@ -0,0 +1,43 @@ + +#pragma once + +#include +#include +#include +#include +#include + +namespace juke { + +class Jukebox { + public: + explicit Jukebox(capo::IEngine& audio_engine); + + /// \brief Load audio file. + /// \param path Path to file. + /// \returns True if file loaded successfully, false otherwise. + bool load_media(std::filesystem::path const& path); + + /// \brief Play audio. + /// \param looping Whether or not the audio loops indefinitely. + void play(bool looping = true); + + /// \brief Pause audio. + void pause(); + + /// \brief Stop audio and set cursor to beginning. This does not work for XM format. + void stop(); + + /// \brief Get the name of the loaded file as a string. + [[nodiscard]] auto get_filename() const& -> std::string { return m_media_file->get_filename(); } + + [[nodiscard]] auto has_file() const -> bool { return static_cast(m_media_file); } + [[nodiscard]] auto is_playing() const -> bool { return m_source->is_playing(); } + [[nodiscard]] auto is_stopped() const -> bool { return !is_playing(); } + + private: + std::unique_ptr m_media_file{}; + std::unique_ptr m_source{}; +}; + +} // namespace juke diff --git a/library/src/CMakeLists.txt b/library/src/CMakeLists.txt index 6b0579e..2ade06a 100644 --- a/library/src/CMakeLists.txt +++ b/library/src/CMakeLists.txt @@ -1,2 +1,6 @@ add_subdirectory(core) + +target_sources(juke PRIVATE + juke.cpp +) diff --git a/library/src/core/CMakeLists.txt b/library/src/core/CMakeLists.txt index 9d7ba19..0dca6b4 100644 --- a/library/src/core/CMakeLists.txt +++ b/library/src/core/CMakeLists.txt @@ -1,8 +1,6 @@ target_sources(juke PRIVATE - Application.cpp AudioFile.cpp - MediaPlayer.cpp XMFile.cpp XMStream.cpp ) diff --git a/library/src/core/MediaPlayer.cpp b/library/src/core/MediaPlayer.cpp deleted file mode 100644 index cd16279..0000000 --- a/library/src/core/MediaPlayer.cpp +++ /dev/null @@ -1,92 +0,0 @@ - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace juke { - -using namespace std::chrono_literals; - -MediaPlayer::MediaPlayer(capo::IEngine& audio_engine) : m_source(audio_engine.create_source()) { - if (!m_source) { throw std::runtime_error{"Failed to create Audio Source"}; } -} - -bool MediaPlayer::load_media(std::filesystem::path const& path) { - try { - auto media_file = std::unique_ptr{}; - if (XMFile::is_xm(path)) { - media_file = std::make_unique(path); - } else { - media_file = std::make_unique(path); - } - m_source->stop(); - if (!media_file->bind_to(*m_source)) { - std::println("ERROR: Failed to bind Source to PCM"); - return false; - } - m_media_file = std::move(media_file); - } catch (MediaError const& e) { // only MediaErrors are caught here, others are raised all the way to main. - std::println("ERROR: {}", e.what()); - return false; - } - - std::println("Media file loaded: {}", path.string()); - - m_source->play(); - m_status = m_source->is_playing() ? MediaStatus::playing : MediaStatus::stopped; - - return true; -} - -void MediaPlayer::handle_input() { - ImGuiWindowFlags flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove; - if (ImGui::Begin("Player", nullptr, flags)) { - - if (auto const duration = m_source->get_duration(); duration > 0s) { - /* Progress Bar */ - auto const fraction = m_source->get_cursor() / duration; - ImGui::ProgressBar(fraction); - } - - /* Control Buttons */ - ImGui::Separator(); - if (ImGui::Button("play")) { - m_source->play(); - m_status = MediaStatus::playing; - } - ImGui::SameLine(); - if (ImGui::Button("pause")) { - m_source->stop(); - m_status = MediaStatus::paused; - } - ImGui::SameLine(); - if (ImGui::Button("stop")) { - m_source->stop(); - m_source->set_cursor({}); // seek to start - m_status = MediaStatus::stopped; - } - ImGui::SameLine(); - auto loop = m_source->is_looping(); - if (ImGui::Checkbox("loop", &loop)) { m_source->set_looping(loop); } - ImGui::Separator(); - ImGui::Text("File Name: "); - ImGui::SameLine(); - m_media_file ? ImGui::TextColored(colors::blue, "%s", m_media_file->get_filename().c_str()) : ImGui::TextColored(colors::grey, ""); - - m_status_string.clear(); - std::format_to(std::back_inserter(m_status_string), "{}", playing() ? "playing" : paused() ? "paused" : "stopped"); - ImGui::Text("Media Status: %s", m_status_string.c_str()); - } - ImGui::End(); -} - -void MediaPlayer::update([[maybe_unused]] std::chrono::duration const dt) { - if (m_status == MediaStatus::playing && !m_source->is_playing()) { m_status = MediaStatus::stopped; } -} - -} // namespace juke diff --git a/library/src/juke.cpp b/library/src/juke.cpp new file mode 100644 index 0000000..7692bf9 --- /dev/null +++ b/library/src/juke.cpp @@ -0,0 +1,51 @@ + +#include +#include +#include +#include +#include +#include + +namespace juke { + +using namespace std::chrono_literals; + +Jukebox::Jukebox(capo::IEngine& audio_engine) : m_source(audio_engine.create_source()) { + if (!m_source) { throw std::runtime_error{"Failed to create Audio Source"}; } +} + +bool Jukebox::load_media(std::filesystem::path const& path) { + try { + auto media_file = std::unique_ptr{}; + if (XMFile::is_xm(path)) { + media_file = std::make_unique(path); + } else { + media_file = std::make_unique(path); + } + m_source->stop(); + if (!media_file->bind_to(*m_source)) { + std::println("ERROR: Failed to bind Source to PCM"); + return false; + } + m_media_file = std::move(media_file); + } catch (MediaError const& e) { // only MediaErrors are caught here, others are raised all the way to main. + std::println("ERROR: {}", e.what()); + return false; + } + + return true; +} + +void Jukebox::play(bool looping) { + m_source->set_looping(looping); + m_source->play(); +} + +void Jukebox::pause() { m_source->stop(); } + +void Jukebox::stop() { + m_source->stop(); + m_source->set_cursor({}); // seek to start. this will do nothing for XM streams +} + +} // namespace juke