To speed up a video and its sound to twice the speed with ffmpeg: ffmpeg -i in.mp4 -filter_complex "[0:v]setpts=PTS/2[v];[0:a]atempo=2[a]" -map "[v]" -map "[a]" out.mp4. setpts=PTS/2 halves every frame’s timestamp, so the picture plays in half the time; atempo=2 plays the sound twice as fast without raising its pitch, so the two stay in step. -map "[v]" and -map "[a]" put the two filtered streams in the output. To slow down to half speed, use setpts=2*PTS and atempo=0.5. For any speed factor S, the picture gets setpts=PTS/S and the sound atempo=S.

ffmpeg -i in.mp4 -filter_complex "[0:v]setpts=PTS/2[v];[0:a]atempo=2[a]" -map "[v]" -map "[a]" out.mp4

Below, every command on this page ran with ffmpeg 4.4.2, the version Ubuntu 22.04 ships, and its output is printed under it. Three things are measured, because none of them is obvious from the command: at 2x ffmpeg drops frames unless you raise the frame rate, at 0.5x it repeats them, and atempo keeps the pitch while the obvious alternative does not.

The test clip

The clip is made by ffmpeg itself from its lavfi test sources, so you can rebuild it: the testsrc2 colour bars, 640x360 at 30 frames per second for 10 seconds, with each frame’s number drawn large in the middle so you can see which frames survive a speed change, and a 440 Hz sine tone, so the pitch can be measured. The numbers are drawn with DejaVu Sans Bold, /usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf, from Ubuntu’s fonts-dejavu-core package, which was already on the machine.

The block also defines three helpers the rest of the page uses: info prints how long the video and the audio last and how many frames the video really has (ffprobe decodes and counts them), dupdrop keeps ffmpeg’s own count of the frames it duplicated and dropped, and strip lays the first 8 frames of a file side by side.

# A 10-second 640x360 test clip at 30 fps with each frame's number drawn in the middle,
# and a 440 Hz tone. The font is DejaVu Sans Bold from Ubuntu's fonts-dejavu-core package.
F=/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf
ffmpeg -v error -f lavfi -i "testsrc2=s=640x360:r=30:d=10,drawtext=fontfile=$F:text='%{frame_num}':fontsize=150:fontcolor=white:box=1:boxcolor=0x00000099:x=(w-text_w)/2:y=(h-text_h)/2" \
  -f lavfi -i "sine=frequency=440:sample_rate=48000:duration=10" \
  -c:v libx264 -pix_fmt yuv420p -c:a aac -shortest in.mp4

# info FILE: the video's duration and its frame count (every frame decoded and counted),
# then the audio's duration.
info() {
  IFS=, read -r d n < <(ffprobe -v error -select_streams v -count_frames -show_entries stream=duration,nb_read_frames -of csv=p=0 "$1")
  a=$(ffprobe -v error -select_streams a -show_entries stream=duration -of csv=p=0 "$1")
  echo "$1: video $d s, $n frames${a:+; audio $a s}"
}
# dupdrop: keep the last frame, dup_frames and drop_frames lines of ffmpeg's -progress report:
# ffmpeg's own count of frames written, duplicated and dropped.
dupdrop() { grep -E '^(frame|dup_frames|drop_frames)=' | tail -3 | paste -sd ' '; }
# strip FILE OUT.png: the first 8 frames of FILE, shrunk and tiled 4 to a row.
strip() { ffmpeg -v error -i "$1" -vf "select='lt(n,8)',scale=192:108,tile=4x2" -frames:v 1 "$2"; }
info in.mp4
in.mp4: video 10.000000 s, 300 frames; audio 10.000000 s

Two ways to write it

When the video and the sound each get one filter, -vf and -af do the same job as -filter_complex with labels, and are shorter:

ffmpeg -v error -i in.mp4 -vf "setpts=PTS/2" -af "atempo=2" out2.mp4
ffmpeg -v error -i out.mp4 -map 0 -f md5 -
ffmpeg -v error -i out2.mp4 -map 0 -f md5 -
MD5=a5e8f9983e257fe8926f22156b4d261d
MD5=a5e8f9983e257fe8926f22156b4d261d

