Banner of automating-video-speed-up-and-audio-replacement-with-ffmpeg-and-bash-15.jpg

Automating Video Speed-Up and Audio Replacement with FFmpeg and Bash


Category: Linux

📅 April 24, 2026   |   👁️ Views: 1

Author:   mosaid

In this tutorial, we are not just writing a quick script — we are building a reusable video processing workflow around FFmpeg and Bash.

The goal is simple on the surface:

• Speed up a video

• Remove its original audio

• Replace it with a looping background track

But instead of treating this as a one-off command, we will break down the why, the mechanics, and the trade-offs, so you can extend this into your own pipelines.


1. The Complete Script

Let’s start with the full working version:

    
#!/bin/bash

set -e

INPUT="$1"
SPEED_FACTOR="${2:-3}"

if [ -z "$INPUT" ]; then
    echo "Usage: $0 input_video.mkv [speed]"
    exit 1
fi

AUDIO_DIR="/home/mosaid/Documents/audio"

echo "Available audio files:"
mapfile -t AUDIO_FILES <<(find "$AUDIO_DIR" -maxdepth 1 -type f -name "*.mp3")

if [ ${#AUDIO_FILES[@]} -eq 0 ]; then
    echo "No mp3 files found."
    exit 1
fi

for i in "${!AUDIO_FILES[@]}"; do
    echo "$((i+1))) $(basename "${AUDIO_FILES[$i]}")"
done

read -rp "Choose an audio file: " AUDIO_CHOICE

AUDIO="${AUDIO_FILES[$((AUDIO_CHOICE-1))]}"

BASENAME="$(basename "${INPUT%.*}")"
MUTED="$(mktemp --suffix=.mp4)"
SPEED="$(mktemp --suffix=.mp4)"
FINAL="${BASENAME}-final.mp4"

ffmpeg -y -i "$INPUT" -c:v copy -an "$MUTED"

ffmpeg -y -i "$MUTED" -filter:v "setpts=PTS/${SPEED_FACTOR}" "$SPEED"

ffmpeg -y -stream_loop -1 -i "$AUDIO" -i "$SPEED" -shortest \
  -map 1:v -map 0:a -c:v copy -c:a aac "$FINAL"

rm -f "$MUTED" "$SPEED"

echo "Output: $FINAL"
    

2. Understanding the Pipeline

This script is intentionally split into three distinct FFmpeg passes. That is not accidental.

• Pass 1 → Strip audio

• Pass 2 → Modify video timing

• Pass 3 → Rebuild final container

This separation keeps each step predictable and avoids subtle FFmpeg sync issues that often appear in one-liners.


3. Why We Remove Audio First

    
ffmpeg -y -i "$INPUT" -c:v copy -an "$MUTED"
    

-c:v copy → No re-encoding (this is critical for performance)

-an → Drop all audio streams

Why do this first?

• It guarantees a clean base (no leftover streams)

• It avoids audio/video desync after time manipulation

• It keeps the pipeline deterministic

Power-user insight:

If you skip this step and try to manipulate both streams at once, FFmpeg may keep timestamps that no longer align.


4. The Core Concept: setpts

    
ffmpeg -y -i "$MUTED" -filter:v "setpts=PTS/${SPEED_FACTOR}" "$SPEED"
    

This is the heart of the script.

PTS = Presentation Timestamp

• Every frame has a timestamp that determines when it is displayed

When you write:

PTS/3 → Frames are shown 3× faster

PTS*2 → Frames are shown slower

So we are not "dropping frames" or "changing FPS" — we are modifying time itself at the container level.

This is why the operation is both:

• Efficient

• Precise


Advanced Note

If you wanted to also adjust audio speed (instead of replacing it), you would need:

    
-filter:a "atempo=2.0"
    

But here we intentionally discard audio entirely, which simplifies everything.


5. Looping Audio Without Guessing Duration

    
ffmpeg -y -stream_loop -1 -i "$AUDIO" -i "$SPEED" -shortest \
  -map 1:v -map 0:a -c:v copy -c:a aac "$FINAL"
    

This is a very clean pattern that many people overlook.

-stream_loop -1 → Infinite loop

-shortest → Output stops when video ends

This avoids:

• Manually trimming audio

• Calculating durations

• Writing fragile logic


Stream Mapping (Important)

-map 1:v → Take video from second input

-map 0:a → Take audio from first input

Without this, FFmpeg may choose streams implicitly — which is unreliable in automation scripts.


6. Temporary Files Strategy

    
MUTED="$(mktemp --suffix=.mp4)"
SPEED="$(mktemp --suffix=.mp4)"
    

Using mktemp instead of fixed filenames is a small but important detail:

• Prevents collisions

• Makes the script safe for parallel execution

• Avoids accidental overwrites

This is one of those habits that separates quick scripts from robust tooling.


7. Interactive Selection vs Automation

The script uses:

    
mapfile -t AUDIO_FILES  <<(find "$AUDIO_DIR" -name "*.mp3")
    

This is simple and portable — but not optimal for heavy usage.

Upgrade: fzf Integration

    
AUDIO=$(find "$AUDIO_DIR" -type f -name "*.mp3" | fzf)
    

• Instant fuzzy search

• No manual indexing

• Better UX for large libraries

This is the kind of improvement power users appreciate immediately.


8. Performance Considerations

This pipeline is efficient because:

• No video re-encoding (stream copy)

• Only one transformation step (PTS)

• Lightweight final muxing

However:

• The setpts step does require re-encoding

• That is unavoidable when modifying timestamps

If performance becomes critical, you can:

• Use hardware acceleration (VAAPI / NVENC)

• Lower resolution before processing


9. Turning This Into a Real Tool

Right now, this is a script. With small changes, it becomes a proper CLI utility:

• Add flags (-i, -s, -a)

• Add logging

• Add dry-run mode

• Add batch processing

Example batch usage:

    
for f in *.mkv; do
    ./process.sh "$f" 2
done
    

10. Where This Fits in Real Workflows

This pattern is surprisingly useful:

Lecture acceleration: compress hours into minutes

Content pipelines: generate background visuals

Automation: integrate with cron or systemd

Media cleanup: replace poor audio tracks

And more importantly:

• It composes well with other Unix tools


Final Thoughts

The real takeaway here is not the script itself — it is the approach:

• Break pipelines into deterministic steps

• Understand what FFmpeg is doing internally

• Prefer explicit mapping and control

Once you think this way, FFmpeg stops being a "command generator" and becomes a reliable building block in your system.

From here, you can extend this into a full automation pipeline, integrate it into your desktop workflow, or even expose it as a service.


← From Terminal to Desktop: Sticky Todo Reminders with Cron, systemd & Conky Supercharging Zsh: Practical Functions and Keybindings for Power Users →