Using FFmpeg's ffplay as a beefed-up audio player 👈 🌶 🌞

With cooler visualizations, progress button, thumbnail and metadata

In a previous blog post, I have shown how to use ffplay as a more sophisticated video player than it is by default. In this blog post, I will show how to use it as an audio player.

Over the last few days, I have been creating videos for my [FFmpeg book](../../ffmpeg-book.html]. Recently, I found the original videos that I thought I had lost. I needed an audio player that displays metadata in more detail so I built on using FFplay. I have also integrated it with my online video downloader script so that it will play videos that I want to listen rather than watch.

If you press the W key, you can cycle through this window that my script creates and two native windows that ffplay usually displays. The only drawback is that it has lost its ability to fast-forward or go backwards. The screen does not act as a giant playback slider.

Code

This amovie filter in ffmpeg cannot handle files with apostrophe (') in the file name. If you were to directly type in the name, it will require five slashes to escape it. It seems almost impossible to do it in a script.

#!/bin/bash
set -u

sFont="/usr/share/fonts/truetype/freefont/FreeSans.ttf"
sFile="$*"

getSafeString() {
  sText="$*"
  sText="${sText//\'/’}"
  MVE_SafeString="${sText//\:/ː}"
  export MVE_SafeString
}

getNameValue() {
  sSourceFile=$1
  sName=$2
  MVE_NameValue=""
  sLine="$(grep -i $sName $sSourceFile)"
  if [ ! -z "$sLine" ]; then
    MVE_NameValue=${sLine##*=}
  fi
  export MVE_NameValue
}

if [ -f "$sFile" ]; then
  if [ "${sFile//\'/}" != "$sFile" ]; then
    notify-send "Audio FFplayer error" "File name contains illegal characters"
        exit
  fi
else
    notify-send "Audio FFplayer error" "Inaccessible/non-existent file"
    exit
fi

sTitle=""
sAlbum=""
sArtist=""
sYear=""
sCopyright=""
sComment=""
sContainer=""
sCodec=""
sChannels=""
sChannelLayout=""
sSampleRate=""
sBitRate=""

sTempFile1=$(mktemp)
sTempFile2=$(mktemp)
ffprobe -loglevel quiet -read_intervals %+60 \
        -select_streams a:0 \
        -show_entries "stream=duration : stream=codec_long_name : 
                       stream=channel_layout : stream=channels : 
                       stream=sample_rate : stream=bit_rate : 
                       format=format_name : format=format_long_name " \
        -print_format "default=nokey=0:noprint_wrappers=1" \
        "$sFile" > "$sTempFile1"

ffprobe -loglevel quiet -read_intervals %+60 \
        -show_entries "tags=title : tags=album: tags=artist : tags=year : 
                       tags=date : tags=copyright : tags=comment " \
        -print_format "default=nokey=0:noprint_wrappers=1" \
        "$sFile" > "$sTempFile2"

cat "$sTempFile1"
cat "$sTempFile2"

getNameValue $sTempFile1 format_long_name
sContainer="$MVE_NameValue"

getNameValue $sTempFile1 codec_long_name
sCodec="$MVE_NameValue"

getNameValue $sTempFile1 sample_rate
sSampleRate="$MVE_NameValue"

getNameValue $sTempFile1 channel_layout
sChannelLayout="$MVE_NameValue"

getNameValue $sTempFile1 channels
sChannels="$MVE_NameValue"

getNameValue $sTempFile1 bit_rate
sBitRate="$MVE_NameValue"

getNameValue $sTempFile1 duration
sDuration="$MVE_NameValue"

getNameValue $sTempFile2 TAG:Title
sTitle="$MVE_NameValue"
getSafeString "$sTitle"
sTitle="$MVE_SafeString"

getNameValue $sTempFile2 TAG:Album
sAlbum="$MVE_NameValue"
getSafeString "$sAlbum"
sAlbum="$MVE_SafeString"

getNameValue $sTempFile2 TAG:Artist
sArtist="$MVE_NameValue"
getSafeString "$sArtist"
sArtist="$MVE_SafeString"

getNameValue $sTempFile2 TAG:Year
sYear="$MVE_NameValue"
if [ -z "$sYear" ]; then
  getNameValue $sTempFile2 TAG:Date
  sYear="$MVE_NameValue"
fi
getSafeString "$sYear"
sYear="$MVE_SafeString"

getNameValue $sTempFile2 TAG:Copyright
sCopyright="${MVE_NameValue}"
getSafeString "$sCopyright"
sCopyright="$MVE_SafeString"
sCopyright="${sCopyright//:/\\:}"

getNameValue $sTempFile2 TAG:Comment
sComment="${MVE_NameValue}"
getSafeString "$sComment"
sComment="$MVE_SafeString"
sComment="${sComment//:/\\:}"

rm $sTempFile1 $sTempFile2

if [ -z "$sChannels" ]; then
  notify-send "Audio FFplayer error" "Unknown/non-existent audio"
  exit
fi

sChartWLocationX=10
if [ $sChannels -gt 3 ]; then
  sChartWLocationX=300
fi

if [ "$sDuration" = "N/A" ]; then
  sTime=$(ffprobe -loglevel quiet -read_intervals %+60 \
                      -select_streams a:0 \
                      -show_entries "stream_tags=duration " \
                      -print_format "default=nokey=1:noprint_wrappers=1" \
                      "$sFile")
  if [ "${#sTime}" -ge 6 ]; then
    sTime=${sTime%.*}
    sHours=${sTime%%:*}
    sHours=${sHours#0}
    sMinutes=${sTime#*:}
    sMinutes=${sMinutes%:*}
    sMinutes=${sMinutes#0}
    sSeconds=${sTime##*:}
    sSeconds=${sSeconds#0}
    sDuration=$(echo ${sHours}*3600 + ${sMinutes}*60 + $sSeconds | bc)
  else
    notify-send "Audio FFplayer error" "Unable to determine duration of audio"
  fi
else
  sDuration=${sDuration%.*}
  sHours="0"
  sMinutes="0"
  sSeconds="0"

  if [ "$sDuration" -ge 3600 ]; then
    sHours=$(echo $sHours / 3600 | bc)
  fi

  sRem=$(echo $sDuration - ${sHours}*3600 | bc)
  if [ "$sRem" -ge 60 ]; then
    sMinutes=$(echo $sRem / 60 | bc)
  fi

  sSeconds=$(echo $sDuration - ${sHours}*3600 - ${sMinutes}*60 | bc)

fi

if [ "$sHours" -lt 10 ]; then
  sHours="0${sHours}"
fi

if [ "$sMinutes" -lt 10 ]; then
  sMinutes="0${sMinutes}"
fi

if [ "$sSeconds" -lt 10 ]; then
  sSeconds="0${sSeconds}"
fi

sTime="${sHours}h ${sMinutes}m ${sSeconds}s"

sBitRate=$(echo $sBitRate / 1000 | bc)

sPlayerTitle="$sTitle"
if [ -z "$sTitle" ]; then
  sPlayerTitle=$(basename "$sFile")
  sTitle="$sPlayerTitle"
fi

sFileName=$(basename "$sFile")
sFileName="${sFileName%.*}"
sImage="/tmp/${sFileName}.png"

ffmpeg -loglevel error -ss 1 -i "$sFile" -frames:v 1 -t 1 "$sImage"
if [ $? -eq 0 ]; then
  ffplay -f lavfi  \
         -i "amovie='${sFile}',asplit=4[a1][a2][a3][out0];
             amovie='${sFile}':s=dv[v];
             [a1]showfreqs=s=250x100:mode=bar:cmode=separate:
                           colors=orange|red[chartf];
             [a2]showvolume=w=250:h=50:p=0.6:dm=2:dmc=red[chartv];
             [a3]showwaves=s=250x100:mode=cline:draw=full:
                           colors=yellow|orange:split_channels=1[chartw];
             color=color=0xFFAAFF22:s=vga[c];
             [v]scale=w=200:h=-2[art];
             [c][art]overlay=x=W-w-20:y=H-h-20,
             drawtext=x=round(W*t/${sDuration}):y=H-th-2:
                      fontfile='${sFont}':
                      fontsize=round(H/20):fontcolor=FF7777FF:
                      text='□',
             drawtext=x=10:y=15:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=20:fontfile='${sFont}':
                          text='${sTitle}',
             drawtext=x=300:y=60:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=15:fontfile='${sFont}':
                          text='Artist\: ${sArtist}',
             drawtext=x=300:y=80:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=15:fontfile='${sFont}':
                          text='Album\: ${sAlbum}',
             drawtext=x=300:y=100:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=15:fontfile='${sFont}':
                          text='Date\: ${sYear}',
             drawtext=x=300:y=120:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=15:fontfile='${sFont}':
                          text='Copyright\: ${sCopyright}',
             drawtext=x=300:y=140:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=15:fontfile='${sFont}':
                          text='Comment\: ${sComment}',
             drawtext=x=300:y=160:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=15:fontfile='${sFont}':
                          text='Duration\: ${sTime}',
             drawtext=x=300:y=180:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=15:fontfile='${sFont}':
                          text='Container\: ${sContainer}',
             drawtext=x=300:y=200:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=15:fontfile='${sFont}':
                          text='Codec\: ${sCodec}',
             drawtext=x=300:y=220:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=15:fontfile='${sFont}':
                          text='Channels\: ${sChannels} \(${sChannelLayout}\)',
             drawtext=x=300:y=240:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=15:fontfile='${sFont}':
                          text='Sample rate\: ${sSampleRate}',
             drawtext=x=300:y=260:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=15:fontfile='${sFont}':
                          text='Bitrate\: ${sBitRate} kbps'[bg];
             [bg][chartf]overlay=x=10:y=40[b1];
             [b1][chartv]overlay=x=10:y=170[b2];
             [b2][chartw]overlay=x=${sChartWLocationX}:y=300[out1]" \
         -autoexit -hide_banner  \
         -window_title "$sPlayerTitle « ffplayer »"
else
  ffplay -f lavfi -loglevel error \
         -i "amovie='${sFile}',asplit=4[a1][a2][a3][out0];
             [a1]showfreqs=s=250x100:mode=bar:cmode=separate:
                           colors=orange|red[chartf];
             [a2]showvolume=w=250:h=50:p=0.6:dm=2:dmc=red[chartv];
             [a3]showwaves=s=250x100:mode=cline:draw=full:
                           colors=yellow|orange:split_channels=1[chartw];
             color=color=0xFFAAFF22:s=vga,
             drawtext=x=round(W*t/${sDuration}):y=H-th-2:
                      fontfile='${sFont}':
                      fontsize=round(H/20):fontcolor=FF7777FF:
                      text='□',
             drawtext=x=10:y=15:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=20:fontfile='${sFont}':
                          text='${sTitle}',
             drawtext=x=300:y=60:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=15:fontfile='${sFont}':
                          text='Artist\: ${sArtist}',
             drawtext=x=300:y=80:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=15:fontfile='${sFont}':
                          text='Album\: ${sAlbum}',
             drawtext=x=300:y=100:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=15:fontfile='${sFont}':
                          text='Date\: ${sYear}',
             drawtext=x=300:y=120:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=15:fontfile='${sFont}':
                          text='Copyright\: ${sCopyright}',
             drawtext=x=300:y=140:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=15:fontfile='${sFont}':
                          text='Comment\: ${sComment}',
             drawtext=x=300:y=160:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=15:fontfile='${sFont}':
                          text='Duration\: ${sTime}',
             drawtext=x=300:y=180:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=15:fontfile='${sFont}':
                          text='Container\: ${sContainer}',
             drawtext=x=300:y=200:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=15:fontfile='${sFont}':
                          text='Codec\: ${sCodec}',
             drawtext=x=300:y=220:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=15:fontfile='${sFont}':
                          text='Channels\: ${sChannels} \(${sChannelLayout}\)',
             drawtext=x=300:y=240:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=15:fontfile='${sFont}':
                          text='Sample rate\: ${sSampleRate}',
             drawtext=x=300:y=260:fontcolor=white:alpha=0.9:shadowx=1:shadowy=2:
                          fontsize=15:fontfile='${sFont}':
                          text='Bitrate\: ${sBitRate} kbps'[bg];
             [bg][chartf]overlay=x=10:y=40[b1];
             [b1][chartv]overlay=x=10:y=170[b2];
             [b2][chartw]overlay=x=${sChartWLocationX}:y=300[out1]" \
         -autoexit -hide_banner  \
         -window_title "$sPlayerTitle « ffplayer »"
fi

if [ -f "$sImage" ]; then
  rm "$sImage"
fi

Save the above bash code as a text file named "audio-ffplayer.txt" preferably in your /opt directory and create a audio-ffplayer.desktop file (contents listed below) file in your /usr/share/applications so that it will show up as an available application in the "Open with" context menu when you right-click on audio files.

[Desktop Entry]
Name=Audio FFplayer
Comment=Play audio files using FFmpeg player
Exec=bash /opt/audio-ffplayer.txt %U
Terminal=false
Type=Application
MimeType=application/ogg;application/x-ogg;audio/aac;audio/flac;audio/midi;audio/mp3;audio/mp4;audio/mpeg;audio/ogg;audio/wav;audio/x-flac;audio/x-mp3;audio/x-mpeg;audio/x-ms-asx;audio/x-ms-wma;audio/x-vorbis+ogg;audio/x-wav;audio/x-wavpack;
Icon=/usr/share/icons/gnome/256x256/mimetypes/audio-x-generic.png
NoDisplay=true

Screenshot

Screenshot of audio player

Video demo

In this video, I encounter an online video my browser cannot display. I then copy the URL and run it with my video downloader script. The script downloads the audio portion of the video, converts it to MP3 (using ffmpeg of course), embeds the thumbnail and launches the MP3 with my audio player script.

Rumble

Before playing this video, click on the Autoplay button to prevent the annoying autoloading of related videos.

Bonus feature

This player can play video files too. They will play downsized in the bottom-right corner where the thumbnail is displayed for audio files. There will be a delay though. Generating a thumbnail (if present) for audio files is quick as the files are small. But for a video files, which are larger, it may take a while.


Become an FFmpeg PRO by reading my book Quick Start Guide to FFmpeg.

Book photo


UPDATE: The code for the audio player was updated to include the option -read_intervals %+60 for the ffprobe commands. Otherwise, the script would start with a delay on large video files.

Link: | Magic Link:

Comments are not enabled yet.

For older posts, check the archives.