Skip to content

Bungee Engine, Memory Cues, and Beatgrid - #14

Open
0cwa wants to merge 90 commits into
mixxx/mainfrom
main
Open

Bungee Engine, Memory Cues, and Beatgrid#14
0cwa wants to merge 90 commits into
mixxx/mainfrom
main

Conversation

@0cwa

@0cwa 0cwa commented Mar 9, 2026

Copy link
Copy Markdown
Owner

The coolest version of mixxx yet

alephlm and others added 30 commits February 10, 2026 02:55
Add downbeatOffset to track
Thanks for noticing @Veganachommunist
Not ideal but at least prevents accidently deleting a little better
0cwa and others added 26 commits March 9, 2026 18:36
…-glitches-based-on-identifie

Fix Bungee audio glitches: correct buffer size and channel pointer handling
The Bungee buffer scaler was causing 8.18x speedup regardless of playback speed setting. A 3-minute song was playing in 22 seconds.

The Bungee library's `speed` parameter represents the **input-to-output frame ratio**, not the effective playback rate. The code was incorrectly multiplying `base_rate` into this speed parameter.

From Bungee's library documentation (Stream.h line 144):
```cpp
request.speed = inputFrameCount / outputFrameCount;
```

This means:
- `speed = 1.0`: No time stretch (normal playback)
- `speed = 2.0`: Output is 2x faster than input (compress time)
- `speed = 0.5`: Output is 2x slower than input (stretch time)

Bungee handles sample rate conversion internally via the `resampleMode` parameter, so `base_rate` should NOT be mixed into the speed parameter.

**Line 148-151 (setScaleParameters)**:
```cpp
// Before:
m_request.speed = m_dBaseRate * m_dTempoRatio;
// After:
// Bungee's speed parameter is the input/output frame ratio.
// Use only the tempo ratio (playback speed) without base_rate.
// Bungee handles sample rate conversion internally via resampleMode.
m_request.speed = m_dTempoRatio;
```

**Lines 222-230 (processGrain)**:
```cpp
// Before:
double speed = m_dBaseRate * m_dTempoRatio;
if (m_bBackwards) {
    speed = -speed;
}
// After:
// For Bungee's request, use only the tempo ratio (input/output frame ratio)
double speed = m_dTempoRatio;
if (m_bBackwards) {
    speed = -speed;
}
// Calculate effective rate for ReadAheadManager (includes base_rate for sample rate conversion)
const double effectiveRate = m_dBaseRate * m_dTempoRatio;
```

**Lines 262-266 (processGrain)**:
```cpp
// Before:
const SINT availableSamples = m_pReadAheadManager->getNextSamples(
        speed,
        m_interleavedReadBuffer.data(),
        samplesNeeded,
        getOutputSignal().getChannelCount());
// After:
const SINT availableSamples = m_pReadAheadManager->getNextSamples(
        effectiveRate,
        m_interleavedReadBuffer.data(),
        samplesNeeded,
        getOutputSignal().getChannelCount());
```

**Lines 391-398 (scaleBuffer flush path)**:
```cpp
// Before:
const SINT samplesToRead = getOutputSignal().frames2samples(kMaxGrainFrames);
const SINT availableSamples = m_pReadAheadManager->getNextSamples(
        (m_bBackwards ? -1.0 : 1.0) * m_dBaseRate * m_dTempoRatio,
        m_interleavedReadBuffer.data(),
        samplesToRead,
        getOutputSignal().getChannelCount());
// After:
const SINT samplesToRead = getOutputSignal().frames2samples(kMaxGrainFrames);
const double effectiveRate = (m_bBackwards ? -1.0 : 1.0) * m_dBaseRate * m_dTempoRatio;
const SINT availableSamples = m_pReadAheadManager->getNextSamples(
        effectiveRate,
        m_interleavedReadBuffer.data(),
        samplesToRead,
        getOutputSignal().getChannelCount());
```

1. **Bungee's speed parameter** now receives only the tempo ratio (playback speed), not including base_rate
2. **ReadAheadManager** receives the effective rate (base_rate * tempo_ratio) for proper position tracking
3. This ensures Bungee's time stretching calculations are correct while the ReadAheadManager still tracks the correct position