The two MD5s are of every decoded video frame and audio sample of each output, and they are equal: the two forms wrote the same video and the same sound. -filter_complex is the one to use when a single filter graph needs both streams, or more than one input. The rest of the page uses the short form.

2x: which frames are kept

Doubling the speed of a 30 frames per second clip gives 60 frames’ worth of pictures per second. The output is still 30 frames per second unless you say otherwise, so half of them have to go. Here is the answer command again with -progress, so ffmpeg reports what it did to the frames, then the same with -r 60 and with -vsync vfr:

ffmpeg -v error -nostats -i in.mp4 -vf "setpts=PTS/2" -af "atempo=2" -progress pipe:1 2x.mp4 | dupdrop
ffmpeg -v error -nostats -i in.mp4 -vf "setpts=PTS/2" -af "atempo=2" -r 60 -progress pipe:1 2x-60.mp4 | dupdrop
ffmpeg -v error -nostats -i in.mp4 -vf "setpts=PTS/2" -af "atempo=2" -vsync vfr -progress pipe:1 2x-vfr.mp4 | dupdrop
info 2x.mp4
info 2x-60.mp4
info 2x-vfr.mp4
strip 2x.mp4 strip-2x.png
strip 2x-60.mp4 strip-2x-60.png
frame=152 dup_frames=0 drop_frames=148
frame=300 dup_frames=0 drop_frames=0
frame=152 dup_frames=0 drop_frames=148
2x.mp4: video 5.066667 s, 152 frames; audio 5.011000 s
2x-60.mp4: video 5.000000 s, 300 frames; audio 5.011000 s
2x-vfr.mp4: video 5.066667 s, 152 frames; audio 5.011000 s

Eight frames of the 2x output in two rows, colour bars with a large white frame number on each: the first row reads 0, 1, 2, 3 and the second 4, 6, 8, 10, so the first five source frames were all kept and from there every other frame was dropped

The first 8 frames of the 2x output, at 30 frames per second. Each number is the frame it came from in the source clip.

Eight frames of the 2x output written with -r 60, colour bars with a large white frame number on each, reading 0 to 7 in order across two rows: every source frame is kept

The same speed-up with -r 60: frames 0 to 7, nothing dropped.

At the default 30 frames per second, ffmpeg wrote 152 of the 300 frames and dropped 148. The strip shows which: frames 0 to 4 are all kept and after that every other one, so the big number jumps by two. The first few squeeze in because ffmpeg lets a frame fall a little behind its slot before dropping it; the documentation of -frame_drop_threshold says “Frame drop threshold, which specifies how much behind video frames can be before they are dropped. In frame rate units, so 1.0 is one frame.” and “The default is -1.1.” The cost is that the video ends up 2 frames longer than half the clip: 5.066667 s against 5.011000 s of sound.

With -r 60 nothing is dropped: 300 frames in 5.000000 s, the motion as smooth as the source. That is how you speed up a video without dropping frames, as long as the player and the place you upload to accept 60 frames per second.

-vsync vfr (variable frame rate) does not help: it wrote the same 152 frames. The documentation describes vfr as “Frames are passed through with their timestamp or dropped so as to prevent 2 frames from having the same timestamp.” setpts changes the timestamps but not the stream’s frame rate, so the output still counts time in thirtieths of a second, and two frames a sixtieth apart land on the same tick.

In today’s documentation -vsync is gone and the option is called -fps_mode. On 4.4.2 the new name is an error:

# Today's name for -vsync is -fps_mode; 4.4.2 does not know it.
ffmpeg -v error -i in.mp4 -vf "setpts=PTS/2" -af "atempo=2" -fps_mode vfr fm.mp4 2>&1 || echo "exit status $?"
Unrecognized option 'fps_mode'.
Error splitting the argument list: Option not found
exit status 1

0.5x: slow motion repeats frames

Half speed is the opposite problem: 30 frames per second of source become 15 per second of output, and a 30 frames per second file needs 30.

