From b910d16261e99f4ce3b2238da23055f539f53506 Mon Sep 17 00:00:00 2001 From: lisyarus Date: Fri, 14 Jul 2023 20:55:18 +0300 Subject: [PATCH] Grand app refactor: move main to sdl2 lib, make the rest of the engine independent of SDL2 --- libs/app/CMakeLists.txt | 2 +- libs/app/include/psemek/app/app.hpp | 67 - libs/app/include/psemek/app/application.hpp | 47 + .../include/psemek/app/application_base.hpp | 31 + .../app/default_application_factory.hpp | 36 + libs/app/include/psemek/app/event_handler.hpp | 20 + libs/app/include/psemek/app/event_state.hpp | 56 + libs/app/include/psemek/app/events.hpp | 52 + libs/app/include/psemek/app/main.hpp | 47 - libs/app/include/psemek/app/scene.hpp | 84 +- .../include/psemek/app/scene_application.hpp | 29 + libs/app/include/psemek/app/scene_manager.hpp | 19 - libs/app/source/app.cpp | 326 ---- libs/app/source/application_base.cpp | 46 + libs/app/source/scene_application.cpp | 56 + libs/audio/CMakeLists.txt | 2 +- .../psemek/audio/audio_file/AudioFile.h | 1592 +++++++++++++++++ libs/audio/include/psemek/audio/engine.hpp | 20 +- libs/audio/source/engine.cpp | 97 - libs/audio/source/track_wav.cpp | 59 +- libs/gfx/include/psemek/gfx/init.hpp | 8 + libs/gfx/source/init.cpp | 22 + libs/sdl2/CMakeLists.txt | 2 +- libs/sdl2/include/psemek/sdl2/events.hpp | 11 + libs/sdl2/include/psemek/sdl2/window.hpp | 28 + libs/sdl2/source/audio_engine.cpp | 111 ++ libs/sdl2/source/events.cpp | 85 + libs/sdl2/source/main.cpp | 68 + libs/sdl2/source/window.cpp | 96 + 29 files changed, 2435 insertions(+), 684 deletions(-) delete mode 100644 libs/app/include/psemek/app/app.hpp create mode 100644 libs/app/include/psemek/app/application.hpp create mode 100644 libs/app/include/psemek/app/application_base.hpp create mode 100644 libs/app/include/psemek/app/default_application_factory.hpp create mode 100644 libs/app/include/psemek/app/event_handler.hpp create mode 100644 libs/app/include/psemek/app/event_state.hpp create mode 100644 libs/app/include/psemek/app/events.hpp delete mode 100644 libs/app/include/psemek/app/main.hpp create mode 100644 libs/app/include/psemek/app/scene_application.hpp delete mode 100644 libs/app/include/psemek/app/scene_manager.hpp delete mode 100644 libs/app/source/app.cpp create mode 100644 libs/app/source/application_base.cpp create mode 100644 libs/app/source/scene_application.cpp create mode 100644 libs/audio/include/psemek/audio/audio_file/AudioFile.h delete mode 100644 libs/audio/source/engine.cpp create mode 100644 libs/gfx/include/psemek/gfx/init.hpp create mode 100644 libs/gfx/source/init.cpp create mode 100644 libs/sdl2/include/psemek/sdl2/events.hpp create mode 100644 libs/sdl2/include/psemek/sdl2/window.hpp create mode 100644 libs/sdl2/source/audio_engine.cpp create mode 100644 libs/sdl2/source/events.cpp create mode 100644 libs/sdl2/source/main.cpp create mode 100644 libs/sdl2/source/window.cpp diff --git a/libs/app/CMakeLists.txt b/libs/app/CMakeLists.txt index 2dde540a..d1c1b35a 100644 --- a/libs/app/CMakeLists.txt +++ b/libs/app/CMakeLists.txt @@ -8,4 +8,4 @@ endif() psemek_add_library(psemek-app ${PSEMEK_APP_HEADERS} ${PSEMEK_APP_SOURCES}) target_include_directories(psemek-app PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") -target_link_libraries(psemek-app PUBLIC psemek-log psemek-util psemek-gfx psemek-ui psemek-sdl2) +target_link_libraries(psemek-app PUBLIC psemek-log psemek-util psemek-gfx psemek-ui) diff --git a/libs/app/include/psemek/app/app.hpp b/libs/app/include/psemek/app/app.hpp deleted file mode 100644 index 66e6bbeb..00000000 --- a/libs/app/include/psemek/app/app.hpp +++ /dev/null @@ -1,67 +0,0 @@ -#pragma once - -#include -#include - -#include - -#include - -#include - -#include - -namespace psemek::app -{ - - static const geom::vector common_resolutions[] = - { - {1024, 768}, - {1280, 720}, - {1280, 1024}, - {1366, 768}, - {1440, 900}, - {1536, 864}, - {1600, 900}, - {1920, 1080}, - }; - - struct app - : scene_base, scene_manager - { - struct options - { - int multisampling = 0; - std::optional> fixed_resolution = std::nullopt; - bool highdpi = false; - }; - - app(std::string const & name); - app(std::string const & name, int multisampling); - app(std::string const & name, options const & opts); - ~app() override; - - virtual bool running() const; - virtual void stop(); - - void on_resize(int width, int height) override; - - void present() override; - - void poll_events(); - void run(); - - void push_scene(std::shared_ptr s) override; - std::shared_ptr pop_scene() override; - - void show_cursor(bool show); - bool vsync() const; - void vsync(bool on); - - float time() const; - - private: - psemek_declare_pimpl - }; - -} diff --git a/libs/app/include/psemek/app/application.hpp b/libs/app/include/psemek/app/application.hpp new file mode 100644 index 00000000..8646c637 --- /dev/null +++ b/libs/app/include/psemek/app/application.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include + +#include +#include + +namespace psemek::app +{ + + struct application + : event_handler + { + // Data sent to platform backend for initialization + struct options + { + std::string name; + int multisampling = 4; + bool highdpi = false; + }; + + // Data received from platform backend after initialization + struct context + { + std::function show_cursor; + std::function vsync; + }; + + struct factory + { + virtual application::options const & options() = 0; + virtual std::unique_ptr create(struct application::options const & options, context const & context) = 0; + + virtual ~factory() {} + }; + + virtual bool running() const = 0; + virtual void stop() = 0; + + virtual void update() = 0; + virtual void present() = 0; + }; + + // Implemented by the user, called by platform backends + std::unique_ptr make_application_factory(); + +} diff --git a/libs/app/include/psemek/app/application_base.hpp b/libs/app/include/psemek/app/application_base.hpp new file mode 100644 index 00000000..2ca21143 --- /dev/null +++ b/libs/app/include/psemek/app/application_base.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +#include + +namespace psemek::app +{ + + struct application_base + : application + { + void on_event(resize_event const &) override; + void on_event(focus_event const &) override; + void on_event(mouse_move_event const &) override; + void on_event(mouse_wheel_event const &) override; + void on_event(mouse_button_event const &) override; + void on_event(key_event const &) override; + + void stop() override; + bool running() const override; + + event_state const & state() const { return state_; } + + private: + bool running_ = true; + event_state state_; + }; + +} diff --git a/libs/app/include/psemek/app/default_application_factory.hpp b/libs/app/include/psemek/app/default_application_factory.hpp new file mode 100644 index 00000000..69df0622 --- /dev/null +++ b/libs/app/include/psemek/app/default_application_factory.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include + +namespace psemek::app +{ + + template + std::unique_ptr default_application_factory(application::options const & options) + { + struct factory_impl + : application::factory + { + application::options opts; + + factory_impl(application::options const & options) + : opts(options) + {} + + application::options const & options() override + { + return opts; + } + + std::unique_ptr create(application::options const & options, application::context const & context) override + { + return std::make_unique(options, context); + } + }; + + return std::make_unique(options); + } + +} diff --git a/libs/app/include/psemek/app/event_handler.hpp b/libs/app/include/psemek/app/event_handler.hpp new file mode 100644 index 00000000..33137872 --- /dev/null +++ b/libs/app/include/psemek/app/event_handler.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace psemek::app +{ + + struct event_handler + { + virtual void on_event(resize_event const &) {} + virtual void on_event(focus_event const &) {} + virtual void on_event(mouse_move_event const &) {} + virtual void on_event(mouse_wheel_event const &) {} + virtual void on_event(mouse_button_event const &) {} + virtual void on_event(key_event const &) {} + + virtual ~event_handler() {} + }; + +} diff --git a/libs/app/include/psemek/app/event_state.hpp b/libs/app/include/psemek/app/event_state.hpp new file mode 100644 index 00000000..aa0277ac --- /dev/null +++ b/libs/app/include/psemek/app/event_state.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include + +#include + +namespace psemek::app +{ + + struct event_state + { + geom::vector size = {0, 0}; + bool focus = true; + geom::point mouse = {0, 0}; + int wheel = 0; + std::unordered_set mouse_button_down; + std::unordered_set key_down; + }; + + inline void apply(event_state & state, resize_event const & event) + { + state.size = event.size; + } + + inline void apply(event_state & state, focus_event const & event) + { + state.focus = event.gained; + } + + inline void apply(event_state & state, mouse_move_event const & event) + { + state.mouse = event.position; + } + + inline void apply(event_state & state, mouse_wheel_event const & event) + { + state.wheel += event.delta; + } + + inline void apply(event_state & state, mouse_button_event const & event) + { + if (event.down) + state.mouse_button_down.insert(event.button); + else + state.mouse_button_down.erase(event.button); + } + + inline void apply(event_state & state, key_event const & event) + { + if (event.down) + state.key_down.insert(event.key); + else + state.key_down.erase(event.key); + } + +} diff --git a/libs/app/include/psemek/app/events.hpp b/libs/app/include/psemek/app/events.hpp new file mode 100644 index 00000000..4c94d514 --- /dev/null +++ b/libs/app/include/psemek/app/events.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include + +namespace psemek::app +{ + + struct resize_event + { + geom::vector size; + }; + + struct focus_event + { + bool gained; + }; + + struct mouse_move_event + { + geom::point position; + }; + + struct mouse_wheel_event + { + int delta; + }; + + enum class mouse_button + { + left, + middle, + right, + }; + + struct mouse_button_event + { + mouse_button button; + bool down; + }; + + enum class keycode + { + // TODO + }; + + struct key_event + { + keycode key; + bool down; + }; + +} diff --git a/libs/app/include/psemek/app/main.hpp b/libs/app/include/psemek/app/main.hpp deleted file mode 100644 index 57dc8a73..00000000 --- a/libs/app/include/psemek/app/main.hpp +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include -#include -#include - -#include - -namespace psemek::app -{ - - template - int main(Args && ... args) try - { - util::clock clock; - -#ifdef PSEMEK_PACKAGE_MODE - log::level stdio_log_level = log::level::info; -#else - log::level stdio_log_level = log::level::debug; -#endif - - log::add_sink(log::default_sink(io::std_out(), stdio_log_level)); - log::register_thread("main"); - - App app(std::forward(args)...); - - log::info() << "Started in " << util::pretty(clock.duration(), std::chrono::milliseconds{1}); - - log::info() << "Running"; - app.run(); - log::info() << "Quitting"; - - return EXIT_SUCCESS; - } - catch (std::exception const & e) - { - log::error() << e.what(); - return EXIT_FAILURE; - } - catch (...) - { - log::error() << "Unknown exception"; - return EXIT_FAILURE; - } - -} diff --git a/libs/app/include/psemek/app/scene.hpp b/libs/app/include/psemek/app/scene.hpp index 94fa968f..45e31d4c 100644 --- a/libs/app/include/psemek/app/scene.hpp +++ b/libs/app/include/psemek/app/scene.hpp @@ -1,93 +1,19 @@ #pragma once -#include - -#include - -#include -#include +#include +#include namespace psemek::app { - struct app; - struct scene + : event_handler { - virtual ~scene() = 0; - - virtual void on_scene_enter(app * /* parent */) {} - virtual void on_scene_exit() {} - - virtual void on_resize(int /* width */, int /* height */) {} - virtual void on_focus_gained() {} - virtual void on_focus_lost() {} - - virtual void on_mouse_move(int /* x */, int /* y */, int /* dx */, int /* dy */) {} - virtual void on_mouse_wheel(int /* delta */) {} - virtual void on_left_button_down() {} - virtual void on_left_button_up() {} - virtual void on_middle_button_down() {} - virtual void on_middle_button_up() {} - virtual void on_right_button_down() {} - virtual void on_right_button_up() {} - - virtual void on_key_down(SDL_Keycode /* key */) {} - virtual void on_key_up(SDL_Keycode /* key */) {} - - virtual void on_text_input(std::string_view /* text */) {} + virtual void on_enter(event_state const &) {} + virtual void on_exit() {} virtual void update() {} virtual void present() {} }; - inline scene::~scene() = default; - - struct scene_base - : scene - { - void on_scene_enter(app * parent) override { parent_ = parent; } - void on_scene_exit() override { parent_ = nullptr; } - - void on_resize(int width, int height) override { width_ = width; height_ = height; } - - void on_mouse_move(int x, int y, int, int) override { mouse_ = geom::point{x, y}; } - void on_left_button_down() override { left_button_down_ = true; } - void on_left_button_up() override { left_button_down_ = false; } - void on_middle_button_down() override { middle_button_down_ = true; } - void on_middle_button_up() override { middle_button_down_ = false; } - void on_right_button_down() override { right_button_down_ = true; } - void on_right_button_up() override { right_button_down_ = false; } - void on_key_down(SDL_Keycode key) override { keys_.insert(key); } - void on_key_up(SDL_Keycode key) override { keys_.erase(key); } - - bool active() const { return parent_ != nullptr; } - app * parent() const { return parent_; } - - bool is_left_button_down() const { return left_button_down_; } - bool is_middle_button_down() const { return middle_button_down_; } - bool is_right_button_down() const { return right_button_down_; } - - std::optional> mouse() const { return mouse_; } - - bool is_key_down(SDL_Keycode key) const { return keys_.count(key) > 0; } - - int width() const { return width_; } - int height() const { return height_; } - - private: - app * parent_ = nullptr; - - int width_ = 0; - int height_ = 0; - - bool left_button_down_ = false; - bool middle_button_down_ = false; - bool right_button_down_ = false; - - std::optional> mouse_; - - std::set keys_; - }; - } diff --git a/libs/app/include/psemek/app/scene_application.hpp b/libs/app/include/psemek/app/scene_application.hpp new file mode 100644 index 00000000..b8c6a8ae --- /dev/null +++ b/libs/app/include/psemek/app/scene_application.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +namespace psemek::app +{ + + struct scene_application + : application_base + { + void on_event(resize_event const &) override; + void on_event(focus_event const &) override; + void on_event(mouse_move_event const &) override; + void on_event(mouse_wheel_event const &) override; + void on_event(mouse_button_event const &) override; + void on_event(key_event const &) override; + + void update() override; + void present() override; + + virtual std::shared_ptr current_scene() = 0; + + private: + template + void on_event_impl(Event const & event); + }; + +} diff --git a/libs/app/include/psemek/app/scene_manager.hpp b/libs/app/include/psemek/app/scene_manager.hpp deleted file mode 100644 index 24b1856f..00000000 --- a/libs/app/include/psemek/app/scene_manager.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include - -#include - -namespace psemek::app -{ - - struct scene_manager - { - virtual void push_scene(std::shared_ptr) = 0; - virtual std::shared_ptr pop_scene() = 0; - - virtual ~scene_manager() {} - }; - - -} diff --git a/libs/app/source/app.cpp b/libs/app/source/app.cpp deleted file mode 100644 index b7baa6ac..00000000 --- a/libs/app/source/app.cpp +++ /dev/null @@ -1,326 +0,0 @@ -#include -#include -#include -#include - -#include - -#include - -namespace psemek::app -{ - - using clock = std::chrono::high_resolution_clock; - - struct app::impl - { - app * parent; - - std::shared_ptr sdl_init; - SDL_Window * window = nullptr; - SDL_GLContext gl_context = nullptr; - - std::vector> scene_stack; - - bool running = false; - - bool had_initial_resize = false; - bool had_scene_exit = false; - - clock::time_point start_time; - - impl(app * parent) - : parent(parent) - , sdl_init(sdl2::init(SDL_INIT_VIDEO)) - {} - - ~impl() - { - if (gl_context) SDL_GL_DeleteContext(gl_context); - if (window) SDL_DestroyWindow(window); - } - - std::shared_ptr get_scene() - { - if (!scene_stack.empty()) - return scene_stack.back(); - return std::shared_ptr(parent, [](scene *){}); - } - }; - - app::app(std::string const & name) - : app(name, 0) - {} - - app::app(std::string const & name, int multisampling) - : app(name, options{multisampling}) - {} - - app::app(std::string const & name, options const & opts) - : pimpl_{make_impl(this)} - { - impl().start_time = clock::now(); - - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, gl::sys::major_version()); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, gl::sys::minor_version()); - SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); - SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); - SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); - SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8); - SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); - SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); - - if (opts.multisampling == 0) - { - SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 0); - } - else - { - SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1); - SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, opts.multisampling); - } - - std::uint32_t flags = SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN; - if (!opts.fixed_resolution) - flags |= SDL_WINDOW_RESIZABLE | SDL_WINDOW_MAXIMIZED; - if (opts.highdpi) flags |= SDL_WINDOW_ALLOW_HIGHDPI; - - int width = opts.fixed_resolution ? (*opts.fixed_resolution)[0] : 1024; - int height = opts.fixed_resolution ? (*opts.fixed_resolution)[1] : 768; - - impl().window = SDL_CreateWindow(name.data(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, width, height, flags); - if (!impl().window) - sdl2::fail("Failed to create window: "); - - impl().gl_context = SDL_GL_CreateContext(impl().window); - if (!impl().gl_context) - sdl2::fail("Failed to create OpenGL context: "); - - SDL_GL_MakeCurrent(impl().window, impl().gl_context); - - if (!gl::sys::initialize()) - throw std::runtime_error("Failed to load OpenGL functions"); - - auto vendor = gl::GetString(gl::VENDOR); - auto renderer = gl::GetString(gl::RENDERER); - - int major, minor; - gl::GetIntegerv(gl::MAJOR_VERSION, &major); - gl::GetIntegerv(gl::MINOR_VERSION, &minor); - log::info() << "Initialized OpenGL " << major << '.' << minor << ", " << vendor << ", " << renderer; - - SDL_GL_GetDrawableSize(impl().window, &width, &height); - scene_base::on_resize(width, height); - - log::info() << "Initial window size: " << width << 'x' << height; - - SDL_StopTextInput(); - } - - app::~app() - {} - - bool app::running() const - { - return impl().running; - } - - void app::stop() - { - impl().running = false; - } - - void app::on_resize(int width, int height) - { - scene_base::on_resize(width, height); - gl::Viewport(0, 0, width, height); - } - - void app::poll_events() - { - auto handler = [this]{ return impl().get_scene(); }; - - for (SDL_Event e; SDL_PollEvent(&e);) switch (e.type) - { - case SDL_QUIT: - stop(); - break; - case SDL_WINDOWEVENT: switch (e.window.event) - { - case SDL_WINDOWEVENT_RESIZED: - impl().had_initial_resize = true; - { - int width, height; - SDL_GL_GetDrawableSize(impl().window, &width, &height); - handler()->on_resize(width, height); - log::info() << "Window resized to " << width << 'x' << height; - } - break; - case SDL_WINDOWEVENT_FOCUS_GAINED: - handler()->on_focus_gained(); - break; - case SDL_WINDOWEVENT_FOCUS_LOST: - handler()->on_focus_lost(); - break; - } - break; - case SDL_MOUSEMOTION: - handler()->on_mouse_move(e.motion.x, e.motion.y, e.motion.xrel, e.motion.yrel); - break; - case SDL_MOUSEWHEEL: - handler()->on_mouse_wheel(e.wheel.y); - break; - case SDL_MOUSEBUTTONDOWN: - switch (e.button.button) - { - case SDL_BUTTON_LEFT: - handler()->on_left_button_down(); - break; - case SDL_BUTTON_MIDDLE: - handler()->on_middle_button_down(); - break; - case SDL_BUTTON_RIGHT: - handler()->on_right_button_down(); - break; - } - break; - case SDL_MOUSEBUTTONUP: - switch (e.button.button) - { - case SDL_BUTTON_LEFT: - handler()->on_left_button_up(); - break; - case SDL_BUTTON_MIDDLE: - handler()->on_middle_button_up(); - break; - case SDL_BUTTON_RIGHT: - handler()->on_right_button_up(); - break; - } - break; - case SDL_KEYDOWN: - handler()->on_key_down(e.key.keysym.sym); - break; - case SDL_KEYUP: - handler()->on_key_up(e.key.keysym.sym); - break; - case SDL_TEXTINPUT: - handler()->on_text_input(e.text.text); - break; - } - } - - void app::present() - { - gl::ClearColor(0.7f, 0.7f, 1.f, 1.f); - gl::Clear(gl::COLOR_BUFFER_BIT); - } - - void app::run() - { - SDL_ShowWindow(impl().window); - impl().running = true; - if (impl().get_scene().get() == this) - on_scene_enter(this); - while (running()) - { - poll_events(); - - auto handler = [this]{ return impl().get_scene(); }; - - auto send_initial_resize = [this, &handler]{ - if (!impl().had_initial_resize) - { - int w, h; - SDL_GetWindowSize(impl().window, &w, &h); - impl().had_initial_resize = true; - handler()->on_resize(w, h); - } - }; - - if (!running()) break; - send_initial_resize(); - handler()->update(); - // The handler might have changed during the update - send_initial_resize(); - handler()->present(); - - SDL_GL_SwapWindow(impl().window); - } - if (!impl().had_scene_exit) - { - impl().had_scene_exit = true; - impl().get_scene()->on_scene_exit(); - } - } - - void app::push_scene(std::shared_ptr s) - { - if (!impl().had_scene_exit) - { - impl().had_scene_exit = true; - impl().get_scene()->on_scene_exit(); - } - - impl().had_initial_resize = false; - impl().had_scene_exit = false; - impl().scene_stack.push_back(std::move(s)); - - impl().get_scene()->on_scene_enter(this); - } - - std::shared_ptr app::pop_scene() - { - if (impl().scene_stack.empty()) - return nullptr; - - if (!impl().had_scene_exit) - { - impl().had_scene_exit = true; - impl().get_scene()->on_scene_exit(); - } - - impl().had_initial_resize = false; - auto s = std::move(impl().scene_stack.back()); - impl().scene_stack.pop_back(); - - impl().get_scene()->on_scene_enter(this); - - return s; - } - - void app::show_cursor(bool show) - { - SDL_ShowCursor(show ? SDL_TRUE : SDL_FALSE); - SDL_SetRelativeMouseMode(show ? SDL_FALSE : SDL_TRUE); - } - - float app::time() const - { - return std::chrono::duration_cast>(clock::now() - impl().start_time).count(); - } - - bool app::vsync() const - { - return SDL_GL_GetSwapInterval() != 0; - } - - void app::vsync(bool on) - { - if (on) - { - // try adaptive vsync - if (SDL_GL_SetSwapInterval(-1) != 0) - { - // failed, try usual vsync then - SDL_GL_SetSwapInterval(1); - } - } - else - { - SDL_GL_SetSwapInterval(0); - } - } - -} diff --git a/libs/app/source/application_base.cpp b/libs/app/source/application_base.cpp new file mode 100644 index 00000000..eacd4a08 --- /dev/null +++ b/libs/app/source/application_base.cpp @@ -0,0 +1,46 @@ +#include + +namespace psemek::app +{ + + void application_base::on_event(resize_event const & event) + { + apply(state_, event); + } + + void application_base::on_event(focus_event const & event) + { + apply(state_, event); + } + + void application_base::on_event(mouse_move_event const & event) + { + apply(state_, event); + } + + void application_base::on_event(mouse_wheel_event const & event) + { + apply(state_, event); + } + + void application_base::on_event(mouse_button_event const & event) + { + apply(state_, event); + } + + void application_base::on_event(key_event const & event) + { + apply(state_, event); + } + + void application_base::stop() + { + running_ = false; + } + + bool application_base::running() const + { + return running_; + } + +} diff --git a/libs/app/source/scene_application.cpp b/libs/app/source/scene_application.cpp new file mode 100644 index 00000000..70ff4990 --- /dev/null +++ b/libs/app/source/scene_application.cpp @@ -0,0 +1,56 @@ +#include + +namespace psemek::app +{ + + void scene_application::on_event(resize_event const & event) + { + on_event_impl(event); + } + + void scene_application::on_event(focus_event const & event) + { + on_event_impl(event); + } + + void scene_application::on_event(mouse_move_event const & event) + { + on_event_impl(event); + } + + void scene_application::on_event(mouse_wheel_event const & event) + { + on_event_impl(event); + } + + void scene_application::on_event(mouse_button_event const & event) + { + on_event_impl(event); + } + + void scene_application::on_event(key_event const & event) + { + on_event_impl(event); + } + + void scene_application::update() + { + if (auto scene = current_scene()) + scene->update(); + } + + void scene_application::present() + { + if (auto scene = current_scene()) + scene->present(); + } + + template + void scene_application::on_event_impl(Event const & event) + { + application_base::on_event(event); + if (auto scene = current_scene()) + scene->on_event(event); + } + +} diff --git a/libs/audio/CMakeLists.txt b/libs/audio/CMakeLists.txt index fb1d4a2e..869d7bcb 100644 --- a/libs/audio/CMakeLists.txt +++ b/libs/audio/CMakeLists.txt @@ -3,4 +3,4 @@ file(GLOB_RECURSE PSEMEK_AUDIO_SOURCES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "s psemek_add_library(psemek-audio ${PSEMEK_AUDIO_HEADERS} ${PSEMEK_AUDIO_SOURCES}) target_include_directories(psemek-audio PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") -target_link_libraries(psemek-audio PUBLIC psemek-sdl2 psemek-geom psemek-util psemek-log psemek-prof) +target_link_libraries(psemek-audio PUBLIC psemek-random psemek-geom psemek-util psemek-log psemek-prof) diff --git a/libs/audio/include/psemek/audio/audio_file/AudioFile.h b/libs/audio/include/psemek/audio/audio_file/AudioFile.h new file mode 100644 index 00000000..51dbc4e8 --- /dev/null +++ b/libs/audio/include/psemek/audio/audio_file/AudioFile.h @@ -0,0 +1,1592 @@ +//======================================================================= +/** @file AudioFile.h + * @author Adam Stark + * @copyright Copyright (C) 2017 Adam Stark + * + * This file is part of the 'AudioFile' library + * + * MIT License + * + * Copyright (c) 2017 Adam Stark + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +//======================================================================= + +#ifndef _AS_AudioFile_h +#define _AS_AudioFile_h + +#if defined (_MSC_VER) +#undef max +#undef min +#define NOMINMAX +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// disable some warnings on Windows +#if defined (_MSC_VER) + __pragma(warning (push)) + __pragma(warning (disable : 4244)) + __pragma(warning (disable : 4457)) + __pragma(warning (disable : 4458)) + __pragma(warning (disable : 4389)) + __pragma(warning (disable : 4996)) +#elif defined (__GNUC__) + _Pragma("GCC diagnostic push") + _Pragma("GCC diagnostic ignored \"-Wconversion\"") + _Pragma("GCC diagnostic ignored \"-Wsign-compare\"") + _Pragma("GCC diagnostic ignored \"-Wshadow\"") +#endif + +//============================================================= +/** The different types of audio file, plus some other types to + * indicate a failure to load a file, or that one hasn't been + * loaded yet + */ +enum class AudioFileFormat +{ + Error, + NotLoaded, + Wave, + Aiff +}; + +//============================================================= +template +class AudioFile +{ +public: + + //============================================================= + typedef std::vector > AudioBuffer; + + //============================================================= + /** Constructor */ + AudioFile(); + + /** Constructor, using a given file path to load a file */ + AudioFile (std::string filePath); + + //============================================================= + /** Loads an audio file from a given file path. + * @Returns true if the file was successfully loaded + */ + bool load (std::string filePath); + + /** Saves an audio file to a given file path. + * @Returns true if the file was successfully saved + */ + bool save (std::string filePath, AudioFileFormat format = AudioFileFormat::Wave); + + //============================================================= + /** Loads an audio file from data in memory */ + bool loadFromMemory (std::vector& fileData); + + //============================================================= + /** @Returns the sample rate */ + uint32_t getSampleRate() const; + + /** @Returns the number of audio channels in the buffer */ + int getNumChannels() const; + + /** @Returns true if the audio file is mono */ + bool isMono() const; + + /** @Returns true if the audio file is stereo */ + bool isStereo() const; + + /** @Returns the bit depth of each sample */ + int getBitDepth() const; + + /** @Returns the number of samples per channel */ + int getNumSamplesPerChannel() const; + + /** @Returns the length in seconds of the audio file based on the number of samples and sample rate */ + double getLengthInSeconds() const; + + /** Prints a summary of the audio file to the console */ + void printSummary() const; + + //============================================================= + + /** Set the audio buffer for this AudioFile by copying samples from another buffer. + * @Returns true if the buffer was copied successfully. + */ + bool setAudioBuffer (AudioBuffer& newBuffer); + + /** Sets the audio buffer to a given number of channels and number of samples per channel. This will try to preserve + * the existing audio, adding zeros to any new channels or new samples in a given channel. + */ + void setAudioBufferSize (int numChannels, int numSamples); + + /** Sets the number of samples per channel in the audio buffer. This will try to preserve + * the existing audio, adding zeros to new samples in a given channel if the number of samples is increased. + */ + void setNumSamplesPerChannel (int numSamples); + + /** Sets the number of channels. New channels will have the correct number of samples and be initialised to zero */ + void setNumChannels (int numChannels); + + /** Sets the bit depth for the audio file. If you use the save() function, this bit depth rate will be used */ + void setBitDepth (int numBitsPerSample); + + /** Sets the sample rate for the audio file. If you use the save() function, this sample rate will be used */ + void setSampleRate (uint32_t newSampleRate); + + //============================================================= + /** Sets whether the library should log error messages to the console. By default this is true */ + void shouldLogErrorsToConsole (bool logErrors); + + //============================================================= + /** A vector of vectors holding the audio samples for the AudioFile. You can + * access the samples by channel and then by sample index, i.e: + * + * samples[channel][sampleIndex] + */ + AudioBuffer samples; + + //============================================================= + /** An optional iXML chunk that can be added to the AudioFile. + */ + std::string iXMLChunk; + + std::function onError; + +private: + + //============================================================= + enum class Endianness + { + LittleEndian, + BigEndian + }; + + //============================================================= + AudioFileFormat determineAudioFileFormat (std::vector& fileData); + bool decodeWaveFile (std::vector& fileData); + bool decodeAiffFile (std::vector& fileData); + + //============================================================= + bool saveToWaveFile (std::string filePath); + bool saveToAiffFile (std::string filePath); + + //============================================================= + void clearAudioBuffer(); + + //============================================================= + int32_t fourBytesToInt (std::vector& source, int startIndex, Endianness endianness = Endianness::LittleEndian); + int16_t twoBytesToInt (std::vector& source, int startIndex, Endianness endianness = Endianness::LittleEndian); + int getIndexOfString (std::vector& source, std::string s); + int getIndexOfChunk (std::vector& source, const std::string& chunkHeaderID, int startIndex, Endianness endianness = Endianness::LittleEndian); + + //============================================================= + uint32_t getAiffSampleRate (std::vector& fileData, int sampleRateStartIndex); + bool tenByteMatch (std::vector& v1, int startIndex1, std::vector& v2, int startIndex2); + void addSampleRateToAiffData (std::vector& fileData, uint32_t sampleRate); + + //============================================================= + void addStringToFileData (std::vector& fileData, std::string s); + void addInt32ToFileData (std::vector& fileData, int32_t i, Endianness endianness = Endianness::LittleEndian); + void addInt16ToFileData (std::vector& fileData, int16_t i, Endianness endianness = Endianness::LittleEndian); + + //============================================================= + bool writeDataToFile (std::vector& fileData, std::string filePath); + + //============================================================= + void reportError (std::string errorMessage); + + //============================================================= + AudioFileFormat audioFileFormat; + uint32_t sampleRate; + int bitDepth; + bool logErrorsToConsole {true}; +}; + +//============================================================= +template +struct AudioSampleConverter +{ + //============================================================= + /** Convert a signed 8-bit integer to an audio sample */ + static T signedByteToSample (int8_t sample); + + /** Convert an audio sample to an signed 8-bit representation */ + static int8_t sampleToSignedByte (T sample); + + //============================================================= + /** Convert an unsigned 8-bit integer to an audio sample */ + static T unsignedByteToSample (uint8_t sample); + + /** Convert an audio sample to an unsigned 8-bit representation */ + static uint8_t sampleToUnsignedByte (T sample); + + //============================================================= + /** Convert a 16-bit integer to an audio sample */ + static T sixteenBitIntToSample (int16_t sample); + + /** Convert a an audio sample to a 16-bit integer */ + static int16_t sampleToSixteenBitInt (T sample); + + //============================================================= + /** Convert a 24-bit value (int a 32-bit int) to an audio sample */ + static T twentyFourBitIntToSample (int32_t sample); + + /** Convert a an audio sample to a 24-bit value (in a 32-bit integer) */ + static int32_t sampleToTwentyFourBitInt (T sample); + + //============================================================= + /** Convert a 32-bit signed integer to an audio sample */ + static T thirtyTwoBitIntToSample (int32_t sample); + + /** Convert a an audio sample to a 32-bit signed integer */ + static int32_t sampleToThirtyTwoBitInt (T sample); + + //============================================================= + /** Helper clamp function to enforce ranges */ + static T clamp (T v1, T minValue, T maxValue); +}; + +//============================================================= +// Pre-defined 10-byte representations of common sample rates +static std::unordered_map > aiffSampleRateTable = { + {8000, {64, 11, 250, 0, 0, 0, 0, 0, 0, 0}}, + {11025, {64, 12, 172, 68, 0, 0, 0, 0, 0, 0}}, + {16000, {64, 12, 250, 0, 0, 0, 0, 0, 0, 0}}, + {22050, {64, 13, 172, 68, 0, 0, 0, 0, 0, 0}}, + {32000, {64, 13, 250, 0, 0, 0, 0, 0, 0, 0}}, + {37800, {64, 14, 147, 168, 0, 0, 0, 0, 0, 0}}, + {44056, {64, 14, 172, 24, 0, 0, 0, 0, 0, 0}}, + {44100, {64, 14, 172, 68, 0, 0, 0, 0, 0, 0}}, + {47250, {64, 14, 184, 146, 0, 0, 0, 0, 0, 0}}, + {48000, {64, 14, 187, 128, 0, 0, 0, 0, 0, 0}}, + {50000, {64, 14, 195, 80, 0, 0, 0, 0, 0, 0}}, + {50400, {64, 14, 196, 224, 0, 0, 0, 0, 0, 0}}, + {88200, {64, 15, 172, 68, 0, 0, 0, 0, 0, 0}}, + {96000, {64, 15, 187, 128, 0, 0, 0, 0, 0, 0}}, + {176400, {64, 16, 172, 68, 0, 0, 0, 0, 0, 0}}, + {192000, {64, 16, 187, 128, 0, 0, 0, 0, 0, 0}}, + {352800, {64, 17, 172, 68, 0, 0, 0, 0, 0, 0}}, + {2822400, {64, 20, 172, 68, 0, 0, 0, 0, 0, 0}}, + {5644800, {64, 21, 172, 68, 0, 0, 0, 0, 0, 0}} +}; + +//============================================================= +enum WavAudioFormat +{ + PCM = 0x0001, + IEEEFloat = 0x0003, + ALaw = 0x0006, + MULaw = 0x0007, + Extensible = 0xFFFE +}; + +//============================================================= +enum AIFFAudioFormat +{ + Uncompressed, + Compressed, + Error +}; + +//============================================================= +/* IMPLEMENTATION */ +//============================================================= + +//============================================================= +template +AudioFile::AudioFile() +{ + bitDepth = 16; + sampleRate = 44100; + samples.resize (1); + samples[0].resize (0); + audioFileFormat = AudioFileFormat::NotLoaded; +} + +//============================================================= +template +AudioFile::AudioFile (std::string filePath) + : AudioFile() +{ + load (filePath); +} + +//============================================================= +template +uint32_t AudioFile::getSampleRate() const +{ + return sampleRate; +} + +//============================================================= +template +int AudioFile::getNumChannels() const +{ + return (int)samples.size(); +} + +//============================================================= +template +bool AudioFile::isMono() const +{ + return getNumChannels() == 1; +} + +//============================================================= +template +bool AudioFile::isStereo() const +{ + return getNumChannels() == 2; +} + +//============================================================= +template +int AudioFile::getBitDepth() const +{ + return bitDepth; +} + +//============================================================= +template +int AudioFile::getNumSamplesPerChannel() const +{ + if (samples.size() > 0) + return (int) samples[0].size(); + else + return 0; +} + +//============================================================= +template +double AudioFile::getLengthInSeconds() const +{ + return (double)getNumSamplesPerChannel() / (double)sampleRate; +} + +//============================================================= +template +void AudioFile::printSummary() const +{ + std::cout << "|======================================|" << std::endl; + std::cout << "Num Channels: " << getNumChannels() << std::endl; + std::cout << "Num Samples Per Channel: " << getNumSamplesPerChannel() << std::endl; + std::cout << "Sample Rate: " << sampleRate << std::endl; + std::cout << "Bit Depth: " << bitDepth << std::endl; + std::cout << "Length in Seconds: " << getLengthInSeconds() << std::endl; + std::cout << "|======================================|" << std::endl; +} + +//============================================================= +template +bool AudioFile::setAudioBuffer (AudioBuffer& newBuffer) +{ + int numChannels = (int)newBuffer.size(); + + if (numChannels <= 0) + { + assert (false && "The buffer you are trying to use has no channels"); + return false; + } + + size_t numSamples = newBuffer[0].size(); + + // set the number of channels + samples.resize (newBuffer.size()); + + for (int k = 0; k < getNumChannels(); k++) + { + assert (newBuffer[k].size() == numSamples); + + samples[k].resize (numSamples); + + for (size_t i = 0; i < numSamples; i++) + { + samples[k][i] = newBuffer[k][i]; + } + } + + return true; +} + +//============================================================= +template +void AudioFile::setAudioBufferSize (int numChannels, int numSamples) +{ + samples.resize (numChannels); + setNumSamplesPerChannel (numSamples); +} + +//============================================================= +template +void AudioFile::setNumSamplesPerChannel (int numSamples) +{ + int originalSize = getNumSamplesPerChannel(); + + for (int i = 0; i < getNumChannels();i++) + { + samples[i].resize (numSamples); + + // set any new samples to zero + if (numSamples > originalSize) + std::fill (samples[i].begin() + originalSize, samples[i].end(), (T)0.); + } +} + +//============================================================= +template +void AudioFile::setNumChannels (int numChannels) +{ + int originalNumChannels = getNumChannels(); + int originalNumSamplesPerChannel = getNumSamplesPerChannel(); + + samples.resize (numChannels); + + // make sure any new channels are set to the right size + // and filled with zeros + if (numChannels > originalNumChannels) + { + for (int i = originalNumChannels; i < numChannels; i++) + { + samples[i].resize (originalNumSamplesPerChannel); + std::fill (samples[i].begin(), samples[i].end(), (T)0.); + } + } +} + +//============================================================= +template +void AudioFile::setBitDepth (int numBitsPerSample) +{ + bitDepth = numBitsPerSample; +} + +//============================================================= +template +void AudioFile::setSampleRate (uint32_t newSampleRate) +{ + sampleRate = newSampleRate; +} + +//============================================================= +template +void AudioFile::shouldLogErrorsToConsole (bool logErrors) +{ + logErrorsToConsole = logErrors; +} + +//============================================================= +template +bool AudioFile::load (std::string filePath) +{ + std::ifstream file (filePath, std::ios::binary); + + // check the file exists + if (! file.good()) + { + reportError ("ERROR: File doesn't exist or otherwise can't load file\n" + filePath); + return false; + } + + std::vector fileData; + + file.unsetf (std::ios::skipws); + + file.seekg (0, std::ios::end); + size_t length = file.tellg(); + file.seekg (0, std::ios::beg); + + // allocate + fileData.resize (length); + + file.read(reinterpret_cast (fileData.data()), length); + file.close(); + + if (file.gcount() != length) + { + reportError ("ERROR: Couldn't read entire file\n" + filePath); + return false; + } + + // Handle very small files that will break our attempt to read the + // first header info from them + if (fileData.size() < 12) + { + reportError ("ERROR: File is not a valid audio file\n" + filePath); + return false; + } + else + { + return loadFromMemory (fileData); + } +} + +//============================================================= +template +bool AudioFile::loadFromMemory (std::vector& fileData) +{ + // get audio file format + audioFileFormat = determineAudioFileFormat (fileData); + + if (audioFileFormat == AudioFileFormat::Wave) + { + return decodeWaveFile (fileData); + } + else if (audioFileFormat == AudioFileFormat::Aiff) + { + return decodeAiffFile (fileData); + } + else + { + reportError ("Audio File Type: Error"); + return false; + } +} + +//============================================================= +template +bool AudioFile::decodeWaveFile (std::vector& fileData) +{ + // ----------------------------------------------------------- + // HEADER CHUNK + std::string headerChunkID (fileData.begin(), fileData.begin() + 4); + //int32_t fileSizeInBytes = fourBytesToInt (fileData, 4) + 8; + std::string format (fileData.begin() + 8, fileData.begin() + 12); + + // ----------------------------------------------------------- + // try and find the start points of key chunks + int indexOfDataChunk = getIndexOfChunk (fileData, "data", 12); + int indexOfFormatChunk = getIndexOfChunk (fileData, "fmt ", 12); + int indexOfXMLChunk = getIndexOfChunk (fileData, "iXML", 12); + + // if we can't find the data or format chunks, or the IDs/formats don't seem to be as expected + // then it is unlikely we'll able to read this file, so abort + if (indexOfDataChunk == -1 || indexOfFormatChunk == -1 || headerChunkID != "RIFF" || format != "WAVE") + { + reportError ("ERROR: this doesn't seem to be a valid .WAV file"); + return false; + } + + // ----------------------------------------------------------- + // FORMAT CHUNK + int f = indexOfFormatChunk; + std::string formatChunkID (fileData.begin() + f, fileData.begin() + f + 4); + //int32_t formatChunkSize = fourBytesToInt (fileData, f + 4); + uint16_t audioFormat = twoBytesToInt (fileData, f + 8); + uint16_t numChannels = twoBytesToInt (fileData, f + 10); + sampleRate = (uint32_t) fourBytesToInt (fileData, f + 12); + uint32_t numBytesPerSecond = fourBytesToInt (fileData, f + 16); + uint16_t numBytesPerBlock = twoBytesToInt (fileData, f + 20); + bitDepth = (int) twoBytesToInt (fileData, f + 22); + + if (bitDepth > sizeof (T) * 8) + { + std::string message = "ERROR: you are trying to read a "; + message += std::to_string (bitDepth); + message += "-bit file using a "; + message += std::to_string (sizeof (T) * 8); + message += "-bit sample type"; + reportError (message); + return false; + } + + uint16_t numBytesPerSample = static_cast (bitDepth) / 8; + + // check that the audio format is PCM or Float or extensible + if (audioFormat != WavAudioFormat::PCM && audioFormat != WavAudioFormat::IEEEFloat && audioFormat != WavAudioFormat::Extensible) + { + reportError ("ERROR: this .WAV file is encoded in a format that this library does not support at present"); + return false; + } + + // check the number of channels is mono or stereo + if (numChannels < 1 || numChannels > 128) + { + reportError ("ERROR: this WAV file seems to be an invalid number of channels (or corrupted?)"); + return false; + } + + // check header data is consistent + if (numBytesPerSecond != static_cast ((numChannels * sampleRate * bitDepth) / 8) || numBytesPerBlock != (numChannels * numBytesPerSample)) + { + reportError ("ERROR: the header data in this WAV file seems to be inconsistent"); + return false; + } + + // check bit depth is either 8, 16, 24 or 32 bit + if (bitDepth != 8 && bitDepth != 16 && bitDepth != 24 && bitDepth != 32) + { + reportError ("ERROR: this file has a bit depth that is not 8, 16, 24 or 32 bits"); + return false; + } + + // ----------------------------------------------------------- + // DATA CHUNK + int d = indexOfDataChunk; + std::string dataChunkID (fileData.begin() + d, fileData.begin() + d + 4); + int32_t dataChunkSize = fourBytesToInt (fileData, d + 4); + + int numSamples = dataChunkSize / (numChannels * bitDepth / 8); + int samplesStartIndex = indexOfDataChunk + 8; + + clearAudioBuffer(); + samples.resize (numChannels); + + for (int i = 0; i < numSamples; i++) + { + for (int channel = 0; channel < numChannels; channel++) + { + int sampleIndex = samplesStartIndex + (numBytesPerBlock * i) + channel * numBytesPerSample; + + if ((sampleIndex + (bitDepth / 8) - 1) >= fileData.size()) + { + reportError ("ERROR: read file error as the metadata indicates more samples than there are in the file data"); + return false; + } + + if (bitDepth == 8) + { + T sample = AudioSampleConverter::unsignedByteToSample (fileData[sampleIndex]); + samples[channel].push_back (sample); + } + else if (bitDepth == 16) + { + int16_t sampleAsInt = twoBytesToInt (fileData, sampleIndex); + T sample = AudioSampleConverter::sixteenBitIntToSample (sampleAsInt); + samples[channel].push_back (sample); + } + else if (bitDepth == 24) + { + int32_t sampleAsInt = 0; + sampleAsInt = (fileData[sampleIndex + 2] << 16) | (fileData[sampleIndex + 1] << 8) | fileData[sampleIndex]; + + if (sampleAsInt & 0x800000) // if the 24th bit is set, this is a negative number in 24-bit world + sampleAsInt = sampleAsInt | ~0xFFFFFF; // so make sure sign is extended to the 32 bit float + + T sample = AudioSampleConverter::twentyFourBitIntToSample (sampleAsInt); + samples[channel].push_back (sample); + } + else if (bitDepth == 32) + { + int32_t sampleAsInt = fourBytesToInt (fileData, sampleIndex); + T sample; + + if (audioFormat == WavAudioFormat::IEEEFloat && std::is_floating_point_v) + { + float f; + memcpy (&f, &sampleAsInt, sizeof(int32_t)); + sample = (T)f; + } + else // assume PCM + { + sample = AudioSampleConverter::thirtyTwoBitIntToSample (sampleAsInt); + } + + samples[channel].push_back (sample); + } + else + { + assert (false); + } + } + } + + // ----------------------------------------------------------- + // iXML CHUNK + if (indexOfXMLChunk != -1) + { + int32_t chunkSize = fourBytesToInt (fileData, indexOfXMLChunk + 4); + iXMLChunk = std::string ((const char*) &fileData[indexOfXMLChunk + 8], chunkSize); + } + + return true; +} + +//============================================================= +template +bool AudioFile::decodeAiffFile (std::vector& fileData) +{ + // ----------------------------------------------------------- + // HEADER CHUNK + std::string headerChunkID (fileData.begin(), fileData.begin() + 4); + //int32_t fileSizeInBytes = fourBytesToInt (fileData, 4, Endianness::BigEndian) + 8; + std::string format (fileData.begin() + 8, fileData.begin() + 12); + + int audioFormat = format == "AIFF" ? AIFFAudioFormat::Uncompressed : format == "AIFC" ? AIFFAudioFormat::Compressed : AIFFAudioFormat::Error; + + // ----------------------------------------------------------- + // try and find the start points of key chunks + int indexOfCommChunk = getIndexOfChunk (fileData, "COMM", 12, Endianness::BigEndian); + int indexOfSoundDataChunk = getIndexOfChunk (fileData, "SSND", 12, Endianness::BigEndian); + int indexOfXMLChunk = getIndexOfChunk (fileData, "iXML", 12, Endianness::BigEndian); + + // if we can't find the data or format chunks, or the IDs/formats don't seem to be as expected + // then it is unlikely we'll able to read this file, so abort + if (indexOfSoundDataChunk == -1 || indexOfCommChunk == -1 || headerChunkID != "FORM" || audioFormat == AIFFAudioFormat::Error) + { + reportError ("ERROR: this doesn't seem to be a valid AIFF file"); + return false; + } + + // ----------------------------------------------------------- + // COMM CHUNK + int p = indexOfCommChunk; + std::string commChunkID (fileData.begin() + p, fileData.begin() + p + 4); + //int32_t commChunkSize = fourBytesToInt (fileData, p + 4, Endianness::BigEndian); + int16_t numChannels = twoBytesToInt (fileData, p + 8, Endianness::BigEndian); + int32_t numSamplesPerChannel = fourBytesToInt (fileData, p + 10, Endianness::BigEndian); + bitDepth = (int) twoBytesToInt (fileData, p + 14, Endianness::BigEndian); + sampleRate = getAiffSampleRate (fileData, p + 16); + + if (bitDepth > sizeof (T) * 8) + { + std::string message = "ERROR: you are trying to read a "; + message += std::to_string (bitDepth); + message += "-bit file using a "; + message += std::to_string (sizeof (T) * 8); + message += "-bit sample type"; + reportError (message); + return false; + } + + // check the sample rate was properly decoded + if (sampleRate == 0) + { + reportError ("ERROR: this AIFF file has an unsupported sample rate"); + return false; + } + + // check the number of channels is mono or stereo + if (numChannels < 1 ||numChannels > 2) + { + reportError ("ERROR: this AIFF file seems to be neither mono nor stereo (perhaps multi-track, or corrupted?)"); + return false; + } + + // check bit depth is either 8, 16, 24 or 32-bit + if (bitDepth != 8 && bitDepth != 16 && bitDepth != 24 && bitDepth != 32) + { + reportError ("ERROR: this file has a bit depth that is not 8, 16, 24 or 32 bits"); + return false; + } + + // ----------------------------------------------------------- + // SSND CHUNK + int s = indexOfSoundDataChunk; + std::string soundDataChunkID (fileData.begin() + s, fileData.begin() + s + 4); + int32_t soundDataChunkSize = fourBytesToInt (fileData, s + 4, Endianness::BigEndian); + int32_t offset = fourBytesToInt (fileData, s + 8, Endianness::BigEndian); + //int32_t blockSize = fourBytesToInt (fileData, s + 12, Endianness::BigEndian); + + int numBytesPerSample = bitDepth / 8; + int numBytesPerFrame = numBytesPerSample * numChannels; + int totalNumAudioSampleBytes = numSamplesPerChannel * numBytesPerFrame; + int samplesStartIndex = s + 16 + (int)offset; + + // sanity check the data + if ((soundDataChunkSize - 8) != totalNumAudioSampleBytes || totalNumAudioSampleBytes > static_cast(fileData.size() - samplesStartIndex)) + { + reportError ("ERROR: the metadatafor this file doesn't seem right"); + return false; + } + + clearAudioBuffer(); + samples.resize (numChannels); + + for (int i = 0; i < numSamplesPerChannel; i++) + { + for (int channel = 0; channel < numChannels; channel++) + { + int sampleIndex = samplesStartIndex + (numBytesPerFrame * i) + channel * numBytesPerSample; + + if ((sampleIndex + (bitDepth / 8) - 1) >= fileData.size()) + { + reportError ("ERROR: read file error as the metadata indicates more samples than there are in the file data"); + return false; + } + + if (bitDepth == 8) + { + T sample = AudioSampleConverter::signedByteToSample (static_cast (fileData[sampleIndex])); + samples[channel].push_back (sample); + } + else if (bitDepth == 16) + { + int16_t sampleAsInt = twoBytesToInt (fileData, sampleIndex, Endianness::BigEndian); + T sample = AudioSampleConverter::sixteenBitIntToSample (sampleAsInt); + samples[channel].push_back (sample); + } + else if (bitDepth == 24) + { + int32_t sampleAsInt = 0; + sampleAsInt = (fileData[sampleIndex] << 16) | (fileData[sampleIndex + 1] << 8) | fileData[sampleIndex + 2]; + + if (sampleAsInt & 0x800000) // if the 24th bit is set, this is a negative number in 24-bit world + sampleAsInt = sampleAsInt | ~0xFFFFFF; // so make sure sign is extended to the 32 bit float + + T sample = AudioSampleConverter::twentyFourBitIntToSample (sampleAsInt); + samples[channel].push_back (sample); + } + else if (bitDepth == 32) + { + int32_t sampleAsInt = fourBytesToInt (fileData, sampleIndex, Endianness::BigEndian); + T sample; + + if (audioFormat == AIFFAudioFormat::Compressed) + sample = (T)reinterpret_cast (sampleAsInt); + else // assume PCM + sample = AudioSampleConverter::thirtyTwoBitIntToSample (sampleAsInt); + + samples[channel].push_back (sample); + } + else + { + assert (false); + } + } + } + + // ----------------------------------------------------------- + // iXML CHUNK + if (indexOfXMLChunk != -1) + { + int32_t chunkSize = fourBytesToInt (fileData, indexOfXMLChunk + 4); + iXMLChunk = std::string ((const char*) &fileData[indexOfXMLChunk + 8], chunkSize); + } + + return true; +} + +//============================================================= +template +uint32_t AudioFile::getAiffSampleRate (std::vector& fileData, int sampleRateStartIndex) +{ + for (auto it : aiffSampleRateTable) + { + if (tenByteMatch (fileData, sampleRateStartIndex, it.second, 0)) + return it.first; + } + + return 0; +} + +//============================================================= +template +bool AudioFile::tenByteMatch (std::vector& v1, int startIndex1, std::vector& v2, int startIndex2) +{ + for (int i = 0; i < 10; i++) + { + if (v1[startIndex1 + i] != v2[startIndex2 + i]) + return false; + } + + return true; +} + +//============================================================= +template +void AudioFile::addSampleRateToAiffData (std::vector& fileData, uint32_t sampleRate) +{ + if (aiffSampleRateTable.count (sampleRate) > 0) + { + for (int i = 0; i < 10; i++) + fileData.push_back (aiffSampleRateTable[sampleRate][i]); + } +} + +//============================================================= +template +bool AudioFile::save (std::string filePath, AudioFileFormat format) +{ + if (format == AudioFileFormat::Wave) + { + return saveToWaveFile (filePath); + } + else if (format == AudioFileFormat::Aiff) + { + return saveToAiffFile (filePath); + } + + return false; +} + +//============================================================= +template +bool AudioFile::saveToWaveFile (std::string filePath) +{ + std::vector fileData; + + int32_t dataChunkSize = getNumSamplesPerChannel() * (getNumChannels() * bitDepth / 8); + int16_t audioFormat = bitDepth == 32 && std::is_floating_point_v ? WavAudioFormat::IEEEFloat : WavAudioFormat::PCM; + int32_t formatChunkSize = audioFormat == WavAudioFormat::PCM ? 16 : 18; + int32_t iXMLChunkSize = static_cast (iXMLChunk.size()); + + // ----------------------------------------------------------- + // HEADER CHUNK + addStringToFileData (fileData, "RIFF"); + + // The file size in bytes is the header chunk size (4, not counting RIFF and WAVE) + the format + // chunk size (24) + the metadata part of the data chunk plus the actual data chunk size + int32_t fileSizeInBytes = 4 + formatChunkSize + 8 + 8 + dataChunkSize; + if (iXMLChunkSize > 0) + { + fileSizeInBytes += (8 + iXMLChunkSize); + } + + addInt32ToFileData (fileData, fileSizeInBytes); + + addStringToFileData (fileData, "WAVE"); + + // ----------------------------------------------------------- + // FORMAT CHUNK + addStringToFileData (fileData, "fmt "); + addInt32ToFileData (fileData, formatChunkSize); // format chunk size (16 for PCM) + addInt16ToFileData (fileData, audioFormat); // audio format + addInt16ToFileData (fileData, (int16_t)getNumChannels()); // num channels + addInt32ToFileData (fileData, (int32_t)sampleRate); // sample rate + + int32_t numBytesPerSecond = (int32_t) ((getNumChannels() * sampleRate * bitDepth) / 8); + addInt32ToFileData (fileData, numBytesPerSecond); + + int16_t numBytesPerBlock = getNumChannels() * (bitDepth / 8); + addInt16ToFileData (fileData, numBytesPerBlock); + + addInt16ToFileData (fileData, (int16_t)bitDepth); + + if (audioFormat == WavAudioFormat::IEEEFloat) + addInt16ToFileData (fileData, 0); // extension size + + // ----------------------------------------------------------- + // DATA CHUNK + addStringToFileData (fileData, "data"); + addInt32ToFileData (fileData, dataChunkSize); + + for (int i = 0; i < getNumSamplesPerChannel(); i++) + { + for (int channel = 0; channel < getNumChannels(); channel++) + { + if (bitDepth == 8) + { + uint8_t byte = AudioSampleConverter::sampleToUnsignedByte (samples[channel][i]); + fileData.push_back (byte); + } + else if (bitDepth == 16) + { + int16_t sampleAsInt = AudioSampleConverter::sampleToSixteenBitInt (samples[channel][i]); + addInt16ToFileData (fileData, sampleAsInt); + } + else if (bitDepth == 24) + { + int32_t sampleAsIntAgain = AudioSampleConverter::sampleToTwentyFourBitInt (samples[channel][i]); + + uint8_t bytes[3]; + bytes[2] = (uint8_t) (sampleAsIntAgain >> 16) & 0xFF; + bytes[1] = (uint8_t) (sampleAsIntAgain >> 8) & 0xFF; + bytes[0] = (uint8_t) sampleAsIntAgain & 0xFF; + + fileData.push_back (bytes[0]); + fileData.push_back (bytes[1]); + fileData.push_back (bytes[2]); + } + else if (bitDepth == 32) + { + int32_t sampleAsInt; + + if (audioFormat == WavAudioFormat::IEEEFloat) + sampleAsInt = (int32_t) reinterpret_cast (samples[channel][i]); + else // assume PCM + sampleAsInt = AudioSampleConverter::sampleToThirtyTwoBitInt (samples[channel][i]); + + addInt32ToFileData (fileData, sampleAsInt, Endianness::LittleEndian); + } + else + { + assert (false && "Trying to write a file with unsupported bit depth"); + return false; + } + } + } + + // ----------------------------------------------------------- + // iXML CHUNK + if (iXMLChunkSize > 0) + { + addStringToFileData (fileData, "iXML"); + addInt32ToFileData (fileData, iXMLChunkSize); + addStringToFileData (fileData, iXMLChunk); + } + + // check that the various sizes we put in the metadata are correct + if (fileSizeInBytes != static_cast (fileData.size() - 8) || dataChunkSize != (getNumSamplesPerChannel() * getNumChannels() * (bitDepth / 8))) + { + reportError ("ERROR: couldn't save file to " + filePath); + return false; + } + + // try to write the file + return writeDataToFile (fileData, filePath); +} + +//============================================================= +template +bool AudioFile::saveToAiffFile (std::string filePath) +{ + std::vector fileData; + + int32_t numBytesPerSample = bitDepth / 8; + int32_t numBytesPerFrame = numBytesPerSample * getNumChannels(); + int32_t totalNumAudioSampleBytes = getNumSamplesPerChannel() * numBytesPerFrame; + int32_t soundDataChunkSize = totalNumAudioSampleBytes + 8; + int32_t iXMLChunkSize = static_cast (iXMLChunk.size()); + + // ----------------------------------------------------------- + // HEADER CHUNK + addStringToFileData (fileData, "FORM"); + + // The file size in bytes is the header chunk size (4, not counting FORM and AIFF) + the COMM + // chunk size (26) + the metadata part of the SSND chunk plus the actual data chunk size + int32_t fileSizeInBytes = 4 + 26 + 16 + totalNumAudioSampleBytes; + if (iXMLChunkSize > 0) + { + fileSizeInBytes += (8 + iXMLChunkSize); + } + + addInt32ToFileData (fileData, fileSizeInBytes, Endianness::BigEndian); + + addStringToFileData (fileData, "AIFF"); + + // ----------------------------------------------------------- + // COMM CHUNK + addStringToFileData (fileData, "COMM"); + addInt32ToFileData (fileData, 18, Endianness::BigEndian); // commChunkSize + addInt16ToFileData (fileData, getNumChannels(), Endianness::BigEndian); // num channels + addInt32ToFileData (fileData, getNumSamplesPerChannel(), Endianness::BigEndian); // num samples per channel + addInt16ToFileData (fileData, bitDepth, Endianness::BigEndian); // bit depth + addSampleRateToAiffData (fileData, sampleRate); + + // ----------------------------------------------------------- + // SSND CHUNK + addStringToFileData (fileData, "SSND"); + addInt32ToFileData (fileData, soundDataChunkSize, Endianness::BigEndian); + addInt32ToFileData (fileData, 0, Endianness::BigEndian); // offset + addInt32ToFileData (fileData, 0, Endianness::BigEndian); // block size + + for (int i = 0; i < getNumSamplesPerChannel(); i++) + { + for (int channel = 0; channel < getNumChannels(); channel++) + { + if (bitDepth == 8) + { + uint8_t byte = static_cast (AudioSampleConverter::sampleToSignedByte (samples[channel][i])); + fileData.push_back (byte); + } + else if (bitDepth == 16) + { + int16_t sampleAsInt = AudioSampleConverter::sampleToSixteenBitInt (samples[channel][i]); + addInt16ToFileData (fileData, sampleAsInt, Endianness::BigEndian); + } + else if (bitDepth == 24) + { + int32_t sampleAsIntAgain = AudioSampleConverter::sampleToTwentyFourBitInt (samples[channel][i]); + + uint8_t bytes[3]; + bytes[0] = (uint8_t) (sampleAsIntAgain >> 16) & 0xFF; + bytes[1] = (uint8_t) (sampleAsIntAgain >> 8) & 0xFF; + bytes[2] = (uint8_t) sampleAsIntAgain & 0xFF; + + fileData.push_back (bytes[0]); + fileData.push_back (bytes[1]); + fileData.push_back (bytes[2]); + } + else if (bitDepth == 32) + { + // write samples as signed integers (no implementation yet for floating point, but looking at WAV implementation should help) + int32_t sampleAsInt = AudioSampleConverter::sampleToThirtyTwoBitInt (samples[channel][i]); + addInt32ToFileData (fileData, sampleAsInt, Endianness::BigEndian); + } + else + { + assert (false && "Trying to write a file with unsupported bit depth"); + return false; + } + } + } + + // ----------------------------------------------------------- + // iXML CHUNK + if (iXMLChunkSize > 0) + { + addStringToFileData (fileData, "iXML"); + addInt32ToFileData (fileData, iXMLChunkSize, Endianness::BigEndian); + addStringToFileData (fileData, iXMLChunk); + } + + // check that the various sizes we put in the metadata are correct + if (fileSizeInBytes != static_cast (fileData.size() - 8) || soundDataChunkSize != getNumSamplesPerChannel() * numBytesPerFrame + 8) + { + reportError ("ERROR: couldn't save file to " + filePath); + return false; + } + + // try to write the file + return writeDataToFile (fileData, filePath); +} + +//============================================================= +template +bool AudioFile::writeDataToFile (std::vector& fileData, std::string filePath) +{ + std::ofstream outputFile (filePath, std::ios::binary); + + if (outputFile.is_open()) + { + for (size_t i = 0; i < fileData.size(); i++) + { + char value = (char) fileData[i]; + outputFile.write (&value, sizeof (char)); + } + + outputFile.close(); + + return true; + } + + return false; +} + +//============================================================= +template +void AudioFile::addStringToFileData (std::vector& fileData, std::string s) +{ + for (size_t i = 0; i < s.length();i++) + fileData.push_back ((uint8_t) s[i]); +} + +//============================================================= +template +void AudioFile::addInt32ToFileData (std::vector& fileData, int32_t i, Endianness endianness) +{ + uint8_t bytes[4]; + + if (endianness == Endianness::LittleEndian) + { + bytes[3] = (i >> 24) & 0xFF; + bytes[2] = (i >> 16) & 0xFF; + bytes[1] = (i >> 8) & 0xFF; + bytes[0] = i & 0xFF; + } + else + { + bytes[0] = (i >> 24) & 0xFF; + bytes[1] = (i >> 16) & 0xFF; + bytes[2] = (i >> 8) & 0xFF; + bytes[3] = i & 0xFF; + } + + for (int i = 0; i < 4; i++) + fileData.push_back (bytes[i]); +} + +//============================================================= +template +void AudioFile::addInt16ToFileData (std::vector& fileData, int16_t i, Endianness endianness) +{ + uint8_t bytes[2]; + + if (endianness == Endianness::LittleEndian) + { + bytes[1] = (i >> 8) & 0xFF; + bytes[0] = i & 0xFF; + } + else + { + bytes[0] = (i >> 8) & 0xFF; + bytes[1] = i & 0xFF; + } + + fileData.push_back (bytes[0]); + fileData.push_back (bytes[1]); +} + +//============================================================= +template +void AudioFile::clearAudioBuffer() +{ + for (size_t i = 0; i < samples.size();i++) + { + samples[i].clear(); + } + + samples.clear(); +} + +//============================================================= +template +AudioFileFormat AudioFile::determineAudioFileFormat (std::vector& fileData) +{ + std::string header (fileData.begin(), fileData.begin() + 4); + + if (header == "RIFF") + return AudioFileFormat::Wave; + else if (header == "FORM") + return AudioFileFormat::Aiff; + else + return AudioFileFormat::Error; +} + +//============================================================= +template +int32_t AudioFile::fourBytesToInt (std::vector& source, int startIndex, Endianness endianness) +{ + if (source.size() >= (startIndex + 4)) + { + int32_t result; + + if (endianness == Endianness::LittleEndian) + result = (source[startIndex + 3] << 24) | (source[startIndex + 2] << 16) | (source[startIndex + 1] << 8) | source[startIndex]; + else + result = (source[startIndex] << 24) | (source[startIndex + 1] << 16) | (source[startIndex + 2] << 8) | source[startIndex + 3]; + + return result; + } + else + { + assert (false && "Attempted to read four bytes from vector at position where out of bounds access would occur"); + return 0; // this is a dummy value as we don't have one to return + } +} + +//============================================================= +template +int16_t AudioFile::twoBytesToInt (std::vector& source, int startIndex, Endianness endianness) +{ + int16_t result; + + if (endianness == Endianness::LittleEndian) + result = (source[startIndex + 1] << 8) | source[startIndex]; + else + result = (source[startIndex] << 8) | source[startIndex + 1]; + + return result; +} + +//============================================================= +template +int AudioFile::getIndexOfString (std::vector& source, std::string stringToSearchFor) +{ + int index = -1; + int stringLength = (int)stringToSearchFor.length(); + + for (size_t i = 0; i < source.size() - stringLength;i++) + { + std::string section (source.begin() + i, source.begin() + i + stringLength); + + if (section == stringToSearchFor) + { + index = static_cast (i); + break; + } + } + + return index; +} + +//============================================================= +template +int AudioFile::getIndexOfChunk (std::vector& source, const std::string& chunkHeaderID, int startIndex, Endianness endianness) +{ + constexpr int dataLen = 4; + + if (chunkHeaderID.size() != dataLen) + { + assert (false && "Invalid chunk header ID string"); + return -1; + } + + int i = startIndex; + while (i < source.size() - dataLen) + { + if (memcmp (&source[i], chunkHeaderID.data(), dataLen) == 0) + { + return i; + } + + i += dataLen; + + // If somehow we don't have 4 bytes left to read, then exit with -1 + if ((i + 4) >= source.size()) + return -1; + + auto chunkSize = fourBytesToInt (source, i, endianness); + i += (dataLen + chunkSize); + } + + return -1; +} + +//============================================================= +template +void AudioFile::reportError (std::string errorMessage) +{ + if (onError) + onError(errorMessage); + if (logErrorsToConsole) + std::cout << errorMessage << std::endl; +} + +//============================================================= +template +typename std::make_unsigned::type convertSignedToUnsigned (SignedType signedValue) +{ + static_assert (std::is_signed::value, "The input value must be signed"); + + typename std::make_unsigned::type unsignedValue = static_cast::type> (1) + std::numeric_limits::max(); + + unsignedValue += signedValue; + return unsignedValue; +} + +//============================================================= +enum SampleLimit +{ + SignedInt16_Min = -32768, + SignedInt16_Max = 32767, + UnsignedInt16_Min = 0, + UnsignedInt16_Max = 65535, + SignedInt24_Min = -8388608, + SignedInt24_Max = 8388607, + UnsignedInt24_Min = 0, + UnsignedInt24_Max = 16777215 +}; + +//============================================================= +template +T AudioSampleConverter::thirtyTwoBitIntToSample (int32_t sample) +{ + if constexpr (std::is_floating_point::value) + { + return static_cast (sample) / static_cast (std::numeric_limits::max()); + } + else if (std::numeric_limits::is_integer) + { + if constexpr (std::is_signed_v) + return static_cast (sample); + else + return static_cast (clamp (static_cast (sample + 2147483648), 0, 4294967295)); + } +} + +//============================================================= +template +int32_t AudioSampleConverter::sampleToThirtyTwoBitInt (T sample) +{ + if constexpr (std::is_floating_point::value) + { + // multiplying a float by a the max int32_t is problematic because + // of roundng errors which can cause wrong values to come out, so + // we use a different implementation here compared to other types + if constexpr (std::is_same_v) + { + if (sample >= 1.f) + return std::numeric_limits::max(); + else if (sample <= -1.f) + return std::numeric_limits::lowest() + 1; // starting at 1 preserves symmetry + else + return static_cast (sample * std::numeric_limits::max()); + } + else + { + return static_cast (clamp (sample, -1., 1.) * std::numeric_limits::max()); + } + } + else + { + if constexpr (std::is_signed_v) + return static_cast (clamp (sample, -2147483648LL, 2147483647LL)); + else + return static_cast (clamp (sample, 0, 4294967295) - 2147483648); + } +} + +//============================================================= +template +T AudioSampleConverter::twentyFourBitIntToSample (int32_t sample) +{ + if constexpr (std::is_floating_point::value) + { + return static_cast (sample) / static_cast (8388607.); + } + else if (std::numeric_limits::is_integer) + { + if constexpr (std::is_signed_v) + return static_cast (clamp (sample, SignedInt24_Min, SignedInt24_Max)); + else + return static_cast (clamp (sample + 8388608, UnsignedInt24_Min, UnsignedInt24_Max)); + } +} + +//============================================================= +template +int32_t AudioSampleConverter::sampleToTwentyFourBitInt (T sample) +{ + if constexpr (std::is_floating_point::value) + { + sample = clamp (sample, -1., 1.); + return static_cast (sample * 8388607.); + } + else + { + if constexpr (std::is_signed_v) + return static_cast (clamp (sample, SignedInt24_Min, SignedInt24_Max)); + else + return static_cast (clamp (sample, UnsignedInt24_Min, UnsignedInt24_Max) + SignedInt24_Min); + } +} + +//============================================================= +template +T AudioSampleConverter::sixteenBitIntToSample (int16_t sample) +{ + if constexpr (std::is_floating_point::value) + { + return static_cast (sample) / static_cast (32767.); + } + else if constexpr (std::numeric_limits::is_integer) + { + if constexpr (std::is_signed_v) + return static_cast (sample); + else + return static_cast (convertSignedToUnsigned (sample)); + } +} + +//============================================================= +template +int16_t AudioSampleConverter::sampleToSixteenBitInt (T sample) +{ + if constexpr (std::is_floating_point::value) + { + sample = clamp (sample, -1., 1.); + return static_cast (sample * 32767.); + } + else + { + if constexpr (std::is_signed_v) + return static_cast (clamp (sample, SignedInt16_Min, SignedInt16_Max)); + else + return static_cast (clamp (sample, UnsignedInt16_Min, UnsignedInt16_Max) + SignedInt16_Min); + } +} + +//============================================================= +template +uint8_t AudioSampleConverter::sampleToUnsignedByte (T sample) +{ + if constexpr (std::is_floating_point::value) + { + sample = clamp (sample, -1., 1.); + sample = (sample + 1.) / 2.; + return static_cast (1 + (sample * 254)); + } + else + { + if constexpr (std::is_signed_v) + return static_cast (clamp (sample, -128, 127) + 128); + else + return static_cast (clamp (sample, 0, 255)); + } +} + +//============================================================= +template +int8_t AudioSampleConverter::sampleToSignedByte (T sample) +{ + if constexpr (std::is_floating_point::value) + { + sample = clamp (sample, -1., 1.); + return static_cast (sample * (T)0x7F); + } + else + { + if constexpr (std::is_signed_v) + return static_cast (clamp (sample, -128, 127)); + else + return static_cast (clamp (sample, 0, 255) - 128); + } +} + +//============================================================= +template +T AudioSampleConverter::unsignedByteToSample (uint8_t sample) +{ + if constexpr (std::is_floating_point::value) + { + return static_cast (sample - 128) / static_cast (127.); + } + else if (std::numeric_limits::is_integer) + { + if constexpr (std::is_unsigned_v) + return static_cast (sample); + else + return static_cast (sample - 128); + } +} + +//============================================================= +template +T AudioSampleConverter::signedByteToSample (int8_t sample) +{ + if constexpr (std::is_floating_point::value) + { + return static_cast (sample) / static_cast (127.); + } + else if constexpr (std::numeric_limits::is_integer) + { + if constexpr (std::is_signed_v) + return static_cast (sample); + else + return static_cast (convertSignedToUnsigned (sample)); + } +} + +//============================================================= +template +T AudioSampleConverter::clamp (T value, T minValue, T maxValue) +{ + value = std::min (value, maxValue); + value = std::max (value, minValue); + return value; +} + +#if defined (_MSC_VER) + __pragma(warning (pop)) +#elif defined (__GNUC__) + _Pragma("GCC diagnostic pop") +#endif + +#endif /* AudioFile_h */ diff --git a/libs/audio/include/psemek/audio/engine.hpp b/libs/audio/include/psemek/audio/engine.hpp index b03d0af4..363f909d 100644 --- a/libs/audio/include/psemek/audio/engine.hpp +++ b/libs/audio/include/psemek/audio/engine.hpp @@ -1,30 +1,20 @@ #pragma once -#include -#include #include -#include -#include - -#include - #include -#include -#include namespace psemek::audio { struct engine { - engine(); - ~engine(); + virtual channel_ptr output() = 0; - channel_ptr output(); - - private: - psemek_declare_pimpl + virtual ~engine() {} }; + // Implemented by platform backend + std::unique_ptr make_engine(); + } diff --git a/libs/audio/source/engine.cpp b/libs/audio/source/engine.cpp deleted file mode 100644 index 746a078b..00000000 --- a/libs/audio/source/engine.cpp +++ /dev/null @@ -1,97 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include - -namespace psemek::audio -{ - - struct engine::impl - { - std::shared_ptr sdl_init; - - SDL_AudioDeviceID device; - - std::vector buffer; - bool thread_registered = false; - - channel_ptr output; - - impl(); - - ~impl() - { - SDL_CloseAudioDevice(device); - } - - static void callback(void * userdata, std::uint8_t * stream, int len); - }; - - engine::impl::impl() - : sdl_init(sdl2::init(SDL_INIT_AUDIO)) - { - SDL_AudioSpec desired, obtained; - desired.freq = frequency; - desired.channels = 2; - desired.format = AUDIO_S16SYS; - desired.samples = 256; - desired.callback = &callback; - desired.userdata = this; - if (device = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, 0); device == 0) - sdl2::fail("SDL_OpenAudioDevice failed:"); - - log::info() << "Initialized audio: " << static_cast(obtained.channels) << " channels, " << obtained.freq << " Hz, " << obtained.samples << " samples"; - - buffer.resize(obtained.samples * obtained.channels); - output = std::make_shared(); - SDL_PauseAudioDevice(device, 0); - } - - void engine::impl::callback(void * userdata, std::uint8_t * dst_u8, int len) - { - static std::string const profiler_str = "audio"; - prof::profiler prof(profiler_str); - - auto self = static_cast(userdata); - stream_ptr output = self->output->stream(); - std::int16_t * dst = reinterpret_cast(dst_u8); - - if (!self->thread_registered) - { - log::register_thread("audio"); - self->thread_registered = true; - } - - std::size_t const size = len / 2; - std::size_t read = 0; - if (output) - read = output->read({self->buffer.data(), size}); - std::fill(self->buffer.data() + read, self->buffer.data() + size, 0.f); - - for (auto s : self->buffer) - *dst++ = static_cast(std::max(std::min((65535.f * s - 1.f) / 2.f, 32767.f), -32768.f)); - } - - engine::engine() - : pimpl_(make_impl()) - {} - - engine::~engine() = default; - - channel_ptr engine::output() - { - return impl().output; - } - -} diff --git a/libs/audio/source/track_wav.cpp b/libs/audio/source/track_wav.cpp index 7b157d03..f2af4072 100644 --- a/libs/audio/source/track_wav.cpp +++ b/libs/audio/source/track_wav.cpp @@ -1,11 +1,9 @@ #include #include #include +#include #include #include -#include - -#include namespace psemek::audio { @@ -13,38 +11,34 @@ namespace psemek::audio namespace { - std::vector convert_audio(SDL_AudioSpec const & spec, std::uint8_t * samples, std::size_t length) + std::vector convert_audio(std::vector> const & channels, int frequency) { - if (spec.channels > 2) - throw std::runtime_error(util::to_string("Can't convert audio with ", static_cast(spec.channels), " channels")); + if (channels.empty() || channels.size() > 2) + throw std::runtime_error(util::to_string("Can't convert audio with ", static_cast(channels.size()), " channels")); - if (spec.format != AUDIO_S16SYS) - throw std::runtime_error(util::to_string("Can't convert audio with format ", spec.format)); + std::vector result(channels[0].size() * 2); + auto out = result.begin(); - auto p = reinterpret_cast(samples); - - std::vector result; - - if (spec.channels == 1) + if (channels.size() == 1) { - result.resize(length); - for (std::size_t i = 0; i < length / 2; ++i) + for (auto p = channels[0].begin(); p != channels[0].end(); ++p) { - float v = (p[i] * 2.f + 1.f) / 65536.f; - result[2 * i + 0] = v; - result[2 * i + 1] = v; + *out++ = *p; + *out++ = *p; } } - else + else if (channels.size() == 2) { - result.resize(length / 2); - for (std::size_t i = 0; i < length / 2; ++i) - result[i] = (p[i] * 2.f + 1.f) / 65536.f; + for (auto p0 = channels[0].begin(), p1 = channels[1].begin(); p0 != channels[0].end(); ++p0, ++p1) + { + *out++ = *p0; + *out++ = *p1; + } } - if (spec.freq != audio::frequency) + if (frequency != audio::frequency) { - audio::resampler resampler(audio::frequency * 1.f / spec.freq); + audio::resampler resampler(audio::frequency * 1.f / frequency); resampler.feed(result); result = resampler.grab_result(); } @@ -56,14 +50,17 @@ namespace psemek::audio track_ptr load_wav(util::span data) { - SDL_AudioSpec spec; - std::uint8_t * samples; - std::uint32_t length; - if (!SDL_LoadWAV_RW(SDL_RWFromConstMem(data.data(), data.size()), 1, &spec, &samples, &length)) - sdl2::fail("SDL_LoadWAV_RW failed:"); + std::vector data_u8(data.size()); + std::copy(data.begin(), data.end(), reinterpret_cast(data_u8.data())); - util::at_scope_exit release_samples([samples]{ SDL_FreeWAV(samples); }); - return load_raw(convert_audio(spec, samples, length)); + AudioFile audio_file; + audio_file.shouldLogErrorsToConsole(false); + audio_file.onError = [](std::string const & error) { + throw std::runtime_error("failed to load WAV file: " + error); + }; + audio_file.loadFromMemory(data_u8); + + return load_raw(convert_audio(audio_file.samples, audio_file.getSampleRate())); } track_ptr load_wav(std::vector const & data) diff --git a/libs/gfx/include/psemek/gfx/init.hpp b/libs/gfx/include/psemek/gfx/init.hpp new file mode 100644 index 00000000..c1ce8998 --- /dev/null +++ b/libs/gfx/include/psemek/gfx/init.hpp @@ -0,0 +1,8 @@ +#pragma once + +namespace psemek::gfx +{ + + void init(); + +} diff --git a/libs/gfx/source/init.cpp b/libs/gfx/source/init.cpp new file mode 100644 index 00000000..8301da15 --- /dev/null +++ b/libs/gfx/source/init.cpp @@ -0,0 +1,22 @@ +#include +#include +#include + +namespace psemek::gfx +{ + + void init() + { + if (!gl::sys::initialize()) + throw std::runtime_error("Failed to load OpenGL functions"); + + auto vendor = gl::GetString(gl::VENDOR); + auto renderer = gl::GetString(gl::RENDERER); + + int major, minor; + gl::GetIntegerv(gl::MAJOR_VERSION, &major); + gl::GetIntegerv(gl::MINOR_VERSION, &minor); + log::info() << "Initialized OpenGL " << major << '.' << minor << ", " << vendor << ", " << renderer; + } + +} diff --git a/libs/sdl2/CMakeLists.txt b/libs/sdl2/CMakeLists.txt index 53e0c832..eb9318e8 100644 --- a/libs/sdl2/CMakeLists.txt +++ b/libs/sdl2/CMakeLists.txt @@ -5,4 +5,4 @@ file(GLOB_RECURSE PSEMEK_SDL2_SOURCES RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}" "so psemek_add_library(psemek-sdl2 ${PSEMEK_SDL2_HEADERS} ${PSEMEK_SDL2_SOURCES}) target_include_directories(psemek-sdl2 PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") -target_link_libraries(psemek-sdl2 PUBLIC psemek-log psemek-util psemek-gfx SDL2) +target_link_libraries(psemek-sdl2 PUBLIC psemek-log psemek-util psemek-gfx psemek-audio psemek-app SDL2) diff --git a/libs/sdl2/include/psemek/sdl2/events.hpp b/libs/sdl2/include/psemek/sdl2/events.hpp new file mode 100644 index 00000000..6b2a60c1 --- /dev/null +++ b/libs/sdl2/include/psemek/sdl2/events.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace psemek::sdl2 +{ + + // Returns true is quit is requested + bool poll_events(app::event_handler & handler); + +} diff --git a/libs/sdl2/include/psemek/sdl2/window.hpp b/libs/sdl2/include/psemek/sdl2/window.hpp new file mode 100644 index 00000000..9beaaf69 --- /dev/null +++ b/libs/sdl2/include/psemek/sdl2/window.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include + +namespace psemek::sdl2 +{ + + struct window + { + window(psemek::app::application::options const & options); + ~window(); + + geom::vector size() const; + + void show(); + void swap(); + void show_cursor(bool show); + void vsync(bool on); + + private: + std::shared_ptr sdl_init_; + SDL_Window * window_ = nullptr; + SDL_GLContext gl_context_ = nullptr; + }; + +} diff --git a/libs/sdl2/source/audio_engine.cpp b/libs/sdl2/source/audio_engine.cpp new file mode 100644 index 00000000..2ce4a50f --- /dev/null +++ b/libs/sdl2/source/audio_engine.cpp @@ -0,0 +1,111 @@ +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace psemek::sdl2 +{ + + namespace + { + + struct audio_engine_impl + : audio::engine + { + audio_engine_impl(); + ~audio_engine_impl(); + + audio::channel_ptr output() override; + + private: + std::shared_ptr sdl_init_; + + SDL_AudioDeviceID device_; + + std::vector buffer_; + bool thread_registered_ = false; + + audio::channel_ptr output_; + + static void callback(void * userdata, std::uint8_t * stream, int len); + }; + + audio_engine_impl::audio_engine_impl() + : sdl_init_(sdl2::init(SDL_INIT_AUDIO)) + { + SDL_AudioSpec desired, obtained; + desired.freq = audio::frequency; + desired.channels = 2; + desired.format = AUDIO_S16SYS; + desired.samples = 256; + desired.callback = &callback; + desired.userdata = this; + if (device_ = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, 0); device_ == 0) + fail("SDL_OpenAudioDevice failed:"); + + log::info() << "Initialized audio: " << static_cast(obtained.channels) << " channels, " << obtained.freq << " Hz, " << obtained.samples << " samples"; + + buffer_.resize(obtained.samples * obtained.channels); + output_ = std::make_shared(); + SDL_PauseAudioDevice(device_, 0); + } + + audio_engine_impl::~audio_engine_impl() + { + SDL_CloseAudioDevice(device_); + } + + void audio_engine_impl::callback(void * userdata, std::uint8_t * dst_u8, int len) + { + static std::string const profiler_str = "audio"; + prof::profiler prof(profiler_str); + + auto self = static_cast(userdata); + auto stream = self->output()->stream(); + std::int16_t * dst = reinterpret_cast(dst_u8); + + if (!self->thread_registered_) + { + log::register_thread("audio"); + self->thread_registered_ = true; + } + + std::size_t const size = len / 2; + std::size_t read = 0; + if (stream) + read = stream->read({self->buffer_.data(), size}); + std::fill(self->buffer_.data() + read, self->buffer_.data() + size, 0.f); + + for (auto s : self->buffer_) + *dst++ = static_cast(std::max(std::min((65535.f * s - 1.f) / 2.f, 32767.f), -32768.f)); + } + + audio::channel_ptr audio_engine_impl::output() + { + return output_; + } + + } + +} + +namespace psemek::audio +{ + + std::unique_ptr make_engine() + { + return std::make_unique(); + } + +} diff --git a/libs/sdl2/source/events.cpp b/libs/sdl2/source/events.cpp new file mode 100644 index 00000000..ee4d6730 --- /dev/null +++ b/libs/sdl2/source/events.cpp @@ -0,0 +1,85 @@ +#include + +#include +#include + +namespace psemek::sdl2 +{ + + namespace + { + + std::optional mouse_button(Uint8 button) + { + switch (button) + { + case SDL_BUTTON_LEFT: + return app::mouse_button::left; + case SDL_BUTTON_MIDDLE: + return app::mouse_button::middle; + case SDL_BUTTON_RIGHT: + return app::mouse_button::right; + default: + return std::nullopt; + } + } + + std::optional keycode(SDL_Keysym) + { + return std::nullopt; + } + + } + + bool poll_events(app::event_handler & handler) + { + for (SDL_Event e; SDL_PollEvent(&e);) switch (e.type) + { + case SDL_QUIT: + return true; + case SDL_WINDOWEVENT: switch (e.window.event) + { + case SDL_WINDOWEVENT_CLOSE: + return true; + case SDL_WINDOWEVENT_RESIZED: + handler.on_event(app::resize_event{{e.window.data1, e.window.data2}}); + break; + case SDL_WINDOWEVENT_SIZE_CHANGED: + handler.on_event(app::resize_event{{e.window.data1, e.window.data2}}); + break; + case SDL_WINDOWEVENT_FOCUS_GAINED: + handler.on_event(app::focus_event{true}); + break; + case SDL_WINDOWEVENT_FOCUS_LOST: + handler.on_event(app::focus_event{false}); + break; + } + break; + case SDL_MOUSEMOTION: + handler.on_event(app::mouse_move_event{{e.motion.x, e.motion.y}}); + break; + case SDL_MOUSEWHEEL: + handler.on_event(app::mouse_wheel_event{e.wheel.y}); + break; + case SDL_MOUSEBUTTONDOWN: + if (auto button = mouse_button(e.button.button)) + handler.on_event(app::mouse_button_event{*button, true}); + break; + case SDL_MOUSEBUTTONUP: + if (auto button = mouse_button(e.button.button)) + handler.on_event(app::mouse_button_event{*button, false}); + break; + case SDL_KEYDOWN: + if (auto key = keycode(e.key.keysym)) + handler.on_event(app::key_event{*key, true}); + break; + case SDL_KEYUP: + if (auto key = keycode(e.key.keysym)) + handler.on_event(app::key_event{*key, false}); + break; + } + + return false; + } + +} diff --git a/libs/sdl2/source/main.cpp b/libs/sdl2/source/main.cpp new file mode 100644 index 00000000..cd1a5592 --- /dev/null +++ b/libs/sdl2/source/main.cpp @@ -0,0 +1,68 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#undef main + +int main() try +{ + using namespace psemek; + + util::clock clock; + +#ifdef PSEMEK_PACKAGE_MODE + log::level const stdio_log_level = log::level::info; +#else + log::level const stdio_log_level = log::level::debug; +#endif + + log::add_sink(log::default_sink(io::std_out(), stdio_log_level)); + log::register_thread("main"); + + auto const factory = app::make_application_factory(); + auto const options = factory->options(); + + sdl2::window window(options); + + gfx::init(); + + app::application::context context; + context.show_cursor = [&](bool show){ window.show_cursor(show); }; + context.vsync = [&](bool on){ window.vsync(on); }; + + auto application = factory->create(options, context); + application->on_event(app::resize_event{window.size()}); + + window.show(); + + log::info() << "Started in " << util::pretty(clock.duration(), std::chrono::milliseconds{1}); + log::info() << "Running"; + + while (application->running()) + { + if (sdl2::poll_events(*application)) + application->stop(); + if (!application->running()) break; + application->update(); + application->present(); + window.swap(); + } + + log::info() << "Quitting"; + return EXIT_SUCCESS; +} +catch (std::exception const & e) +{ + psemek::log::error() << e.what(); + return EXIT_FAILURE; +} +catch (...) +{ + psemek::log::error() << "Unknown exception"; + return EXIT_FAILURE; +} diff --git a/libs/sdl2/source/window.cpp b/libs/sdl2/source/window.cpp new file mode 100644 index 00000000..13fbe273 --- /dev/null +++ b/libs/sdl2/source/window.cpp @@ -0,0 +1,96 @@ +#include +#include +#include + +namespace psemek::sdl2 +{ + + window::window(psemek::app::application::options const & options) + : sdl_init_(init(SDL_INIT_EVENTS | SDL_INIT_VIDEO)) + { + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, gl::sys::major_version()); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, gl::sys::minor_version()); + SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); + SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); + + if (options.multisampling == 0) + { + SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 0); + } + else + { + SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1); + SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, options.multisampling); + } + + std::uint32_t flags = SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_MAXIMIZED | SDL_WINDOW_BORDERLESS | SDL_WINDOW_FULLSCREEN_DESKTOP; + if (options.highdpi) flags |= SDL_WINDOW_ALLOW_HIGHDPI; + + SDL_DisplayMode display_mode; + SDL_GetCurrentDisplayMode(0, &display_mode); + + window_ = SDL_CreateWindow(options.name.data(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, display_mode.w, display_mode.h, flags); + if (!window_) + sdl2::fail("Failed to create window: "); + + gl_context_ = SDL_GL_CreateContext(window_); + if (!gl_context_) + sdl2::fail("Failed to create OpenGL context: "); + + SDL_GL_MakeCurrent(window_, gl_context_); + } + + geom::vector window::size() const + { + geom::vector result; + SDL_GL_GetDrawableSize(window_, &result[0], &result[1]); + return result; + } + + void window::show() + { + SDL_ShowWindow(window_); + } + + void window::swap() + { + SDL_GL_SwapWindow(window_); + } + + void window::show_cursor(bool show) + { + SDL_ShowCursor(show ? SDL_TRUE : SDL_FALSE); + SDL_SetRelativeMouseMode(show ? SDL_FALSE : SDL_TRUE); + } + + void window::vsync(bool on) + { + if (on) + { + // try adaptive vsync + if (SDL_GL_SetSwapInterval(-1) != 0) + { + // failed, try usual vsync then + SDL_GL_SetSwapInterval(1); + } + } + else + { + SDL_GL_SetSwapInterval(0); + } + } + + window::~window() + { + if (gl_context_) + SDL_GL_DeleteContext(gl_context_); + if (window_) + SDL_DestroyWindow(window_); + } + +}