Skip to content

feat: add macOS platform support#146

Closed
somian99 wants to merge 1 commit intomainfrom
feat/macos-support
Closed

feat: add macOS platform support#146
somian99 wants to merge 1 commit intomainfrom
feat/macos-support

Conversation

@somian99
Copy link
Copy Markdown
Collaborator

@somian99 somian99 commented Mar 28, 2026

Summary

  • add macOS platform windowing, utils, and threading support in AestraPlat
  • add macOS/CoreAudio RtAudio driver integration in AestraAudio
  • update build/configuration and compatibility shims needed for the macOS app build

Verification

  • cmake --preset full-fast
  • cmake --build --preset full-fast-release

Summary by CodeRabbit

Release Notes

  • New Features

    • Full macOS support including SDL2-based windowing with Retina DPI scaling, CoreAudio backend via RtAudio, and native platform utilities.
    • Added playlist change observer mechanism for real-time UI synchronization.
    • Enhanced pattern cloning with customizable naming.
    • DPI-aware rendering and display scaling.
  • Bug Fixes

    • Fixed UUID printing format issues.
    • Corrected easing animation calculations.
    • Improved tempo map resolution.
  • Documentation

    • Added macOS implementation and build documentation.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 28, 2026

Walkthrough

Comprehensive macOS support implementation including a new platform abstraction layer with SDL2-based windowing, native utilities, and thread management; CoreAudio backend integration via RtAudio with device enumeration and non-interleaved buffer handling; plus supporting API improvements for automation visibility, pattern management, playlist observation, and DPI-aware rendering.

Changes

Cohort / File(s) Summary
macOS Platform Layer Implementation
AestraPlat/src/macOS/PlatformWindow*, PlatformUtils*, PlatformThread*
New SDL2-based OpenGL 3.3 window with Retina DPI scaling, event routing (keyboard/mouse/text input), and lifecycle management; native utilities for timing (mach_absolute_time), system info (sysctl), clipboard, and app data paths; thread priority management with realtime audio support.
macOS Audio Backend
AestraAudio/src/macOS/RtAudioDriver.*
New CoreAudio driver wrapping RtAudio with device enumeration, stream lifecycle, non-interleaved buffer handling, atomic callback management, and xrun tracking.
Build System - macOS Integration
AestraAudio/CMakeLists.txt, AestraPlat/CMakeLists.txt, AestraUI/CMakeLists.txt, CMakeLists.txt, CMakePresets.json
macOS-specific build branches for audio (RtAudio/CoreAudio linking), platform (SDL2 conditional detection with fallback), UI (FreeType system discovery); SDL2 platform condition; cache variable naming standardization.
Audio Core & DSP Updates
AestraAudio/include/AestraUUID.h, AestraAudio/include/DSP/SampleRateConverter.h, AestraAudio/include/IO/WaveformCache.h
Format specifier fix for 64-bit UUID fields; deleted move operations (immovable SampleRateConverter and WaveformCache); cstddef include.
Audio Models - Automation, Clips, Patterns
AestraAudio/include/Core/AutomationCurve.h, AestraAudio/include/Models/ClipInstance.h, AestraAudio/include/Models/ClipSource.h, AestraAudio/include/Models/PatternSource.h
Added visible flag to AutomationCurve; added mute flag and LocalEdits alias to ClipInstance; repositioned WaveformCache getter in ClipSource; formatting cleanup in PatternSource.
Playlist & Pattern Management
AestraAudio/include/Models/PatternManager.h, AestraAudio/include/Models/PlaylistMixer.h, AestraAudio/include/Models/PlaylistModel.h
Pattern creation with optional payload default; refactored clonePattern with ID/name generation and selective field copying; added setResamplingQuality int overload; added observer notifications to playlist mutations; enhanced BPM conversion guard; moveClip signature change to return bool; added moveClip overload.
Track & Playback Management
AestraAudio/include/Models/TrackManager.h, AestraAudio/include/Playback/TimelineClock.h, AestraAudio/include/Models/MeterSnapshot.h
TrackManager: added getTrackCount/isMetronomeEnabled getters, graph invalidation via markModified, ChannelSlotMap rebuild integration, stream stop clears recording/scrubbing flags; TimelineClock: getCurrentTempo now resolves via tempo map at beat 0; MeterSnapshot formatting cleanup.
UI DPI & Rendering System
AestraUI/Graphics/NUIRenderer.h, AestraUI/Graphics/OpenGL/NUIRendererGL.*, AestraUI/Graphics/OpenGL/NUIRenderCache.cpp, AestraUI/Platform/NUIPlatformBridge.cpp
Added DPI scale hook and framebuffer dimension accessors to base NUIRenderer; NUIRendererGL tracks DPI scale and framebuffer dimensions with offscreen preserve/restore; scissor/clip rects now DPI-scaled; explicit window-renderer DPI synchronization on resize/DPI-change events.
UI Animation Easing
AestraUI/Core/NUIAnimation.cpp, AestraUI/Core/NUITypes.h
Removed pre-decrement side effects (--t) in easing functions; replaced with explicit local variable computation (u = t - 1.0f) for easeOutCubic/Quart/Quint and easeInOutQuart/Quint.
UI Components & Window Management
Source/Components/TrackUIComponent.h, Source/Core/AestraWindowManager.cpp, Source/Panels/WindowPanel.cpp, Source/Settings/AudioSettingsPage.cpp
TrackUIComponent: added override specifiers; AestraWindowManager: DPI-aware layout sizing and renderer DPI scaling on initialize/resize; WindowPanel: safe child pointer access in debug trace; AudioSettingsPage: int cast for resampling quality setter.
Configuration & Preferences
Source/Core/Preferences.cpp
Removed blank line in includes (formatting).
Documentation
MACOS_SUPPORT.md, MAINTENANCE_SUMMARY.md
New macOS support overview and maintenance pass summary documenting platform layer, audio backend, build integration, API fixes, and verification steps.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant WM as AestraWindowManager
    participant Win as PlatformWindowmacOS
    participant Rend as NUIRendererGL
    participant SDL as SDL2
    
    App->>WM: initialize(WindowDesc)
    WM->>Win: create(desc)
    Win->>SDL: SDL_CreateWindow
    SDL-->>Win: SDL window + GL context
    Win->>Rend: setDPIScale(dpiScale)
    Rend->>Rend: updateFramebufferSize()
    WM->>Rend: resize(actualWidth, actualHeight)
    WM->>WM: setRootComponentBounds(logicalSize)
    Note over WM,Rend: DPI-aware layout and rendering ready
Loading
sequenceDiagram
    participant App as Application
    participant TM as TrackManager
    participant MM as PlaylistMixer
    participant RTA as RtAudioDriver
    participant RtAudio as RtAudio(CoreAudio)
    
    App->>TM: initialize with audio config
    TM->>RTA: openDevice(AudioDeviceConfig)
    RTA->>RtAudio: RtAudio::openStream()
    RtAudio-->>RTA: stream opened
    TM->>RTA: startStream(callback)
    RTA->>RtAudio: startStream()
    Note over RTA,RtAudio: Audio frames flowing
    loop Audio Callback
        RtAudio->>RTA: rtAudioCallback(frames)
        RTA->>RTA: prepare non-interleaved buffers
        RTA->>MM: IAudioCallback::process(input, output, frames)
        MM-->>RTA: processed audio
    end
    TM->>RTA: stopStream()
    RTA->>RtAudio: stopStream()
Loading
sequenceDiagram
    participant PM as PlaylistModel
    participant Obs as ChangeObserver
    participant UI as UI Components
    
    PM->>PM: addChangeObserver(callback)
    Obs->>Obs: store callback
    Note over PM,Obs: Observer registered
    PM->>PM: createLane() / addClip() / setClipDuration()
    PM->>PM: notifyObservers()
    loop Each Registered Observer
        PM->>Obs: invoke callback()
        Obs->>UI: update UI state
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • kemsue

Poem

🐰 Hopping with joy through macOS lands,
CoreAudio sings in SDL's hands!
DPI scales dance, animations now gleam,
Platform abstractions fulfill the dream—
From Cocoa to Cairo, observers now cheer, ✨🎵

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.64% 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 pull request title 'feat: add macOS platform support' accurately summarizes the main change: comprehensive macOS support addition including platform layer, audio backend, and build system integration.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/macos-support

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.

@qodo-code-review
Copy link
Copy Markdown
Contributor

ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

Review Summary by Qodo

Add macOS platform support with CoreAudio audio driver

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add complete macOS platform support with SDL2 windowing and CoreAudio
• Implement macOS-specific utilities, threading, and audio driver
• Fix DPI scaling for Retina displays in UI renderer
• Resolve API mismatches and improve code consistency
• Update build system for macOS framework linking
Diagram
flowchart LR
  A["macOS Platform Layer"] -->|SDL2 Windowing| B["PlatformWindowmacOS"]
  A -->|System Utils| C["PlatformUtilsmacOS"]
  A -->|Thread Priority| D["PlatformThreadmacOS"]
  E["CoreAudio Backend"] -->|RtAudio Driver| F["RtAudioDriver"]
  B -->|DPI Scale| G["NUIRendererGL"]
  G -->|Framebuffer Size| H["Retina Support"]
  I["CMake Build"] -->|Framework Linking| J["Cocoa/OpenGL/CoreAudio"]
  J -->|Executable| K["Aestra macOS App"]
Loading

Grey Divider

File Changes

1. AestraPlat/src/macOS/PlatformWindowmacOS.h ✨ Enhancement +109/-0

macOS SDL2 window implementation header

AestraPlat/src/macOS/PlatformWindowmacOS.h


2. AestraPlat/src/macOS/PlatformWindowmacOS.cpp ✨ Enhancement +433/-0

SDL2 window lifecycle and event handling

AestraPlat/src/macOS/PlatformWindowmacOS.cpp


3. AestraPlat/src/macOS/PlatformUtilsmacOS.h ✨ Enhancement +31/-0

macOS platform utilities interface

AestraPlat/src/macOS/PlatformUtilsmacOS.h


View more (37)
4. AestraPlat/src/macOS/PlatformUtilsmacOS.cpp ✨ Enhancement +119/-0

Native macOS system utilities implementation

AestraPlat/src/macOS/PlatformUtilsmacOS.cpp


5. AestraPlat/src/macOS/PlatformThreadmacOS.cpp ✨ Enhancement +76/-0

macOS thread priority and realtime audio support

AestraPlat/src/macOS/PlatformThreadmacOS.cpp


6. AestraAudio/src/macOS/RtAudioDriver.h ✨ Enhancement +72/-0

CoreAudio RtAudio driver interface

AestraAudio/src/macOS/RtAudioDriver.h


7. AestraAudio/src/macOS/RtAudioDriver.cpp ✨ Enhancement +202/-0

CoreAudio device enumeration and audio streaming

AestraAudio/src/macOS/RtAudioDriver.cpp


8. AestraPlat/src/Platform.cpp ✨ Enhancement +24/-7

