I have a couple of radio plays of various series. Some are already single-track, some are multi-track. I want all to be single-track. I don't mind re-encoding; in fact I want to transfer them to my mobile device and prefer opus output.

From inside a folder of a single audiobook, this seems to do the trick, converting mp3 to opus:

ffmpeg -i "concat:$(ls *.mp3 | tr '\n' '|')" -acodec opus test.opus

Now, I really have a lot of multi-track radio-plays that I want to convert. I'd like to define a function that I can use with either find or pipe the results from ls into.

I fumbled about with variants of this:

function audioconcat { folder=$1; iformat=$2; oformat=$3; echo $folder; echo $iformat; echo $oformat; ffmpeg -i \'concat:$(find "$folder" -name *.$iformat | tr '\n' '|' | tr ' ' '\ ' | head -c -1)\' -acodec $oformat \'$folder.$oformat\'; }

So the idea is to look for files of the given input format inside the folder, give them into ffmpeg concatenate, encode the stream to the given output format and save it as a single file, using the folder name.

However, I always seem to have problems with white spaces and/or my nested function calls.

What can I do to fix my function? Alternatively, what better method is there to do the conversion as described above?


You have demonstrated multiple anti-patterns in your code which could be improved. See Why you shouldn't parse the output of ls(1). You don't need to parse the output of ls command and avoid using multiple shell pipe-lines with tr command and find.

It is recommended better to use the glob options provided by the native shell which in your case should be the bash shell.

The piece of code $(ls *.mp3 | tr '\n' '|') could very well be written in bash with the options it provide for file globbing as

shopt -s nullglob

This extended shell option is enabled to ensure empty glob result is skipped instead of being processed when populating into the array. You should cd into the folder and do below. Note the final | after the array, since you originally had it in the list too. Remove it if its not needed.

fileString=$( IFS='|'; echo "${mp3FileList[*]}|" )

Now the above variable would contain the list of files in a | separated format with a | at the last which can then be passed to your ffmpeg command as

ffmpeg -i "concat:${fileString}" -acodec opus test.opus

Regarding your second requirement to pass multiple options to the script. You could extend this script to do

audioConcat() {
    (( "$#" < 3 )) && { printf 'insufficient arguments supplied' >&2; exit 1 ; }
    cd "$1" || { printf 'unable to navigate to target\n' >&2; exit 2 ; }

    shopt -q nullglob; nullglob_set=$?
    ((nullglob_set)) && shopt -s nullglob

    local fileList
    local fileString


    if (( ${#fileList[@]} )); then
        fileString=$( IFS='|'; echo "${fileList[*]}" )
        ffmpeg -i "concat:${fileString}" -acodec "$3" "$1.$3"  
        printf 'unable to find files of extension %s\n' "$2" >&2
        exit 3 

    ((nullglob_set)) && shopt -u nullglob      

Remember when calling the function, pass your arguments as

audioConcat '/path/to/mp3files/' 'mp3' 'opus'

Would highly recommend you to comment out the ffmpeg line in the above function and see if the variables are created as needed before calling the actual command. Also confirm if you need the trailing | in your file list.

Quick summary of the constructs used in the function

  1. The nullglob option set during pathname expansion would avoid expanding an empty glob, i.e. when no .mp3 files are found the array would be empty while expansion instead of an unexpanded glob
  2. $(IFS='|'; echo "${mp3FileList[*]}) is a neat trick to produce to print the output in | separated format. We are modifying the IFS in a sub-shell (the Input Field Separator) so it would not be modified globally. The array expansion with [*] would concatenate the string with the IFS value set.

Some misc. notes to consider:- Using exit from in the function would actually exit the current shell you are running the function. It might not be recommended when using from command-line and more suitable when ran from a script with a proper interpreter she-bang set, in which case it would exit from the sub-shell launched to run the script. If you plan to use from the command line more often, replace the exit calls with return.

  • Style comments: return from function rather than exit (exit would terminate the shell session). The function unsets nullglob unconditionally, regardless of what it was from the start. – Kusalananda Feb 26 '18 at 8:15
  • @Kusalananda: Really appreciate the feedback. for 1) assumed that the function would be used in a script, in which case using exit would return from the sub-shell (would add a note) 2) Did you mean to check if nullglob was already set before enabling it? (not quite sure) – Inian Feb 26 '18 at 8:19
  • 1) Ah, there's different views about how to deal with errors. I tend to signal an error condition and let the caller act on it without imposing an action. 2) Yes, exactly. That is, if one wants to be nice and not affect the calling script that may be using nullglob for doing other things. – Kusalananda Feb 26 '18 at 8:25
  • 1
    Thank you very much for the answer. I appreciate you both pointing out the improvements to my fumbled attempts and a working solution. I added an "cd .." statement at the end such that I may run the script on multiple folders from a bash loop. This works like a charm (after finding out how spaced folder names turn out nicely in for loops). – PiHalbe Feb 26 '18 at 8:31
  • 1
    @PiHalbe Ideally, the calling script/shell would change the directory. Doing cd .. in the function may lead to unexpected results in some circumstances. My comments above, and this one, has to do with side effects of the function. It should have as few side effects as possible (IMHO). – Kusalananda Feb 26 '18 at 8:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.