В предыдущем посте обрисовал идею с пакетной нормализацией громкости аудио/видео файлов.
Настала пора выложить реализацию этой идеи. Решение получилось гибкое и масштабируемое.
Для использования необходимо запастись знаниями матчасти звука и видео, мануалами по SoX — Sound eXchange, FFmpeg, BS1770GAIN, а так же моим любимым пакетом AutoIt.
На Windows Server, а так же некоторых других МелкоМягких платформах ffmpeg версии 3.1 и старше больше не работает.
Реализация состоит в следующем:
Довольно не сложный скрипт (autoit) слушает ini файл, секциями которого являются папки, которые нужно слушать. На каждую секцию открывается свой воркер (экземпляр скрипта), который считывает конфиг перед каждым прохдом по медиа файлу. При удалении секции, воркер закрывается. При закрытии главного воркера, все открытые воркеры закрываются.
Конфиг воркера имеет вид:
Вот, собственно, и весь нехитрый скрипт. Очень удобно. Папочки создает сам, файлы отслеживает, обрабатывает, складирует как надо, глючит очень редко, почти никогда. Память не жрет, ведет себя достойно ) Пользуйтесь на здоровье!
Настала пора выложить реализацию этой идеи. Решение получилось гибкое и масштабируемое.
Для использования необходимо запастись знаниями матчасти звука и видео, мануалами по SoX — Sound eXchange, FFmpeg, BS1770GAIN, а так же моим любимым пакетом AutoIt.
На Windows Server, а так же некоторых других МелкоМягких платформах ffmpeg версии 3.1 и старше больше не работает.
Реализация состоит в следующем:
Довольно не сложный скрипт (autoit) слушает ini файл, секциями которого являются папки, которые нужно слушать. На каждую секцию открывается свой воркер (экземпляр скрипта), который считывает конфиг перед каждым прохдом по медиа файлу. При удалении секции, воркер закрывается. При закрытии главного воркера, все открытые воркеры закрываются.
Конфиг воркера имеет вид:
[\\host\path\] destination=\\host\path\ bak=\\host\path\ stmp=tmp\ tmp1=tmp\ tmp2=tmp\ otmp=tmp\ destinationExtension=.avi threads=16 prepare_ffmpeg_cmd=-flags +ilme+ildct -deinterlace -c:v copy -c:a copy ffmpeg_cmd=-flags +ilme+ildct -deinterlace -c:v copy -c:a copy sox_cmd=compand 0.1,0.3 -90,-90,-70,-55,-50,-35,-31,-31,-21,-21,0,-20 0 0 0.1
Мануал:
Читать мануал
Куда складировать итоговые файлы.
Куда складировать исходные файлы.
У меня исходные файлы лежат в сетевой корзине, файлы бекапятся в ту же корзину, в отдельную папочку.
Подразумевается что на машине, которая выполняет обработку видеофайлов есть несколько физических носителей.
Например, у меня исходные файлы лежат в одной сетевой корзине, готовые файлы лежат во второй сетевой корзине, а обработку производит третья машина.
В третью машину я воткнул три жестких диска для временных файлов.
Файл с сетевой корзины прилетает на хард0
Если в конфиге присутствует значение sox_cmd, то аудиоданные считываются с хард2 на и результат обработки записывается хард1
При нормализации громкости аудиоданные считываются с хард2 или хард1 (sox_cmd?) и записываются на хард1
Исходный аудио поток видеофайла читается с хард0 на хард2
При сборке готового видеофайла видеопоток считывается с хард0 (stmp), аудиопоток считывется с хард1(tmp1), а результат записывается на хард2
Контейнер результирующего видеофайла
Записывает в командную строку сборки результирующего видеофайла threads=(threads) из конфига.
Если не пусто, то выполняется
Иначе, исходный файл копируется из bak в stmp
Считывается имя файла.
Если в конце имени файла присутствует конструкция вида {HH MM SS mm ss}, то начиная с кадра HH MM SS хвост файла будет приведен к хронометражу mm ss методом ускорения/замедления без искажения аудиодорожки.
Выполняется сборка результирующего файла командой
Перед нормализацией звука будет применен аудиофильтр
1) destination
Куда складировать итоговые файлы.
2) bak
Куда складировать исходные файлы.
У меня исходные файлы лежат в сетевой корзине, файлы бекапятся в ту же корзину, в отдельную папочку.
3) временные каталоги
Подразумевается что на машине, которая выполняет обработку видеофайлов есть несколько физических носителей.
Например, у меня исходные файлы лежат в одной сетевой корзине, готовые файлы лежат во второй сетевой корзине, а обработку производит третья машина.
В третью машину я воткнул три жестких диска для временных файлов.
stmp
$sFile = $stmp & $tempFile & $sExtension
Файл с сетевой корзины прилетает на хард0
tmp1
$audioInputSox = $tmp1 & $tempFile & "_sox.wav" $audioOutput = $tmp1 & $tempFile & "_norm.wav"
Если в конфиге присутствует значение sox_cmd, то аудиоданные считываются с хард2 на и результат обработки записывается хард1
При нормализации громкости аудиоданные считываются с хард2 или хард1 (sox_cmd?) и записываются на хард1
tmp2
$audioInput = $tmp2 & $tempFile & ".wav"
Исходный аудио поток видеофайла читается с хард0 на хард2
otmp
$outFile = $otmp & $tempFile & "_out" & $destinationExtension
При сборке готового видеофайла видеопоток считывается с хард0 (stmp), аудиопоток считывется с хард1(tmp1), а результат записывается на хард2
4) destinationExtension
Контейнер результирующего видеофайла
5) threads
Записывает в командную строку сборки результирующего видеофайла threads=(threads) из конфига.
6) prepare_ffmpeg_cmd
Если не пусто, то выполняется
ffmpeg -y -ss 0:0:0.0 -r 25 -i [bak] [prepare_ffmpeg_cmd] [stmp]Иначе, исходный файл копируется из bak в stmp
7) ffmpeg_cmd
Считывается имя файла.
Если в конце имени файла присутствует конструкция вида {HH MM SS mm ss}, то начиная с кадра HH MM SS хвост файла будет приведен к хронометражу mm ss методом ускорения/замедления без искажения аудиодорожки.
Выполняется сборка результирующего файла командой
ffmpeg -i [stmp(video)] -i [tmp1(normalized -23LUFS audio)] [ffmpeg_cmd] -map 0:v -map 1:a -threads [threads] [otmp] -ysox_cmd
Перед нормализацией звука будет применен аудиофильтр
sox [tmp2] [tmp1] [sox_cmd]Вот, собственно, и весь нехитрый скрипт. Очень удобно. Папочки создает сам, файлы отслеживает, обрабатывает, складирует как надо, глючит очень редко, почти никогда. Память не жрет, ведет себя достойно ) Пользуйтесь на здоровье!
Читать код скрипта
#Region ;**** Directives created by AutoIt3Wrapper_GUI **** #AutoIt3Wrapper_Icon=C:\Program Files (x86)\AutoIt3\Icons\MyAutoIt3_Blue.ico #AutoIt3Wrapper_Compile_Both=y #AutoIt3Wrapper_UseX64=y #EndRegion ;**** Directives created by AutoIt3Wrapper_GUI **** #include <Array.au3> #include <File.au3> Func randomString($digits) Local $pwd = "" Local $aSpace[3] For $i = 1 To $digits $aSpace[0] = Chr(Random(65, 90, 1)) ;A-Z $aSpace[1] = Chr(Random(97, 122, 1)) ;a-z $aSpace[2] = Chr(Random(48, 57, 1)) ;0-9 $pwd &= $aSpace[Random(0, 2, 1)] Next Return $pwd EndFunc $iniFile = "watch.ini" Dim $run[0][2] Dim $newRun[0] Func TerminateChilds() For $i = 0 to UBound($run) - 1 ProcessClose($run[$i][0]) Next EndFunc Local $source If $CmdLine[0] == 0 Then Local $i, $j, $exists, $pid OnAutoItExitRegister ( "TerminateChilds" ) While 1 $source = IniReadSectionNames($iniFile) For $i = 0 To UBound($run) - 1 $exists = False For $j = 1 To $source[0] If $source[$j] == $run[$i][1] Then $exists = True Next If Not $exists Then ProcessClose($run[$i][0]) _ArrayDelete($run, $i) ContinueLoop EndIf Next For $i = 1 To $source[0] $exists = False For $j = 0 To UBound($run) - 1 If $source[$i] == $run[$j][1] Then $exists = True Next If Not $exists Then $pid = Run(@ScriptName & " """ & $source[$i] & """") Dim $temp[1][2] = [[$pid, $source[$i]]] _ArrayAdd($run, $temp) ContinueLoop EndIf Next For $i = 0 To UBound($run) - 1 If ProcessExists($run[$i][0]) == 0 Then $pid = Run(@ScriptName & " """ & $run[$i][1] & """") $run[$i][0] = $pid ContinueLoop EndIf Next Sleep(1000) WEnd EndIf MsgBox($MB_SYSTEMMODAL, $CmdLine[1], "I am started " & @CRLF & $CmdLine[1], 10) Func Terminated() MsgBox($MB_SYSTEMMODAL, $CmdLine[1], "I am terminated " & @CRLF & $CmdLine[1], 10) EndFunc OnAutoItExitRegister ( "Terminated" ) TraySetToolTip($CmdLine[1]) $tools = "bs1770gain-tools\" Local $source = $CmdLine[1] Local $destination = IniRead($iniFile, $source, "destination", Null) Local $bak = IniRead($iniFile, $source, "bak", Null) Local $stmp = IniRead($iniFile, $source, "stmp", Null) Local $tmp1 = IniRead($iniFile, $source, "tmp1", Null) Local $tmp2 = IniRead($iniFile, $source, "tmp2", Null) Local $otmp = IniRead($iniFile, $source, "otmp", Null) Local $ffmpeg_cmd = IniRead($iniFile, $source, "ffmpeg_cmd", Null) Local $destinationExtension = IniRead($iniFile, $source, "destinationExtension", Null) Local $threads = IniRead($iniFile, $source, "threads", Null) Local $sox_cmd = IniRead($iniFile, $source, "sox_cmd", Null) If Not FileExists($source) Then DirCreate($source) If Not FileExists($bak) Then DirCreate($bak) If Not FileExists($destination) Then DirCreate($destination) If Not FileExists($stmp) Then DirCreate($stmp) If Not FileExists($tmp1) Then DirCreate($tmp1) If Not FileExists($tmp2) Then DirCreate($tmp2) If Not FileExists($otmp) Then DirCreate($otmp) Local $tempFile Local $sFile Local $descriptionFile Local $audioInput Local $audioOutput Local $outFile Local $sTitr Local $eTitr While 1 Local $files = _FileListToArray($source, "*", $FLTA_FILES, False) Local $i = 1 For $i = 1 To Ubound($files) - 1 Local $f = $files[$i] Local $sDrive = "", $sDir = "", $sFileName = "", $sExtension = "" Local $aPathSplit = _PathSplit($f, $sDrive, $sDir, $sFileName, $sExtension) Local $h = FileOpen($source & $sFileName & $sExtension, $FO_APPEND) If $h == -1 Then ContinueLoop FileClose($h) Sleep(50) Local $h = FileOpen($source & $sFileName & $sExtension, $FO_APPEND) If $h == -1 Then ContinueLoop FileClose($h) Sleep(50) Local $h = FileOpen($source & $sFileName & $sExtension, $FO_APPEND) If $h == -1 Then ContinueLoop FileClose($h) Sleep(50) Local $h = FileOpen($source & $sFileName & $sExtension, $FO_APPEND) If $h == -1 Then ContinueLoop FileClose($h) $bak = IniRead($iniFile, $source, "bak", Null) $destination = IniRead($iniFile, $source, "destination", Null) $stmp = IniRead($iniFile, $source, "stmp", Null) $tmp1 = IniRead($iniFile, $source, "tmp1", Null) $tmp2 = IniRead($iniFile, $source, "tmp2", Null) $otmp = IniRead($iniFile, $source, "otmp", Null) $ffmpeg_cmd = IniRead($iniFile, $source, "ffmpeg_cmd", Null) $destinationExtension = IniRead($iniFile, $source, "destinationExtension", Null) $threads = IniRead($iniFile, $source, "threads", Null) $sox_cmd = IniRead($iniFile, $source, "sox_cmd", Null) $pre_cmd = IniRead($iniFile, $source, "prepare_ffmpeg_cmd", Null) $tempFile = randomString(8) $bak &= $sFileName & $sExtension $sFile = $stmp & $tempFile & $sExtension $descriptionFile = $tmp1 & $tempFile & $sExtension & ".ini" $audioInput = $tmp2 & $tempFile & ".wav" $audioInputSox = $tmp1 & $tempFile & "_sox.wav" $audioOutput = $tmp1 & $tempFile & "_norm.wav" $outFile = $otmp & $tempFile & "_out" & $destinationExtension If FileMove($source & $sFileName & $sExtension, $bak, $FC_OVERWRITE) == 0 Then ContinueLoop If Not $pre_cmd Then If FileCopy($bak, $sFile, $FC_OVERWRITE) == 0 Then ContinueLoop Else $cmd_pre = $tools & "ffmpeg -y -ss 0:0:0.0 -r 25 -i """ & $bak & """ " & $pre_cmd & " " & $sFile RunWait($cmd_pre) EndIf Sleep(100) ;$log = FileOpen($tempFile & ".bat", $FO_OVERWRITE + $FO_UTF8 + $FO_CREATEPATH) $cmd_info = "cmd /c """ & $tools & "ffprobe -v quiet -print_format ini -show_format -show_streams " & $sFile & " > """ & $descriptionFile & """" ;FileWriteLine($log, $cmd_info) RunWait($cmd_info) $dur = Number(IniRead($descriptionFile, "streams.stream.0", "duration", Null)) $cmd_AudioInput = $tools & "ffmpeg -ss 0:0:0 -i " & $sFile & " -t " & $dur & " -vn -c:a pcm_s16le -af ""pan=stereo| FL < FL + 0.5*FC + 0.6*BL + 0.6*SL | FR < FR + 0.5*FC + 0.6*BR + 0.6*SR"" -ac 2 " & $audioInput & " -y -threads " & $threads ;FileWriteLine($log, $cmd_AudioInput) RunWait($cmd_AudioInput) Sleep(100) $audioOutput = "tmp\" & $tempFile & ".flac" If IsString($sox_cmd) And $sox_cmd <> "" Then $audioOutput = "tmp\" & $tempFile & "_sox.flac" $cmd_Sox = $tools & "sox " & $audioInput & " " & $audioInputSox & " " & $sox_cmd ;FileWriteLine($log, $cmd_Sox) RunWait($cmd_Sox) $audioInput = $audioInputSox EndIf $cmd_BS1770gain = "bs1770gain --ebu """ & $audioInput & """ -ao ""tmp""" ;FileWriteLine($log, $cmd_BS1770gain) RunWait($cmd_BS1770gain) Sleep(100) $a = StringRegExp($sFileName, "^.+{(\d{2}) (\d{2}) (\d{2}) (\d{2}) (\d{2})}$", $STR_REGEXPARRAYGLOBALMATCH) If @error Then $cmd_Output = $tools & "ffmpeg -i " & $sFile & " -i " & $audioOutput & " " & $ffmpeg_cmd & " -map 0:v -map 1:a -threads " & $threads & " " & $outFile & " -y" ;FileWriteLine($log, $cmd_Output) RunWait($cmd_Output) Else $titr_h = Number($a[0]) $titr_m = Number($a[1]) $titr_s = Number($a[2]) $dur_m = Number($a[3]) $dur_s = Number($a[4]) $dur = $dur - ($titr_h*60*60 + $titr_m*60 + $titr_s) $dstDur = $dur_m*60 + $dur_s $outDur = $titr_h*60*60 + $titr_m*60 + $titr_s + $dur_m*60 + $dur_s $speed = $dstDur / $dur $codec = IniRead($descriptionFile, "streams.stream.0", "codec_name", Null) $sTitr = $tmp1 & $tempFile & "_stitr" & $sExtension $eTitr = $tmp1 & $tempFile & "_etitr" & $sExtension $cmd_ETirt = $tools & "ffmpeg -y -ss " & $titr_h & ":" & $titr_m & ":" & $titr_s & " -i " & $sFile & " -filter:v ""setpts=" & $speed & "*PTS"" -t 00:" & $dur_m & ":" & $dur_s & " -c:v " & $codec & " -qscale:v 0 -flags +ilme+ildct -deinterlace -an " & $eTitr $cmd_STitr = $tools & "ffmpeg -y -ss 0:0:0 -i " & $sFile & " -t " & $titr_h & ":" & $titr_m & ":" & $titr_s & " -c:v copy -an " & $sTitr $cmd_Output = $tools & "ffmpeg -y -i concat:""" & $sTitr & "|" & $eTitr & """ -i " & $audioOutput & " -t " & $outDur & " " & $ffmpeg_cmd & " -map 0:v -map 1:a -threads " & $threads & " " & $outFile ;FileWriteLine($log, $cmd_ETirt) ;FileWriteLine($log, $cmd_STitr) ;FileWriteLine($log, $cmd_Output) RunWait($cmd_ETirt) RunWait($cmd_STitr) RunWait($cmd_Output) EndIf ;FileClose($log) FileMove($outFile, $destination & $sFileName & $destinationExtension, $FC_OVERWRITE) Sleep(100) FileDelete($sFile) FileDelete($descriptionFile) FileDelete($sTitr) FileDelete($eTitr) FileDelete($tmp2 & $tempFile & ".wav") FileDelete($tmp1 & $tempFile & "_sox.wav") FileDelete($tmp1 & $tempFile & "_norm.wav") FileDelete($audioOutput) ;Exit Next Sleep(1000) WEnd
