Skip to content

feat(sound): add Vehicle sound engine#438

Merged
ripel2 merged 13 commits into
mainfrom
vehicle-sound
Jan 25, 2026
Merged

feat(sound): add Vehicle sound engine#438
ripel2 merged 13 commits into
mainfrom
vehicle-sound

Conversation

@MasterLaplace

@MasterLaplace MasterLaplace commented Jan 24, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Sound example with VS Code run/debug and asset copying; engine-backed audio with pitch control, synchronized looping, and runtime playback controls.
    • Vehicle RPM telemetry resource and periodic RPM update for real-time monitoring.
  • Bug Fixes / Reliability

    • Safer audio startup/teardown to avoid uninitialized cleanup.
    • Deferred audio callback error reporting surfaced during regular updates.
  • Documentation

    • README documenting enabling, building, and debugging the Sound example.

✏️ Tip: You can customize this high-level summary in your review settings.

@MasterLaplace MasterLaplace self-assigned this Jan 24, 2026
@MasterLaplace MasterLaplace added the enhancement New feature or request label Jan 24, 2026
@coderabbitai

coderabbitai Bot commented Jan 24, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@MasterLaplace has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 14 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds a Sound usage example and xmake script; introduces Physics vehicle telemetry and a VehicleRPMUpdate system; and extends SoundManager with engine-backed playback, pitch control, guarded init/teardown, deferred audio-callback error reporting, and move semantics.

Changes

Cohort / File(s) Summary
Sound Usage Example
examples/sound_usage/README.md, examples/sound_usage/src/main.cpp, examples/sound_usage/xmake.lua
New example and build script: README with VS Code/run instructions, example app that registers Sound plugin and plays a sound, and xmake script that builds, copies assets, and configures debug/release options.
Physics Vehicle Telemetry
src/plugin/physics/src/Physics.hpp, src/plugin/physics/src/plugin/PhysicsPlugin.cpp, src/plugin/physics/src/resource/VehicleTelemetry.hpp, src/plugin/physics/src/system/VehicleRPMUpdate.hpp, src/plugin/physics/src/system/VehicleRPMUpdate.cpp
Adds Physics::Resource::VehicleTelemetry (SetRPM/GetRPM/Clear), implements Physics::System::VehicleRPMUpdate to sample controller RPMs, and registers the telemetry resource and RPM update in the physics plugin.
Sound Manager Engine Integration
src/plugin/sound/src/resource/SoundManager.hpp, src/plugin/sound/src/resource/SoundManager.cpp, src/plugin/sound/src/plugin/PluginSound.cpp, src/plugin/sound/src/system/InitSound.hpp, src/plugin/sound/src/system/InitSound.cpp
Adds lazy engine/device init flags, per-sound engine state, deferred callback error flags and a ReportAudioErrors system, enhanced callback mixing/format handling, guarded cleanup, public SetPitch(...), and updated move semantics for SoundManager.

Sequence Diagram(s)

sequenceDiagram
    participant Core as Engine::Core
    participant SM as SoundManager
    participant Engine as Miniaudio Engine
    participant Device as Audio Device

    Core->>SM: Play(soundName)
    alt engine-backed playback
        SM->>Engine: lazy init engine/device
        Engine-->>SM: init success
        SM->>Device: create/start ma_sound for soundName
        Device-->>SM: started
        SM-->>Core: playback started
    else decoder fallback
        SM->>SM: start PCM decoder playback
        SM-->>Core: playback started
    end

    Core->>SM: SetPitch(soundName, pitch)
    SM->>Engine: ensure engine initialized
    Engine-->>SM: ready
    SM->>Device: update ma_sound pitch
    SM-->>Core: pitch applied

    Note over Engine,SM: Audio thread may set callback error flags
    Core->>SM: ReportAudioErrors() (Update)
    SM-->>Core: logs deferred callback errors
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • ripel2
  • Miou-zora

Poem

🐰 I hopped through code and found a beat,

Assets copied, engines warmed to greet,
RPMs counted, pitches tuned with care,
Sounds and wheels twirl through the air,
A rabbit applauds the build and the beat 🎶

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(sound): add Vehicle sound engine' aligns with the main changes, which include Sound plugin enhancements (engine-backed audio, pitch control, error reporting) and physics plugin integration (VehicleTelemetry, VehicleRPMUpdate for RPM tracking). However, the title is somewhat narrow as it emphasizes 'Vehicle sound engine' while the changeset encompasses broader sound system improvements and physics-sound integration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@MasterLaplace MasterLaplace marked this pull request as ready for review January 25, 2026 09:32
@MasterLaplace MasterLaplace requested a review from a team January 25, 2026 09:33

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/plugin/sound/src/resource/SoundManager.hpp (2)