Add macOS platform factory initialization

AestraPlat/src/Platform.cpp


9. AestraPlat/CMakeLists.txt ⚙️ Configuration changes +27/-1

macOS SDL2 and framework linking configuration

AestraPlat/CMakeLists.txt


10. AestraAudio/CMakeLists.txt ⚙️ Configuration changes +36/-0

macOS audio library and RtAudio integration

AestraAudio/CMakeLists.txt


11. AestraUI/Graphics/OpenGL/NUIRendererGL.h ✨ Enhancement +12/-2

Add DPI scale and framebuffer size tracking

AestraUI/Graphics/OpenGL/NUIRendererGL.h


12. AestraUI/Graphics/OpenGL/NUIRendererGL.cpp ✨ Enhancement +47/-21

Implement DPI scaling for Retina display support

AestraUI/Graphics/OpenGL/NUIRendererGL.cpp


13. AestraUI/Graphics/NUIRenderer.h ✨ Enhancement +9/-0

Add DPI scale virtual methods to base renderer

AestraUI/Graphics/NUIRenderer.h


14. AestraUI/Platform/NUIPlatformBridge.cpp ✨ Enhancement +2/-0

Wire DPI scale updates to renderer

AestraUI/Platform/NUIPlatformBridge.cpp


15. AestraUI/Core/NUIAnimation.cpp 🐞 Bug fix +6/-3

Fix easing function variable shadowing

AestraUI/Core/NUIAnimation.cpp


16. AestraUI/Graphics/OpenGL/NUIRenderCache.cpp 🐞 Bug fix +6/-4

Apply DPI scaling to scissor box calculations

AestraUI/Graphics/OpenGL/NUIRenderCache.cpp


17. AestraAudio/include/Models/PatternManager.h ✨ Enhancement +27/-22

Add default parameter and reorder methods

AestraAudio/include/Models/PatternManager.h


18. AestraAudio/include/Models/PlaylistModel.h ✨ Enhancement +31/-3

Add change observers and improve API consistency

AestraAudio/include/Models/PlaylistModel.h


19. AestraAudio/include/Models/TrackManager.h ✨ Enhancement +47/-23

Improve method organization and add missing getters

AestraAudio/include/Models/TrackManager.h


20. AestraAudio/include/Models/ClipInstance.h ✨ Enhancement +3/-0

Add LocalEdits alias and muted field

AestraAudio/include/Models/ClipInstance.h


21. AestraAudio/include/Models/ClipSource.h ✨ Enhancement +1/-2

Add waveform cache getter method

AestraAudio/include/Models/ClipSource.h


22. AestraAudio/include/Models/PlaylistMixer.h ✨ Enhancement +4/-2

Add int overload for resampling quality setter

AestraAudio/include/Models/PlaylistMixer.h


23. AestraAudio/include/Playback/TimelineClock.h 🐞 Bug fix +1/-1

Fix tempo getter to use tempo map

AestraAudio/include/Playback/TimelineClock.h


24. AestraAudio/include/Core/AutomationCurve.h ✨ Enhancement +6/-5

Add visible field and fix method ordering

AestraAudio/include/Core/AutomationCurve.h


25. AestraAudio/include/DSP/SampleRateConverter.h 🐞 Bug fix +3/-3

Disable move operations due to atomic state

AestraAudio/include/DSP/SampleRateConverter.h


26. AestraAudio/include/IO/WaveformCache.h 🐞 Bug fix +3/-3

Disable move operations due to atomic state

AestraAudio/include/IO/WaveformCache.h


27. AestraAudio/include/AestraUUID.h 🐞 Bug fix +3/-2

Fix format specifier for portable UUID printing

AestraAudio/include/AestraUUID.h


28. Source/Core/AestraWindowManager.cpp 🐞 Bug fix +28/-5

Set DPI scale and fix window size initialization

Source/Core/AestraWindowManager.cpp


29. Source/Core/Preferences.cpp Formatting +0/-1

Remove unnecessary blank line

Source/Core/Preferences.cpp


30. Source/Panels/WindowPanel.cpp 🐞 Bug fix +3/-2

Fix iterator dereference in debug trace

Source/Panels/WindowPanel.cpp


31. Source/Settings/AudioSettingsPage.cpp 🐞 Bug fix +1/-1

Add explicit cast for resampling quality

Source/Settings/AudioSettingsPage.cpp


32. Source/Components/TrackUIComponent.h ✨ Enhancement +3/-3

Add override specifiers to virtual methods

Source/Components/TrackUIComponent.h


33. AestraUI/Core/NUITypes.h 🐞 Bug fix +26/-5

Fix easing function variable shadowing issues

AestraUI/Core/NUITypes.h


34. CMakeLists.txt ⚙️ Configuration changes +9/-8

Add SDL2 detection for macOS platform

CMakeLists.txt


35. AestraUI/CMakeLists.txt ⚙️ Configuration changes +27/-3

Add FreeType system library fallback for macOS

AestraUI/CMakeLists.txt


36. CMakePresets.json 🐞 Bug fix +4/-5

Fix cache variable naming consistency

CMakePresets.json


37. MACOS_SUPPORT.md 📝 Documentation +157/-0

Comprehensive macOS implementation documentation

MACOS_SUPPORT.md


38. MAINTENANCE_SUMMARY.md 📝 Documentation +148/-0

Maintenance work summary and build verification

MAINTENANCE_SUMMARY.md


39. AestraAudio/include/Models/MeterSnapshot.h Additional files +0/-1

...

AestraAudio/include/Models/MeterSnapshot.h


40. AestraAudio/include/Models/PatternSource.h Additional files +1/-2

...

AestraAudio/include/Models/PatternSource.h


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown
Contributor

qodo-code-review bot commented Mar 28, 2026

Code Review by Qodo

🐞 Bugs (5) 📘 Rule violations (0) 📎 Requirement gaps (0) 📐 Spec deviations (0)

Grey Divider


Action required

1. No macOS driver registry 🐞 Bug ✓ Correctness
Description
On APPLE, AestraAudioMacOS builds only src/macOS/RtAudioDriver.cpp and does not compile any macOS
RegisterPlatformDrivers implementation, so AudioDeviceManager ends up calling the no-op stub and
registers zero real drivers on macOS (no device enumeration/streaming).
Code

AestraAudio/CMakeLists.txt[R494-529]

+elseif(APPLE)
+    # macOS - CoreAudio via RtAudio
+    set(AESTRA_AUDIO_MACOS_SOURCES
+        src/macOS/RtAudioDriver.cpp
+    )
+    
+    set(AESTRA_AUDIO_MACOS_HEADERS
+        src/macOS/RtAudioDriver.h
+        include/Drivers/AudioPlatformRegistry.h
+        ${RTAUDIO_HEADERS}
+    )
+    
+    add_library(AestraAudioMacOS STATIC
+        ${AESTRA_AUDIO_MACOS_SOURCES}
+        ${AESTRA_AUDIO_MACOS_HEADERS}
+        ${RTAUDIO_SOURCES}
+    )
+    
+    target_include_directories(AestraAudioMacOS
+        PUBLIC
+            # Nothing public
+        PRIVATE
+            ${CMAKE_CURRENT_SOURCE_DIR}/include
+            ${CMAKE_CURRENT_SOURCE_DIR}/src/macOS
+            ${RTAUDIO_DIR}
+    )
+    
+    target_link_libraries(AestraAudioMacOS
+        PUBLIC
+            AestraAudioCore
+        PRIVATE
+            ${RTAUDIO_LIBS}
+    )
+    
+    add_library(AestraAudio ALIAS AestraAudioMacOS)
+
Evidence
AudioDeviceManager unconditionally calls RegisterPlatformDrivers() during initialize; on macOS the
build does not include a platform registry implementation (unlike Linux/Windows), so the fallback
stub is used and no drivers are added.

AestraAudio/CMakeLists.txt[494-529]
AestraAudio/src/Core/AudioDeviceManager.cpp[31-60]
AestraAudio/src/Core/DriverRegistryStub.cpp[1-14]
AestraAudio/src/Linux/LinuxDriverRegistry.cpp[14-24]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
On macOS, `AudioDeviceManager::initialize()` calls `RegisterPlatformDrivers(*this)`, but the APPLE build of `AestraAudioMacOS` does not compile any macOS implementation of `RegisterPlatformDrivers`. The only available definition is the no-op stub, so zero real audio drivers are registered.

## Issue Context
Linux and Windows both compile a platform registry translation unit (e.g. `src/Linux/LinuxDriverRegistry.cpp`, `src/Win32/WindowsDriverRegistry.cpp`) that registers at least one real driver.

## Fix Focus Areas
- Add a macOS registry TU implementing `Aestra::Audio::RegisterPlatformDrivers(AudioDeviceManager&)` and (optionally) `GetPlatformAudioInfo()`.
- Ensure the macOS registry registers a driver that implements `IAudioDriver` (either a macOS RtAudio-based `IAudioDriver` implementation or reuse/adapt the existing Linux `RtAudioDriver` pattern).
- Add the new registry source file to the APPLE `AESTRA_AUDIO_MACOS_SOURCES` list.

### Fix Focus Areas (exact locations)
- AestraAudio/CMakeLists.txt[494-529]
- AestraAudio/src/Core/AudioDeviceManager.cpp[31-60]
- AestraAudio/src/Core/DriverRegistryStub.cpp[1-14]
- AestraAudio/src/Linux/LinuxDriverRegistry.cpp[14-24]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. RtAudio fills one channel 🐞 Bug ✓ Correctness
Description
The macOS RtAudio callback passes only channel 0 to IAudioCallback::process even though the stream
is opened with RTAUDIO_NONINTERLEAVED and may have multiple output channels, leaving other channels
uninitialized and producing garbage audio.
Code

AestraAudio/src/macOS/RtAudioDriver.cpp[R96-192]

