Skip to content

Add WSOLA pitch compensation to timewarp - #1357

Merged
bmatherly merged 7 commits into
mltframework:masterfrom
Matheus290:feature/wsola
Nov 7, 2026
Merged

bmatherly merged 7 commits into
mltframework:masterfrom
Matheus290:feature/wsola

Conversation

@Matheus290

@Matheus290 Matheus290 commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

This adds a WSOLA pitch change filter, interchangeable with rbpitch and with no external dependencies, and makes it a second pitch compensation method for timewarp, as discussed.

What it does

  • New audio filter wsola in the plus module. It takes the same parameters as rbpitch (octaveshift, pitchscale, animatable, and stretch) and, like it, keeps the number of samples and the frequency, so it works on its own and the two can be swapped.
  • It changes the pitch in two steps: a sinc resampler resamples the audio by pitchscale, which changes the pitch, and WSOLA then time stretches it back to the requested length. Both are ports from Chromium (media/base/sinc_resampler and media/filters/audio_renderer_algorithm with wsola_internals, BSD licensed; the license is kept in src/modules/plus/wsola/). The algorithms, constants and index arithmetic are unchanged; only Chromium's support types were replaced, and the resampler's AVX2 path was left out.
  • producer_timewarp: new property warp_pitch_method. With warp_pitch=1, wsola selects the new filter. Any other value uses rbpitch exactly as before, so existing projects are not affected. Both filters get pitchscale = 1/speed.

Testing

  • New test_wsola (Qt test): pitch preserved through timewarp at 0.5x, 2x and 3x; the filter on its own shifts a tone by pitchscale 0.5, 1.5, 2 and octaveshift -1, with the requested number of samples and no gaps; a seek does not change the pitch.
  • Measured latency (onset of a blipflash blip, with and without the filter): 22 ms at pitchscale 0.5 to 72 ms at 2 (rbpitch: 25–67 ms). Through timewarp: about 60 ms at 0.5x and 2x. The delay is constant over time.
  • On a 1 kHz tone, the energy outside the tone stays more than 100 dB down at every scale tested.
  • clang-format 14 clean; the .yml files validate against metaschema.yaml.
  • ctest: everything passes except TestProperties::RadixRespondsToLocale, which fails the same way on master on this machine (no comma-decimal locale).

How to listen

Any clip with speech or music works (close the melt window to stop):

melt timewarp:2.0:clip.mp4 warp_pitch=1 warp_pitch_method=rubberband
melt timewarp:2.0:clip.mp4 warp_pitch=1 warp_pitch_method=wsola
melt clip.mp4 -attach wsola pitchscale=1.5
melt clip.mp4 -attach rbpitch pitchscale=1.5

To run the automated test: ctest -R wsola --output-on-failure.

Kdenlive has said it wants to offer this; a matching merge request there will select the method per clip.

This was written with the help of Claude (AI).

🤖 Generated with Claude Code

Add a wsola module with a "wsola" audio filter: a port of Chromium's
WSOLA time stretcher (media/filters/audio_renderer_algorithm and
wsola_internals, BSD licensed). The algorithm, constants and index
arithmetic are kept as upstream; only Chromium's support types were
replaced.

The filter takes the audio that timewarp labels at frequency * speed,
time stretches it by 1/speed and returns it at the original frequency,
so the duration is unchanged and the pitch is restored. It has no
external dependencies.

timewarp gets a new property, warp_pitch_method. "wsola" selects the new
filter for pitch compensation when warp_pitch is on; any other value, or
a build without the module, keeps using rbpitch as before.

Add test_wsola.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@bmatherly bmatherly left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the typical latency for this service? Also, can you check what the typical latency is for rubberband? I wonder how they compare?

Comment thread src/modules/wsola/factory.c Resolved
Comment thread src/modules/plus/filter_wsola.yml Resolved
Matheus290 and others added 2 commits November 5, 2026 10:23
The filter has no external dependencies, so it does not need a module of
its own. The Chromium port goes to src/modules/plus/wsola/, like
ebur128, and the MOD_WSOLA option is removed. The test now depends on
MOD_PLUS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The filter expects the audio that timewarp delivers: the original
samples labeled with the frequency multiplied by the speed. Used on any
other audio, it labeled the output with a wrong frequency, which the
resampler then compounded frame after frame. timewarp now marks its
audio with _timewarp_audio_speed, and the filter passes any audio
without that mark through unchanged, with a warning.

Add OtherAudioPassesThrough to test_wsola.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread src/modules/plus/filter_wsola.cpp Resolved
Matheus290 and others added 2 commits November 5, 2026 11:55
Port media/base/sinc_resampler from Chromium, next to the WSOLA port and
under the same BSD license, so that the wsola filter can change the
pitch without an external library. The algorithm is unchanged; the
Chromium support types were replaced and the AVX2 path, which needs run
time CPU detection, was left out. It is built in the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Make wsola a pitch change filter that can be used on its own and is
interchangeable with rbpitch: it takes octaveshift, pitchscale and
stretch, and keeps the number of samples and the frequency. The audio
is first resampled by pitchscale with the sinc resampler, which changes
the pitch, and WSOLA then time stretches it back to the requested
length. Resampling first means WSOLA always works on audio whose real
sample rate is the labeled frequency, also inside timewarp.