Expected behavior after fix:
- 1.0x speed: Normal playback
- 0.5x speed: Half-speed playback with correct pitch
- 2.0x speed: 2x faster playback with correct pitch
- 0.1x speed: 10% speed playback with correct pitch (should not have super-deep pitch issue)

This fix aligns with how RubberBand handles time ratios:
- RubberBand uses `setTimeRatio(1.0 / (base_rate * tempo_ratio))` - it calculates the inverse
- Bungee uses `speed = tempo_ratio` directly - it uses the ratio directly

The key difference is that RubberBand's `timeRatio` is stretched/unstretched duration ratio, while Bungee's `speed` is input/output frame ratio.
Fixed the issue where Bungee keylock causes super fast speedup in Release
builds but works correctly in RelWithDebInfo builds. The problem was
related to compiler optimization flags (-ffast-math -O3) affecting
floating-point math in the scaleBuffer return value calculation.

The fix uses m_effectiveRate instead of m_dBaseRate * m_dTempoRatio directly
in the accumulation loop, matching the pattern used by RubberBand and
SoundTouch scalers. This prevents floating-point reassociation issues that
can occur with aggressive compiler optimizations.

Changes:
- Update m_effectiveRate in setScaleParameters()
- Use m_effectiveRate in scaleBuffer() return value calculation
- Reset m_effectiveRate in clear() for consistent state
…build wiring, docs and tests

BNG-01 through BNG-05, BNG-07:

- EngineBufferScaleBungee: replace per-grain isolated reads with a sliding
  deinterleaved planar input window aligned to Bungee InputChunk ranges.
  Correct muteHead/muteTail are computed and passed to analyseGrain(); the
  old 'muteHead=0 always' approach violated Bungee's streaming contract and
  was the root cause of the garbled/8x-speed keylock regression.

- Remove the discarded-read fallback path that advanced ReadAheadManager
  without consuming the returned samples, causing playback-position drift.

- Use Bungee::Stretcher::next() for grain-position stepping instead of a
  hand-rolled constant increment.

- Preallocate all buffers in onSignalChanged(); no heap allocation in the
  steady-state engine path.

- Document rate/position/reset semantics and the InputChunk sliding-window
  contract in enginebufferscalebungee.h so future maintainers need not
  rediscover them from branch archaeology.

- Strengthen enginebufferscalebungeetest: ReadAheadManager mock tracks call
  counts and sample totals; new tests for zero-speed, buffer clearing,
  EOF/read-failure handling, overlapping-grain buffer reuse, rapid parameter
  changes, and signal-format changes.

- CMakeLists.txt: gate the MSVC-only Bungee patch inside if(MSVC), and fix
  the WORKING_DIRECTORY to CMAKE_CURRENT_SOURCE_DIR so patch paths
  (lib/bungee/src/...) resolve correctly. Linux configure no longer dirties
  lib/bungee.

All 23 EngineBuffer* tests pass (12 Bungee + 11 EngineBufferTest).

lib/bungee sources are not modified; all changes are in Mixxx integration.
…selection (BNG-06)

Three integration tests using the real EngineBufferScaleBungee (not a mock)
to verify engine-level state management when Bungee is the active keylock
engine:

- BungeeEngineSelected: selecting KeylockEngine::Bungee wires m_pScaleKeylock
  to m_pScaleBungee, and enabling keylock advances m_pScale accordingly.

- BungeeKeylockToggleDoesNotCrash: rapidly toggling keylock on/off while
  Bungee is active produces finite (non-NaN) audio and does not crash.

- BungeeKeylockEngineSwitch: switching between SoundTouch and Bungee
  mid-playback keeps m_pScaleKeylock consistent and the output finite.

Tests inherit BaseSignalPathTest directly (real CachingReader + real
ReadAheadManager) without calling setScalerForTest(), so the normal
engine-selection code path is exercised.

Added FRIEND_TEST declarations to enginebuffer.h for the three new tests.
Registered enginebufferbungeetest.cpp in CMakeLists.txt under the BUNGEE
feature flag (same guard as enginebufferscalebungeetest.cpp).

