В предыдущем посте обрисовал идею с пакетной нормализацией громкости аудио/видео файлов.
Настала пора выложить реализацию этой идеи. Решение получилось гибкое и масштабируемое.
Для использования необходимо запастись знаниями матчасти звука и видео, мануалами по 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\pathbak=\\host\pathstmp=tmptmp1=tmptmp2=tmpotmp=tmpdestinationExtension=.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] -y
sox_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
Поделиться с друзьями