This replaces the speed parameter and the mark that timewarp put on its
audio: timewarp now sets pitchscale = 1 / speed for both filters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Matheus290

Copy link
Copy Markdown
Contributor Author

Thanks for the review, and sorry for the delay. I've pushed changes for all three points:

plus module. The filter is now in plus, with the Chromium code in src/modules/plus/wsola/ like ebur128, and copyright Meltytech. MOD_WSOLA is gone.

Same parameters as rubberband. wsola is now a regular pitch change filter with the same interface as rbpitch: octaveshift, pitchscale (animatable) and stretch, keeping the number of samples and the frequency. It does not know whether timewarp uses it. To change the pitch without an external library, I also ported Chromium's sinc resampler (same BSD license): the audio is resampled by pitchscale, which changes the pitch, and WSOLA stretches it back to the requested length. timewarp now sets pitchscale = 1/speed for both filters, so the two are interchangeable there too. The earlier commit that made wsola pass through audio from anything but timewarp is superseded by this.

Latency. Measured end to end with blipflash (onset of the blip with and without the filter, at 48 kHz):

pitchscale wsola rbpitch
0.5 22 ms 55–67 ms
0.75 35 ms 43–51 ms
1.5 47 ms 43–47 ms
2 72 ms 25–27 ms

Through timewarp: at 0.5x, wsola 63 ms and rubberband 25 ms; at 2x, wsola 58 ms and rubberband between 91 and 126 ms (it varies). So they are in the same range. wsola's delay grows with the pitch scale, because the resampling step stretches WSOLA's own look-ahead, while rbpitch tends to go the other way. wsola's delay is also constant over time.

On a 1 kHz tone, wsola keeps the energy outside the tone more than 100 dB down at all of the scales above, where rbpitch measured 7–53 dB. Both hit the right pitch.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved sample-accounting, overlap-selection, and pitch-range defects can produce silence or incorrect audio, while filter destruction leaks resources and the seek test fails to exercise nonzero seeks.

5 open issues
What changed in this PR

Adds a dependency-free WSOLA pitch filter to MLT’s plus module and makes it selectable for timewarp pitch compensation, while retaining Rubber Band as the default.

Changes:

  • Adds Chromium-derived sinc resampling and WSOLA time stretching.
  • Introduces warp_pitch_method and registers the new filter with its metadata.
  • Adds Qt tests for pitch shifts, timewarp compensation, sample counts, audio gaps and seeking.
File Description
src/​tests/​test_wsola/​test_wsola.cpp Adds standalone filter and timewarp audio tests.
src/​tests/​CMakeLists.txt Enables WSOLA tests when plus is built.
src/​modules/​plus/​wsola/​wsola_algorithm.h Declares audio buffers and time-stretching interfaces.
src/​modules/​plus/​wsola/​wsola_algorithm.cpp Implements WSOLA with optimized similarity searches.
src/​modules/​plus/​wsola/​sinc_resampler.h Declares the sinc resampler interface.
src/​modules/​plus/​wsola/​sinc_resampler.cpp Implements sample-rate conversion for pitch shifting.
src/​modules/​plus/​filter_wsola.yml Documents filter parameters and latency reporting.
src/​modules/​plus/​filter_wsola.cpp Integrates resampling and time stretching into an MLT filter.
src/​modules/​plus/​factory.c Registers WSOLA and its metadata.
src/​modules/​plus/​CMakeLists.txt Builds WSOLA sources and installs metadata.
src/​modules/​core/​producer_timewarp.yml Documents pitch-compensation method selection.
src/​modules/​core/​producer_timewarp.c Selects and switches pitch-compensation filters.

Review effort: Balanced

Comment thread src/modules/core/producer_timewarp.c Resolved
Comment thread src/modules/plus/filter_wsola.cpp Resolved
Comment thread src/tests/test_wsola/test_wsola.cpp Resolved
Comment thread src/modules/core/producer_timewarp.yml Resolved
Matheus290 and others added 2 commits November 6, 2026 21:16
Without warp_pitch_method, timewarp uses rubberband if it is available and
wsola otherwise. An explicit method also falls back to the other filter when
the selected one is not available.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Accept a pitchscale down to 0.05, like rbpitch, so that timewarp at 20x
  is compensated correctly.
- Close the parent service when the filter is closed.
- Let FullSearch() pick a candidate with a negative similarity.
- Pass the audio through only at a rate of exactly 1: near 1, the output
  did not have the requested number of samples and the queue drifted.
- The seek test now plays from the position it seeks to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bmatherly
bmatherly merged commit 2a51ed9 into mltframework:master Nov 7, 2026
15 checks passed
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.

4 participants