VFR to CFR in FFmpeg: Fix Audio Desync (2026 Guide)
Your clip starts perfectly in sync and ends four seconds off. The camera didn't write a broken file — it wrote a variable frame rate (VFR) file, and your editor assumes a constant frame rate (CFR). Premiere Pro, DaVinci Resolve, and After Effects all map audio against a fixed frame clock, so when the video's real timestamps drift, sync drifts with them.
TL;DR — the exact fix
One command, and the flags that actually matter
The fps filter converts to CFR. aresample=async=1 repairs the audio timestamps. -r alone does neither.
VFR to CFR — sync-safe conversion
ffmpeg -fflags +genpts -i input.mp4 -vf "fps=30000/1001,format=yuv420p" -af "aresample=async=1:first_pts=0" -c:v libx264 -crf 18 -preset slow -c:a aac -b:a 192k -ar 48000 -fps_mode cfr -movflags +faststart output.mp4
| Setting | Value | Why |
|---|---|---|
| Frame resample | fps=30000/1001 | Matches 29.97 exactly. Use 24, 25, 30, 60000/1001, or 60 to match your project |
| Audio repair | aresample=async=1:first_pts=0 | Rebuilds audio timestamps against the new video clock |
| Audio rate | -ar 48000 | Video timeline standard; avoids timeline resampling |
| Video quality | -crf 18 -preset slow | Visually near-lossless H.264 |
| Mux mode | -fps_mode cfr | Replaces the deprecated -vsync cfr |
What VFR and CFR actually mean
CFR writes a frame every fixed interval — at 30fps, exactly 33.333 ms apart, every time. VFR writes a frame only when the picture changes, then stamps it with the real capture time. A static shot might produce four frames in a second; motion produces thirty.
| Property | CFR | VFR |
|---|---|---|
| Frame interval | Constant (33.333 ms at 30fps) | Variable |
| File size | Predictable | Smaller for static scenes |
| Editor compatibility | Universal | Poor — drift, stutter, frame mismatch |
| Typical sources | Camcorders, OBS, ProRes/DNxHR | Phone cameras, WebRTC, call recorders, many screen recorders |
| Audio sync over 40 min | Holds | Drifts by seconds |
The drift is arithmetic, not a bug. An editor conforming VFR to a fixed rate assumes every frame lasts 1/30 s. If the real average was 29.7 fps, a 40-minute clip accumulates roughly 24 seconds of phantom timeline — the audio, recorded on its own steady clock, slides out of position as you approach the end.
Step 1 — Confirm the file is actually VFR
Do not guess. Run this first:
Check reported frame rates
ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate,avg_frame_rate,time_base -of default=noprint_wrappers=1 input.mp4
- r_frame_rate is the nominal rate the container advertises.
- avg_frame_rate is the real average.
If the two values differ, the file is VFR. A file reporting r_frame_rate=30/1 and avg_frame_rate=2997/100 is variable.
When both look identical but you still get drift, check the raw frame timestamps:
Inspect frame timestamps
ffprobe -v error -select_streams v:0 -show_entries frame=pts_time -of csv=p=0 input.mp4
A CFR file shows a uniform step (0.000, 0.033, 0.067). Uneven gaps confirm VFR. On FFmpeg builds older than 5.x, replace frame=pts_time with frame=pkt_pts_time.
Step 2 — Convert to CFR at the right frame rate
Match the output rate to your project rate, not to what the phone claims:
| Project timeline | Use this fps value |
|---|---|
| 23.976 | fps=24000/1001 |
| 24 | fps=24 |
| 25 (PAL) | fps=25 |
| 29.97 | fps=30000/1001 |
| 30 | fps=30 |
| 59.94 | fps=60000/1001 |
| 60 | fps=60 |
A rounded rate is the most common self-inflicted desync: conforming a 29.97 clip to 30 introduces about one frame of error every 33 seconds — roughly 72 frames across a 40-minute recording.
Step 3 — Pick settings by source type
| Source | Typical symptom | Command core | Audio fix |
|---|---|---|---|
| iPhone / Android camera | Drift in Premiere, breaks on conform | fps=30000/1001 | aresample=async=1:first_pts=0 |
| WebRTC / browser recording | Choppy playback, unstable duration | fps=30 | -ar 48000 plus aresample |
| Meeting / call recorder | Drift plus thin-sounding audio | fps=30 | -ar 48000 (often not 48 kHz) |
| Screen / game capture | High bitrate, uneven frame steps | fps=60 | -ar 48000 |
| Phone slow motion (120/240 fps) | Do not run the fps filter — keep the source rate | c copy, conform in the NLE | not applicable |
Delivery targets for the same job
| Target | Video | Quality | Audio | Container |
|---|---|---|---|---|
| Editing intermediate | ProRes 422 Proxy (-profile:v 0) | n/a, intra-frame | pcm_s16le -ar 48000 | .mov |
| Editing (Avid / Windows) | DNxHR LB (-profile:v dnxhr_lb) | n/a, intra-frame | pcm_s16le -ar 48000 | .mov |
| Delivery / archive | H.264 | -crf 18 -preset slow | aac -b:a 192k -ar 48000 | .mp4 |
| Smaller delivery | H.265 | -crf 20 -preset medium | aac -b:a 192k -ar 48000 | .mp4 |
If the goal is also a smaller file after the sync fix, see how to make a video file smaller with FFmpeg.
Fixing audio that is already out of sync
Two different faults need two different tools. Diagnose before you touch anything.
Constant offset — early or late by the same amount everywhere
Delay audio by 0.5 s (audio was early)
ffmpeg -i input.mp4 -itsoffset 0.5 -i input.mp4 -map 0:v:0 -map 1:a:0 -c copy output.mp4
Advance audio by 0.5 s (audio was late)
ffmpeg -i input.mp4 -itsoffset -0.5 -i input.mp4 -map 0:v:0 -map 1:a:0 -c copy output.mp4
Progressive drift — fine at the start, worse by the end
The audio needs time-stretching, not shifting. Measure both streams:
Video duration
ffprobe -v error -select_streams v:0 -show_entries stream=duration -of csv=p=0 input.mp4
Audio duration
ffprobe -v error -select_streams a:0 -show_entries stream=duration -of csv=p=0 input.mp4
Then compute atempo = audio_duration ÷ video_duration. If video is 3600.000 s and audio is 3599.020 s:
Stretch audio to match the video clock
ffmpeg -i input.mkv -c:v copy -af "atempo=0.999728" -c:a aac -b:a 192k -ar 48000 output.mp4
atempo accepts 0.5 to 2.0 per instance; chain filters (atempo=0.8,atempo=0.8) for anything beyond that. If timestamps are simply wrong while the audio data is intact, let aresample repair them instead:
Repair timestamps without stretching
ffmpeg -i input.mkv -c:v copy -c:a aac -b:a 192k -ar 48000 -af "aresample=async=1:first_pts=0" output.mp4
Why -r 30 alone does not fix desync
Three flags look similar and behave completely differently. This is where most guides send you wrong.
| Flag | Position | What it actually does | Effect on sync |
|---|---|---|---|
| -r 30 | Before -i | Reinterprets the source as 30fps, changing duration | Breaks sync, can cause slow motion |
| -r 30 | After -i | Drops or duplicates frames to reach 30fps | Can leave audio timestamps untouched |
| -vf "fps=30" | Filter | Resamples every frame timestamp to a true 30fps clock | Fixes the video clock |
| -fps_mode cfr | Output option | Writes constant-rate timestamps, replaces -vsync cfr | Belt-and-braces alongside fps |
The fps filter is the correct tool because it rewrites the timeline, not just the frame count. It duplicates frames where the source was slow and drops them where it was fast, so total duration is preserved and the audio stays anchored.
- Never use -c copy to fix VFR. Stream copy preserves the original timestamps exactly, so the drift survives the remux untouched.
- -vsync is deprecated. It was superseded by -fps_mode in FFmpeg 5.1. Older builds may still accept it with a warning; new scripts should use -fps_mode.
For editors: build a CFR intermediate instead
If the footage is going into an NLE, a CFR delivery encode is the wrong output. Transcode once to an intra-frame intermediate — it decodes instantly, so it also removes the playback lag that 4K H.264 and HEVC cause.
1080p ProRes Proxy with PCM audio — the standard editing intermediate
ffmpeg -i input.mp4 -c:v prores_ks -profile:v 0 -pix_fmt yuv422p10le -vf "fps=30000/1001,scale=1920:-2:flags=lanczos" -c:a pcm_s16le -ar 48000 -fps_mode cfr -movflags +faststart output.mov
| Codec | Profile flag | Data rate (1080p29.97) | Approx. size / hour |
|---|---|---|---|
| ProRes Proxy | -profile:v 0 | 45 Mbps | ~20 GB |
| ProRes LT | -profile:v 1 | 102 Mbps | ~45 GB |
| ProRes 422 | -profile:v 2 | 147 Mbps | ~66 GB |
| ProRes 422 HQ | -profile:v 3 | 220 Mbps | ~99 GB |
| DNxHR LB | -profile:v dnxhr_lb | ~36 Mbps | ~16 GB |
For a full breakdown of when each codec wins, see ProRes vs DNxHR compared.
Premiere Pro users
The Modify → Interpret Footage → Conform to Frame Rate option is non-destructive but only re-times existing frames. It trades drift for judder and does not repair variable timing. Transcode instead.
Batch-convert a folder of VFR clips
macOS and Linux, using bash
convert-vfr-folder.sh
#!/usr/bin/env bash
set -euo pipefail
SRC="input"
OUT="cfr"
FPS="30000/1001"
mkdir -p "$OUT"
while IFS= read -r f; do
base=$(basename "$f")
base=${base%.*}
out="$OUT/$base.mp4"
echo "Converting: $out"
ffmpeg -hide_banner -nostdin -y -fflags +genpts -i "$f" -vf "fps=$FPS,format=yuv420p" -af "aresample=async=1:first_pts=0" -c:v libx264 -crf 18 -preset slow -c:a aac -b:a 192k -ar 48000 -fps_mode cfr -movflags +faststart "$out"
done < <(find "$SRC" -type f -iname '*.mp4' -o -type f -iname '*.mov' -o -type f -iname '*.mkv')Windows, using PowerShell
convert-vfr-folder.ps1
$Src = "input"; $Out = "cfr"; $Fps = "30000/1001"
New-Item -ItemType Directory -Force -Path $Out | Out-Null
Get-ChildItem -Path $Src -Recurse -File -Include *.mp4,*.mov,*.mkv | ForEach-Object {
$rel = $_.FullName.Substring((Resolve-Path $Src).Path.Length + 1)
$outFile = Join-Path $Out ([System.IO.Path]::ChangeExtension($rel, ".mp4"))
New-Item -ItemType Directory -Force -Path (Split-Path $outFile -Parent) | Out-Null
Write-Host "Converting: $outFile"
$ffArgs = @('-hide_banner','-nostdin','-y','-fflags','+genpts','-i',$_.FullName,
'-vf',"fps=$Fps,format=yuv420p",'-af','aresample=async=1:first_pts=0',
'-c:v','libx264','-crf','18','-preset','slow',
'-c:a','aac','-b:a','192k','-ar','48000',
'-fps_mode','cfr','-movflags','+faststart',$outFile)
& ffmpeg @ffArgs
}If the whole folder is heading to a mixing stage rather than an editor, batch audio loudness normalisation belongs in the same pass.
Verify the fix
Confirm constant frame rate
ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate,avg_frame_rate -of default=noprint_wrappers=1 output.mp4
r_frame_rate and avg_frame_rate must now be identical. If they are, the timeline clock is constant and the drift is gone.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Audio still drifts after -r 30 | Output -r left timestamps variable | Use -vf "fps=30" plus -fps_mode cfr |
| Sync is fine in VLC, broken in Premiere | The NLE conforms VFR differently from the player | Transcode to CFR before import |
| Output plays at half speed | -r placed before -i reinterprets the source | Move rate control into the filter |
| Duration changed after conversion | Rate mismatch, 29.97 forced to 30 | Use the exact fractional rate |
| Unknown option fps_mode | FFmpeg older than 5.1 | Upgrade, or use -vsync cfr on legacy builds |
| Audio still off by a fixed amount | Constant offset, not drift | Apply -itsoffset |
| -c:a copy fails with -af | Stream copy cannot be filtered | Re-encode audio as AAC |
| genpts warnings, broken timestamps | Missing PTS in the source | Add -fflags +genpts before -i |
Do it offline across a whole folder
If the folder holds hundreds of clips, the terminal loop above is the honest free answer. Formatif Pro ($8.95 one-time) does the same CFR conversion offline on Windows and Linux — point it at the folder, set the frame rate and codec once, and it processes every file with per-file status instead of a silent failed loop. It handles the audio resample in the same pass, so sync survives. You can download the free trial and compare on your own footage.
VFR and CFR questions, answered
What is the difference between VFR and CFR?
CFR writes every frame at a fixed interval, so the timeline is predictable. VFR writes frames only when the picture changes and stamps each with its real capture time. Editors assume CFR, which is why VFR files drift.
Why is my iPhone video only out of sync in Premiere Pro?
Phones record VFR. Players like VLC ignore the frame clock and play timestamps as they arrive, so they stay in sync. Premiere Pro conforms the video to the sequence rate and maps audio against that fixed clock, which exposes the accumulated timing error.
Does converting VFR to CFR reduce quality?
The fps filter itself is lossless — it duplicates and drops frames, it does not interpolate or alter pixels. The only quality loss comes from the encoder you choose. At CRF 18 with the slow preset it is not visible; a ProRes intermediate is effectively transparent.
Why doesn't -r 30 fix the desync?
As an output option it adjusts frame count without rebuilding the timeline clock, and as an input option it reinterprets the source and changes duration. The fps filter is what actually resamples timestamps.
Is -vsync still valid?
It is deprecated. FFmpeg 5.1 introduced -fps_mode (cfr, vfr, passthrough, auto) as the replacement. Legacy builds accept -vsync cfr with a warning; new scripts should use -fps_mode cfr.
Which frame rate should I convert to?
Always your project rate. A 29.97 source in a 29.97 timeline is perfect; forcing it to 30 creates about one frame of error every 33 seconds.
Can I fix VFR without re-encoding?
No. Stream copy (-c copy) preserves the original timestamps, which is exactly what is broken. VFR to CFR requires resampling, so at least the video must be re-encoded.
Keep learning
Stop fighting frame rates in the terminal.
Formatif Pro converts VFR footage to a constant frame rate across whole folders, offline, with the audio repaired in the same pass. Formatif Pro $8.95, Formatif AI $12.95, or the bundle at $16.95. All one-time purchases with a 3-day free trial.
Written by
Founder of Formatif, a local-first offline media processing suite for Windows and Linux. Hadi writes about video, image, and audio formats, compression, and privacy-first desktop workflows.