278-285: UnregisterSound must uninit engine-backed sounds to prevent resource leaks.

The destructor properly cleans up engineSound when hasEngineSound is true (lines 10–11 of SoundManager.cpp), but UnregisterSound skips this cleanup. According to miniaudio documentation, ma_sound_uninit() is required for every successful ma_sound_init_from_file() call. Mirror the destructor cleanup pattern before erasing:

🔧 Suggested fix
         if (it != _soundsToPlay.end())
         {
+            if (it->second.hasEngineSound)
+                ma_sound_uninit(&it->second.engineSound);
             ma_decoder_uninit(&it->second.decoder);
             _soundsToPlay.erase(it);
         }

326-343: Engine-backed sounds must seek to start when stopped.

In miniaudio, ma_sound_stop() does not reset the playback position—the sound must be explicitly rewound via ma_sound_seek_to_pcm_frame(). The decoder path correctly seeks to frame 0, but the engine path does not, causing the next Play() to resume from the stopped position rather than the beginning.

🔧 Suggested fix
             if (snd.usingEngine && snd.hasEngineSound)
             {
                 ma_sound_stop(&snd.engineSound);
+                ma_sound_seek_to_pcm_frame(&snd.engineSound, 0);
                 ma_decoder_seek_to_pcm_frame(&snd.decoder, 0);
             }
🤖 Fix all issues with AI agents
In `@src/plugin/physics/src/system/VehicleRPMUpdate.cpp`:
- Around line 29-31: The downcast using static_cast on
internal.vehicleConstraint->GetController() is unsafe given RTTI is disabled in
Jolt; either explicitly document the invariant (that VehicleConstraint instances
are always constructed with JPH::WheeledVehicleControllerSettings so
GetController() returns a JPH::WheeledVehicleController or nullptr) by adding a
clear comment near the cast in VehicleRPMUpdate.cpp referencing
VehicleConstraint and WheeledVehicleController, or if unsafe downcasts are
possible, enable RTTI via CPP_RTTI_ENABLED and the matching compiler flags or
replace the cast with a Jolt-native type-check (per Jolt API) before casting;
update VehicleSystem.cpp references if you change the build or type-checking
approach so the contract is obvious.