ffmpeg -v error -nostats -i in.mp4 -vf "setpts=2*PTS" -af "atempo=0.5" -progress pipe:1 half.mp4 | dupdrop
ffmpeg -v error -nostats -i in.mp4 -vf "setpts=2*PTS" -af "atempo=0.5" -r 15 -progress pipe:1 half-15.mp4 | dupdrop
info half.mp4
info half-15.mp4
strip half.mp4 strip-half.png
frame=599 dup_frames=299 drop_frames=0
frame=300 dup_frames=0 drop_frames=0
half.mp4: video 19.966667 s, 599 frames; audio 19.988000 s
half-15.mp4: video 20.000000 s, 300 frames; audio 19.988000 s

Eight frames of the half-speed output in two rows, colour bars with a large white frame number on each, reading 0, 1, 1, 2 and 2, 3, 3, 4: after the first frame, every source frame appears twice

The first 8 frames of the 0.5x output at 30 frames per second: each source frame is shown twice.

The half-speed clip lasts 19.966667 s and has 599 frames: ffmpeg duplicated 299 and dropped none. Each source frame is simply shown twice (the very first one only once), so slow motion made this way looks like the original played at 15 frames per second, which is jerky for fast movement. With -r 15 the file is honest about it: 300 frames, 0 duplicated, the same motion in half the frames.

Neither way invents new pictures in between. ffmpeg’s minterpolate filter does that by estimating the motion between frames; it was not measured for this page.

How far atempo goes on 4.4.2

The atempo section of the ffmpeg filters documentation, read on 2026-10-12, says “Tempo must be in the [0.5, 100.0] range.” The 4.4.2 binary says the same: ffmpeg -h filter=atempo lists tempo “from 0.5 to 100”, and a value outside it is refused:

# Each error line names a memory address (" @ 0x...") that changes from run to run; sed drops it.
for t in 0.25 101; do
  { ffmpeg -v error -i in.mp4 -vn -af "atempo=$t" -f null - 2>&1 || echo "exit status $?"; } | sed 's/ @ 0x[0-9a-f]*//'
done
[atempo] Value 0.250000 for parameter 'tempo' out of range [0.5 - 100]
    Last message repeated 1 times
[atempo] Error setting option tempo to value 0.25.
[Parsed_atempo_0] Error applying options to the filter.
[AVFilterGraph] Error initializing filter 'atempo' with args '0.25'
Error reinitializing filters!
Failed to inject frame into filter network: Numerical result out of range
Error while processing the decoded data for stream #0:1
exit status 1
[atempo] Value 101.000000 for parameter 'tempo' out of range [0.5 - 100]
    Last message repeated 1 times
[atempo] Error setting option tempo to value 101.
[Parsed_atempo_0] Error applying options to the filter.
[AVFilterGraph] Error initializing filter 'atempo' with args '101'
Error reinitializing filters!
Failed to inject frame into filter network: Numerical result out of range
Error while processing the decoded data for stream #0:1
exit status 1

ffmpeg prints the range it allows and exits with status 1.

4x, 10x and quarter speed: chaining atempo

Below 0.5 you have to chain: atempo=0.5,atempo=0.5 is a quarter of the speed, because each filter in a chain multiplies the tempo again. Above 2 a single atempo works on 4.4.2, but the docs warn that “tempo greater than 2 will skip some samples rather than blend them in”, and suggest chaining instead, “it is always possible to daisy-chain several instances of atempo to achieve the desired product tempo.” So here are 4x both ways, quarter speed, and 10x both as one atempo=10 and as a chain of factors of 2 or less (2 x 2 x 2 x 1.25 = 10):