+    RtAudio::StreamOptions options;
+    options.flags = RTAUDIO_MINIMIZE_LATENCY | RTAUDIO_NONINTERLEAVED;
+    options.numberOfBuffers = 2;
+    options.priority = 0;
+
+    unsigned int bufferFrames = bufferSize_;
+    const RtAudioErrorType error = rtAudio_->openStream(
+        &outputParams_, inputParams, RTAUDIO_FLOAT32, config.sampleRate, &bufferFrames, &RtAudioDriver::rtAudioCallback,
+        this, &options);
+    if (!isRtAudioOk(error)) {
+        AESTRA_LOG_ERROR("Failed to open audio device");
+        return false;
+    }
+
+    bufferSize_ = bufferFrames;
+    outputBuffers_.resize(numOutputChannels_);
+    for (auto& buf : outputBuffers_) {
+        buf.resize(bufferSize_);
+    }
+    outputBufferPtrs_.resize(numOutputChannels_);
+
+    isStreamOpen_ = true;
+
+    RtAudio::DeviceInfo info = rtAudio_->getDeviceInfo(deviceId);
+    AESTRA_LOG_INFO("Audio device opened: " << info.name << " @ " << config.sampleRate << "Hz");
+    return true;
+}
+
+void RtAudioDriver::closeDevice() {
+    stopStream();
+    if (isStreamOpen_ && rtAudio_) {
+        try {
+            rtAudio_->closeStream();
+        } catch (...) {}
+        isStreamOpen_ = false;
+    }
+}
+
+bool RtAudioDriver::startStream(IAudioCallback* callback) {
+    if (!isStreamOpen_ || isStreamRunning_)
+        return false;
+
+    callback_.store(callback, std::memory_order_release);
+
+    if (!isRtAudioOk(rtAudio_->startStream())) {
+        AESTRA_LOG_ERROR("Failed to start audio stream");
+        callback_.store(nullptr);
+        return false;
+    }
+
+    isStreamRunning_ = true;
+    AESTRA_LOG_INFO("Audio stream started");
+    return true;
+}
+
+void RtAudioDriver::stopStream() {
+    if (!isStreamRunning_ || !rtAudio_)
+        return;
+
+    try {
+        rtAudio_->stopStream();
+    } catch (...) {}
+
+    isStreamRunning_ = false;
+    callback_.store(nullptr);
+}
+
+int RtAudioDriver::rtAudioCallback(void* outputBuffer, void* inputBuffer, unsigned int nFrames,
+                                   double streamTime, RtAudioStreamStatus status, void* userData) {
+    (void)streamTime;
+    
+    auto* driver = static_cast<RtAudioDriver*>(userData);
+
+    if (status) {
+        driver->xrunCount_++;
+    }
+
+    IAudioCallback* cb = driver->callback_.load(std::memory_order_acquire);
+    if (!cb) {
+        // Silence
+        float* out = static_cast<float*>(outputBuffer);
+        std::memset(out, 0, nFrames * driver->numOutputChannels_ * sizeof(float));
+        return 0;
+    }
+
+    // Setup non-interleaved buffer pointers
+    float* out = static_cast<float*>(outputBuffer);
+    for (unsigned int ch = 0; ch < driver->numOutputChannels_; ch++) {
+        driver->outputBufferPtrs_[ch] = out + (ch * nFrames);
+    }
+
+    // Process audio through callback
+    const float* in = static_cast<const float*>(inputBuffer);
+    
+    // Call the audio callback with non-interleaved data
+    cb->process(in, driver->outputBufferPtrs_[0], nFrames);
+
Evidence
RtAudio’s NONINTERLEAVED mode concatenates per-channel blocks back-to-back in the output buffer; the
driver computes pointers for each channel block but then only calls process() with channel 0’s
pointer, so other channels are never written.

AestraAudio/src/macOS/RtAudioDriver.cpp[96-104]
AestraAudio/src/macOS/RtAudioDriver.cpp[181-192]
AestraAudio/External/rtaudio/RtAudio.h[121-136]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`RtAudioDriver::rtAudioCallback` sets up pointers for each non-interleaved output channel, but only invokes `IAudioCallback::process` with `outputBufferPtrs_[0]`, so channels 1..N are left uninitialized.

## Issue Context
The stream is opened with `RTAUDIO_NONINTERLEAVED`, meaning the output buffer contains `nFrames` samples per channel concatenated back-to-back. A correct implementation must ensure every channel’s block is written (or switch to interleaved mode and pass the interleaved buffer to the callback).

## Fix Focus Areas
- Decide on one buffer contract for `IAudioCallback`:
 - If the engine expects interleaved: remove `RTAUDIO_NONINTERLEAVED` and pass `outputBuffer` directly.
 - If you want non-interleaved: change `IAudioCallback::process` to accept all channels (e.g., `float** outputs` or `(float* output, unsigned int channels)` with documented layout) and write all channels.
- Ensure the callback silences *all* output channels in the “no callback” path.

### Fix Focus Areas (exact locations)
- AestraAudio/src/macOS/RtAudioDriver.cpp[96-104]
- AestraAudio/src/macOS/RtAudioDriver.cpp[181-192]
- AestraAudio/src/macOS/RtAudioDriver.h[26-31]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. macOS SDL calls unguarded 🐞 Bug ✓ Correctness
Description
Platform::initialize() on macOS calls SDL_Init/SDL_SetHint even when AESTRA_HAS_SDL2 is not defined,
but SDL headers are only included under AESTRA_HAS_SDL2, causing build failures when SDL2 isn’t
found (which CMake explicitly allows).
Code

AestraPlat/src/Platform.cpp[R18-27]

#elif AESTRA_PLATFORM_MACOS
-// macOS stub implementation - runtime parity pending
-// No Cocoa/AppKit headers required for headless builds
+#ifdef AESTRA_HAS_SDL2
+#include "macOS/PlatformUtilsmacOS.h"
+#include "macOS/PlatformWindowmacOS.h"
+
+#include <SDL2/SDL.h>
+#endif
#endif

namespace Aestra {
Evidence
The macOS SDL include is guarded by AESTRA_HAS_SDL2, but the macOS initialize implementation is not,
while the macOS CMake path can omit defining AESTRA_HAS_SDL2 if SDL2 is missing. That combination
makes SDL symbols unavailable at compile time.

AestraPlat/src/Platform.cpp[18-25]
AestraPlat/src/Platform.cpp[90-103]
AestraPlat/CMakeLists.txt[36-53]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Platform::initialize()` on macOS calls SDL APIs unconditionally, but SDL is only included/available when `AESTRA_HAS_SDL2` is defined. The CMake configuration explicitly allows SDL2 to be missing on macOS (it prints a warning and does not define `AESTRA_HAS_SDL2`), so this currently fails to compile in that configuration.

## Issue Context
Linux already has the correct pattern (`#ifdef AESTRA_HAS_SDL2` around SDL init with an `#else` error path). macOS should match that structure.

## Fix Focus Areas
- Wrap the macOS SDL init/hints and `PlatformUtilsmacOS` instantiation in `#ifdef AESTRA_HAS_SDL2`.
- Add an `#else` branch that logs a clear error and returns false (mirroring the Linux implementation).

### Fix Focus Areas (exact locations)
- AestraPlat/src/Platform.cpp[18-25]
- AestraPlat/src/Platform.cpp[90-103]
- AestraPlat/CMakeLists.txt[36-53]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Observers called under lock 🐞 Bug ⛯ Reliability
Description
PlaylistModel calls notifyObservers() while holding m_mutex (e.g.,
createLane/addClip/removeClip/moveClip/clear), so an observer that calls back into PlaylistModel can
deadlock or cause re-entrant mutation under the same lock.
Code

AestraAudio/include/Models/PlaylistModel.h[R62-68]

        PlaylistLaneID id = lane.id;
        m_lanes.push_back(std::move(lane));
        m_laneMap[id] = m_lanes.size() - 1;
+        notifyObservers();

        return id;
    }
Evidence
Mutating methods take a unique_lock on m_mutex and invoke notifyObservers() before the lock is
released; notifyObservers() calls arbitrary user code without releasing the lock first.

AestraAudio/include/Models/PlaylistModel.h[56-68]
AestraAudio/include/Models/PlaylistModel.h[91-109]
AestraAudio/include/Models/PlaylistModel.h[512-518]
AestraAudio/include/Models/PlaylistModel.h[553-559]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`PlaylistModel` invokes `notifyObservers()` while holding `m_mutex`. Observers are arbitrary callbacks and can easily re-enter `PlaylistModel` APIs, leading to deadlocks or re-entrant mutation.

## Issue Context
This is a standard observer-pattern pitfall: don’t call user code while holding internal locks.

## Fix Focus Areas
- Copy `m_changeObservers` (or move into a local vector) while holding the lock, then release the lock, then invoke callbacks.
- Apply this pattern consistently to all methods that currently call `notifyObservers()` under lock.

### Fix Focus Areas (exact locations)
- AestraAudio/include/Models/PlaylistModel.h[56-68]
- AestraAudio/include/Models/PlaylistModel.h[91-109]
- AestraAudio/include/Models/PlaylistModel.h[214-257]
- AestraAudio/include/Models/PlaylistModel.h[512-518]
- AestraAudio/include/Models/PlaylistModel.h[553-559]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Timebase init data race 🐞 Bug ⛯ Reliability
Description
PlatformUtilsmacOS uses an unsynchronized static bool for one-time initialization of
mach_timebase_info, creating a data race if getTime() is called concurrently from multiple threads.
Code

AestraPlat/src/macOS/PlatformUtilsmacOS.cpp[R17-34]

+// Static initialization for mach timebase
+static mach_timebase_info_data_t s_timebaseInfo;
+static bool s_timebaseInitialized = false;
+
+static void initTimebase() {
+    if (!s_timebaseInitialized) {
+        mach_timebase_info(&s_timebaseInfo);
+        s_timebaseInitialized = true;
+    }
+}
+
+double PlatformUtilsmacOS::getTime() const {
+    initTimebase();
+    uint64_t time = mach_absolute_time();
+    // Convert to nanoseconds, then to seconds
+    double nanoseconds = static_cast<double>(time * s_timebaseInfo.numer) / s_timebaseInfo.denom;
+    return nanoseconds / 1e9;
+}
Evidence
initTimebase() reads/writes s_timebaseInitialized without any synchronization, and getTime() is a
general-purpose utility likely callable from multiple threads.

AestraPlat/src/macOS/PlatformUtilsmacOS.cpp[17-34]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`PlatformUtilsmacOS::getTime()` calls `initTimebase()`, which uses a plain `static bool` to guard one-time initialization. If multiple threads call `getTime()` concurrently, this is a data race.

## Issue Context
This utility is likely used across UI/render/audio threads.

## Fix Focus Areas
- Replace the `static bool` guard with `std::once_flag` + `std::call_once`, or use an `std::atomic<bool>` with proper memory ordering.

### Fix Focus Areas (exact locations)
- AestraPlat/src/macOS/PlatformUtilsmacOS.cpp[17-34]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@github-actions
Copy link
Copy Markdown

📚 API Documentation Quality Check

Status: ❌ Needs Improvement

Metric Count
Documentation Errors 1276
Undocumented Items 0
Missing Parameter Docs 0

❌ Please fix documentation errors before merging.

📖 Documentation Guidelines
  • Document all public classes, functions, and members
  • Use Doxygen-compatible comment syntax
  • Include @param for all parameters
  • Include @return for return values
  • Add usage examples for complex APIs

See coding-style.md for details.

Comment on lines +494 to +529
elseif(APPLE)
# macOS - CoreAudio via RtAudio
set(AESTRA_AUDIO_MACOS_SOURCES
src/macOS/RtAudioDriver.cpp
)

set(AESTRA_AUDIO_MACOS_HEADERS
src/macOS/RtAudioDriver.h
include/Drivers/AudioPlatformRegistry.h
${RTAUDIO_HEADERS}
)

add_library(AestraAudioMacOS STATIC
${AESTRA_AUDIO_MACOS_SOURCES}
${AESTRA_AUDIO_MACOS_HEADERS}
${RTAUDIO_SOURCES}
)

target_include_directories(AestraAudioMacOS
PUBLIC
# Nothing public
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}/src/macOS
${RTAUDIO_DIR}
)

