feat(sound): add Vehicle sound engine#438
Conversation
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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. 📝 WalkthroughWalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ 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. Comment |
There was a problem hiding this comment.
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
engineSoundwhenhasEngineSoundis true (lines 10–11 of SoundManager.cpp), butUnregisterSoundskips this cleanup. According to miniaudio documentation,ma_sound_uninit()is required for every successfulma_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 viama_sound_seek_to_pcm_frame(). The decoder path correctly seeks to frame 0, but the engine path does not, causing the nextPlay()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,_rpmByEntitycan grow over time. Consider clearing on entity-destroy events or adding aClearAll()helper for world resets.
There was a problem hiding this comment.
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
hasEngineSoundis true,ma_sound_uninitisn'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 inlineUnregisterSoundmethod 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_uninitinside 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).
Withplayback.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@notesays pitch is disabled, but the method applies pitch.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
SetPitchlazily initializesengineSoundviama_sound_init_from_file, butUnregisterSoundonly uninitializes the decoder, leaking the engine sound resource. The destructor properly cleans up bothengineSoundand decoder;UnregisterSoundshould 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 hitERROR_UNKNOWN_FORMATand 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_s16Also 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") endThen users would only need step 3 (adding
--with_sound_usage_example=yto 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.
76cd2ce to
8a3822e
Compare
There was a problem hiding this comment.
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.
…vehicle telemetry
…io processing details
71ae385 to
14d62fa
Compare
|



Summary by CodeRabbit
New Features
Bug Fixes / Reliability
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.