ffmpeg -v error -i in.mp4 -vf "setpts=PTS/4" -af "atempo=2,atempo=2" 4x-chain.mp4
ffmpeg -v error -i in.mp4 -vf "setpts=PTS/4" -af "atempo=4" 4x.mp4
ffmpeg -v error -i in.mp4 -vf "setpts=4*PTS" -af "atempo=0.5,atempo=0.5" quarter.mp4
ffmpeg -v error -i in.mp4 -vf "setpts=PTS/10" -af "atempo=10" 10x.mp4
ffmpeg -v error -i in.mp4 -vf "setpts=PTS/10" -af "atempo=2,atempo=2,atempo=2,atempo=1.25" 10x-chain.mp4
for f in 4x-chain.mp4 4x.mp4 quarter.mp4 10x.mp4 10x-chain.mp4; do info "$f"; done
4x-chain.mp4: video 2.566667 s, 77 frames; audio 2.513917 s
4x.mp4: video 2.566667 s, 77 frames; audio 2.515000 s
quarter.mp4: video 39.966667 s, 1199 frames; audio 39.956000 s
10x.mp4: video 1.066667 s, 32 frames; audio 0.981000 s
10x-chain.mp4: video 1.066667 s, 32 frames; audio 1.009708 s

All five come out at the intended length: 77 frames for 4x (a quarter of 300, plus 2 let in at the start, as at 2x), 1199 for quarter speed, and 32 for 10x, where nine of every ten source frames are dropped. The sound of the single atempo=10 is 0.981000 s and of the chain 1.009708 s, against 1.066667 s of video; when the two lengths matter, as for a clip that has to end on a beat, measure both.

Pitch: atempo keeps it, asetrate does not

atempo changes how long the sound lasts and leaves its pitch alone. The other way to change the speed of sound is to tell ffmpeg the samples were recorded at a different rate with asetrate; the docs say plainly what that does, “Set the sample rate without altering the PCM data. This will result in a change of speed and pitch.” Both are measured here on the 440 Hz tone, by counting how often the waveform crosses zero in half a second of each file:

# hz FILE: save FILE's sound from 0.25 s to 0.75 s as 16-bit mono samples (FILE.s16), find
# every point where the waveform changes sign (placed between the two samples by straight-line
# interpolation), and turn the count and the time from the first to the last into a frequency.
# A sine wave crosses zero twice per cycle.
hz() {
  ffmpeg -v error -i "$1" -map 0:a -ss 0.25 -t 0.5 -ac 1 -f s16le "$1.s16"
  od -An -v -td2 -w2 "$1.s16" | awk '
    NR > 1 && (p < 0) != ($1 < 0) { t = NR - 2 + p / (p - $1); if (!n) t0 = t; t1 = t; n++ }
    { p = $1 }
    END { printf "%d zero crossings, %.1f Hz\n", n, (n - 1) / 2 / ((t1 - t0) / 48000) }'
}
# The naive way: tell ffmpeg the samples are at twice (or half) their real rate, then resample.
ffmpeg -v error -i in.mp4 -vf "setpts=PTS/2" -af "asetrate=96000,aresample=48000" naive-2x.mp4
ffmpeg -v error -i in.mp4 -vf "setpts=2*PTS" -af "asetrate=24000,aresample=48000" naive-half.mp4
for f in in.mp4 2x.mp4 half.mp4 4x.mp4 10x.mp4 quarter.mp4 naive-2x.mp4 naive-half.mp4; do
  echo "$f: $(hz "$f")"
done
info naive-2x.mp4
info naive-half.mp4
in.mp4: 440 zero crossings, 440.0 Hz
2x.mp4: 440 zero crossings, 440.1 Hz
half.mp4: 439 zero crossings, 440.0 Hz
4x.mp4: 440 zero crossings, 440.2 Hz
10x.mp4: 440 zero crossings, 440.1 Hz
quarter.mp4: 440 zero crossings, 440.1 Hz
naive-2x.mp4: 879 zero crossings, 880.0 Hz
naive-half.mp4: 219 zero crossings, 220.0 Hz
naive-2x.mp4: video 5.066667 s, 152 frames; audio 5.002000 s
naive-half.mp4: video 19.966667 s, 599 frames; audio 20.010000 s