target_link_libraries(AestraAudioMacOS
PUBLIC
AestraAudioCore
PRIVATE
${RTAUDIO_LIBS}
)

add_library(AestraAudio ALIAS AestraAudioMacOS)

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.

Action required

1. No macos driver registry 🐞 Bug ✓ Correctness

On APPLE, AestraAudioMacOS builds only src/macOS/RtAudioDriver.cpp and does not compile any macOS
RegisterPlatformDrivers implementation, so AudioDeviceManager ends up calling the no-op stub and
registers zero real drivers on macOS (no device enumeration/streaming).
Agent Prompt
## Issue description
On macOS, `AudioDeviceManager::initialize()` calls `RegisterPlatformDrivers(*this)`, but the APPLE build of `AestraAudioMacOS` does not compile any macOS implementation of `RegisterPlatformDrivers`. The only available definition is the no-op stub, so zero real audio drivers are registered.

## Issue Context
Linux and Windows both compile a platform registry translation unit (e.g. `src/Linux/LinuxDriverRegistry.cpp`, `src/Win32/WindowsDriverRegistry.cpp`) that registers at least one real driver.

## Fix Focus Areas
- Add a macOS registry TU implementing `Aestra::Audio::RegisterPlatformDrivers(AudioDeviceManager&)` and (optionally) `GetPlatformAudioInfo()`.
- Ensure the macOS registry registers a driver that implements `IAudioDriver` (either a macOS RtAudio-based `IAudioDriver` implementation or reuse/adapt the existing Linux `RtAudioDriver` pattern).
- Add the new registry source file to the APPLE `AESTRA_AUDIO_MACOS_SOURCES` list.

### Fix Focus Areas (exact locations)
- AestraAudio/CMakeLists.txt[494-529]
- AestraAudio/src/Core/AudioDeviceManager.cpp[31-60]
- AestraAudio/src/Core/DriverRegistryStub.cpp[1-14]
- AestraAudio/src/Linux/LinuxDriverRegistry.cpp[14-24]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +96 to +192
RtAudio::StreamOptions options;
options.flags = RTAUDIO_MINIMIZE_LATENCY | RTAUDIO_NONINTERLEAVED;
options.numberOfBuffers = 2;
options.priority = 0;

unsigned int bufferFrames = bufferSize_;
const RtAudioErrorType error = rtAudio_->openStream(
&outputParams_, inputParams, RTAUDIO_FLOAT32, config.sampleRate, &bufferFrames, &RtAudioDriver::rtAudioCallback,
this, &options);
if (!isRtAudioOk(error)) {
AESTRA_LOG_ERROR("Failed to open audio device");
return false;
}

bufferSize_ = bufferFrames;
outputBuffers_.resize(numOutputChannels_);
for (auto& buf : outputBuffers_) {
buf.resize(bufferSize_);
}
outputBufferPtrs_.resize(numOutputChannels_);

isStreamOpen_ = true;

RtAudio::DeviceInfo info = rtAudio_->getDeviceInfo(deviceId);
AESTRA_LOG_INFO("Audio device opened: " << info.name << " @ " << config.sampleRate << "Hz");
return true;
}

void RtAudioDriver::closeDevice() {
stopStream();
if (isStreamOpen_ && rtAudio_) {
try {
rtAudio_->closeStream();
} catch (...) {}
isStreamOpen_ = false;
}
}

bool RtAudioDriver::startStream(IAudioCallback* callback) {
if (!isStreamOpen_ || isStreamRunning_)
return false;

callback_.store(callback, std::memory_order_release);

if (!isRtAudioOk(rtAudio_->startStream())) {
AESTRA_LOG_ERROR("Failed to start audio stream");
callback_.store(nullptr);
return false;
}

isStreamRunning_ = true;
AESTRA_LOG_INFO("Audio stream started");
return true;
}

void RtAudioDriver::stopStream() {
if (!isStreamRunning_ || !rtAudio_)
return;

try {
rtAudio_->stopStream();
} catch (...) {}

isStreamRunning_ = false;
callback_.store(nullptr);
}

