diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 988b3dc..a5508f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,95 +1,51 @@ -name: ci-pr -on: [pull_request, workflow_dispatch] +name: Build +on: [push, pull_request] +defaults: + run: + shell: pwsh jobs: - x64-linux-gcc: - runs-on: ubuntu-latest + build: + name: ${{ matrix.platform.name }} ${{ matrix.config.name }} + runs-on: ${{ matrix.platform.os }} + env: + CMAKE_BUILD_PARALLEL_LEVEL: 4 + strategy: + fail-fast: false + matrix: + platform: + - { name: "Windows VS2022", os: windows-2022, flags: "" } + - { name: "Linux GCC", os: ubuntu-latest, flags: "" } + - { name: "Linux Clang", os: ubuntu-latest, flags: "-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++" } + - { name: "macOS AppleClang", os: macos-latest, flags: "" } + config: + - { name: "Static", flags: "-DBUILD_SHARED_LIBS=FALSE" } steps: - - uses: actions/checkout@v4 - - name: init - run: uname -m; sudo apt update -yqq && sudo apt install -yqq ninja-build mesa-common-dev libwayland-dev libxkbcommon-dev wayland-protocols extra-cmake-modules - - name: configure - run: cmake -S . --preset=ninja-gcc -B build -DGLFW_BUILD_X11=OFF -DJUKE_USE_LIBXMP=OFF -DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14 - - name: build debug - run: cmake --build build --config=Debug -- -v - - name: build release - run: cmake --build build --config=Release -- -v - - name: test debug - run: cd build && ctest -V -C Debug - - name: test release - run: cd build && ctest -V -C Release - x64-linux-clang: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: init - run: uname -m; sudo apt update -yqq && sudo apt install -yqq ninja-build clang-19 mesa-common-dev libwayland-dev libxkbcommon-dev wayland-protocols extra-cmake-modules - - name: configure - run: cmake -S . --preset=ninja-clang -B build -DGLFW_BUILD_X11=OFF -DJUKE_USE_LIBXMP=OFF -DCMAKE_C_COMPILER=clang-19 -DCMAKE_CXX_COMPILER=clang++-19 - - name: build debug - run: cmake --build build --config=Debug -- -v - - name: build release - run: cmake --build build --config=Release -- -v - - name: test debug - run: cd build && ctest -V -C Debug - - name: test release - run: cd build && ctest -V -C Release - arm64-linux-gcc: - runs-on: ubuntu-24.04-arm - steps: - - uses: actions/checkout@v4 - - name: init - run: uname -m; sudo apt update -yqq && sudo apt install -yqq ninja-build mesa-common-dev libwayland-dev libxkbcommon-dev wayland-protocols extra-cmake-modules - - name: configure - run: cmake -S . --preset=ninja-gcc -B build -DGLFW_BUILD_X11=OFF -DJUKE_USE_LIBXMP=OFF -DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14 - - name: build debug - run: cmake --build build --config=Debug -- -v - - name: build release - run: cmake --build build --config=Release -- -v - - name: test debug - run: cd build && ctest -V -C Debug - - name: test release - run: cd build && ctest -V -C Release - arm64-linux-clang: - runs-on: ubuntu-24.04-arm - steps: - - uses: actions/checkout@v4 - - name: init - run: uname -m; sudo apt update -yqq && sudo apt install -yqq ninja-build clang-19 mesa-common-dev libwayland-dev libxkbcommon-dev wayland-protocols extra-cmake-modules - - name: configure - run: cmake -S . --preset=ninja-clang -B build -DGLFW_BUILD_X11=OFF -DJUKE_USE_LIBXMP=OFF -DCMAKE_C_COMPILER=clang-19 -DCMAKE_CXX_COMPILER=clang++-19 - - name: build debug - run: cmake --build build --config=Debug -- -v - - name: build release - run: cmake --build build --config=Release -- -v - - name: test debug - run: cd build && ctest -V -C Debug - - name: test release - run: cd build && ctest -V -C Release - x64-windows-vs22: - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - name: configure - run: cmake -S . --preset=vs22 -B build -DJUKE_USE_LIBXMP=OFF - - name: build debug - run: cmake --build build --config=Debug --parallel - - name: build release - run: cmake --build build --config=Release --parallel - - name: test debug - run: cd build && ctest -V -C Debug - - name: test release - run: cd build && ctest -V -C Release - x64-windows-clang: - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - name: configure - run: cmake -S . --preset=ninja-clang -B build -DJUKE_USE_LIBXMP=OFF - - name: build debug - run: cmake --build build --config=Debug -- -v - - name: build release - run: cmake --build build --config=Release -- -v - - name: test debug - run: cd build && ctest -V -C Debug - - name: test release - run: cd build && ctest -V -C Release + - name: Checkout + uses: actions/checkout@v4 + - name: Install Dependencies + run: | + if ($env:RUNNER_OS -eq 'Windows') { + choco install ninja -y + } elseif ($env:RUNNER_OS -eq 'Linux') { + sudo apt-get update + sudo apt-get install -y ninja-build libxrandr-dev libxcursor-dev libxi-dev libudev-dev libflac-dev libvorbis-dev libgl1-mesa-dev libegl1-mesa-dev libfreetype-dev + } elseif ($env:RUNNER_OS -eq 'macOS') { + brew install ninja + } else { + Write-Error "Unsupported OS: $env:RUNNER_OS" + exit 1 + } + - name: Configure + run: | + if ($env:RUNNER_OS -eq 'Windows') { + . "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\Launch-VsDevShell.ps1" -Arch amd64 -HostArch amd64 + Set-Location $env:GITHUB_WORKSPACE + } + cmake -B build -G "Ninja Multi-Config" ${{ matrix.platform.flags }} ${{ matrix.config.flags }} + - name: Build + run: | + if ($env:RUNNER_OS -eq 'Windows') { + . "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\Launch-VsDevShell.ps1" -Arch amd64 -HostArch amd64 + Set-Location $env:GITHUB_WORKSPACE + } + cmake --build build --config Release \ No newline at end of file diff --git a/app/src/MediaPlayer.cpp b/app/src/MediaPlayer.cpp index 7071205..a905f42 100644 --- a/app/src/MediaPlayer.cpp +++ b/app/src/MediaPlayer.cpp @@ -26,6 +26,8 @@ void MediaPlayer::handle_input() { static auto gain{1.f}; static auto pan{0.f}; static auto pitch{1.f}; + static auto lo_cutoff{sample_rate_v}; + static auto hi_cutoff{0.f}; static float pos[] = {0.f, 0.f, 0.f}; /* Metadata & Info */ @@ -67,6 +69,11 @@ void MediaPlayer::handle_input() { m_jukebox.set_position(capo::Vec3f{pos[0], pos[1], pos[2]}); if (ImGui::Button("reset")) { pos[0] = pos[1] = pos[2] = 0.f; } } + + ImGui::SliderFloat("Low Pass", &lo_cutoff, 0.f, sample_rate_v, "%.1f", ImGuiSliderFlags_Logarithmic); + m_jukebox.set_cutoff(FilterType::low, lo_cutoff, sample_rate_v); + ImGui::SliderFloat("High Pass:", &hi_cutoff, 0.f, sample_rate_v, "%.1f", ImGuiSliderFlags_Logarithmic); + m_jukebox.set_cutoff(FilterType::high, hi_cutoff, sample_rate_v); ImGui::TreePop(); } } diff --git a/library/include/juke/core/AudioFile.hpp b/library/include/juke/core/AudioFile.hpp index 3fc9df1..42967fb 100644 --- a/library/include/juke/core/AudioFile.hpp +++ b/library/include/juke/core/AudioFile.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include namespace juke { @@ -10,6 +11,7 @@ class AudioFile : public IMediaFile { explicit AudioFile(std::filesystem::path const& path); bool bind_to(capo::ISource& source) final { return source.bind_to(m_buffer); } + void set_cutoff(FilterType type, float to_cutoff, float sample_rate) override {} private: std::shared_ptr m_buffer{std::make_shared()}; diff --git a/library/include/juke/core/Common.hpp b/library/include/juke/core/Common.hpp new file mode 100644 index 0000000..e87ead4 --- /dev/null +++ b/library/include/juke/core/Common.hpp @@ -0,0 +1,10 @@ + +#pragma once + +namespace juke { + +enum class FilterType : std::uint8_t { high, low }; + +constexpr auto sample_rate_v = 44100.0f; + +} // namespace juke diff --git a/library/include/juke/core/MediaFile.hpp b/library/include/juke/core/MediaFile.hpp index 6852a83..4bfc9c0 100644 --- a/library/include/juke/core/MediaFile.hpp +++ b/library/include/juke/core/MediaFile.hpp @@ -2,6 +2,7 @@ #pragma once #include +#include #include namespace juke { @@ -11,6 +12,7 @@ class IMediaFile : public capo::Polymorphic { explicit IMediaFile(std::filesystem::path const& path) : m_filename(path.filename().string()) {} virtual bool bind_to(capo::ISource& source) = 0; + virtual void set_cutoff(FilterType type, float to_cutoff, float sample_rate) = 0; [[nodiscard]] std::string const& get_filename() const { return m_filename; } diff --git a/library/include/juke/core/XMFile.hpp b/library/include/juke/core/XMFile.hpp index 1dd1157..bf9ecb5 100644 --- a/library/include/juke/core/XMFile.hpp +++ b/library/include/juke/core/XMFile.hpp @@ -13,6 +13,8 @@ class XMFile : public IMediaFile { bool bind_to(capo::ISource& source) final { return source.bind_to(m_stream); } + void set_cutoff(FilterType type, float to_cutoff, float sample_rate) override { m_stream->set_cutoff(type, to_cutoff, sample_rate); } + private: std::shared_ptr m_stream{}; }; diff --git a/library/include/juke/core/XMStream.hpp b/library/include/juke/core/XMStream.hpp index b6e04a5..6f81faf 100644 --- a/library/include/juke/core/XMStream.hpp +++ b/library/include/juke/core/XMStream.hpp @@ -2,6 +2,9 @@ #include #include +#include +#include +#include #include #include @@ -13,6 +16,8 @@ class XMStream : public capo::IStreamPipe { explicit XMStream(std::filesystem::path const& path); + void set_cutoff(FilterType type, float to_cutoff, float sample_rate); + private: struct Impl; struct Deleter { @@ -25,6 +30,9 @@ class XMStream : public capo::IStreamPipe { auto set_looping(bool looping) -> bool final; std::unique_ptr m_impl{}; + + LowPassFilter m_lowpass; + HighPassFilter m_highpass; }; } // namespace juke diff --git a/library/include/juke/effect/HighPassFilter.hpp b/library/include/juke/effect/HighPassFilter.hpp new file mode 100644 index 0000000..28395db --- /dev/null +++ b/library/include/juke/effect/HighPassFilter.hpp @@ -0,0 +1,37 @@ + +#pragma once + +#include + +namespace juke { + +class HighPassFilter { + public: + HighPassFilter(float cutoff_hz, float sample_rate) { set_cutoff(cutoff_hz, sample_rate); } + + void set_cutoff(float cutoff_hz, float sample_rate) { + if (cutoff_hz <= 0.0f) { + m_alpha = 0.0f; + return; + } + + auto const dt = 1.0f / sample_rate; + auto const rc = 1.0f / (2.0f * std::numbers::pi * cutoff_hz); + m_alpha = rc / (rc + dt); + } + + float process(float input) { + if (m_alpha == 0.0f) return input; + auto output = m_alpha * (m_prev_out + input - m_prev_in); + m_prev_in = input; + m_prev_out = output; + return output; + } + + private: + float m_prev_in{}; + float m_prev_out{}; + float m_alpha{}; +}; + +} // namespace juke diff --git a/library/include/juke/effect/LowPassFilter.hpp b/library/include/juke/effect/LowPassFilter.hpp new file mode 100644 index 0000000..bb14b61 --- /dev/null +++ b/library/include/juke/effect/LowPassFilter.hpp @@ -0,0 +1,28 @@ + +#pragma once + +#include + +namespace juke { + +class LowPassFilter { + public: + LowPassFilter(float cutoff_hz, float sample_rate) { set_cutoff(cutoff_hz, sample_rate); } + + void set_cutoff(float cutoff_hz, float sample_rate) { + auto const rc = 1.0f / (2.0f * std::numbers::pi * cutoff_hz); + auto const dt = 1.0f / sample_rate; + m_alpha = dt / (rc + dt); + } + + float process(float in) { + m_prev_out = m_alpha * in + (1.0f - m_alpha) * m_prev_out; + return m_prev_out; + } + + private: + float m_prev_out{}; + float m_alpha{}; +}; + +} // namespace juke diff --git a/library/include/juke/juke.hpp b/library/include/juke/juke.hpp index dcff637..9630cf6 100644 --- a/library/include/juke/juke.hpp +++ b/library/include/juke/juke.hpp @@ -28,6 +28,9 @@ class Jukebox { /// \brief Stop audio and set cursor to beginning. This does not work for XM format. void stop(); + // utility functions + void set_cutoff(FilterType type, float cutoff_hz, float sample_rate); + /* capo API wrapper functions */ // setters @@ -39,7 +42,7 @@ class Jukebox { void set_position(capo::Vec3f pos) { m_source->set_position(pos); } void set_spatialized(bool spatialized) { m_source->set_spatialized(spatialized); } - //getters + // getters [[nodiscard]] auto get_gain() const -> float { return m_source->get_gain(); } [[nodiscard]] auto get_pitch() const -> float { return m_source->get_pitch(); } [[nodiscard]] auto get_pan() const -> float { return m_source->get_pan(); } diff --git a/library/src/core/XMStream.cpp b/library/src/core/XMStream.cpp index af1a76c..08ffe12 100644 --- a/library/src/core/XMStream.cpp +++ b/library/src/core/XMStream.cpp @@ -24,7 +24,7 @@ void XMStream::Deleter::operator()(Impl* ptr) const noexcept { std::default_delete{}(ptr); } -XMStream::XMStream(std::filesystem::path const& path) : m_impl(new Impl) { +XMStream::XMStream(std::filesystem::path const& path) : m_impl(new Impl), m_lowpass{sample_rate_v, sample_rate_v}, m_highpass{0.f, sample_rate_v} { if (!m_impl->load_module(path)) { throw MediaError{"Failed to open XM file: " + path.generic_string()}; } xmp_start_player(m_impl->context, static_cast(capo::Buffer::sample_rate_v), 0); } @@ -43,8 +43,14 @@ void XMStream::push_samples(std::vector& out) { // wrap buffer into strongly-typed span for easy iteration and normalization. auto const src = std::span{static_cast(frame_info.buffer), static_cast(frame_info.buffer_size) / sizeof(std::int16_t)}; // need to normalize each u16 sample [-32k,32k] to f32 [-1,1]. + static constexpr auto sample_max_v = static_cast(std::numeric_limits::max()); - for (std::int16_t const sample : src) { out.push_back(static_cast(sample) / sample_max_v); } + for (std::int16_t const sample : src) { + auto normalized = static_cast(sample) / sample_max_v; + auto high_passed = m_highpass.process(normalized); + auto band_passed = m_lowpass.process(high_passed); + out.push_back(band_passed); + } } auto XMStream::set_looping(bool const looping) -> bool { @@ -52,6 +58,14 @@ auto XMStream::set_looping(bool const looping) -> bool { return true; } +void XMStream::set_cutoff(FilterType type, float to_cutoff, float sample_rate) { + switch (type) { + case FilterType::high: m_highpass.set_cutoff(to_cutoff, sample_rate); break; + case FilterType::low: m_lowpass.set_cutoff(to_cutoff, sample_rate); break; + default: break; + } +} + #else void XMStream::Deleter::operator()(Impl* /*ptr*/) const noexcept {} diff --git a/library/src/juke.cpp b/library/src/juke.cpp index 7692bf9..e5906a8 100644 --- a/library/src/juke.cpp +++ b/library/src/juke.cpp @@ -48,4 +48,6 @@ void Jukebox::stop() { m_source->set_cursor({}); // seek to start. this will do nothing for XM streams } +void Jukebox::set_cutoff(FilterType type, float cutoff_hz, float sample_rate) { m_media_file->set_cutoff(type, cutoff_hz, sample_rate); } + } // namespace juke