The source is 440.0 Hz, and after atempo at 2x, 0.5x, 4x, 10x and quarter speed the tone is still within 0.2 Hz of it. The asetrate versions are 880.0 Hz and 220.0 Hz: exactly double and half, a full octave up (the chipmunk voice) and down. Their lengths are right too, which is why the trick is tempting; only the pitch is wrong. One thing this measures and one it does not: a pure tone shows the pitch is kept, but not how speech or music sounds after atempo, which at high factors can sound choppy.

Two errors you will meet

A clip with no sound track, such as a screen recording made without audio, makes the answer command fail, because [0:a] asks for an audio stream that is not there. And a speed change cannot be done with -c copy, because the frames have to be re-encoded with their new timestamps:

# A clip with no sound at all, then the answer command on it.
ffmpeg -v error -i in.mp4 -an -c:v copy silent.mp4
{ ffmpeg -v error -i silent.mp4 -filter_complex "[0:v]setpts=PTS/2[v];[0:a]atempo=2[a]" -map "[v]" -map "[a]" s2x.mp4 2>&1 || echo "exit status $?"; } | sed 's/ @ 0x[0-9a-f]*//'
# Filters cannot be combined with -c copy.
{ ffmpeg -v error -i in.mp4 -vf "setpts=PTS/2" -af "atempo=2" -c copy copy.mp4 2>&1 || echo "exit status $?"; } | sed 's/ @ 0x[0-9a-f]*//'
# Without the audio half, the silent clip works.
ffmpeg -v error -i silent.mp4 -vf "setpts=PTS/2" s2x.mp4
info s2x.mp4
Stream specifier ':a' in filtergraph description [0:v]setpts=PTS/2[v];[0:a]atempo=2[a] matches no streams.
exit status 1
Filtergraph 'setpts=PTS/2' was defined for video output stream 0:0 but codec copy was selected.
Filtering and streamcopy cannot be used together.
exit status 1
s2x.mp4: video 5.066667 s, 152 frames

For a clip with no sound, drop the audio half and use -vf "setpts=PTS/2" alone.

The frames

The three strips above are the PNGs this run wrote:

sha256sum strip-2x.png strip-2x-60.png strip-half.png
cc3b2d8d070f44ed1b43552a32046eed2515eeb1771cba64ac87b36ee2f8c34e  strip-2x.png
62a89e69be2e2dcd4308e4cadb79a8d99b55fa7742005f0629e8f7ac44260fd6  strip-2x-60.png
3d5cc90f7f5140066250a07838fdddb96258e49d086b823a9b67bfc1b36720eb  strip-half.png

Where ffmpeg 4.4.2 and today’s documentation differ

The filter options on this page are from the ffmpeg filters documentation for atempo, setpts and asetrate, and the frame rate options from the video and advanced options of the ffmpeg documentation, all read on 2026-10-12 and checked against what 4.4.2 prints:

  • atempo: the same range in both, 0.5 to 100.
  • -fps_mode: in today’s docs, an unknown option in 4.4.2. 4.4.2 has -vsync (“video sync method” in its help), which today’s page no longer mentions; -vsync vfr ran on 4.4.2 above.
  • setpts: today’s docs add a strip_fps option; 4.4.2’s setpts has only expr.
  • -r as an output option means the same in both: “Duplicate or drop frames right before encoding them to achieve constant output frame rate fps.”

Speed changes in a Short

The Raspberry Pi pipeline that makes YouTube Shorts assembles each Short with ffmpeg; as that post describes the step, clips are trimmed to match the narration and the audio is mixed. Its filter graph already uses setpts, as setpts=PTS-STARTPTS, which only restarts the timestamps at zero after the trim; nothing is sped up. If a clip is too long for its line of narration, setpts with atempo is the alternative to cutting it: at 2x the voice stays at its pitch, and at 30 frames per second half the frames go, so use -r 60 if the motion matters.

What this page does not cover: the measurements are on a synthetic clip and a pure tone. Real footage may have a variable frame rate to begin with, and atempo on speech or music should be judged by ear as well as measured.

← Back to all posts

Follow new posts by RSS: paste dankdev.com/feed.xml into any feed reader. It is a plain XML file — no signup, no email address, no account. Posts land there the same day they go up, and there is nothing to unsubscribe from later.

Back to the top