Add per-clip loop-seam crossfade for launcher (session-view) playback#3
Conversation
AudioClipBase gains a loopCrossfade TimeDuration property (default 0, backward-compatible). When non-zero on a launcher clip, the last N samples of every loop cycle are equal-power blended with the first N samples of the cycle, smoothing the seam without changing the cycle period. Implementation lives in BeatRangeReader (the autoTempo / launcher path in WaveNodeRealTime). EditNodeBuilder converts the clip's seconds duration to project-beat duration using the edit's BPM at graph-build time and threads it through BeatConfig. The cap-to-half-loop guard prevents two adjacent crossfade regions from overlapping. Arrangement clips and warped clips are unchanged — they don't go through BeatRangeReader's seam logic.
| // editDuration scales with the proportion of the read this region covers. | ||
| const auto regionEditDuration = editDuration * (p1 - p0); | ||
|
|
||
| const bool headOk = source->read ({ headStartTime, headEndTime }, |
There was a problem hiding this comment.
This read uses the same TimeRangeReader/resampler/time-stretcher chain as the main playback stream. Because TimeRangeReader::read seeks and mutates the downstream reader state, the loop-head read can leave the main reader primed from the head audio while the next block is treated as contiguous tail/loop playback. With the Lagrange path, for example, setPosition does not reset interpolation state, so this can leak the crossfade side-read into subsequent output. This needs an independent reader/state path, or explicit isolation/restoration/reset of the main stream state.
| regionEditDuration, | ||
| isContiguous, | ||
| playbackSpeedRatio); | ||
| (void) headOk; // best effort — silence on cache miss is preferable to skipping the blend |
There was a problem hiding this comment.
Ignoring headOk after clearing headView means a failed/cache-miss loop-head read still applies the equal-power fade-out to the existing tail and mixes in silence. That turns a read failure into an audible seam dropout. If the side-read fails, skip the blend for this region or propagate failure so the existing read-failure fade/retry handling can engage.
| : getPosition().getLength(); | ||
| auto cap = TimeDuration::fromSeconds (loopLen.inSeconds() * 0.5); | ||
| length = juce::jlimit (TimeDuration(), cap, length); | ||
| loopCrossfade = length; |
There was a problem hiding this comment.
This setter writes the new cached value, but IDs::loopCrossfade is not handled in AudioClipBase::valueTreePropertyChanged, so changing the property may not call changed() and rebuild the launcher playback graph until some unrelated property changes. Please add it to the same property-change branch as fades/loop lengths.
|
|
||
| fadeIn.referTo (state, IDs::fadeIn, um); | ||
| fadeOut.referTo (state, IDs::fadeOut, um); | ||
| loopCrossfade.referTo (state, IDs::loopCrossfade, um); |
There was a problem hiding this comment.
Since this adds a new persisted CachedValue, AudioClipBase::cloneFrom should also copy loopCrossfade alongside fadeIn, fadeOut, and the loop properties. Duplicated/cloned clips otherwise lose the new per-clip seam setting.
- Reset the shared reader chain after each loop-head side-read so the Lagrange interpolator's history buffer (and any other downstream state) cannot leak into the next block's main read. The next read re-seeks via setPosition; the reset gives it a clean interp slate. - Skip the equal-power blend on a failed side-read instead of mixing silence into a faded-down tail — turns a cache miss back into a one-block stitch artefact rather than an audible seam dropout. - Hook IDs::loopCrossfade in valueTreePropertyChanged so changing the property triggers the same playback-graph rebuild path as fadeIn / fadeOut and the loop properties. - Copy loopCrossfade in cloneFrom so duplicated clips preserve the per-clip seam setting alongside fadeIn / fadeOut.
The TE modules are JUCE INTERFACE libraries — header-only until a consumer pulls them in — so CI needs an actual link target to compile the .cpp files. Adds: - ci/compile_check.cpp: tiny console-app entry that constructs an Engine and exits, just to force every module .cpp to be compiled. - CMakeLists.txt: TE_BUILD_COMPILE_CHECK option (off by default) that pulls compile_check.cpp into a juce_add_console_app target and links the three TE modules. - .github/workflows/build.yml: matrix build on ubuntu-latest and macos-latest, Release config, runs on push to main/master/feat/**/ fix/**, on every PR, and on manual dispatch. Per-ref concurrency cancellation so superseded runs don't queue. Forks without CI silently miss compile breakage between MAGDA bumps — this catches it before merge instead of at the consumer's build.
The fork's per-device gain/metering integration in EditNodeBuilder.cpp includes "DeviceMeteringManager.hpp" — a header that lives in MAGDA, not in this repo. MAGDA's build adds it to the include path; the fork's standalone CI doesn't. Wrap the include with __has_include and gate the two use sites on the resulting MAGDA_HAS_DEVICE_METERING macro so the file builds either way. The DeviceGainNode class itself only uses tracktion::graph types and stays unconditional — when the manager isn't present it's just unused.
- Trigger on push to main / master / feat/** / fix/** only — PR runs duplicate the push CI on the same SHAs and just consume minutes. - Split into three explicit jobs (ubuntu, macos, windows) so each can use its native dependency setup. The matrix-with-conditional-steps pattern got unwieldy once Windows needed MSVC dev-cmd. - Windows uses windows-latest-8core to match magda-core's CI runner; MSVC x64 via ilammy/msvc-dev-cmd@v1.
AudioClipBase gains a loopCrossfade TimeDuration property (default 0, backward-compatible). When non-zero on a launcher clip, the last N samples of every loop cycle are equal-power blended with the first N samples of the cycle, smoothing the seam without changing the cycle period.
Implementation lives in BeatRangeReader (the autoTempo / launcher path in WaveNodeRealTime). EditNodeBuilder converts the clip's seconds duration to project-beat duration using the edit's BPM at graph-build time and threads it through BeatConfig. The cap-to-half-loop guard prevents two adjacent crossfade regions from overlapping.
Arrangement clips and warped clips are unchanged — they don't go through BeatRangeReader's seam logic.