''''normalize.tcl'''' is a simple TCL script that drives 'ffmpeg' to normalise audio levels for a group of mp3 files. For each mp3 file found into a directory, it reads the mean volume level, calculates the average level among various files and adjusts the volume level of each of them. ====== #!/bin/sh #\ exec tclsh "$0" ${1+"$@"} # Copyright 2015 Tholis Biroi (tholis DOT biroi AT yahoo DOT it) # # This file is part of 'normalize.tcl'. # 'normalize.tcl' is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # 'normalize.tcl' is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # # See the GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with 'normalize.tcl'. If not, see http://www.gnu.org/licenses/. # # # # 'normalize.tcl' is a simple TCL script that drives 'ffmpeg' to normalise # audio levels for a group of mp3 files. # For each mp3 file found into a directory, it reads the mean volume level, # calculates the average level among various files and adjusts the volume # level of each of them. # # Global debugging variable # To override this option set '-d' on the command line set debugOn false # log -- # # puts "" wrapper # proc log {label args} { global debugOn switch $label { "info" { puts {*}$args } "error" { puts "error: [join {*}$args]" } "warning" { puts "warning: [join {*}$args]" } "debug" { if {$debugOn == "true"} { puts "debug: [join {*}$args]" } } default { # do nothing in any case } } } # get_volumes -- # # Exec 'ffmpeg' in order to get the volume mean level # # proc get_volume {mp3} { if {$mp3 == {}} { log error "Empty file name." return {} } # Set volume variable set volume {} # Set 'ffmpeg' command set cmd "ffmpeg -i \"$mp3\" -af \"volumedetect\" -f null /dev/null" log debug "'ffmpeg' cmd= $cmd" # Exec 'ffmpeg' if {[catch {eval exec -ignorestderr $cmd 2>@1} out]} { log error "'ffmpeg' execution command failed." log debug "reason= $out" return {} } # In order to avoid 'case sensitive' parsing, the output of the # command is converted to uppercase set Out [string toupper $out] log debug "'ffmpeg' out= $Out" # Now scan the out a line at time searching for 'MEAN_VOLUME:' # output string label set lines [split $Out "\n"] foreach line $lines { log debug "$line" # first of all search for 'VOLUMEDETECT' string and if foud # search for 'MEAN_VOLUME:' string. if {[string first VOLUMEDETECT $line] == -1} { # Not found, skip line parsing continue } # 'VOLUMEDETECT' string found, search for 'MEAN_VOLUME' string set pos [string first MEAN_VOLUME $line] if { $pos != -1} { set start [expr {$pos + 11}] set volStr [string range $line $start end] log debug "volStr= $volStr" # Extract and trim the first word as volume set words [split $volStr] log debug "words= $words" set volume [string trim [lindex $words 1]] log debug "volume= $volume" } } return $volume } ;# end get_volume # set_volume -- # # Exec 'ffmpeg' to re-encode the mp3 # proc set_volume {mp3 actualVol targetVol} { if {($mp3 == {}) || ($actualVol == {}) || ($actualVol == {})} { log error "One or more parameter are empty" return {} } # Create filename output set mp3Root [file rootname $mp3] set mp3OutFile "${mp3Root}.norm.mp3" # If normalized file already exists, will be deleted if {[file exists $mp3OutFile]} { catch {file delete -force -- $mp3OutFile} } # calculate the delta volume set deltaVol [expr {$targetVol - $actualVol}] # Set 'ffmpeg' command set cmd "ffmpeg -y -i \"$mp3\" -af \"volume=${deltaVol}dB\" \"$mp3OutFile\"" # Exec 'ffmpeg' if {[catch {eval exec -ignorestderr $cmd 2>@1} out]} { log error "'ffmpeg' execution command failed." log debug "reason= $out" return {} } # For debug purposes set Out [string toupper $out] log debug "'ffmpeg' out= $Out" return $deltaVol } ;# end set_volume # byebye -- # proc byebye {} { log info "" log info "Bye!" log info "" } ;# end byebye # print_help -- # # Prints a little help # proc print_help {} { global argv0 log info "" log info "Usage: $argv0 \[-h|--help\]|" log info "" log info "-h|--help Print thi help" log info " Directory path containing mp3 to normalize" log info "" byebye return } ;# end print_help # Main ----------------------------------------------------------------- log info "" log info "MP3 normalizer v0.9 21 mar 2015" log info "" log info "" # Save current dir set currDir [pwd] log debug "Working dir= $currDir" # Control input parameters to setup working dir # If no parameter is passed a little help is printed on screen if {$argc == 0} { print_help exit 0 } # If more than one parameter is passed if {$argc != 1} { log error "Wrong number of arguments." log error "Use '-h' or '--help' option to print usage info." byebye exit 1 } # If only one paramter is passed, it could be the help option or the # desired working path if {([lindex $argv 0] == "-h") || ([lindex $argv 0] == "--help")} { print_help exit 0 } # Save the passed workDir in order to make some controls set workDir [lindex $argv 0] # The path passed must be a directory path if { ![file isdirectory $workDir] } { log error "The argument passed is not a valid directory path" byebye exit 1 } # The argument passed must be an existing directory if { ![file exists $workDir] } { log error "Directory '$workDir' does not exists" byebye exit 1 } # Move on working dir cd $workDir # Get the list of files in the current directory set mp3Files [glob -nocomplain *.mp3] if {$mp3Files == {}} { log info "No .mp3 files found on working dir: '$workDir'" byebye exit 1 } # Exclude from this list files with exetension *.norm.mp3" set mp3FileList {} foreach mp3 $mp3Files { set rootFname [file rootname $mp3] set ext [file extension $rootFname] if {$ext == ".norm"} { # Skip already normalized files from mp3 list continue } lappend mp3FileList $mp3 } # Init the mp3 array #set mp3Ar {} log info "List of file mp3 to be normalized:" # Foreach *.mp3 file foreach mp3 $mp3FileList { log info " '$mp3'" # Extract volumes set vol [get_volume $mp3] if {$vol == {}} { log warning "No volume information found for file: $mp3" } else { # Fill the array of volumes set mp3Ar($mp3) $vol } } log info "" # parray only for debugging #parray mp3Ar # Calculating the average volume set avgVol 0 set mp3List [array names mp3Ar] set numFiles [llength $mp3List] foreach mp3 $mp3List { set avgVol [expr {$mp3Ar($mp3) + $avgVol}] } set avgVolume [expr {$avgVol/double($numFiles)}] set avgVol [format "%0.1f" $avgVolume] log info "Avg Volume= $avgVol" log info "" # Now foreach file calculate delta volume to normalize it log info "File normalization at $avgVol dB" foreach mp3 $mp3List { log info " '$mp3' from $mp3Ar($mp3) to $avgVol" if {[set_volume $mp3 $mp3Ar($mp3) $avgVol] == {}} { log info "warning: Set volume failed for file '$mp3'" } } log info "" log info "Done." # Before exit return to run dir cd $currDir byebye exit 0 ====== <>AUDIO