DEV Community

Apiumhub
Apiumhub

Posted on • Originally published at apiumhub.com on

How to change audio encoding

Today I found myself in a somewhat annoying situation. After obtaining in a totally legal and lawful way a large collection of mp4 videos, I loaded them on my external hard drive, plugged the hard drive into my TV, hit play on the first video… and the audio was not recognized. Drama and despair, there is no possibility of finding these videos in another format, they only exist in this one, so I got down to work.

What is audio encoding?

If you, like me, are somewhat ignorant in these aspects of multimedia, I will briefly explain why my TV could not play the audio of these mp4 files.

An mp4 file can be explained as a package that compresses audio and video separately. Both video and audio have their own encoding, which in short is the way their data is stored and transmitted.

Therefore, if the player (my TV) does not have the appropriate codecs, it will not know how to read that encoding format. Codecs are the packages of algorithms that allow encoding, in this case, decoding, the video/audio data.

And if my TV does not have the codecs to read that particular audio encoding, what do I do? Well, change the encoding of the videos. And this is where FFmpeg comes in.

FFmpeg is an open-source project to manage multimedia files that allows you to see the detail of these with FFprobe, play them with FFplay, and also allows you to modify them with FFmpeg.

rruiz@rruiz:~/Videos$ ffprobe video1.mp4 
ffprobe version 4.2.7-0ubuntu0.1 Copyright (c) 2007-2022 the FFmpeg developers
  built with gcc 9 (Ubuntu 9.4.0-1ubuntu1~20.04.1)
  configuration: --prefix=/usr --extra-version=0ubuntu0.1 --toolchain=hardened --libdir=/usr/lib/x86_64-linux-gnu --incdir=/usr/include/x86_64-linux-gnu --arch=amd64 --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-avisynth --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-nvenc --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared
  libavutil 56. 31.100 / 56. 31.100
  libavcodec 58. 54.100 / 58. 54.100
  libavformat 58. 29.100 / 58. 29.100
  libavdevice 58. 8.100 / 58. 8.100
  libavfilter 7. 57.100 / 7. 57.100
  libavresample 4. 0. 0 / 4. 0. 0
  libswscale 5. 5.100 / 5. 5.100
  libswresample 3. 5.100 / 3. 5.100
  libpostproc 55. 5.100 / 55. 5.100
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'video1.mp4':
  Metadata:
    major_brand : isom
    minor_version : 512
    compatible_brands: isomiso2avc1mp41
    title : video 1 (1080p)
    artist : WinX HD Video Converter Deluxe
    encoder : Lavf58.29.100
  Duration: 00:24:39.56, start: 0.000000, bitrate: 3737 kb/s
    Stream #0:0(und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p, 1920x1080 [SAR 1:1 DAR 16:9], 3455 kb/s, 30 fps, 30 tbr, 15360 tbn, 60 tbc (default)
    Metadata:
      handler_name : VideoHandler
    Stream #0:1(cat): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 276 kb/s (default)
    Metadata:
      handler_name : SoundHandler

Enter fullscreen mode Exit fullscreen mode

Here we can see that stream #0:0(und) contains the video track in h264 encoding, which a quick search on the internet will tell us is a video encoding standard also known as AVC (advanced video encoding) or MPEG-4 Part 10. But this is not the part that interests us.

In the stream #0:1(cat) we find the audio track, with aac encoding (advanced audio encoding), this is the encoding that, for some reason, my TV does not know how to play.

It is not the first time that this happens to me, generally, it happens with mkv videos, a video format that does not have a standard of encodings as defined as mp4 and that causes more easily that the player does not have the appropriate codec. However, for some reason, either the aac version of these videos or any other, the audio of these mp4 videos is not compatible with my TV.

How can I change the video encoding?

If with FFprobe I can get the information from the video, with FFmpeg I can change it.

ffmpeg -i video1.mp4 -acodec mp3 -vcodec copy encoded_video1.mp4

Enter fullscreen mode Exit fullscreen mode

The documentation of FFmpeg is long and complete, but in our case, we only need to know a couple of options:

i: input, the video we want to modify, in this case, video1.mp4.

acodec: the audio codec we want the output file to have, in our case mp3.

vcodec: the video codec we want the output file to have, as we don’t want to change it, we put copy.

Finally, we put the name of the output file, in this case, encoded_video1.mp4.

Automate it

Great, I already know how to change the encoding of a file, but I have many videos. I can’t be typing the command one by one, what options do I have?

for loop

for filename in *.mp4; do 
    ffmpeg -i $filename -acodec mp3 -vcodec copy encoded_$filename;
done

Enter fullscreen mode Exit fullscreen mode

A for loop would be the first choice, but it has a few drawbacks, it will process the files one by one. My PC has 16 cores and FFmpeg by default uses only one thread. It is inefficient and slow for many medium-large files.