int RtAudioDriver::rtAudioCallback(void* outputBuffer, void* inputBuffer, unsigned int nFrames,
double streamTime, RtAudioStreamStatus status, void* userData) {
(void)streamTime;

auto* driver = static_cast<RtAudioDriver*>(userData);

if (status) {
driver->xrunCount_++;
}

IAudioCallback* cb = driver->callback_.load(std::memory_order_acquire);
if (!cb) {
// Silence
float* out = static_cast<float*>(outputBuffer);
std::memset(out, 0, nFrames * driver->numOutputChannels_ * sizeof(float));
return 0;
}

// Setup non-interleaved buffer pointers
float* out = static_cast<float*>(outputBuffer);
for (unsigned int ch = 0; ch < driver->numOutputChannels_; ch++) {
driver->outputBufferPtrs_[ch] = out + (ch * nFrames);
}

// Process audio through callback
const float* in = static_cast<const float*>(inputBuffer);

// Call the audio callback with non-interleaved data
cb->process(in, driver->outputBufferPtrs_[0], nFrames);

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.

Action required

2. Rtaudio fills one channel 🐞 Bug ✓ Correctness

The macOS RtAudio callback passes only channel 0 to IAudioCallback::process even though the stream
is opened with RTAUDIO_NONINTERLEAVED and may have multiple output channels, leaving other channels
uninitialized and producing garbage audio.
Agent Prompt
## Issue description
`RtAudioDriver::rtAudioCallback` sets up pointers for each non-interleaved output channel, but only invokes `IAudioCallback::process` with `outputBufferPtrs_[0]`, so channels 1..N are left uninitialized.

## Issue Context
The stream is opened with `RTAUDIO_NONINTERLEAVED`, meaning the output buffer contains `nFrames` samples per channel concatenated back-to-back. A correct implementation must ensure every channel’s block is written (or switch to interleaved mode and pass the interleaved buffer to the callback).

## Fix Focus Areas
- Decide on one buffer contract for `IAudioCallback`:
  - If the engine expects interleaved: remove `RTAUDIO_NONINTERLEAVED` and pass `outputBuffer` directly.
  - If you want non-interleaved: change `IAudioCallback::process` to accept all channels (e.g., `float** outputs` or `(float* output, unsigned int channels)` with documented layout) and write all channels.
- Ensure the callback silences *all* output channels in the “no callback” path.

### Fix Focus Areas (exact locations)
- AestraAudio/src/macOS/RtAudioDriver.cpp[96-104]
- AestraAudio/src/macOS/RtAudioDriver.cpp[181-192]
- AestraAudio/src/macOS/RtAudioDriver.h[26-31]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 18 to 27
#elif AESTRA_PLATFORM_MACOS
// macOS stub implementation - runtime parity pending
// No Cocoa/AppKit headers required for headless builds
#ifdef AESTRA_HAS_SDL2
#include "macOS/PlatformUtilsmacOS.h"
#include "macOS/PlatformWindowmacOS.h"

#include <SDL2/SDL.h>
#endif
#endif

namespace Aestra {
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.

Action required

3. Macos sdl calls unguarded 🐞 Bug ✓ Correctness

Platform::initialize() on macOS calls SDL_Init/SDL_SetHint even when AESTRA_HAS_SDL2 is not defined,
but SDL headers are only included under AESTRA_HAS_SDL2, causing build failures when SDL2 isn’t
found (which CMake explicitly allows).
Agent Prompt
## Issue description
`Platform::initialize()` on macOS calls SDL APIs unconditionally, but SDL is only included/available when `AESTRA_HAS_SDL2` is defined. The CMake configuration explicitly allows SDL2 to be missing on macOS (it prints a warning and does not define `AESTRA_HAS_SDL2`), so this currently fails to compile in that configuration.

## Issue Context
Linux already has the correct pattern (`#ifdef AESTRA_HAS_SDL2` around SDL init with an `#else` error path). macOS should match that structure.

## Fix Focus Areas
- Wrap the macOS SDL init/hints and `PlatformUtilsmacOS` instantiation in `#ifdef AESTRA_HAS_SDL2`.
- Add an `#else` branch that logs a clear error and returns false (mirroring the Linux implementation).

### Fix Focus Areas (exact locations)
- AestraPlat/src/Platform.cpp[18-25]
- AestraPlat/src/Platform.cpp[90-103]
- AestraPlat/CMakeLists.txt[36-53]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 26

Caution

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

⚠️ Outside diff range comments (2)
AestraAudio/include/Models/PlaylistModel.h (1)

227-248: ⚠️ Potential issue | 🔴 Critical

Validate the destination lane before removing the clip.

If newLaneId is syntactically valid but no longer exists in m_laneMap, this branch erases the clip from its old lane, leaves m_clipLaneMap stale, and still returns true. That breaks the contract documented on Line 212 to stay in the current lane when the target is invalid.

🩹 Proposed fix
-        if (newLaneId.isValid()) {
+        if (newLaneId.isValid()) {
             auto oldLaneIt = m_clipLaneMap.find(clipId);
-            if (oldLaneIt != m_clipLaneMap.end() && oldLaneIt->second != newLaneId) {
+            auto newLaneIdxIt = m_laneMap.find(newLaneId);
+            if (newLaneIdxIt != m_laneMap.end() &&
+                oldLaneIt != m_clipLaneMap.end() &&
+                oldLaneIt->second != newLaneId) {
                 // Remove from old lane
                 auto oldLaneIdxIt = m_laneMap.find(oldLaneIt->second);
                 if (oldLaneIdxIt != m_laneMap.end()) {
                     auto& oldClips = m_lanes[oldLaneIdxIt->second].clips;
                     oldClips.erase(std::remove_if(oldClips.begin(), oldClips.end(),
                                                   [&clipId](const ClipInstance& c) { return c.id == clipId; }),
                                    oldClips.end());
                 }

                 // Add to new lane
-                auto newLaneIdxIt = m_laneMap.find(newLaneId);
-                if (newLaneIdxIt != m_laneMap.end()) {
-                    m_lanes[newLaneIdxIt->second].clips.push_back(clipCopy);
-                    m_clipLaneMap[clipId] = newLaneId;
-                }
+                m_lanes[newLaneIdxIt->second].clips.push_back(clipCopy);
+                m_clipLaneMap[clipId] = newLaneId;

                 notifyClipChanged(clipId);
                 notifyObservers();
                 return true;
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AestraAudio/include/Models/PlaylistModel.h` around lines 227 - 248, The code
removes a clip from its old lane before verifying the destination exists, which
can leave m_clipLaneMap stale when newLaneId is valid syntactically but absent
from m_laneMap; fix by first checking m_laneMap.find(newLaneId) and only
proceeding to erase from m_lanes[old] and push into m_lanes[new] (and update
m_clipLaneMap) if the newLaneId is present; if not present, do nothing and
return false so that clipId, m_clipLaneMap, and lane contents remain unchanged;
ensure notifyClipChanged(clipId) and notifyObservers() are called only after a
successful move.
AestraUI/Graphics/OpenGL/NUIRendererGL.cpp (1)

1630-1640: 🧹 Nitpick | 🔵 Trivial

Remove the stray debugging/editor notes.

This block reads like leftover investigation chatter from an earlier compile error, not source documentation. It obscures the actual centering logic for no benefit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AestraUI/Graphics/OpenGL/NUIRendererGL.cpp` around lines 1630 - 1640, Remove
the stray debugging/editor note block that was accidentally left in
NUIRendererGL.cpp: delete the commented investigation chatter around measureText
and NUISize (references to measureText, NUISize, textSize) so only the actual
centering logic and any single legitimate comment remain; ensure you do not
remove the real declarations or usages of measureText, NUISize, or textSize—just
strip the leftover notes to restore the clean centering code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@AestraAudio/include/Models/ClipInstance.h`:
- Around line 63-64: Add a short clarifying comment above the using alias
LocalEdits = ClipEdits; explaining why the alias exists (e.g., to convey
semantic intent for per-instance/local edit semantics or to reserve the name for
future specialized behavior) and when to prefer LocalEdits versus ClipEdits;
reference the alias symbol LocalEdits and the underlying type ClipEdits so
reviewers can easily find and understand the intention in ClipInstance.h.
- Around line 76-78: Remove the dead instance-level field `muted` from the
ClipInstance class: this field is never read or written—mute state is stored in
`ClipInstance::edits.muted` (type `ClipEdits`). Delete the `bool muted = false;`
declaration in ClipInstance and ensure no remaining references exist (the
serializer and UI already use `edits.muted` as seen in ProjectSerializer and
TrackUIComponent).

In `@AestraAudio/include/Models/PatternManager.h`:
- Around line 112-123: In clonePattern (function PatternID clonePattern) the
cloned PatternSource currently doesn't preserve the source's mixer routing;
update the clone creation to copy source->m_mixerChannel into the new pattern
(pattern->m_mixerChannel = source->m_mixerChannel) so the cloned pattern retains
its mixer assignment, or alternatively add a clear comment in
PatternSource/clonePattern explaining why m_mixerChannel is intentionally reset.
- Around line 130-132: removePattern(PatternID id) erases the unique_ptr-held
PatternSource in m_patterns causing shared_ptrs returned by getAllPatterns()
(which use no-op deleters and raw pointers) to dangle; fix by either converting
m_patterns to hold std::shared_ptr<PatternSource> so getAllPatterns() can return
shared_ptrs that correctly extend object lifetime, or change getAllPatterns() to
return non-owning references (e.g., PatternID list or raw/non-owning pointers)
instead of shared_ptrs; update removePattern, m_patterns' type, and
getAllPatterns()/PatternSource handling consistently so ownership semantics are
clear and no use-after-free can occur.

In `@AestraAudio/include/Models/PlaylistMixer.h`:
- Around line 12-14: The int overload setResamplingQuality(int) casts blindly to
ClipResamplingQuality; either remove this overload or add validation to ensure
the incoming integer is within the valid enum range (0–3) before casting—e.g.,
check if quality >= 0 && quality <= 3, log or return early on invalid values,
otherwise call
setResamplingQuality(static_cast<ClipResamplingQuality>(quality)); reference the
setResamplingQuality(int) and ClipResamplingQuality symbols and update callers
if you choose to remove the overload to force enum usage.

In `@AestraAudio/include/Models/PlaylistModel.h`:
- Line 316: The code currently pushes observers via addChangeObserver and
invokes them from notifyObservers while holding m_mutex/unique_lock on the
non-recursive std::shared_mutex, which can deadlock if observers access the
model and races with addChangeObserver; fix by snapshotting m_changeObservers
under the lock (e.g., copy to a local vector), then release the lock and
iterate/call the observers from that local copy; apply the same pattern wherever
notifyObservers is called (also replace direct iteration over m_changeObservers
with a locked copy in those locations).

In `@AestraAudio/src/macOS/RtAudioDriver.cpp`:
- Around line 96-99: The hardcoded RtAudio::StreamOptions.numberOfBuffers = 2
should be made configurable via the existing AudioDeviceConfig so callers can
tune latency vs underrun risk; update the code that constructs
RtAudio::StreamOptions (look for RtAudio::StreamOptions and numberOfBuffers in
RtAudioDriver.cpp) to read a new field (e.g., audioDeviceConfig.numberOfBuffers
or provide a getter) from AudioDeviceConfig, fall back to the current default of
2 when unset, and validate the value (clamp to a sensible range like 1..8)
before assigning to options.numberOfBuffers.
- Around line 124-132: In RtAudioDriver::closeDevice, avoid silently swallowing
all exceptions: catch the exception thrown by rtAudio_->closeStream() (while
still preventing propagation) and log the error using the existing logging
facility (or add one) with context including RtAudioDriver::closeDevice,
isStreamOpen_ and rtAudio_ before setting isStreamOpen_ = false; keep the
try/catch to prevent throwing but ensure the catch records the exception
message/what() or a generic error if unavailable so failures during
closeStream() are visible for debugging.
- Around line 181-194: The callback is only given outputBufferPtrs_[0] so
multi-channel output is left unprocessed; update the code to either (A) call the
callback per channel in a loop (for each ch in driver->numOutputChannels_ call
cb->process(input channel pointer or per-channel input,
driver->outputBufferPtrs_[ch], nFrames)) so the driver performs
channel-by-channel processing, or (B) change the IAudioCallback::process
signature to accept an array of output pointers (e.g., const float* const
output[] or float** output) and then pass driver->outputBufferPtrs_ directly;
modify usages of IAudioCallback::process accordingly to handle non-interleaved
multi-channel buffers and ensure driver->outputBufferPtrs_ and
driver->numOutputChannels_ are correctly used.

In `@AestraAudio/src/macOS/RtAudioDriver.h`:
- Around line 15-24: AudioDeviceConfig currently declares four independent
members (inputChannels, outputChannels, numInputChannels, numOutputChannels)
which can diverge; pick a single canonical pair (e.g.,
inputChannels/outputChannels) and remove the duplicate num* members or replace
them with inline accessors that forward to the canonical members so they cannot
get out of sync, then update any callers (including openDevice()) to read/write
only the canonical members or the new accessors; ensure defaults remain the same
and no direct storage for num* remains to prevent silent divergence.

In `@AestraPlat/CMakeLists.txt`:
- Around line 113-121: The macOS link block conditionally links SDL2 using
SDL2_FOUND which misses the case where SDL2 is provided as an imported target
(add_subdirectory) and causes unresolved symbols; update the
target_link_libraries call for target AestraPlat to gate SDL2 linkage with
generator expression $<TARGET_EXISTS:SDL2::SDL2> (matching the source-selection
logic that sets AESTRA_HAS_SDL2) instead of using SDL2_FOUND so the SDL2::SDL2
target is linked when it exists.

In `@AestraPlat/src/macOS/PlatformThreadmacOS.cpp`:
- Around line 23-27: When handling ThreadPriority::Low (the
setpriority(PRIO_PROCESS, 0, 10) call), check setpriority's return value for -1
and inspect errno; on failure log or propagate the error (include errno or
strerror(errno) for context) and return false instead of always returning true —
update the block that currently handles ThreadPriority::Low to validate
setpriority's result, handle errno, and return an appropriate failure indicator.
- Around line 38-62: The function setCurrentThreadPriority incorrectly returns
true for unhandled ThreadPriority values and wrongly uses
setpriority(PRIO_PROCESS, ...) which affects the whole process; change the
default return to false and replace all uses of setpriority(PRIO_PROCESS, ...)
(seen in this file and the RealtimeAudio fallback) with per-thread APIs: use
pthread_setschedparam for SCHED_FIFO/SCHED_OTHER adjustments where appropriate,
and for realtime audio prefer thread_policy_set with
THREAD_TIME_CONSTRAINT_POLICY or pthread_set_qos_class_self_np for QoS-based
priorities; update the RealtimeAudio branch to attempt thread_policy_set or
pthread_setschedparam and only fall back to a per-thread qos change, returning
true on success and false on failure.

In `@AestraPlat/src/macOS/PlatformUtilsmacOS.cpp`:
- Around line 40-56: The three macOS stub methods
(PlatformUtilsmacOS::openFileDialog, saveFileDialog, selectFolderDialog)
currently just log and return an empty string; replace each with a native Cocoa
implementation using NSOpenPanel/NSSavePanel (move/rename the file to .mm if
needed) that runs on the main thread, configures allowed file types and
directory selection based on the existing parameters (use NSOpenPanel for
open/select-folder and NSSavePanel for save), presents the panel modally,
captures the selected NSURL, converts to std::string (UTF-8) for the return, and
properly handle cancelation by returning empty string; ensure to guard Cocoa
code with `#ifdef` __APPLE__ and keep function signatures (openFileDialog,
saveFileDialog, selectFolderDialog) unchanged.
- Around line 77-81: The getProcessorCount implementation uses an uninitialized
local int count and ignores sysctlbyname()'s return value, causing undefined
behavior on failure; in PlatformUtilsmacOS::getProcessorCount() initialize count
to 0, call sysctlbyname("hw.logicalcpu", &count, &size, nullptr, 0), check the
returned errno/return value (or retval) and only use count if the call succeeded
and count > 0, otherwise return a safe default (e.g., 1); mirror the error-check
pattern used in getSystemMemory() to log or handle failures consistently.
- Around line 17-33: Replace the file-scope globals and unsynchronized init with
a function-local thread-safe static for the timebase and compute using a safe
cast: remove or stop using s_timebaseInfo and s_timebaseInitialized, and in
PlatformUtilsmacOS::getTime() call mach_absolute_time(), then ensure a
function-local static mach_timebase_info_data_t (initialized via
mach_timebase_info inside the static initializer or use std::call_once) so the
first-time init is atomic/thread-safe; compute nanoseconds by casting the
uint64_t time to double before multiplying by the timebase numer and dividing by
denom (e.g., double nanoseconds = static_cast<double>(time) * info.numer /
info.denom) to avoid integer overflow and return nanoseconds/1e9.

In `@AestraPlat/src/macOS/PlatformWindowmacOS.cpp`:
- Around line 330-333: The setMouseCapture implementation
(PlatformWindowmacOS::setMouseCapture) currently toggles SDL state without
checking m_window; guard against a null m_window before calling
SDL_SetWindowGrab and SDL_SetRelativeMouseMode by returning early (or only
performing SDL calls) when m_window is null, while still updating
m_mouseCaptured as appropriate; reference m_window, m_mouseCaptured,
SDL_SetWindowGrab, SDL_SetRelativeMouseMode and consider lifecycle hooks such as
create() and destroy() to ensure no null window is passed to SDL.
- Around line 162-186: The SDL_TEXTINPUT handler in PlatformWindowmacOS.cpp
currently decodes only the first UTF-8 codepoint from e.text.text and drops the
rest; change the logic in the SDL_TEXTINPUT case (where the current
single-codepoint decode is implemented) to iterate over the null-terminated
UTF-8 buffer byte-by-byte, decode each full UTF-8 sequence into a Unicode
codepoint (handling 1-4 byte sequences and validating continuation bytes), and
invoke m_charCallback(codepoint) for every decoded codepoint until the buffer
terminator; keep existing branch decoding logic but perform it inside a loop
that advances the pointer by the number of bytes consumed for each codepoint and
stops at '\0'.
- Around line 144-149: The SDL_MOUSEWHEEL handler in PlatformWindowmacOS.cpp
unconditionally reads e.wheel.preciseY (broken on SDL < 2.0.18) and ignores
e.wheel.direction (SDL_MOUSEWHEEL_FLIPPED), so update the SDL_MOUSEWHEEL case:
read delta from e.wheel.preciseY only when SDL_VERSION_ATLEAST(2,0,18)
(otherwise fall back to e.wheel.y), then if e.wheel.direction ==
SDL_MOUSEWHEEL_FLIPPED multiply delta by -1 to correct macOS natural scrolling,
and finally call m_mouseWheelCallback(delta); modify the SDL_MOUSEWHEEL switch
block (the code that computes delta and calls m_mouseWheelCallback) to implement
these checks.

In `@AestraPlat/src/Platform.cpp`:
- Around line 90-102: The macOS initialization block currently calls SDL_Init,
SDL_SetHint and constructs PlatformUtilsmacOS unconditionally; wrap this entire
branch in the same AESTRA_HAS_SDL2 preprocessor guard used for the includes so
that when SDL2 is unavailable you return false instead of referencing SDL
symbols and PlatformUtilsmacOS at compile time. Specifically, enclose the
SDL_Init/SDL_SetHint calls and the s_utils = new PlatformUtilsmacOS() /
AESTRA_LOG_INFO lines within `#ifdef` AESTRA_HAS_SDL2 ... `#else` { return false; }
`#endif` (matching the Linux path behavior) so SDL_Init, SDL_SetHint and
PlatformUtilsmacOS are only used when AESTRA_HAS_SDL2 is defined.

In `@AestraUI/CMakeLists.txt`:
- Around line 228-254: The FreeType fallback only hints Homebrew paths; update
the CMake find step so find_library (AESTRAUI_FREETYPE_LIBRARY) and find_path
(AESTRAUI_FREETYPE_INCLUDE_DIR) also include common Linux locations (e.g.
/usr/lib, /usr/lib64, /usr/local/lib for libraries and /usr/include,
/usr/include/freetype2 for headers) in their HINTS or add additional
PATHS/PATH_SUFFIXES so the imported target AestraUI_Freetype will be found on
Linux systems as well.

In `@AestraUI/Graphics/OpenGL/NUIRendererGL.cpp`:
- Around line 520-533: The clip coordinate scaling is using dpiScale_ which is
the window scale, causing offscreen/cached passes (started by
renderToTextureBegin() / beginOffscreen()) to scissor against the wrong pixel
bounds; replace uses of dpiScale_ when computing
scaledX1/scaledY1/scaledX2/scaledY2 and bottomGL with the active render-target
ratio (the same ratio used to compute the current
framebufferWidth_/framebufferHeight_), i.e. use the render-target scale variable
(the one used when you set framebufferWidth_/framebufferHeight_ during
renderToTextureBegin()/beginOffscreen()) instead of dpiScale_ so scissor boxes
align to the actual target pixels.

In `@CMakeLists.txt`:
- Around line 64-72: The SDL2 inclusion block is never run on macOS because
AESTRA_PLATFORM_MACOS is never defined; update the platform check in the SDL2
block to use CMake's built-in APPLE variable instead of AESTRA_PLATFORM_MACOS.
Concretely, change the condition in the SDL2 include block (the if(...) that
currently references AESTRA_PLATFORM_LINUX OR AESTRA_PLATFORM_MACOS) to
reference AESTRA_PLATFORM_LINUX OR APPLE (or alternatively initialize
AESTRA_PLATFORM_MACOS from APPLE earlier), ensuring the find_package(SDL2 ...) /
add_subdirectory(external/SDL2) logic executes on macOS.

In `@MACOS_SUPPORT.md`:
- Line 127: Summary: macOS Support note mentions x86_64 only; add native ARM64
testing to CI. Update the CI build matrix to include an ARM64 job alongside
x86_64 (so both x86_64 and ARM64 architectures are built and tested), ensure
jobs run on macOS runners that support Apple Silicon (or on self-hosted M1/M2/M3
machines), set the appropriate architecture build flags/vars (e.g., ARCH/GOARCH
or equivalent build targets) for the ARM64 job, and produce native ARM64 or
universal2 artifacts instead of relying only on Rosetta 2 emulation; reflect
this change in the MACOS_SUPPORT.md note about testing and CI.
- Around line 58-82: The markdown has missing blank lines around headings and
fenced code blocks in the "Build Instructions" section; update MACOS_SUPPORT.md
so each heading ("Build Instructions", "Prerequisites", "Configure and Build")
is preceded and followed by a blank line and ensure every fenced code block (the
```bash blocks) is separated from surrounding text with blank lines (add a blank
line before the opening ``` and after the closing ```). This will resolve the
lint warnings about surrounding blank lines for headings and code fences.

In `@MAINTENANCE_SUMMARY.md`:
- Around line 85-98: Update the "Build Instructions" section in
MAINTENANCE_SUMMARY.md to remove the hardcoded user-specific path; replace
occurrences of "/Users/cedrick/Aestra" with a relative path or generic
placeholder such as "Aestra" or "<repo-root>" (e.g., change the `cd
/Users/cedrick/Aestra` step to `cd Aestra` or `cd <repo-root>`), and update
verification examples accordingly (e.g., `file build/bin/Aestra` instead of
`file /Users/cedrick/Aestra/build/bin/Aestra`) so the steps work for any
developer cloning the repository.

---

Outside diff comments:
In `@AestraAudio/include/Models/PlaylistModel.h`:
- Around line 227-248: The code removes a clip from its old lane before
verifying the destination exists, which can leave m_clipLaneMap stale when
newLaneId is valid syntactically but absent from m_laneMap; fix by first
checking m_laneMap.find(newLaneId) and only proceeding to erase from
m_lanes[old] and push into m_lanes[new] (and update m_clipLaneMap) if the
newLaneId is present; if not present, do nothing and return false so that
clipId, m_clipLaneMap, and lane contents remain unchanged; ensure
notifyClipChanged(clipId) and notifyObservers() are called only after a
successful move.

In `@AestraUI/Graphics/OpenGL/NUIRendererGL.cpp`:
- Around line 1630-1640: Remove the stray debugging/editor note block that was
accidentally left in NUIRendererGL.cpp: delete the commented investigation
chatter around measureText and NUISize (references to measureText, NUISize,
textSize) so only the actual centering logic and any single legitimate comment
remain; ensure you do not remove the real declarations or usages of measureText,
NUISize, or textSize—just strip the leftover notes to restore the clean
centering code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 404711d6-1f29-4cbe-9dbc-192233df43e8

📥 Commits

Reviewing files that changed from the base of the PR and between 2ab297f and c43d7f5.

📒 Files selected for processing (40)
  • AestraAudio/CMakeLists.txt
  • AestraAudio/include/AestraUUID.h
  • AestraAudio/include/Core/AutomationCurve.h
  • AestraAudio/include/DSP/SampleRateConverter.h
  • AestraAudio/include/IO/WaveformCache.h
  • AestraAudio/include/Models/ClipInstance.h
  • AestraAudio/include/Models/ClipSource.h
  • AestraAudio/include/Models/MeterSnapshot.h
  • AestraAudio/include/Models/PatternManager.h
  • AestraAudio/include/Models/PatternSource.h
  • AestraAudio/include/Models/PlaylistMixer.h
  • AestraAudio/include/Models/PlaylistModel.h
  • AestraAudio/include/Models/TrackManager.h
  • AestraAudio/include/Playback/TimelineClock.h
  • AestraAudio/src/macOS/RtAudioDriver.cpp
  • AestraAudio/src/macOS/RtAudioDriver.h
  • AestraPlat/CMakeLists.txt
  • AestraPlat/src/Platform.cpp
  • AestraPlat/src/macOS/PlatformThreadmacOS.cpp
  • AestraPlat/src/macOS/PlatformUtilsmacOS.cpp
  • AestraPlat/src/macOS/PlatformUtilsmacOS.h
  • AestraPlat/src/macOS/PlatformWindowmacOS.cpp
  • AestraPlat/src/macOS/PlatformWindowmacOS.h
  • AestraUI/CMakeLists.txt
  • AestraUI/Core/NUIAnimation.cpp
  • AestraUI/Core/NUITypes.h
  • AestraUI/Graphics/NUIRenderer.h
  • AestraUI/Graphics/OpenGL/NUIRenderCache.cpp
  • AestraUI/Graphics/OpenGL/NUIRendererGL.cpp
  • AestraUI/Graphics/OpenGL/NUIRendererGL.h
  • AestraUI/Platform/NUIPlatformBridge.cpp
  • CMakeLists.txt
  • CMakePresets.json
  • MACOS_SUPPORT.md
  • MAINTENANCE_SUMMARY.md
  • Source/Components/TrackUIComponent.h
  • Source/Core/AestraWindowManager.cpp
  • Source/Core/Preferences.cpp
  • Source/Panels/WindowPanel.cpp
  • Source/Settings/AudioSettingsPage.cpp
💤 Files with no reviewable changes (2)
  • AestraAudio/include/Models/MeterSnapshot.h
  • Source/Core/Preferences.cpp

Comment on lines +63 to +64
using LocalEdits = ClipEdits;

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.

🧹 Nitpick | 🔵 Trivial

Consider adding a comment explaining LocalEdits alias purpose.

If LocalEdits is intended for semantic distinction or future expansion, a brief comment would help maintainability. Otherwise, direct usage of ClipEdits may be clearer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AestraAudio/include/Models/ClipInstance.h` around lines 63 - 64, Add a short
clarifying comment above the using alias LocalEdits = ClipEdits; explaining why
the alias exists (e.g., to convey semantic intent for per-instance/local edit
semantics or to reserve the name for future specialized behavior) and when to
prefer LocalEdits versus ClipEdits; reference the alias symbol LocalEdits and
the underlying type ClipEdits so reviewers can easily find and understand the
intention in ClipInstance.h.

Comment on lines +76 to 78
bool muted = false;

ClipEdits edits;
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how both muted fields are used to understand intended semantics
rg -n '\.muted\b' --type=cpp -C 2 | head -80

# Check for any logic combining the two muted states
rg -n 'edits\.muted|instance.*muted' --type=cpp -C 3

Repository: currentsuspect/Aestra

Length of output: 5985


🏁 Script executed:

#!/bin/bash
# Check the ClipInstance.h header structure
cat -n AestraAudio/include/Models/ClipInstance.h | grep -A 10 -B 10 "muted\|ClipEdits"

Repository: currentsuspect/Aestra

Length of output: 1757


🏁 Script executed:

#!/bin/bash
# More thorough search for any usage of muted as a member of ClipInstance (not edits)
# Search patterns: obj.muted, instance.muted, clip.muted (not clip.edits.muted)
rg -n '\bmuted\b' --type=cpp -A 2 -B 2 | grep -v 'edits\.muted\|channel->muted\|info\.muted\|existing->muted' | head -100

Repository: currentsuspect/Aestra

Length of output: 6943


🏁 Script executed:

#!/bin/bash
# Search for any direct assignment or read of ClipInstance muted field
# Look for patterns like: instance.muted, clip.muted (not clip.edits.muted), ->muted (for pointers)
rg -n 'clip\s*\.\s*muted(?!\s*\.)|instance\s*\.\s*muted|->muted' --type=cpp | grep -v 'edits\|channel\|lane\|mixer\|info\|existing'

Repository: currentsuspect/Aestra

Length of output: 341


🏁 Script executed:

#!/bin/bash
# Search for ClipInstance constructor usage or initialization
ast-grep --pattern 'ClipInstance $$ = $$$' | head -50

Repository: currentsuspect/Aestra

Length of output: 47


🏁 Script executed:

#!/bin/bash
# Simple search for clip.muted or instance.muted patterns
rg -n '\bclip\b.*\bmuted\b' --type=cpp | head -50

Repository: currentsuspect/Aestra

Length of output: 424


🏁 Script executed:

#!/bin/bash
# Alternative: search for assignment to muted field on ClipInstance context
rg -n '\bmuted\s*=' --type=cpp -B 3 | grep -A 3 -B 3 'ClipInstance\|clip\.' | head -80

Repository: currentsuspect/Aestra

Length of output: 1788


Remove unused instance-level muted field.

The instance-level muted field on line 76 is dead code—it is never read or written anywhere in the codebase. All actual mute state is managed through ClipInstance::edits.muted, which is used in serialization (ProjectSerializer.cpp), UI rendering (TrackUIComponent.cpp), and tests.

Remove the unused field to eliminate confusion and keep the codebase clean.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AestraAudio/include/Models/ClipInstance.h` around lines 76 - 78, Remove the
dead instance-level field `muted` from the ClipInstance class: this field is
never read or written—mute state is stored in `ClipInstance::edits.muted` (type
`ClipEdits`). Delete the `bool muted = false;` declaration in ClipInstance and
ensure no remaining references exist (the serializer and UI already use
`edits.muted` as seen in ProjectSerializer and TrackUIComponent).

Comment on lines +112 to +123
PatternID clonePattern(PatternID sourceId) {
auto* source = getPattern(sourceId);
if (!source) return PatternID{};

PatternID newId{nextId++};
auto pattern = std::make_unique<PatternSource>();
pattern->id = newId;
pattern->name = source->name + " (Copy)";
pattern->lengthBeats = source->lengthBeats;
pattern->type = source->type;
pattern->payload = source->payload;
m_patterns[newId.value] = std::move(pattern);
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="$(fd '^PatternSource\.h$' | head -n1)"
if [[ -z "${file}" ]]; then
  echo "PatternSource.h not found" >&2
  exit 1
fi

echo "Inspecting ${file}"
sed -n '1,240p' "${file}"

Repository: currentsuspect/Aestra

Length of output: 2782


Clone silently drops the mixer channel assignment.

The clonePattern method does not copy m_mixerChannel, causing cloned patterns to lose their mixer routing. Add pattern->m_mixerChannel = source->m_mixerChannel; to preserve this state, or explicitly document why clones should start with a reset channel assignment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AestraAudio/include/Models/PatternManager.h` around lines 112 - 123, In
clonePattern (function PatternID clonePattern) the cloned PatternSource
currently doesn't preserve the source's mixer routing; update the clone creation
to copy source->m_mixerChannel into the new pattern (pattern->m_mixerChannel =
source->m_mixerChannel) so the cloned pattern retains its mixer assignment, or
alternatively add a clear comment in PatternSource/clonePattern explaining why
m_mixerChannel is intentionally reset.

Comment on lines +130 to +132
void removePattern(PatternID id) {
m_patterns.erase(id.value);
}
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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "PatternManager.h" -type f

Repository: currentsuspect/Aestra

Length of output: 110


🏁 Script executed:

cat -n "AestraAudio/include/Models/PatternManager.h"

Repository: currentsuspect/Aestra

Length of output: 5040


🏁 Script executed:

rg "getAllPatterns" --type cpp --type h -B 2 -A 5

Repository: currentsuspect/Aestra

Length of output: 4009


🏁 Script executed:

rg "removePattern" --type cpp --type h -B 2 -A 5

Repository: currentsuspect/Aestra

Length of output: 1102


removePattern() introduces a use-after-free vulnerability in getAllPatterns().

getAllPatterns() returns std::shared_ptr<PatternSource> objects with no-op deleters, containing raw pointers to objects owned by the unique_ptr in m_patterns. When removePattern() erases the map entry, it deletes the underlying PatternSource object, but any retained shared_ptr instances still reference the freed memory. This is a critical memory safety issue.

The code comment at line 96 acknowledges this is "not ideal but works for shim," indicating the developers were aware of the design flaw.

Resolve by either:

  1. Backing m_patterns with shared_ptr instead of unique_ptr to share ownership, or
  2. Refactoring getAllPatterns() to return non-owning views (IDs or raw pointers) instead of shared_ptr.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AestraAudio/include/Models/PatternManager.h` around lines 130 - 132,
removePattern(PatternID id) erases the unique_ptr-held PatternSource in
m_patterns causing shared_ptrs returned by getAllPatterns() (which use no-op
deleters and raw pointers) to dangle; fix by either converting m_patterns to
hold std::shared_ptr<PatternSource> so getAllPatterns() can return shared_ptrs
that correctly extend object lifetime, or change getAllPatterns() to return
non-owning references (e.g., PatternID list or raw/non-owning pointers) instead
of shared_ptrs; update removePattern, m_patterns' type, and
getAllPatterns()/PatternSource handling consistently so ownership semantics are
clear and no use-after-free can occur.

Comment on lines +12 to +14
static void setResamplingQuality(int quality) {
setResamplingQuality(static_cast<ClipResamplingQuality>(quality));
}
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check ClipResamplingQuality enum definition to determine valid range
ast-grep --pattern $'enum class ClipResamplingQuality {
  $$$
}'

# Fallback: search for enum definition
rg -n 'enum.*ClipResamplingQuality' -A 10

Repository: currentsuspect/Aestra

Length of output: 18416


🏁 Script executed:

# Find all calls to setResamplingQuality with int argument
rg -n 'setResamplingQuality\s*\(\s*\d+\s*\)|setResamplingQuality\s*\(\s*\w+\s*\)' -A 2 -B 2

# Also search for the implementation in PlaylistMixer
rg -n 'setResamplingQuality' -A 3 -B 1 AestraAudio/include/Models/PlaylistMixer.h

# Check if there's a definition in PlaylistMixer.h
cat -n AestraAudio/include/Models/PlaylistMixer.h | head -50

Repository: currentsuspect/Aestra

Length of output: 1191


🏁 Script executed:

# Search for calls to setResamplingQuality(int) - look for numeric literals or int variables
rg -n 'setResamplingQuality\(' --type cpp --type h | grep -E '\(.*\)' | head -30

# More specific: search for setResamplingQuality calls with int or numeric values
rg -n 'PlaylistMixer.*setResamplingQuality|\.setResamplingQuality' -B 2 -A 2

# Check AudioSettingsPage.cpp mentioned in original review
fd 'AudioSettingsPage' -type f | head -5

Repository: currentsuspect/Aestra

Length of output: 2198


🏁 Script executed:

# Examine AudioSettingsPage.cpp around the call to setResamplingQuality
sed -n '370,400p' Source/Settings/AudioSettingsPage.cpp

Repository: currentsuspect/Aestra

Length of output: 1288


🏁 Script executed:

# Check what determines qIdx and its range
sed -n '360,390p' Source/Settings/AudioSettingsPage.cpp

Repository: currentsuspect/Aestra

Length of output: 1219


Int overload lacks validation — consider removing or adding bounds checks.

The setResamplingQuality(int) overload casts any int directly to ClipResamplingQuality without validating it's in the valid range [0–3]. While the current call site in AudioSettingsPage.cpp validates before calling, the function signature itself permits invalid values and offers no protection against future misuse.

Consider either:

  • Removing the int overload to force explicit enum usage
  • Adding bounds validation (return early or log if out of range)
  • Documenting the valid range and expected usage pattern
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AestraAudio/include/Models/PlaylistMixer.h` around lines 12 - 14, The int
overload setResamplingQuality(int) casts blindly to ClipResamplingQuality;
either remove this overload or add validation to ensure the incoming integer is
within the valid enum range (0–3) before casting—e.g., check if quality >= 0 &&
quality <= 3, log or return early on invalid values, otherwise call
setResamplingQuality(static_cast<ClipResamplingQuality>(quality)); reference the
setResamplingQuality(int) and ClipResamplingQuality symbols and update callers
if you choose to remove the overload to force enum usage.

Comment on lines +520 to +533
const float scaledX1 = x1 * dpiScale_;
const float scaledY1 = y1 * dpiScale_;
const float scaledX2 = x2 * dpiScale_;
const float scaledY2 = y2 * dpiScale_;

int glX = static_cast<int>(std::floor(scaledX1));
int glRight = static_cast<int>(std::ceil(scaledX2));
int glWidth = std::max(0, glRight - glX);

// Convert to GL coords (bottom-up)
// Range in UI (y-down): [y1, y2]
// Range in GL (y-up): [height_ - y2, height_ - y1]

float bottomGL = static_cast<float>(height_) - y2;
float topGL = static_cast<float>(height_) - y1;
float bottomGL = static_cast<float>(framebufferHeight_) - scaledY2;
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.

⚠️ Potential issue | 🟠 Major

Use the active render-target ratio here instead of dpiScale_.

renderToTextureBegin() and beginOffscreen() temporarily replace framebufferWidth_/framebufferHeight_ with the target size, but dpiScale_ still holds the window scale. Multiplying clip coordinates by dpiScale_ here makes offscreen/cached passes scissor at the wrong pixel bounds on Retina displays.

🎯 Suggested fix
-    const float scaledX1 = x1 * dpiScale_;
-    const float scaledY1 = y1 * dpiScale_;
-    const float scaledX2 = x2 * dpiScale_;
-    const float scaledY2 = y2 * dpiScale_;
+    const float scaleX = width_ > 0
+        ? static_cast<float>(framebufferWidth_) / static_cast<float>(width_)
+        : 1.0f;
+    const float scaleY = height_ > 0
+        ? static_cast<float>(framebufferHeight_) / static_cast<float>(height_)
+        : 1.0f;
+    const float scaledX1 = x1 * scaleX;
+    const float scaledY1 = y1 * scaleY;
+    const float scaledX2 = x2 * scaleX;
+    const float scaledY2 = y2 * scaleY;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const float scaledX1 = x1 * dpiScale_;
const float scaledY1 = y1 * dpiScale_;
const float scaledX2 = x2 * dpiScale_;
const float scaledY2 = y2 * dpiScale_;
int glX = static_cast<int>(std::floor(scaledX1));
int glRight = static_cast<int>(std::ceil(scaledX2));
int glWidth = std::max(0, glRight - glX);
// Convert to GL coords (bottom-up)
// Range in UI (y-down): [y1, y2]
// Range in GL (y-up): [height_ - y2, height_ - y1]
float bottomGL = static_cast<float>(height_) - y2;
float topGL = static_cast<float>(height_) - y1;
float bottomGL = static_cast<float>(framebufferHeight_) - scaledY2;
const float scaleX = width_ > 0
? static_cast<float>(framebufferWidth_) / static_cast<float>(width_)
: 1.0f;
const float scaleY = height_ > 0
? static_cast<float>(framebufferHeight_) / static_cast<float>(height_)
: 1.0f;
const float scaledX1 = x1 * scaleX;
const float scaledY1 = y1 * scaleY;
const float scaledX2 = x2 * scaleX;
const float scaledY2 = y2 * scaleY;
int glX = static_cast<int>(std::floor(scaledX1));
int glRight = static_cast<int>(std::ceil(scaledX2));
int glWidth = std::max(0, glRight - glX);
// Convert to GL coords (bottom-up)
// Range in UI (y-down): [y1, y2]
// Range in GL (y-up): [height_ - y2, height_ - y1]
float bottomGL = static_cast<float>(framebufferHeight_) - scaledY2;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AestraUI/Graphics/OpenGL/NUIRendererGL.cpp` around lines 520 - 533, The clip
coordinate scaling is using dpiScale_ which is the window scale, causing
offscreen/cached passes (started by renderToTextureBegin() / beginOffscreen())
to scissor against the wrong pixel bounds; replace uses of dpiScale_ when
computing scaledX1/scaledY1/scaledX2/scaledY2 and bottomGL with the active
render-target ratio (the same ratio used to compute the current
framebufferWidth_/framebufferHeight_), i.e. use the render-target scale variable
(the one used when you set framebufferWidth_/framebufferHeight_ during
renderToTextureBegin()/beginOffscreen()) instead of dpiScale_ so scissor boxes
align to the actual target pixels.

Comment on lines +64 to +72
# SDL2 (required for Linux and macOS platform layers)
if(AESTRA_PLATFORM_LINUX OR AESTRA_PLATFORM_MACOS)
find_package(SDL2 QUIET)
if(NOT SDL2_FOUND)
if(EXISTS "${CMAKE_SOURCE_DIR}/external/SDL2/CMakeLists.txt")
add_subdirectory(external/SDL2)
else()
message(STATUS "SDL2 not found. Install with: brew install sdl2 (macOS) or apt install libsdl2-dev (Linux)")
endif()
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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C2 '\bAESTRA_PLATFORM_MACOS\b' --glob 'CMakeLists.txt' --glob '*.cmake' --glob 'CMakePresets.json'

Repository: currentsuspect/Aestra

Length of output: 387


🏁 Script executed:

# Check the entire CMakeLists.txt for variable definitions and initialization patterns
head -100 CMakeLists.txt | cat -n

Repository: currentsuspect/Aestra

Length of output: 4317


🏁 Script executed:

# Search for all AESTRA_PLATFORM_* variable definitions across the entire repository
rg -n 'AESTRA_PLATFORM_' --glob 'CMakeLists.txt' -A2 -B2

Repository: currentsuspect/Aestra

Length of output: 1230


🏁 Script executed:

# Check if there are any other CMake files that might define AESTRA_PLATFORM_MACOS
fd -e cmake -e txt | xargs rg 'AESTRA_PLATFORM' 2>/dev/null || echo "No matches in other files"

Repository: currentsuspect/Aestra

Length of output: 347


🏁 Script executed:

# Look for any cache variable definitions or set() calls that might initialize platform flags
rg -n 'set\(AESTRA_PLATFORM' -A1 -B1

Repository: currentsuspect/Aestra

Length of output: 224


AESTRA_PLATFORM_MACOS is never defined; SDL2 block is dead code for macOS.

AESTRA_PLATFORM_LINUX is set only when UNIX AND NOT APPLE (line 45). On macOS, neither platform variable is initialized, so the SDL2 dependency block (line 65) never executes. Replace with the built-in APPLE CMake variable:

Fix
-if(AESTRA_PLATFORM_LINUX OR AESTRA_PLATFORM_MACOS)
+if(AESTRA_PLATFORM_LINUX OR APPLE)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CMakeLists.txt` around lines 64 - 72, The SDL2 inclusion block is never run
on macOS because AESTRA_PLATFORM_MACOS is never defined; update the platform
check in the SDL2 block to use CMake's built-in APPLE variable instead of
AESTRA_PLATFORM_MACOS. Concretely, change the condition in the SDL2 include
block (the if(...) that currently references AESTRA_PLATFORM_LINUX OR
AESTRA_PLATFORM_MACOS) to reference AESTRA_PLATFORM_LINUX OR APPLE (or
alternatively initialize AESTRA_PLATFORM_MACOS from APPLE earlier), ensuring the
find_package(SDL2 ...) / add_subdirectory(external/SDL2) logic executes on
macOS.

Comment on lines +58 to +82
## Build Instructions

### Prerequisites

```bash
# Install dependencies via Homebrew
brew install cmake sdl2 freetype
```

### Configure and Build

```bash
cd Aestra

# Configure
cmake -B build -DAESTRA_ENABLE_TESTS=OFF

# Build everything
cmake --build build -j$(sysctl -n hw.ncpu)

# Or build specific targets
cmake --build build --target AestraHeadless # Headless only
cmake --build build --target AestraPlat # Platform layer
cmake --build build --target AestraAudioMacOS # Audio driver
```
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.

⚠️ Potential issue | 🟡 Minor

Fix markdown formatting issues.

Static analysis flagged several markdown lint issues: headings should be surrounded by blank lines, and a fenced code block at line 81 should be surrounded by blank lines.

📝 Proposed fix for markdown formatting
 ## Build Instructions
 
 ### Prerequisites
+
 ```bash
 # Install dependencies via Homebrew
-brew install cmake sdl2 freetype
+brew install cmake sdl2 freetype

Configure and Build

cd Aestra

# Configure
cmake -B build -DAESTRA_ENABLE_TESTS=OFF

# Build everything
cmake --build build -j$(sysctl -n hw.ncpu)

# Or build specific targets
cmake --build build --target AestraHeadless  # Headless only
cmake --build build --target AestraPlat      # Platform layer
cmake --build build --target AestraAudioMacOS # Audio driver
</details>

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.22.0)</summary>

[warning] 61-61: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

---

[warning] 63-63: Trailing spaces
Expected: 0 or 2; Actual: 1

(MD009, no-trailing-spaces)

---

[warning] 66-66: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

---

[warning] 69-69: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

---

[warning] 75-75: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

---

[warning] 81-81: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @MACOS_SUPPORT.md around lines 58 - 82, The markdown has missing blank lines
around headings and fenced code blocks in the "Build Instructions" section;
update MACOS_SUPPORT.md so each heading ("Build Instructions", "Prerequisites",
"Configure and Build") is preceded and followed by a blank line and ensure every
fenced code block (the bash blocks) is separated from surrounding text with blank lines (add a blank line before the opening and after the closing ```).
This will resolve the lint warnings about surrounding blank lines for headings
and code fences.


</details>

<!-- fingerprinting:phantom:medusa:ocelot:f3804fe6-1862-405a-ac8b-d553feaf11c2 -->

<!-- This is an auto-generated comment by CodeRabbit -->


1. Implement native Cocoa file dialogs (NSOpenPanel/NSSavePanel)
2. Add VST3 plugin hosting support
3. Test on Apple Silicon (M1/M2/M3) - currently built for x86_64
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.

🧹 Nitpick | 🔵 Trivial

Apple Silicon testing is critical for production macOS support.

The note about testing on Apple Silicon (M1/M2/M3) is important. The current x86_64 build should work via Rosetta 2, but native ARM64 builds will provide better performance. Consider adding ARM64 to the build matrix in CI.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MACOS_SUPPORT.md` at line 127, Summary: macOS Support note mentions x86_64
only; add native ARM64 testing to CI. Update the CI build matrix to include an
ARM64 job alongside x86_64 (so both x86_64 and ARM64 architectures are built and
tested), ensure jobs run on macOS runners that support Apple Silicon (or on
self-hosted M1/M2/M3 machines), set the appropriate architecture build
flags/vars (e.g., ARCH/GOARCH or equivalent build targets) for the ARM64 job,
and produce native ARM64 or universal2 artifacts instead of relying only on
Rosetta 2 emulation; reflect this change in the MACOS_SUPPORT.md note about
testing and CI.

Comment on lines +85 to +98
## Build Instructions

```bash
cd /Users/cedrick/Aestra

# Configure
cmake -B build -DAESTRA_ENABLE_TESTS=OFF

# Build (parallel)
cmake --build build -j$(sysctl -n hw.ncpu)

# Run
./build/bin/Aestra
```
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.

⚠️ Potential issue | 🟡 Minor

Remove user-specific paths from documentation.

The hardcoded path /Users/cedrick/Aestra is user-specific and won't apply to other developers. Use relative paths or generic placeholders.

📝 Proposed fix
 ## Build Instructions
 
 ```bash
-cd /Users/cedrick/Aestra
+cd Aestra
 
 # Configure
 cmake -B build -DAESTRA_ENABLE_TESTS=OFF

And for verification:

 ## Verification
 
 ```bash
 # Verify executable
-$ file /Users/cedrick/Aestra/build/bin/Aestra
+$ file build/bin/Aestra
 Mach-O 64-bit executable x86_64
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MAINTENANCE_SUMMARY.md` around lines 85 - 98, Update the "Build Instructions"
section in MAINTENANCE_SUMMARY.md to remove the hardcoded user-specific path;
replace occurrences of "/Users/cedrick/Aestra" with a relative path or generic
placeholder such as "Aestra" or "<repo-root>" (e.g., change the `cd
/Users/cedrick/Aestra` step to `cd Aestra` or `cd <repo-root>`), and update
verification examples accordingly (e.g., `file build/bin/Aestra` instead of
`file /Users/cedrick/Aestra/build/bin/Aestra`) so the steps work for any
developer cloning the repository.

@currentsuspect currentsuspect deleted the feat/macos-support branch March 30, 2026 07:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants