-
-
Notifications
You must be signed in to change notification settings - Fork 8
feat(audio): add C++ realtime DSP core for v0.2.0 #322
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
f81d7d2
feat(audio): add native C++ DSP project
TheZupZup 3f8907c
feat(audio): define allocation-free DSP chain
TheZupZup 9126a65
feat(audio): implement EQ preamp and peak limiter DSP
TheZupZup b878379
feat(audio): expose stable C ABI for future mobile binding
TheZupZup 6f5390b
feat(audio): bridge DSP through C ABI
TheZupZup dcdf4fd
test(audio): cover DSP bypass EQ limiter and C ABI
TheZupZup d0e1d5b
perf(audio): add realtime DSP safety benchmark
TheZupZup 23a3329
fix(audio): include algorithm in realtime benchmark
TheZupZup 0182d3b
docs(audio): document native DSP contribution area
TheZupZup 3654059
ci(audio): build test and benchmark C++ DSP
TheZupZup 8638915
Merge branch 'main' into feature/v0.2.0-cpp-audio-dsp
TheZupZup File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| name: C++ audio DSP | ||
|
|
||
| on: | ||
| pull_request: | ||
| paths: | ||
| - 'native/linthra_audio/**' | ||
| - '.github/workflows/cpp-audio-dsp.yml' | ||
| push: | ||
| branches: [main] | ||
| paths: | ||
| - 'native/linthra_audio/**' | ||
| - '.github/workflows/cpp-audio-dsp.yml' | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| audio-dsp: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - name: Configure | ||
| run: >- | ||
| cmake -S native/linthra_audio -B build/linthra_audio | ||
| -DCMAKE_BUILD_TYPE=Release | ||
|
|
||
| - name: Build | ||
| run: cmake --build build/linthra_audio --parallel | ||
|
|
||
| - name: Unit and realtime tests | ||
| run: ctest --test-dir build/linthra_audio --output-on-failure | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| cmake_minimum_required(VERSION 3.16) | ||
| project(linthra_audio LANGUAGES CXX) | ||
|
|
||
| set(CMAKE_CXX_STANDARD 17) | ||
| set(CMAKE_CXX_STANDARD_REQUIRED ON) | ||
| set(CMAKE_CXX_EXTENSIONS OFF) | ||
|
|
||
| add_library(linthra_audio STATIC | ||
| src/dsp.cpp | ||
| src/c_api.cpp | ||
| ) | ||
|
|
||
| target_include_directories(linthra_audio | ||
| PUBLIC | ||
| ${CMAKE_CURRENT_SOURCE_DIR}/include | ||
| ) | ||
|
|
||
| target_compile_options(linthra_audio PRIVATE | ||
| $<$<CXX_COMPILER_ID:Clang,GNU>:-Wall -Wextra -Wpedantic -Werror> | ||
| ) | ||
|
|
||
| add_executable(linthra_audio_tests tests/dsp_test.cpp) | ||
| target_link_libraries(linthra_audio_tests PRIVATE linthra_audio) | ||
|
|
||
| add_executable(linthra_audio_benchmark tests/realtime_benchmark.cpp) | ||
| target_link_libraries(linthra_audio_benchmark PRIVATE linthra_audio) | ||
|
|
||
| enable_testing() | ||
| add_test(NAME linthra_audio_tests COMMAND linthra_audio_tests) | ||
| add_test(NAME linthra_audio_realtime_budget COMMAND linthra_audio_benchmark) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # linthra_audio (C++) | ||
|
|
||
| `linthra_audio` is Linthra's realtime DSP core. It gives C++/audio contributors a real part of the project to own while keeping the existing player stable until the mobile binding is ready. | ||
|
|
||
| ## Current DSP | ||
|
|
||
| - fixed-size, allocation-free processing state | ||
| - mono/stereo float PCM | ||
| - preamp | ||
| - up to 8 parametric peaking-EQ bands | ||
| - stereo-linked peak limiter with immediate attack and smooth release | ||
| - transparent bypass when processing is disabled | ||
| - C ABI for a future Android/JNI or Dart FFI boundary | ||
| - 48 kHz stereo realtime regression benchmark | ||
|
|
||
| The processing callback does not allocate memory and does not take locks. Configuration computes coefficients outside the callback. | ||
|
|
||
| ## Build and test | ||
|
|
||
| ```bash | ||
| cmake -S native/linthra_audio -B build/linthra_audio -DCMAKE_BUILD_TYPE=Release | ||
| cmake --build build/linthra_audio --parallel | ||
| ctest --test-dir build/linthra_audio --output-on-failure | ||
| ``` | ||
|
|
||
| ## Important boundary | ||
|
|
||
| This PR does not replace `just_audio` or silently alter playback. Linthra currently uses `just_audio`/the platform decoder pipeline, so inserting native PCM DSP safely requires a dedicated Android audio-processor binding. The C++ core is intentionally validated first; the binding can then be reviewed for audio focus, buffering, F-Droid reproducibility, and bypass correctness without also reviewing the DSP math. | ||
|
|
||
| ## Good contribution areas | ||
|
|
||
| C++ contributors can work on response tests, limiter behaviour, SIMD implementations, filter types, channel-layout support, loudness/peak analysis, and the future Android audio-processor bridge without needing Flutter UI knowledge. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| #pragma once | ||
|
|
||
| #include <array> | ||
| #include <cstddef> | ||
| #include <cstdint> | ||
|
|
||
| namespace linthra::audio { | ||
|
|
||
| constexpr std::size_t kMaxEqBands = 8; | ||
|
|
||
| struct PeakingEqBand { | ||
| bool enabled = false; | ||
| float frequency_hz = 1000.0F; | ||
| float gain_db = 0.0F; | ||
| float q = 1.0F; | ||
| }; | ||
|
|
||
| struct DspConfig { | ||
| float preamp_db = 0.0F; | ||
| std::array<PeakingEqBand, kMaxEqBands> bands{}; | ||
| std::size_t band_count = 0; | ||
| bool limiter_enabled = true; | ||
| float limiter_threshold_db = -0.3F; | ||
| float limiter_release_ms = 80.0F; | ||
| }; | ||
|
|
||
| /// Allocation-free, lock-free processing chain for mono/stereo floating-point | ||
| /// PCM. Configuration work may calculate coefficients; process() only performs | ||
| /// bounded arithmetic over already-prepared state. | ||
| class DspChain { | ||
| public: | ||
| explicit DspChain(float sample_rate) noexcept; | ||
|
|
||
| void configure(const DspConfig& config) noexcept; | ||
| void reset() noexcept; | ||
|
|
||
| /// Processes interleaved mono/stereo samples in place. Unsupported channel | ||
| /// counts are bypassed rather than risking a bad audio callback. | ||
| void process(float* interleaved, std::size_t frames, std::uint32_t channels) noexcept; | ||
|
|
||
| [[nodiscard]] float sample_rate() const noexcept { return sample_rate_; } | ||
| [[nodiscard]] DspConfig config() const noexcept { return config_; } | ||
|
|
||
| private: | ||
| struct Biquad { | ||
| float b0 = 1.0F; | ||
| float b1 = 0.0F; | ||
| float b2 = 0.0F; | ||
| float a1 = 0.0F; | ||
| float a2 = 0.0F; | ||
| std::array<float, 2> z1{}; | ||
| std::array<float, 2> z2{}; | ||
| bool enabled = false; | ||
|
|
||
| void configure_peaking(float sample_rate, const PeakingEqBand& band) noexcept; | ||
| void reset() noexcept; | ||
| float process(float sample, std::size_t channel) noexcept; | ||
| }; | ||
|
|
||
| float sample_rate_; | ||
| DspConfig config_{}; | ||
| std::array<Biquad, kMaxEqBands> biquads_{}; | ||
| float preamp_linear_ = 1.0F; | ||
| float limiter_threshold_linear_ = 1.0F; | ||
| float limiter_gain_ = 1.0F; | ||
| float limiter_release_step_ = 1.0F; | ||
| }; | ||
|
|
||
| } // namespace linthra::audio |
52 changes: 52 additions & 0 deletions
52
native/linthra_audio/include/linthra_audio/linthra_audio.h
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| #pragma once | ||
|
|
||
| #include <stddef.h> | ||
| #include <stdint.h> | ||
|
|
||
| #ifdef __cplusplus | ||
| extern "C" { | ||
| #endif | ||
|
|
||
| #define LINTHRA_AUDIO_MAX_EQ_BANDS 8 | ||
|
|
||
| typedef struct LinthraAudioEqBand { | ||
| int32_t enabled; | ||
| float frequency_hz; | ||
| float gain_db; | ||
| float q; | ||
| } LinthraAudioEqBand; | ||
|
|
||
| typedef struct LinthraAudioConfig { | ||
| float preamp_db; | ||
| LinthraAudioEqBand bands[LINTHRA_AUDIO_MAX_EQ_BANDS]; | ||
| size_t band_count; | ||
| int32_t limiter_enabled; | ||
| float limiter_threshold_db; | ||
| float limiter_release_ms; | ||
| } LinthraAudioConfig; | ||
|
|
||
| typedef struct LinthraAudioDsp LinthraAudioDsp; | ||
|
|
||
| /// Creates one DSP instance for a fixed output sample rate. | ||
| LinthraAudioDsp* linthra_audio_create(float sample_rate); | ||
|
|
||
| void linthra_audio_destroy(LinthraAudioDsp* dsp); | ||
|
|
||
| /// Replaces all DSP parameters. Safe to call between audio callbacks; the | ||
| /// eventual mobile binding is responsible for serializing configuration against | ||
| /// process calls so this core never needs a lock in the realtime path. | ||
| void linthra_audio_configure(LinthraAudioDsp* dsp, const LinthraAudioConfig* config); | ||
|
|
||
| void linthra_audio_reset(LinthraAudioDsp* dsp); | ||
|
|
||
| /// Processes interleaved float PCM in place. The current core supports mono or | ||
| /// stereo and bypasses unsupported channel layouts. | ||
| void linthra_audio_process( | ||
| LinthraAudioDsp* dsp, | ||
| float* interleaved, | ||
| size_t frames, | ||
| uint32_t channels); | ||
|
|
||
| #ifdef __cplusplus | ||
| } | ||
| #endif |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| #include "linthra_audio/linthra_audio.h" | ||
|
|
||
| #include <algorithm> | ||
| #include <new> | ||
|
|
||
| #include "linthra_audio/dsp.hpp" | ||
|
|
||
| struct LinthraAudioDsp { | ||
| explicit LinthraAudioDsp(float sample_rate) : chain(sample_rate) {} | ||
| linthra::audio::DspChain chain; | ||
| }; | ||
|
|
||
| extern "C" LinthraAudioDsp* linthra_audio_create(float sample_rate) { | ||
| if (!(sample_rate > 0.0F)) { | ||
| return nullptr; | ||
| } | ||
| return new (std::nothrow) LinthraAudioDsp(sample_rate); | ||
| } | ||
|
|
||
| extern "C" void linthra_audio_destroy(LinthraAudioDsp* dsp) { | ||
| delete dsp; | ||
| } | ||
|
|
||
| extern "C" void linthra_audio_configure( | ||
| LinthraAudioDsp* dsp, | ||
| const LinthraAudioConfig* config) { | ||
| if (dsp == nullptr || config == nullptr) { | ||
| return; | ||
| } | ||
|
|
||
| linthra::audio::DspConfig native{}; | ||
| native.preamp_db = config->preamp_db; | ||
| native.band_count = std::min( | ||
| config->band_count, | ||
| static_cast<size_t>(LINTHRA_AUDIO_MAX_EQ_BANDS)); | ||
| native.limiter_enabled = config->limiter_enabled != 0; | ||
| native.limiter_threshold_db = config->limiter_threshold_db; | ||
| native.limiter_release_ms = config->limiter_release_ms; | ||
|
|
||
| for (size_t index = 0; index < native.band_count; ++index) { | ||
| native.bands[index] = linthra::audio::PeakingEqBand{ | ||
| config->bands[index].enabled != 0, | ||
| config->bands[index].frequency_hz, | ||
| config->bands[index].gain_db, | ||
| config->bands[index].q, | ||
| }; | ||
| } | ||
|
|
||
| dsp->chain.configure(native); | ||
| } | ||
|
|
||
| extern "C" void linthra_audio_reset(LinthraAudioDsp* dsp) { | ||
| if (dsp != nullptr) { | ||
| dsp->chain.reset(); | ||
| } | ||
| } | ||
|
|
||
| extern "C" void linthra_audio_process( | ||
| LinthraAudioDsp* dsp, | ||
| float* interleaved, | ||
| size_t frames, | ||
| uint32_t channels) { | ||
| if (dsp != nullptr) { | ||
| dsp->chain.process(interleaved, frames, channels); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.