44/44 EngineBuffer* tests pass.
…hrough BNG-12)

BNG-08 — Output interleave optimisation:
Add copyOutputFrames() helper to EngineBufferScaleBungee.  For the common
stereo case it delegates to SampleUtil::interleaveBuffer() (SIMD-optimised
on x86/ARM); for N-channel audio it falls back to a scalar loop.  Replaces
two identical hand-rolled interleave loops in processGrain().  Also swap
the bare <QtDebug> include for the proper util/assert.h (which includes
<QtDebug> and defines DEBUG_ASSERT).

BNG-09 — Prefs UI correctness:
The dual-threading checkbox is already hidden and disabled for non-RubberBand
engines (including Bungee) by the existing updateKeylockDualThreadingCheckbox()
logic.  Clarify the tooltip copy from "only available with RubberBand" to
"only available with the RubberBand engine" so it reads correctly if the
checkbox is ever made visible in future.

BNG-10 — Maintainer documentation:
Add docs/bungee-integration.md covering: architecture overview, rate/position
semantics, the InputChunk sliding-window contract, why muteHead=0 was wrong,
output interleave, reset/flush protocol, memory layout, the Windows patch
scope, and what not to change casually.

BNG-11 — CI regression gate:
Add -DBUNGEE=ON to the Ubuntu 24.04 CI build so that
EngineBufferScaleBungeeTest.* and EngineBufferBungeeTest.* run on every push.
Bungee is vendored under lib/bungee/ so no new CI dependencies are needed.

BNG-12 — Final cleanup:
No stale theory comments remain in the Bungee sources.  The overall diff
across this branch tells a coherent story: root-cause identification to
sliding-window rewrite to RAMAN accounting fix to CMake guard to tests to
integration tests to performance and docs and CI.

15/15 Bungee tests pass (12 unit + 3 integration).
Fix Bungee keylock super fast speedup in Release builds
…analyseGrain overflow

Two related bugs caused a deterministic heap corruption crash detectable
via SIGABRT from malloc_printerr when Bungee keylock was active:

Bug 1 — discardBufferedInputBefore position tracking
When framePosition > m_bufferedInputEndFrame (all buffered data is before
the requested position — which happens at very high playback speeds where
the grain hops outrun the read buffer), the function discarded all frames
correctly via memmove(0 bytes) but then only advanced m_bufferedInputBeginFrame
to m_bufferedInputEndFrame (the old end) rather than all the way to
framePosition.  This left a gap:

  m_bufferedInputBeginFrame = m_bufferedInputEndFrame_old  (WRONG)
  m_bufferedInputEndFrame   = m_bufferedInputBeginFrame    (= old end)

On the next processGrain call, dataOffset was computed as
  inputChunk.begin - m_bufferedInputBeginFrame
which equalled framePosition - m_bufferedInputEndFrame_old, a value that
could exceed (m_channelStride - grainSize).  Bungee then created an Eigen
map starting at m_channelBufferPtrs[0] + dataOffset and iterated over the
full grainSize rows, reading past the end of m_contiguousChannelBuffer for
channel 1 and corrupting adjacent heap memory.  Malloc detected this later
(in an unrelated thread cleaning up its arena) and called abort().

Fix: when remainingFrames <= 0 (all data discarded), set
m_bufferedInputBeginFrame = framePosition (not += bufferedFrames).

Bug 2 — DEBUG_ASSERT too weak
The existing assert only checked dataOffset <= m_channelStride, but the
actual requirement for the analyseGrain call is
  dataOffset + grainSize <= m_channelStride.
Strengthened the assert and added a run-time guard that forces a reset
instead of overflowing if the invariant is ever violated.

Bonus — zero-initialise the input buffer
On the very first grain after reset (request.position = 0), Bungee's
InputChunk is {-halfFrames, +halfFrames}.  We supply data for
[0, halfFrames) but the Eigen map covers the full [0, 2*halfFrames) range.
The upper half was previously uninitialized; zero-filling prevents Bungee
from seeing garbage floats (including possible NaN) in that region.

