Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@olilarkin
Copy link
Member

@olilarkin olilarkin commented Dec 26, 2025

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/

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/
@olilarkin olilarkin marked this pull request as draft December 26, 2025 22:27
@claude
Copy link

claude bot commented Dec 26, 2025

Pull Request Review: Sample-Accurate MIDI Timing for AUv3

Great 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 Aspects

1. Correct Core Implementation

The buffer-splitting approach in IPlugAUv3.mm is well-designed:

  • Events are correctly sorted by timestamp (per AUv3 spec)
  • Buffer segments are processed between events
  • Zero-length segments are properly skipped
  • Comments clearly explain the algorithm

2. Good Code Quality

  • Clear, descriptive comments throughout
  • Proper use of static_cast for type conversions
  • Removed the old commented-out code (good cleanup)
  • Follows iPlug2 style conventions

3. Excellent Test Coverage

The Swift-based test suite (Tests/AUv3TimingTest/) is comprehensive:

  • Tests multiple sample offsets
  • Clear pass/fail criteria with tolerance
  • Good documentation in README.md
  • Offline rendering approach is appropriate

4. Documentation

  • Commit messages are detailed and explain the "why"
  • Inline comments clarify non-obvious behavior (e.g., parameter ramps, offset 0 for MIDI)

🔍 Issues & Concerns

1. 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 ProcessBlock throws an exception, the pointers won't be restored, leaving mScratchData in a corrupted state.

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 eventTime < startTime be:

  • Processed once at offset 0 and then skipped? (current behavior)
  • Dropped entirely as "late" events?

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));

OnParamChange is called outside the mutex, but it may need to read parameter values. Is this intentional?

Then at line 215-217:

ENTER_PARAMS_MUTEX
ProcessBuffers(0.f, static_cast<int>(framesToProcess), static_cast<int>(bufferOffset));
LEAVE_PARAMS_MUTEX

Concern: 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?

  • Default changed from 10ms to 0ms
  • Minimum changed from 1ms to 0ms

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:

  1. Kept only for testing purposes?
  2. Documented in the PR description?
  3. Reverted before merge?

The PR title says "DROP?: add AUv3 TimingTester" for the second commit, but this change is in the first commit.


🛡️ Security Concerns

None identified. The changes don't introduce:

  • Buffer overflows (bounds are checked)
  • Integer overflows (reasonable audio buffer sizes)
  • Race conditions (mutex usage appears correct, pending clarification above)

⚡ Performance Considerations

1. Buffer Splitting Overhead

The 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 Frequency

As 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 Coverage

Strengths:

  • Tests multiple offsets (50, 100, 256, 400)
  • Tolerance of ±2 samples is reasonable
  • Offline rendering eliminates timing variance
  • Clear pass/fail criteria

Gaps:

  1. No test for multiple events in one buffer - What happens with [noteOn@50, noteOff@100, noteOn@150]?
  2. No test for parameter events - Only MIDI is tested
  3. No test for zero-offset events - Edge case worth covering
  4. No test for events beyond buffer end - Though the code handles this

Recommendation: Add a multi-event test case to verify the loop logic.


📋 Best Practices & Style

Good:

  • ✅ 2-space indentation (iPlug2 convention)
  • ✅ Clear variable names (bufferOffset, nextEventOffset)
  • ✅ Comments explain why, not just what
  • ✅ C++17 features used appropriately (static_cast)

Minor:

  • Line 173: Comment says "Offset is 0" but could clarify this is relative offset (absolute timing handled by buffer splitting)

🎯 Recommendations Summary

Before Merge:

  1. Fix pointer arithmetic safety in IPlugProcessor.cpp:533-560
  2. Clarify/document the IPlugInstrument attack parameter change
  3. Review mutex locking strategy for parameter events
  4. Add test case for multiple events in one buffer

Optional Improvements:

  1. Profile with dense MIDI to verify performance
  2. Add parameter event test to the test suite
  3. Consider documenting the "events in the past" behavior

🏁 Verdict

Overall 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)
Files reviewed: IPlugAUv3.mm, IPlugProcessor.cpp/h, IPlugInstrument.cpp, AUv3TimingTest/*

@olilarkin olilarkin force-pushed the auv3/sample-accurate branch from 66fb7a3 to b1f552e Compare December 31, 2025 17:55
@olilarkin olilarkin marked this pull request as ready for review December 31, 2025 17:55
@claude
Copy link

claude bot commented Dec 31, 2025

Code Review: Sample-Accurate MIDI Timing for AUv3

Summary: 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

  1. Well-documented approach with clear comments
  2. Clean algorithm handling sorted events per AUv3 spec
  3. Zero-length segment handling for simultaneous events
  4. Proper pointer restoration in buffer offset implementation

Critical Issue: Potential Buffer Overflow

Location: 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 Issues

Medium - 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 Coverage

Missing automated tests for this critical timing functionality. Recommend adding tests for MIDI events at specific sample offsets.

Performance Considerations

  1. Mutex overhead per segment - consider holding mutex for entire buffer
  2. Branch prediction with nested loops - profile with MIDI-heavy workloads
  3. Cache locality improved with buffer splitting (good trade-off)

Style Compliance

Follows CLAUDE.md conventions: 2-space indentation, naming conventions, C++17 usage, clear comments.

Recommendation

Approve with changes. Implementation is sound, but bounds checking must be added before merge.

Great work on improving MIDI timing precision!

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