DOS batch script to perform volume normalization + conversion to MP3 (using ffmpeg)
-
This was enough of an endeavor that I figured I'd share it so maybe someone else might also benefit from it.
teh codez
@echo off setlocal enabledelayedexpansion ::by default, this batch file needs to be located in the same folder as ffmpeg.exe (typically the bin folder) ::otherwise, specify its full path, so that the batch file knows where the ffmpeg.exe binary is located: set "ffmpeg=%~dp0ffmpeg.exe" ::set the audio bitrate for output files (192 kbps)... if you want a different quality, you can change this value set "ab=192k" ::set the options to write ID3v1 and ID3v2.3 metadata, and to include album art in JPEG format set "metadata_opts=-id3v2_version 3 -write_id3v1 1 -vcodec mjpeg -q:v 3 -huffman optimal" if [%1] == [] echo Nothing to do. Did you forget something? & pause & exit /b :start echo %1 ::by default, the destination file will have the same drive + path + name as the source (~dpn), but a .mp3 extension ::if the source file already has a .mp3 extension, however, then " (normalized)" will also be added to the name ::you can change these, but note that the .mp3 extension tells ffmpeg to encode the file as .mp3, so don't omit it set "output_file=%~dpn1.mp3" if /i "%~x1"==".mp3" set "output_file=%~dpn1 (normalized).mp3" ::if you'd like to instead place the output files in a "Normalized" subfolder, uncomment this line: ::set "output_file=%~dp1Normalized\%~n1.mp3" ::if the destination file already exists, it will be skipped... a message is printed, but the script doesn't pause if exist "!output_file!" echo "!output_file!" already exists. & goto continue if not exist "!output_file!\.." mkdir "!output_file!\.." if not exist "!output_file!\.." echo Unable to find or create the output location. & pause & goto continue ::to use the (significantly faster) volume filter, instead of loudnorm, uncomment out the following "goto volume" line ::goto volume ::volume produces the same audio you'd get by using Audacity's amplify filter for max amplification without clipping ::loudnorm implements the EBU R128 loudness normalization algorithm ::note that the volume filter should work on most versions, but loudnorm may require a fairly recent version of ffmpeg :loudnorm ::first pass - run ffmpeg and get stats, then loop through its output (stdout+stderr) line by line to parse the info set "loudnorm_opts=loudnorm=I=-16:dual_mono=true:TP=-1.5:LRA=11:print_format=summary" for /f "delims=" %%i in ('call "!ffmpeg!" -i %1 -filter:a !loudnorm_opts! -f null nul 2^>^&1') do ( set "l=%%i" if "!l:~0,17!" == "Input Integrated:" set "i_i=!l:~20,6!" if "!l:~0,16!" == "Input True Peak:" set "i_tp=!l:~20,6!" if "!l:~0,10!" == "Input LRA:" set "i_lra=!l:~20,6!" if "!l:~0,16!" == "Input Threshold:" set "i_thr=!l:~20,6!" if "!l:~0,14!" == "Target Offset:" set "t_o=!l:~20,6!" ) ::second pass - pass values obtained from the first pass back in again as arguments for loudnorm using a linear filter set "audio_filter=measured_I=!i_i!:measured_TP=!i_tp!:measured_LRA=!i_lra!:measured_thresh=!i_thr!:offset=!t_o!" set "audio_filter=loudnorm=I=-16:TP=-1.5:LRA=11:!audio_filter: =!:linear=true" goto second_pass :volume ::first pass - run ffmpeg with the volumedetect filter to analyze the audio and get the max volume (in decibels) for /f "delims=" %%i in ('call "!ffmpeg!" -i %1 -filter:a volumedetect -f null nul 2^>^&1') do ( set "l=%%i" if "!l:~35,10!" == "max_volume" set "max_volume=!l:~47!" ) ::negate max_volume to get the maximum amplification that can be applied to the audio without causing clipping set "max_volume=!max_volume: dB=!" if not "!max_volume:~0,1!" == "-" set "amplify=-!max_volume!" if "!max_volume:~0,1!" == "-" set "amplify=!max_volume:-=!" ::second pass - pass the negative of the value of max_volume back in as the amplification value for the volume filter set "audio_filter="volume=!amplify!dB"" goto second_pass :second_pass echo ==^> "!output_file!" ::the ffmpeg output is suppressed... if you'd like to see it, remove ">nul 2>&1" from the command line "!ffmpeg!" -n -i %1 !metadata_opts! -filter:a !audio_filter! -ab !ab! "!output_file!" >nul 2>&1 :continue ::shift the batch file's arguments and then see if there's anything left for us to do shift if not [%1] == [] goto start
It should be pretty well internally documented. There are a handful of things that you can change if you want, but it should work as written if you save it as a
.bat
file in the same folder whereffmpeg.exe
is located. Drag-and-drop files onto it, and it will perform volume normalization and create MP3 files in the same location as the source files.I've included two different loudness filters, one that uses ffmpeg's
loudnorm
filter (EBU R128 loudness normalization), and one that uses thevolume
filter. Note that the latter is significantly quicker. The default isloudnorm
(there's a commented-outgoto
in the batch script that can be un-commented to usevolume
).Metadata and album artwork in original files should be preserved in the output... album art will be re-encoded in JPEG format (with the
mjpeg
codec) at a fairly high quality setting.If you have a bunch of audio files to process, running one instance may not utilize as much CPU (and therefore not be as fast) as running several instances in parallel.
It doesn't like filenames with
!
or^
characters (and there's no error handling for this)... other than that, it should handle pretty much anything, I think, but don't push it too hard...There is probably some limit to how many files can be dropped onto the batch script at one time... this limitation would be due to the way that Windows passes the filenames to the script. It could be modified to process all of the MP3 files in a folder, instead, but
Line 26, if un-commented, will change it so instead of placing the output files in the same folder as the source files, a
Normalized
subfolder will be created (if necessary) and the output files will be placed there instead. This will make it unnecessary for it to add(normalized)
to make the output filenames unique. Just remove::
from the beginning of this line:::set "output_file=%~dp1Normalized\%~n1.mp3"
Finally, the version of ffmpeg that I've used was
ffmpeg-4.1-win32-static
. If you have problems, try either upgrading your version or switching to the oldervolume
filter, and then it should probably work.
-
Disappointing. I was actually hoping you were running this on DOS. I was going to ask "why DOS?".
-
@mikehurley Well, I guess technically I'd have to say that it runs in a Windows command prompt. I'd still refer to it as a DOS batch script, though, because that's where the batch scripting language originated.
I'm not sure how far backward portable it'd be, though. It probably wouldn't even run on DOS.
-
@anotherusername If you stubbed the dependencies I don't see why not, from what I remember of writing DOS batch scripts with menus 'n' shit back in the day.
I still think it's wrong that they just won't let everyone use Unix, but oh well. When it becomes available in your universe maybe check it out, the scripting is less horrific
-
@Gribnit said in DOS batch script to perform volume normalization + conversion to MP3 (using ffmpeg):
@anotherusername If you stubbed the dependencies I don't see why not, from what I remember of writing DOS batch scripts with menus 'n' shit back in the day.
I still think it's wrong that they just won't let everyone use Unix, but oh well. When it becomes available in your universe maybe check it out, the scripting is less horrificOr just use powershell. That's what I use for all my handy-to-have scripts at home.
-
@Gribnit The main things I'm not sure would port backward are:
for /f
which executes a command, captures its output stream, tokenizes it by delimiters, and iterates through the output tokens- I don't know how much of the string/filename variable manipulation was supported back in the day
Without the former, the output would probably need to be redirected to a temporary file, and then it might be possible to parse the file... as far as the second, working around it would depend on what exactly worked and what didn't.
-
I'd only suggest switching to VBR (in ffmpeg, if you want a ~192kbps target, it's -q:a=2, where 0 is maximum quality, practically indistinguishable from 320kbps CBR), unless the target is some old device (which will read the file correctly but might display time incorrectly, and in some cases, may skip silence - I have listening tests for my students that I have to manually pause during the 45 seconds or so of time given to read through the test because our devices just skip through them). Keep in mind that loudnorm does apply some dynamic audio compression (it's practically "transparent" but there is some) and, even though MP3 is not an archival format, it may provide undesirable results.
-
@admiral_p Thanks, I'll try that. It's
-q:a 2
though. It didn't like-q:a=2
at all.
-
It's interesting how folks solve the same problems. I've been using MediaMonkey as my library-based music player for many years. It includes a flexible format conversion and normalization module to set up presets, per-device settings, and so on. I can just plug in my phone, right-click a playlist (or whatever) and pick "Sync to (phone name)".
I do use a VBScript to add album and track Replay Gain tags to my ripped CDs. It's not all that interesting (one call to metaflac in the guts of a "loop through all the subfolders" script) and isn't well commented, but FWIW:
Add Replay Gain to All.vbs
Option Explicit Const scriptName = "Add Replay Gain To All" Const scriptVer = "1.1.0" Const errNeedPath = "To use this script, drop a folder onto the script icon." Dim gShellObj Set gShellObj = WScript.CreateObject("WScript.Shell") Dim gFolderCount gFolderCount = 0 Main ' Customize this to do whatever you need it to do. Function DoTheWork(ByRef folderObj) Dim returnMe returnMe = True Dim filePaths filePaths = "" Dim fileObj, fileCollection Set fileCollection = folderObj.Files For Each fileObj in fileCollection If (StrComp(Right(fileObj.Name, 5), ".flac", 1) = 0) Then ' Do the thing (in this case, collect the paths for later) filePaths = filePaths & " " & fileObj.ShortPath End If Next If (Len(filePaths) > 0) Then 'WScript.Echo("metaflac --add-replay-gain " & filePaths) Call gShellObj.Run("F:\Utils\flac\win32\metaflac --add-replay-gain " & filePaths, 1, True) gFolderCount = gFolderCount + 1 End If DoTheWork = returnMe End Function ' Customize this to report something to the user. ' The default result is the top-level boolean from LoopWithin. Sub ReportResults(ByVal result) If (result) Then Call MsgBox("Updated " & CStr(gFolderCount) & " album" & Pluralize(gFolderCount) & ".", 0, scriptName) Else Call MsgBox("An error occurred while doing the work.", 0, scriptName) End If End Sub Function LoopWithin(ByRef folderObj, ByVal lookInSubdirs) Dim returnMe returnMe = True If returnMe And lookInSubdirs Then Dim subObj, subCollection Set subCollection = folderObj.SubFolders For Each subObj in subCollection returnMe = LoopWithin(subObj, lookInSubdirs) Next End If If returnMe then returnMe = DoTheWork(folderObj) End If LoopWithin = returnMe End Function Function Pluralize(ByVal count) If (count <> 1) Then Pluralize = "s" Else Pluralize = "" End If End Function Sub Main Dim i, args, continueOn continueOn = True Set args = WScript.Arguments If (args.Count <> 1) Then WScript.Echo(errNeedPath) continueOn = False End If If continueOn Then Dim fso, folderObj Set fso = CreateObject("Scripting.FileSystemObject") Set folderObj = fso.GetFolder(args(0)) ReportResults(LoopWithin(folderObj, True)) End If End Sub