In `@src/plugin/sound/src/resource/SoundManager.hpp`:
- Around line 414-419: The doc comment for SetPitch contradicts the
implementation: the `@note` says pitch is "currently disabled" but the SetPitch
method in SoundManager.hpp actively applies pitch; update the comment to reflect
actual behavior by removing or revising the `@note` so it states that pitch is
applied (and any known limitations) and ensure the `@brief/`@param remain accurate
for the SetPitch method.
- Around line 130-146: The audio callback currently calls
ma_decoder_init_file/ma_decoder_uninit (creating tempDecoder) which does
filesystem I/O and allocations on the audio thread; move decoder initialization
out of the callback and into your sound loading path so each Sound has a
pre-initialized decoder (e.g., replace per-callback tempDecoder with a
persistent ma_decoder stored on the Sound object and initialized by the loader),
then in the callback only call ma_decoder_read_pcm_frames and
ma_decoder_seek_to_pcm_frame on the existing sound.decoder (or use the resource
manager's async streaming API) to avoid any
ma_decoder_init_file/ma_decoder_uninit or file I/O inside the real-time
callback.
- Around line 184-200: The output conversion only handles ma_format_f32 and
ma_format_s16 so when pDevice->playback.format is ma_format_unknown or any other
native format (e.g., ma_format_s24, ma_format_s32, ma_format_u8) the buffer is
left silent; either force the device to a supported format during initialization
(set playback.format to ma_format_f32 or ma_format_s16 when creating/opening
pDevice) or extend the conversion in the audio callback that uses pOutput,
mixBuffer and totalSamples to explicitly handle all ma_format_* cases (add
branches for ma_format_s24, ma_format_s32, ma_format_u8, etc., converting the
float mixBuffer samples via proper scaling and clipping before storing into
pOutput) and ensure the ma_format_unknown case is resolved to a concrete format
before writing.
🧹 Nitpick comments (1)
src/plugin/physics/src/resource/VehicleTelemetry.hpp (1)

17-43: Consider cleanup strategy for destroyed vehicles.
If entity IDs aren’t recycled, _rpmByEntity can grow over time. Consider clearing on entity-destroy events or adding a ClearAll() helper for world resets.

Comment thread src/plugin/physics/src/system/VehicleRPMUpdate.cpp
Comment thread src/plugin/sound/src/resource/SoundManager.hpp Outdated
Comment thread src/plugin/sound/src/resource/SoundManager.hpp Outdated
Comment thread src/plugin/sound/src/resource/SoundManager.hpp
@MasterLaplace MasterLaplace changed the title Vehicle sound feat(sound): add Vehicle sound engine Jan 25, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/plugin/sound/src/resource/SoundManager.hpp (1)

274-281: UnregisterSound leaks engine-backed resources.

When hasEngineSound is true, ma_sound_uninit isn't called before removing the sound, leaving the engine sound node uninitialized and causing a resource leak. The destructor properly handles this cleanup (SoundManager.cpp lines 10–11), but the inline UnregisterSound method does not.

🔧 Proposed fix
    if (it != _soundsToPlay.end())
    {
+       if (it->second.hasEngineSound)
+       {
+           ma_sound_uninit(&it->second.engineSound);
+       }
        ma_decoder_uninit(&it->second.decoder);
        _soundsToPlay.erase(it);
    }
🤖 Fix all issues with AI agents
In `@src/plugin/sound/src/resource/SoundManager.hpp`:
- Around line 118-150: The audio callback in SoundManager is performing
blocking/allocating logging/formatting (calls to Log::Error and fmt::format when
handling ma_decoder_init_file/ma_decoder_read_pcm_frames errors for entries in
_soundsToPlay and tempDecoder), which can cause dropouts; replace those direct
Log::Error/fmt::format calls inside the callback with a non-blocking mechanism
(e.g., push a small error code/message id into a lock-free queue or set an
atomic flag/struct with the decoder name and ma_result), and perform the actual
formatted logging on a non-audio thread that reads that queue/atomics; ensure
you update all occurrences (the error branch after
ma_decoder_init_file/ma_decoder_read_pcm_frames and any other
Log::Error/fmt::format calls in the callback) to use the deferred-reporting
mechanism and keep the callback allocation-free and non-blocking.
- Around line 331-339: The engine sound isn't being rewound after stopping, so
update the branch handling snd.usingEngine && snd.hasEngineSound to explicitly
seek the engine sound back to frame 0 after calling ma_sound_stop; i.e., after
ma_sound_stop(&snd.engineSound) call invoke the appropriate seek on
snd.engineSound (e.g., ma_sound_seek_to_pcm_frame or equivalent) to reset its
playback cursor so subsequent Play starts from the beginning while leaving the
existing ma_decoder_seek_to_pcm_frame(&snd.decoder, 0) behavior intact.
♻️ Duplicate comments (3)
src/plugin/sound/src/resource/SoundManager.hpp (3)

130-146: Decoder init/uninit in the audio callback (duplicate of earlier review).
Same concern as previously raised: ma_decoder_init_file/ma_decoder_uninit inside the callback performs I/O/allocations and isn’t real‑time safe.


181-195: Format handling still ignores non‑f32/s16 devices (duplicate of earlier review).
With playback.format = ma_format_unknown, devices can choose s24/s32/u8; the conversion path only handles f32/s16.

Also applies to: 208-208


410-415: SetPitch note contradicts behavior (duplicate of earlier review).
The @note says pitch is disabled, but the method applies pitch.

Comment thread src/plugin/sound/src/resource/SoundManager.hpp Outdated
Comment thread src/plugin/sound/src/resource/SoundManager.hpp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@src/plugin/sound/src/resource/SoundManager.hpp`:
- Around line 132-136: The doc comment above the mixing implementation in
SoundManager (the description that begins "Mixes all active sounds with volume
into a temporary f32 buffer...") is stale: update it to list all supported
output formats (f32, s16, s32, u8) instead of only f32 and s16 and adjust the
note about clamping to indicate integer formats are converted and clamped
appropriately (e.g., to [-1.0,1.0] before scaling to integer ranges) so the
documentation matches the switch handling in the mixing/conversion code in
SoundManager.hpp.
- Around line 86-122: The move assignment operator currently overwrites _device
and _engine without releasing any live resources, causing leaks; before copying
from other in SoundManager::operator=(SoundManager &&other) you must release the
current instance's initialized resources (check _deviceInit and _engineInit)
using the same cleanup logic used in the destructor (e.g., call the existing
cleanup/uninit methods or perform the device/engine shutdown and reset), set
those init flags to false, then proceed to assign _device, _engine, _deviceInit,
and _engineInit from other and finally clear other's flags as already done.
- Around line 487-533: SetPitch currently initializes an engine-backed ma_sound
via ma_sound_init_from_file which always starts at frame 0 causing an audible
jump if the decoder was already playing; after creating the engine sound (in the
ma_sound_init_from_file success branch, before ma_sound_start) query the
decoder's current playback cursor with
ma_decoder_get_cursor_in_pcm_frames(&snd.decoder) (or equivalent) and call
ma_sound_seek_pcm_frames(&snd.engineSound, cursor) to sync the engine sound to
the decoder position (use snd.decoder, snd.engineSound,
ma_decoder_get_cursor_in_pcm_frames, ma_sound_seek_pcm_frames) before starting
playback so there is no discontinuity.
- Around line 327-335: RegisterSound currently initializes ma_decoder_config
with ma_format_unknown, causing decoder outputs to potentially be non-float
while data_callback reads into std::array<float, 4096 * 2>; change the decoder
config to use ma_format_f32 so the decoder produces 32-bit float samples that
match the float buffer. Specifically, update the ma_decoder_config
initialization in RegisterSound (the variable decoderConfig used to call
ma_decoder_init_file for sound.decoder) to ma_format_f32 and ensure any
downstream assumptions in data_callback and sound.decoderInitialized remain
consistent with float samples.

Comment thread src/plugin/sound/src/resource/SoundManager.hpp
Comment thread src/plugin/sound/src/resource/SoundManager.hpp Outdated
Comment thread src/plugin/sound/src/resource/SoundManager.hpp Outdated
Comment thread src/plugin/sound/src/resource/SoundManager.hpp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/plugin/sound/src/resource/SoundManager.hpp (1)

355-362: UnregisterSound must also uninitialize engine-backed sounds.

SetPitch lazily initializes engineSound via ma_sound_init_from_file, but UnregisterSound only uninitializes the decoder, leaking the engine sound resource. The destructor properly cleans up both engineSound and decoder; UnregisterSound should do the same.

Suggested fix
     if (it != _soundsToPlay.end())
     {
+        if (it->second.hasEngineSound)
+            ma_sound_uninit(&it->second.engineSound);
         ma_decoder_uninit(&it->second.decoder);
         _soundsToPlay.erase(it);
     }
🤖 Fix all issues with AI agents
In `@examples/sound_usage/README.md`:
- Line 5: The issue is that examples/sound_usage/xmake.lua contains
includes("../../xmake.lua") which forces a manual edit to avoid circular
includes when the root xmake.lua already pulls in example xmake files; remove
that hard-coded include and instead make the example xmake.lua self-contained by
guarding its include or exporting a flag the root xmake.lua can set.
Specifically, in examples/sound_usage/xmake.lua remove or wrap the
includes("../../xmake.lua") with a conditional check (e.g., only include when
not already included or when a standalone build flag is present), and update the
root xmake.lua to conditionally include example xmake files or set the
standalone flag before including so examples do not need to change; apply the
same pattern to the other eight example xmake.lua files.

In `@src/plugin/sound/src/resource/SoundManager.hpp`:
- Around line 104-128: The move-assignment operator for SoundManager currently
overwrites _soundsToPlay and causes leaked miniaudio resources; before
assigning/moving over _soundsToPlay, iterate the existing _soundsToPlay map and
explicitly uninitialize each Sound's miniaudio members (e.g., call
ma_sound_uninit(&sound.ma_sound) and ma_decoder_uninit(&sound.ma_decoder) or
invoke an existing Sound::shutdown/uninit helper) and then clear the map, then
proceed with moving other members and assigning the new _soundsToPlay; do this
cleanup inside SoundManager::operator=(SoundManager&&) before _soundsToPlay =
std::move(other._soundsToPlay).
♻️ Duplicate comments (1)
src/plugin/sound/src/resource/SoundManager.hpp (1)

215-253: Still missing s24 handling when format is unknown.

With _deviceConfig.playback.format = ma_format_unknown, miniaudio can choose s24 on some devices. The switch doesn’t handle s24, so you’ll hit ERROR_UNKNOWN_FORMAT and output silence. Either force a supported format or add s24 conversion.

✅ Simplest fix: force a known format
-        _deviceConfig.playback.format = ma_format_unknown;
+        _deviceConfig.playback.format = ma_format_f32; // or ma_format_s16

Also applies to: 264-265

🧹 Nitpick comments (2)
examples/sound_usage/README.md (2)

7-14: Consider integrating the option into the root xmake.lua directly.

Requiring users to manually add build configuration to the root file is error-prone and doesn't follow best practices. If this example is part of the repository, the root xmake.lua should already contain the option (possibly disabled by default).

💡 Suggestion

Add the option directly to the root xmake.lua in this PR so users only need to enable it via the command-line flag:

option("with_sound_usage_example", {default = false, description = "Enable Sound Usage Example"})

if has_config("with_sound_usage_example") then
    includes("examples/sound_usage/xmake.lua")
end

Then users would only need step 3 (adding --with_sound_usage_example=y to their settings).


18-18: Clarify the mode-switching instruction.

The phrase "Switch the mode" is vague. Consider being more explicit about what modes exist (e.g., debug/release) and why this step forces a setup update.

📝 Suggested clarification
-4. Switch the mode to force the update of the setup. (debug to release or release to debug)
+4. Switch the build mode (e.g., from debug to release or vice versa) in VS Code to trigger xmake reconfiguration and apply the new option.

Comment thread examples/sound_usage/README.md
Comment thread src/plugin/sound/src/resource/SoundManager.hpp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/plugin/sound/src/resource/SoundManager.hpp`:
- Around line 152-156: The format-conversion/mixing code uses plain += into
pOutput which can overflow when custom callbacks pre-fill the buffer; update the
mixing logic (the block handling f32, s32, s16, u8 conversions that follows the
custom-callback invocation and the pOutput buffer writes) to perform saturating
addition: for s32/s16/u8 read the existing destination sample, add the converted
sample using saturating math (clamp to INT32_MAX/INT32_MIN for s32,
INT16_MAX/INT16_MIN for s16, 0/255 for u8), and write the clamped result back to
pOutput; for f32 clamp the summed value into [-1.0f, 1.0f] before storing.
Ensure this change touches the same conversion/mixing function that references
pOutput and follows the custom callbacks (the section around the current custom
callback lines 168–172 and the later conversion block noted in the comment).
- Around line 33-47: UnregisterSound currently unconditionally calls
ma_decoder_uninit and never releases engine-backed sound resources; update
UnregisterSound to mirror the cleanup in the move assignment operator: if
sound.decoderInitialized is true then call ma_decoder_uninit(&sound.decoder) and
set decoderInitialized=false, and if sound.hasEngineSound (or sound.usingEngine)
is true then call ma_sound_uninit(&sound.engineSound) and set
hasEngineSound/usingEngine=false (and clear isPlaying/isPaused as needed) so you
don't uninitialize uninitialized decoders or leak engine sound resources; ensure
you clear or reset related fields (volume, loop flags, loop frame indices) like
the move-assignment does.
- Around line 516-543: The engine is being initialized with ma_engine_init(NULL,
&_engine) which lets miniaudio pick its own sample rate, causing seek offsets
because the decoder is at 44.1kHz; instead build an ma_engine_config via
ma_engine_config_init(), set its sampleRate to the actual device sample rate you
query (e.g., via ma_context_get_devices / ma_context_get_device_info or the
device info you already use) and call ma_engine_init(&config, &_engine); update
the code around ma_engine_init, keeping references to ma_engine_init, ma_engine,
ma_decoder_get_cursor_in_pcm_frames, and ma_sound_seek_to_pcm_frame so the
cursor is fetched from snd.decoder and seeks on snd.engineSound after the engine
is created with the correct sampleRate.

Comment thread src/plugin/sound/src/resource/SoundManager.hpp
Comment thread src/plugin/sound/src/resource/SoundManager.hpp
Comment thread src/plugin/sound/src/resource/SoundManager.hpp
@sonarqubecloud

Copy link
Copy Markdown

@ripel2 ripel2 merged commit cc11c88 into main Jan 25, 2026
14 checks passed
@ripel2 ripel2 deleted the vehicle-sound branch January 25, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants