Converting WMA to MP3 in bulk with FFmpeg
My dad asked me to convert his music in bulk to MP3, for better compatibility with all of his devices. He had previously ripped his CD collection using Window Media Player, which produces Window Media Audio (WMA) files by default.
I thought this should be relatively simple in Linux using FFmpeg, which I was aware of but had never used directly. I found many, many, example scripts on the Web but none that I both understood and would run without errors. After a few hours (days) of reading man pages, learning about Bash and tinkering, I had a working script that I was happy with and understood.
Appeal to reason
Running scripts from the Internet that you don’t understand is a recipe for disaster. This is the complete script below that I used to convert the files; you use it at your own risk. I hope it serves as a useful example.
#!/bin/bash
dir=/home/alasdair/music
find "$dir" -type f -iname "*.wma" -print0 | \
while IFS= read -r -d '' ifile; do
ofile="${ifile/%.[wW][mM][aA]}".mp3
if [ ! -f "$ofile" ]
then
ffmpeg -i "$ifile" -aq 5 "$ofile" < /dev/null
fi
done
I’ll go through it line by line and explain what is happening.
Finding the WMA files
I have assigned the path where the music is stored to a variable called dir
. It’s convenient to have the path in a single place, at the top, where I can easily find and change it.
dir=/home/alasdair/music
I'm using the find
command to find and list all of the WMA files in the music directory and its subdirectories (artsit, album etc).
find "$dir" -type f -iname "*.wma" -print0 | \
I have used the following parameters:
"$dir"
is the variable storing the path to be searched. It is double quoted as it might contain spaces amongst other characters.-type f
to search only for files; not folders for example.-iname "*.wma"
for any file names ending with .wma. I have used-iname
rather than-name
as I want the matching to be case insensitive. I have double quoted the search pattern as the shell would otherwise expand the asterisk itself rather than passing it through to the find command.-print0
to print the full file names separated by a null character. File names and paths can contain new lines and other characters but not null characters, so they make an ideal separator.
The pipe (vertical bar) character takes the output from this find
command and uses it as the input for the next command. The final backslash means that this line is to be continued on the next line.
Looping through each file found
I now want to
while IFS= read -r -d '' ifile; do
…
done
I’m using the read
command to read the list of files from standard input, in this case piped from the output of find
.
-r
to not treat backslashes as escape characters: ‘raw’ input.-d ''
to use a null character as the delimiter, rather than a newline.
Each file path will be stored in the variable ifile
, code between do
and done
will be run and then the loop will repeated for each file.
Naming the output file
I am saving each MP3 file to the same folder as the original WMA file and with the same name, except that the .wma extension will be replaced with .mp3. I'll explain the evolution of this line in stages.
The output file variable could be assigned the same value as the input file like this. Note the use of double quotes again.
ofile="$ifile"
There is also an alternate curly bracket syntax that can be used.
ofile="${file}"
Changing the file extension
This advantage of the latter syntax is that it allows us to specify a pattern in the variable that will be found and replaced. The pattern is given after the first slash and the replacement after the second slash. In this case to replace .wmv with .mp3.
ofile="${ifile/%.wma/.mp3}"
Think through the consequences
However if the file did not have a .wma extension then it would not be replaced by .mp3: the original file name would be used as-is. This could lead to a confusing situation where the original file is overwritten and now has the wrong file extension.
For this reason I have not provided a replacement and the .wma extension will removed (replaced with nothing) if found. I then add the .mp3 extension at the end, regardless of any replacement that has or has not taken place.
ofile="${ifile/%.wma}".mp3
Finished at LAST?
We're getting closer now but there is still a problem. The pattern matching is case sensitive and so a file extension of .WMA would not be matched. This is exactly the sort of situation that would lead to the original file being overwritten as disscussed previously.
I could convert the file names to upper or lower case but as that can be avoided, I don't find it an acceptable solution. I have rewritten it like this instead. The square brackets mean that any of the enclosed characters, in this instance both uppercase and lowercase versions, will match.
ofile="${ifile/%.[wW][mM][aA]}.mp3"
Checking if the file already exists
I don’t want to overwrite an existing files, so I am going to check that the output file doesn't already exist first.
if [ ! -f "$ofile" ]
then
…
fi
Converting the file to MP3
Both the input and output file variables are double quoted as they may contain spaces amongst other characters.
ffmpeg -i "$ifile" -aq 5 "$ofile" < /dev/null
-i "$ifile"
is the input file (WMA).-aq 5
sets the quality to level 5 dynamic bitrate."$ofile"
is the output file (MP3).< /dev/null
redirects standard input to null/nothing.
Comments
Sean
Thank you very much for this script. I used it today to convert 1000s of wma to mp3, and it worked a treat.
Harley
Fantastic script. Thanks for the walk through. Very clear, and it worked perfectly on my music library. Now I can listen to any of my tracks on my mopidy music player.