Jonathan Will

Projects and Research

View Full Portfolio

Turn PDF Slides and Voice Recordings Into a Presentation Video

2020

With everything moving online this semester, a lot of presentations have turned into “please hand in a video of your talk”. The usual advice is to fire up a screen recorder, click through your slides and talk for twenty minutes straight. If you stumble over a sentence on slide 14, you either live with it or start over.

There is a nicer way: record one short audio clip per slide and let a script stitch everything together.

The idea

You need two things:

  • Your slides as a single PDF (e.g. exported from LaTeX Beamer, LibreOffice, …)
  • One mp3 file per slide, named so they sort in the right order (01.mp3, 02.mp3, …)

The script then

  1. trims the silence at the start and end of each recording (no more awkward pauses while you reach for the mouse),
  2. renders every PDF page into an image,
  3. shows each slide for exactly as long as its recording lasts,
  4. and puts it all into one presentation.mp4.

Messed up slide 14? Re-record 14.mp3 and run the script again. That’s it.

Usage

Put the PDF and an audio directory into a folder:

my_talk/
├── slides.pdf
└── audio/
    ├── 01.mp3
    ├── 02.mp3
    └── 03.mp3

Then just run:

cd my_talk
make_video

The only dependencies are ffmpeg and pdftoppm (from poppler), which you most likely already have installed.

The script

#!/bin/sh
# Turn a PDF with n slides and n mp3 files (one per slide) into presentation.mp4
# Usage: run in a directory containing one PDF and audio/01.mp3, audio/02.mp3, ...
set -e

die() { echo "$*" >&2; exit 1; }

set -- ./*.pdf
[ $# -eq 1 ] && [ -f "$1" ] || die 'Exactly one PDF file required'
pdf=$1

tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT

# One image per slide: slide-01.png, slide-02.png, ...
pdftoppm -png -scale-to 1920 "$pdf" "$tmp/slide"
set -- "$tmp"/slide-*.png
[ $# -eq "$(ls audio/*.mp3 | wc -l)" ] || die "Need exactly one mp3 per slide ($# slides)"

# Cut silence at the start and end of each recording and
# show each slide for exactly as long as its recording lasts
trim='silenceremove=start_periods=1:start_threshold=-48dB'
for audio in audio/*.mp3; do
    wav="$tmp/${1##*/}.wav"
    ffmpeg -v error -i "$audio" -af "$trim,areverse,$trim,areverse" "$wav"
    duration=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$wav")
    printf "file '%s'\nduration %s\n" "$1" "$duration" >> "$tmp/slides.txt"
    printf "file '%s'\n" "$wav" >> "$tmp/audio.txt"
    last=$1; shift
done
# The concat demuxer ignores the last duration unless the last file is repeated
printf "file '%s'\n" "$last" >> "$tmp/slides.txt"

# Variable frame rate: every slide is essentially a single frame
ffmpeg -v error -y \
    -f concat -safe 0 -i "$tmp/slides.txt" \
    -f concat -safe 0 -i "$tmp/audio.txt" \
    -vf 'scale=trunc(iw/2)*2:trunc(ih/2)*2,format=yuv420p' -fps_mode vfr \
    -c:v libx264 -tune stillimage -c:a aac presentation.mp4

The silence trimming uses a little trick: ffmpeg’s silenceremove filter is good at cutting silence from the start of a recording, so the script trims, reverses the audio, trims again and reverses it back.

Why is resulting video file so small?

The relevant part is the last step. A presentation is mostly a still image with someone talking over it, so there’s no point in encoding 30 identical frames per second. Instead, the script feeds ffmpeg a list like this

file '/tmp/tmp.x8Kd2/slide-01.png'
duration 12.84
file '/tmp/tmp.x8Kd2/slide-02.png'
duration 31.2
...

and encodes it with a variable frame rate. So each slide ends up as a single frame that stays on screen until the next one comes along. A 15 minute talk with full HD slides comes out at barely more than the size of the audio.

Good luck with your next remote talk!