Date: 2026-03-08
Branch: develop
Commit: b2d2543 (UUID fix)
Auditor: Resonance with GPT-5.4
| Category | Issues Found | Severity |
|---|---|---|
| Thread Safety | 3 | 🔴 High |
| Memory Management | 2 | 🟡 Medium |
| API Design | 2 | 🟡 Medium |
| Performance | 2 | 🟢 Low |
| Testing Gaps | 1 | 🟡 Medium |
Overall Assessment: Solid foundation with Phase 2 undo/redo complete, but critical thread safety issues need addressing before v1 Beta.
Location: AestraAudio/src/Commands/CommandHistory.cpp
Problem:
void CommandHistory::pushAndExecute(std::shared_ptr<ICommand> cmd) {
std::lock_guard<std::mutex> lock(m_mutex);
// ... execute ...
if (m_onStateChanged) {
m_onStateChanged(); // 🔴 Called WITH lock held!
}
}Risk: If callback tries to query CommandHistory (e.g., canUndo()), it will deadlock (reentrant mutex attempt).
Fix:
void CommandHistory::pushAndExecute(std::shared_ptr<ICommand> cmd) {
bool stateChanged = false;
{
std::lock_guard<std::mutex> lock(m_mutex);
// ... execute ...
stateChanged = true;
}
if (stateChanged && m_onStateChanged) {
m_onStateChanged(); // ✅ Called WITHOUT lock
}
}Location: AestraAudio/include/Models/PlaylistModel.h
Problem:
void moveClip(const ClipInstanceID& clipId, double newStartBeat, ...) {
std::lock_guard<std::mutex> lock(m_mutex);
auto* clip = getClipInternal(clipId); // 🔴 Also locks m_mutex!
// ...
}getClipInternal() is a private method that expects the lock to be held, but moveClip() takes the lock then calls it. This works only if getClipInternal() doesn't lock — but looking at the code, it doesn't lock, so this is actually okay... BUT the pattern is fragile.
Risk: Future refactoring could introduce double-lock deadlock.
Fix: Document lock expectations with comments:
// REQUIRES: m_mutex is held by caller
ClipInstance* getClipInternal(const ClipInstanceID& clipId);Location: AestraAudio/include/Models/PlaylistModel.h + Audio Thread
Problem: The audio thread reads PlaylistModel via buildRuntimeSnapshot() while the UI thread modifies it via commands. The mutex protects the model, but:
- Lock contention: Audio thread holds lock during entire snapshot build
- Priority inversion: UI thread could block audio thread
Current Code:
std::unique_ptr<PlaylistRuntimeSnapshot> buildRuntimeSnapshot(...) const {
std::lock_guard<std::mutex> lock(m_mutex); // 🔴 Blocks audio thread
// ... builds entire snapshot ...
}Risk: Audio glitches if UI thread holds lock during snapshot.
Fix Options:
- Double-buffering: Keep two copies, swap atomically
- Read-write lock: Allow concurrent reads from audio thread
- Lock-free snapshot: Copy-on-write pattern
Recommended: Implement std::shared_mutex (C++17) for reader-writer pattern:
mutable std::shared_mutex m_mutex;
// Audio thread (reader)
std::shared_lock<std::shared_mutex> lock(m_mutex);
// UI thread (writer)
std::unique_lock<std::shared_mutex> lock(m_mutex);Location: AestraAudio/src/Commands/CommandHistory.cpp
Problem: setMaxHistorySize() exists but default is 100. Each command stores captured state. For large projects with many clips, this could be significant memory.
Fix: Add memory-based limit in addition to count-based:
void setMaxHistorySize(size_t count, size_t maxMemoryBytes = 100 * 1024 * 1024);Location: AestraAudio/include/Commands/ICommand.h
Problem:
virtual void redo() { execute(); } // 🟡 May not be correct for all commandsThis assumes execute() is idempotent, which may not hold if commands modify state on first execution.
Fix: Make redo() pure virtual to force explicit implementation:
virtual void redo() = 0; // Force implementers to think about itLocation: Tests/Commands/
Problem: All command tests are single-threaded. No tests verify:
- Command execution during audio processing
- Rapid undo/redo from UI
- Concurrent command pushes from different threads
Fix: Add thread-safety tests:
TEST(CommandHistoryThreadSafety, ConcurrentPushAndUndo) {
// Spawn threads pushing commands while others undo
// Verify no crashes, no deadlocks
}Location: AestraAudio/src/Commands/CommandHistory.cpp
Problem: erase(begin()) in a loop is O(n²) for large trims.
Current:
while (m_undoStack.size() > m_maxHistorySize) {
m_undoStack.erase(m_undoStack.begin()); // O(n) each
}Fix: Use std::deque for O(1) pop_front, or batch erase:
if (m_undoStack.size() > m_maxHistorySize) {
size_t excess = m_undoStack.size() - m_maxHistorySize;
m_undoStack.erase(m_undoStack.begin(), m_undoStack.begin() + excess);
}Location: AestraAudio/include/Core/MixerBus.h
Problem: Parameters are atomic but read once per process call, not per sample. Actually this is fine — the atomic load is done once per buffer, not per sample.
Status: ✅ Actually okay, disregard.
- Clean Architecture: Clear separation between Core, Audio, Platform, UI
- Modern C++17: Using smart pointers, atomics, mutexes appropriately
- Phase 2 Complete: Undo/redo infrastructure is solid
- Test Coverage: Unit tests exist for core commands
- Documentation: PHASE2_AUDIT.md shows good tracking
- Fix CommandHistory callback deadlock (Issue #1)
- Add
std::shared_mutexto PlaylistModel (Issue #3)
- Document lock requirements in PlaylistModel (Issue #2)
- Add concurrent command tests (Issue #6)
- Make ICommand::redo() pure virtual (Issue #5)
- Implement memory-based history limit (Issue #4)
- Optimize trimHistory() (Issue #7)
I can immediately apply fixes for issues #1, #2, and #5. Should I proceed?
Estimated Time: 30 minutes
Risk: Low (well-tested patterns)
Benefit: Eliminates deadlock risks, improves thread safety