FFmpeg -threads

FFmpeg has the possibility to define a threadpool to be used in the processing:

ffmpeg -threads 4 -i video1.mp4 -acodec mp3 -vcodec copy encoded_video1.mp4

Enter fullscreen mode Exit fullscreen mode

-threads 0 implies that FFmpeg will choose the optimal amount of threads, with -threads 1 it will be monothread, and so on, as long as it can parallelize.

However, the amount of threads it will use really depends on the version of FFmpeg in question, the codec you are using, and the cores of your PC. It is not very consistent, and in my case even if I put 4 threads on it, it has never gone beyond 1.

Forks? Parallel!

If FFmpeg does not allow me to exploit the 16 cores of my PC, I will have to create the threads myself, and just when I was refreshing how to handle a thread pool in bash to process files in batches, I find parallel.

Paralell is a gnu/bash command that allows you to pass it a list of N arguments and a command. It will execute the command N times each time with one of the arguments by default in parallel with as many jobs as your PC has cores.

This way if your PC has 4 cores and you pass it 40 arguments, it will parallelize 4 of them, processing the next one when it has a free core. Great, just what I needed.

However, it allows you to indicate the maximum number of jobs you want it to use, so as not to saturate the PC, with -jobs. They also recommend using -ungroup for more efficiency, this allows you to paint the asap logs, resulting in logs from different commands mixed together.

ls *.mp4 | parallel --ungroup --jobs 6 echo

Enter fullscreen mode Exit fullscreen mode

In this example, the arguments that we pass to parallel are the list of mp4 files in the current folder, and the command that it will execute in parallel is… echo. It is not useful, but it is useful to see the syntax.

It is time to assemble a small script to change the encoding of the videos in a folder:

#!/bin/bash

encode() { ffmpeg -i $1 -acodec mp3 -vcodec copy encoded_$1; }

export -f encode

ls *.mp4 | parallel --ungroup --jobs 14 encode

Enter fullscreen mode Exit fullscreen mode

In this simple script, we define an encode function that contains the use of the FFmpeg command as explained above.

Then we export the function so that it can be used by parallel.

Finally, we call parallel with all the mp4s in the current folder, using a maximum of 14 simultaneous jobs (cores).

Now that we already have a working prototype, we have to improve it to be able to use it in different folders:

#!/bin/bash

DIRECTORY=$1
JOBS=14

encode() { ffmpeg -i $1 -acodec mp3 -vcodec copy encoded_$1; }

export -f encode

cd "$DIRECTORY"
ls *.mp4 | parallel --ungroup --jobs $JOBS encode

Enter fullscreen mode Exit fullscreen mode

We extract the source directory of the files from the $1 argument of the script and take the number of jobs out of a variable to make it more visible and easier to change.

A cd to the directory in question, and that’s it. The reason why the cd uses “” is so that it also works for directory names containing spaces.

Finally, we improve logging and video output:

#!/bin/bash

DIRECTORY=$1
JOBS=14
ENCODED_DIRECTORY=encoded

echo "changing audio encode to aac for mp4 files in directory $DIRECTORY, $JOBS files at a time, storing the newly encoded files in $DIRECTORY/$ENCODED_DIRECTORY"

encode() { echo "processing $2..."; ffmpeg -loglevel 0 -nostats -i $2 -acodec mp3 -vcodec copy $1/$2; }

export -f encode

cd "$DIRECTORY"

mkdir -p $ENCODED_DIRECTORY

ls *.mp4 | parallel --ungroup --jobs $JOBS encode $ENCODED_DIRECTORY
Enter fullscreen mode Exit fullscreen mode

The -loglevel 0 -nostats options of FFmpeg allow its execution with loglevel panic, in case something fails we will not see it unless it is a fatal error, but the normal execution will be faster and cleaner. We can always remove these options later to see what is failing or change the loglevel to fatal(8), error(16), warning(24), etc.

We will create an “encoded” subdirectory, specified in the ENCODED_DIRECTORY variable. We will pass this variable to the encode command inside the parallel so that the function receives it as the first argument, and we will use it in the function to save the output of FFmpeg in a subdirectory while keeping the name.

To understand it better, we can use the -dryrun option of parallel:

rruiz@rruiz:~/Videos$ ls *.mp4 | parallel --dryrun --jobs 14 encode "subdirectory"
encode subdirectory video1.mp4
encode subdirectory video2.mp4
encode subdirectory video3.mp4
encode subdirectory video4.mp4
Enter fullscreen mode Exit fullscreen mode

And that’s it, now we can change the encoding of mp4 files in whole directories by shooting:

rruiz@rruiz:~/Videos$ ./encode_mp4.sh directorio_1

Enter fullscreen mode Exit fullscreen mode

Top comments (0)