All 15 Bungee tests (12 unit + 3 integration) continue to pass.
Merge resolution for src/library/rekordbox/rekordboxfeature.cpp:

The conflict arose because both branches independently evolved the
readAnalyze() memory-cue loop from the common ancestor (dd97c25):

  - main (2ede2e0 "Rekordbox hotcues stay hotcues distinct from memory
    cues") refactored the loop to use the new setMemoryCue() helper for
    all memory cues, but inadvertently stripped the setMainCuePosition()
    call that mapped the first chronological memory cue to Mixxx's main
    cue (the default playback-start marker).

  - feature/bungee (28c87ad "crash fix") kept the original
    mainCueFound + setMainCuePosition pattern from the ancestor, and
    correctly added null-checks for pMainCue and memoryCueOrLoop.color
    to fix a latent null-dereference.  However it kept setHotCue() in
    the else branch, which predates setMemoryCue() and would regress the
    2ede2e0 fix by giving subsequent memory cues the wrong cue type.

Resolution: combine both improvements:
  - Restore bool mainCueFound / setMainCuePosition (lost from main)
  - Add pMainCue null-check and color optional-check (from feature/bungee)
  - Use setMemoryCue() in the else branch (from main) so subsequent memory
    cues and loops retain CueType::Memory / CueType::Loop, not HotCue

Also kept from feature/bungee (non-conflicting, already staged):
  - key_id INTEGER column in the rekordbox temp table + KeyUtils binding
    (needed so columncache.cpp key-sort SQL works in the Rekordbox view)
  - #include track/keyutils.h

Already staged from feature/bungee before conflict:
  - docs/bungee-integration.md markdown table formatting (d1006cb)
  - enginebufferscalebungee.cpp heap-corruption fix (87e4849)
Re-validate the BNG-12 final review claim against the post-87e48497c1
state of the integration code, and update the user-facing integration
doc so a future maintainer reading only docs/bungee-integration.md and
the current code can reconstruct the buffer-window invariant without
consulting commit history.

Code review findings (no source changes required):

  * No stale comments or dead code paths from the crash-debugging era
    remain in src/engine/bufferscalers/enginebufferscalebungee.{h,cpp}.
  * The buffer-window state model
    (m_bufferedInputBeginFrame / m_bufferedInputEndFrame / m_channelStride)
    is now documented coherently in one place in the header (the new
    "Buffer-window invariant (BNG-13)" section, added together with the
    regression tests).
  * discardBufferedInputBefore() and processGrain() honour a single
    stated invariant: dataOffset + grainSize <= m_channelStride.  Both
    branches of discardBufferedInputBefore (partial / full discard) and
    the runtime guard in processGrain are documented in the header.
  * The original BNG-12 acceptance bullets ("focused diff, legible
    commits, no misleading commentary") still hold once BNG-13 has
    landed -- the regression test commit is the only addition and it
    has its own ticket.

Doc updates (docs/bungee-integration.md):

  - Add a "buffer-window invariant" subsection under the InputChunk
    contract that states the post-BNG-13 contract explicitly:
    begin <= chunk.begin and dataOffset + grainSize <= channelStride,
    plus the partial- and full-discard branches and the
    high-speed grain-outrun regime.
  - Add a subsection explaining why m_contiguousChannelBuffer is
    zero-initialised in onSignalChanged (muted half of the very first
    post-reset grain).
  - Extend the "What not to change casually" table with two new rows:
    the discardBufferedInputBefore full-discard branch (pinned by
    EngineBufferScaleBungeeBufferWindowTest) and the
    m_contiguousChannelBuffer zero-init.
  - Add a "Test surface" section enumerating the unit / regression /
    integration test files and the .github/workflows/bungee-asan.yml
    workflow so future maintainers can find the regression net
    immediately.

Refs: TODO.md BNG-14.  Depends on BNG-13.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

This PR is marked as stale because it has been open 90 days with no activity.

@github-actions github-actions Bot added the stale label Aug 4, 2026
@0cwa

0cwa commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

not stale :(

@github-actions github-actions Bot removed the stale label Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants