sync: masked waveform correlation, stretch sync, manual start time

- Waveform sync no longer treats uncached waveform regions as silence:
  the envelope extraction now reports a per-window validity mask and the
  correlation skips invalid windows on either side, improving accuracy
  for partially cached clips
- Add stretch/speed sync: AudioWaveformSync::EstimateStretchAndOffset
  searches a playback-rate range plus offset, and a new timeline
  context action 'Synchronize by Waveform (Adjust Speed)' applies the
  estimated rate as a clip speed change (with undo) when plain offset
  alignment is inconclusive
- Footage properties dialog gains a Source Start Time field so the
  value used by source-time sync can be viewed and edited manually
  instead of relying solely on auto-detected metadata; applied via an
  undo command, with Footage::ClearSourceStartTime() for removal
- Regression tests for masked correlation, stretch estimation, the
  envelope validity mask, and source-start-time set/clear
This commit is contained in:
2026-07-16 23:09:12 +08:00
parent 547c2480e0
commit caafac4203
13 changed files with 544 additions and 26 deletions
@@ -81,20 +81,26 @@ GetSelectedWaveformSyncClips(const QVector<Block *> &blocks)
QVector<double> ExtractWaveformCacheEnvelope(const WaveformSyncClip &clip,
int sample_rate,
size_t window_samples)
size_t window_samples,
QVector<bool> *valid_mask)
{
QVector<double> envelope;
if (sample_rate <= 0 || !window_samples) {
return envelope;
}
if (valid_mask) {
valid_mask->clear();
}
const rational window_time(static_cast<int>(window_samples), sample_rate);
// Only trust regions that have actually been validated. Unvalidated cache
// returns zero samples, which both drags the correlation score down and
// can produce false peaks if one clip happens to have more cached data
// than another. Using zero placeholders keeps every envelope aligned to
// the same absolute timeline.
// than another. Zero placeholders keep every envelope aligned to the same
// absolute timeline, while the validity mask lets the correlation skip
// those placeholders entirely.
const TimeRangeList validated_ranges =
clip.waveform->GetValidatedRanges().Intersects(clip.media_range);
@@ -103,8 +109,10 @@ QVector<double> ExtractWaveformCacheEnvelope(const WaveformSyncClip &clip,
const rational length = qMin(window_time, clip.media_range.out() - t);
const TimeRange window(t, t + length);
const bool window_valid = validated_ranges.contains(window);
double peak = 0.0;
if (validated_ranges.contains(window)) {
if (window_valid) {
const AudioVisualWaveform::Sample summary =
clip.waveform->GetSummaryFromTime(t, length);
@@ -118,6 +126,9 @@ QVector<double> ExtractWaveformCacheEnvelope(const WaveformSyncClip &clip,
}
envelope.append(peak);
if (valid_mask) {
valid_mask->append(window_valid);
}
}
return envelope;
}