-
-
Notifications
You must be signed in to change notification settings - Fork 318
AUv3: Implement sample accurate MIDI #1313
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Previously, AUv3 processed all MIDI events before audio processing, meaning events effectively occurred at sample 0 regardless of their actual offset. This could introduce up to one buffer of timing jitter (~5ms at 256 samples/48kHz). Changes: - Add ProcessBuffers() overloads with buffer offset parameter to IPlugProcessor, allowing processing of audio segments starting at any position within the buffer - Rewrite AUv3 ProcessWithEvents() to split the buffer around MIDI/parameter events, processing audio in segments between events - Handle events at their exact sample positions within the buffer - Remove the incomplete commented-out buffer splitting code This makes MIDI timing in AUv3 sample-accurate, matching the precision expected by the AUv3 specification where events carry sample-accurate timestamps. Reference: https://cp3.io/posts/sample-accurate-midi-timing/
Pull Request Review: Sample-Accurate MIDI Timing for AUv3Great work on implementing sample-accurate MIDI timing for AUv3! This is a significant improvement that addresses a real limitation in the previous implementation. Here's my detailed review: ✅ Positive Aspects1. Correct Core ImplementationThe buffer-splitting approach in
2. Good Code Quality
3. Excellent Test CoverageThe Swift-based test suite (
4. Documentation
🔍 Issues & Concerns1. Critical: Pointer Arithmetic Safety (IPlugProcessor.cpp:547-559)for (int i = 0; i < nIn; ++i)
ppInData[i] += bufferOffset;
for (int i = 0; i < nOut; ++i)
ppOutData[i] += bufferOffset;
ProcessBlock(ppInData, ppOutData, nFrames);
for (int i = 0; i < nIn; ++i)
ppInData[i] -= bufferOffset;
for (int i = 0; i < nOut; ++i)
ppOutData[i] -= bufferOffset;Issue: If Recommendation: Use RAII or copy the pointers: sample* ppInDataCopy[nIn];
sample* ppOutDataCopy[nOut];
for (int i = 0; i < nIn; ++i)
ppInDataCopy[i] = ppInData[i] + bufferOffset;
for (int i = 0; i < nOut; ++i)
ppOutDataCopy[i] = ppOutData[i] + bufferOffset;
ProcessBlock(ppInDataCopy, ppOutDataCopy, nFrames);2. Potential Bug: Negative Event Offsets (IPlugAUv3.mm:158-161)int32_t eventOffset = static_cast<int32_t>(eventTime - startTime);
if (eventOffset < 0)
eventOffset = 0;Issue: Events scheduled in the past are processed at offset 0, which could cause them to be processed multiple times if the loop continues. Question: Should events with
The current implementation processes them, which is probably correct, but worth clarifying. 3. Performance: Redundant Mutex Locking (IPlugAUv3.mm:189-192)ENTER_PARAMS_MUTEX
GetParam(paramIdx)->Set(value);
LEAVE_PARAMS_MUTEX
OnParamChange(paramIdx, EParamSource::kHost, static_cast<int>(bufferOffset));
Then at line 215-217: ENTER_PARAMS_MUTEX
ProcessBuffers(0.f, static_cast<int>(framesToProcess), static_cast<int>(bufferOffset));
LEAVE_PARAMS_MUTEXConcern: The mutex is locked/unlocked multiple times per buffer. For a buffer with many events, this could add overhead. Suggestion: Consider holding the mutex for the entire event processing loop if thread safety requirements allow. 4. Inconsistency: Attack Parameter Change (IPlugInstrument.cpp:10)- GetParam(kParamAttack)->InitDouble("Attack", 10., 1., 1000., ...
+ GetParam(kParamAttack)->InitDouble("Attack", 0., 0., 1000., ...Question: Is this change related to sample-accurate MIDI testing?
This seems like it's to make the timing test more responsive (instant attack), but it changes the default behavior of IPlugInstrument. Should this be:
The PR title says "DROP?: add AUv3 TimingTester" for the second commit, but this change is in the first commit. 🛡️ Security ConcernsNone identified. The changes don't introduce:
⚡ Performance Considerations1. Buffer Splitting OverheadThe new implementation processes audio in smaller chunks. For a buffer with events at samples [50, 100, 256, 400], you'll make 5 ProcessBlock calls instead of 1. Analysis: This is the correct tradeoff for sample-accurate timing. Most buffers won't have many events, so overhead should be minimal. 2. Mutex Lock/Unlock FrequencyAs noted above, frequent mutex operations could add latency in pathological cases (e.g., 1 event per sample). Recommendation: Profile with a stress test that sends dense MIDI data. 🧪 Test CoverageStrengths:
Gaps:
Recommendation: Add a multi-event test case to verify the loop logic. 📋 Best Practices & StyleGood:
Minor:
🎯 Recommendations SummaryBefore Merge:
Optional Improvements:
🏁 VerdictOverall Assessment: This is a solid implementation of an important feature. The core algorithm is correct, the test suite is well-designed, and the code is generally high quality. Recommendation: ✅ Approve with minor revisions The pointer arithmetic issue should be fixed before merge, and the attack parameter change should be explained/documented. Otherwise, this is ready to go. Great work! This will significantly improve MIDI timing precision in AUv3 plugins. Reviewed by: Claude Code (Sonnet 4.5) |
66fb7a3 to
b1f552e
Compare
Code Review: Sample-Accurate MIDI Timing for AUv3Summary: This PR implements sample-accurate MIDI timing for AUv3 by splitting the audio buffer around events. The approach is sound and addresses a real timing issue. Strengths
Critical Issue: Potential Buffer OverflowLocation: IPlug/IPlugProcessor.cpp:533-560 The new ProcessBuffers() overload lacks bounds validation. If bufferOffset + nFrames exceeds mBlockSize, out-of-bounds access will occur. Must fix: Add validation: assert(bufferOffset >= 0);
assert(bufferOffset + nFrames <= mBlockSize);Other IssuesMedium - Missing optimization: When pEvents is null, avoid loop overhead with early return (IPlugAUv3.mm:149-220) Low - Integer overflow: Cast from AUEventSampleTime to int32_t could overflow (IPlugAUv3.mm:158) Test CoverageMissing automated tests for this critical timing functionality. Recommend adding tests for MIDI events at specific sample offsets. Performance Considerations
Style ComplianceFollows CLAUDE.md conventions: 2-space indentation, naming conventions, C++17 usage, clear comments. RecommendationApprove with changes. Implementation is sound, but bounds checking must be added before merge. Great work on improving MIDI timing precision! |
Previously, AUv3 processed all MIDI events before audio processing, meaning events effectively occurred at sample 0 regardless of their actual offset. This could introduce up to one buffer of timing jitter (~5ms at 256 samples/48kHz).
Changes:
Add ProcessBuffers() overloads with buffer offset parameter to IPlugProcessor, allowing processing of audio segments starting at any position within the buffer
Rewrite AUv3 ProcessWithEvents() to split the buffer around MIDI/parameter events, processing audio in segments between events
Handle events at their exact sample positions within the buffer
Remove the incomplete commented-out buffer splitting code
This makes MIDI timing in AUv3 sample-accurate, matching the precision expected by the AUv3 specification where events carry sample-accurate timestamps.
Reference: https://cp3.io/posts/sample-accurate-midi-timing/