Huge rework of this script
- Implement different conversions for lossless and lossy sources - Remove unused functions - Cleanup comments I noticed that FLAC --> FLAC normalisation will change the bit depth from 16 to 24. Wasn't able to find a simple solution for it, so I asked on the ffmpeg-user mailing list https://ffmpeg.org/pipermail/ffmpeg-user/2024-March/057774.html
This commit is contained in:
parent
ca183a436a
commit
78ee74ed9d
@ -32,80 +32,32 @@ from typing import Any, Optional
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# FIXME
|
|
||||||
# inputfile = (
|
|
||||||
# '/home/marc/Downloads/FalKKonE - 01 Aria (From "Berserk: The Golden Age Arc").flac'
|
|
||||||
# )
|
|
||||||
# inputfile = "/home/marc/Downloads/test441.opus"
|
|
||||||
# outputfile = "/home/marc/Downloads/test441_out.opus"
|
|
||||||
|
|
||||||
# srcfolder = "/home/marc/Downloads/MusikRaw"
|
|
||||||
# destfolder = "/home/marc/Downloads/Musik"
|
|
||||||
|
|
||||||
musicfile_extensions = (".flac", ".wav", ".mp3", ".m4a", ".aac", ".opus")
|
musicfile_extensions = (".flac", ".wav", ".mp3", ".m4a", ".aac", ".opus")
|
||||||
|
|
||||||
|
|
||||||
def get_format(inputfile) -> str:
|
def loudness_info(inputfile) -> dict[str, str]:
|
||||||
# get codec format
|
|
||||||
# https://stackoverflow.com/a/29610897
|
|
||||||
# this shows the codecs of all audio streams present in the file, which shouldn't matter unless you have more than one stream
|
|
||||||
ff = ffmpy.FFprobe(
|
|
||||||
inputs={inputfile: None},
|
|
||||||
global_options=(
|
|
||||||
"-v quiet",
|
|
||||||
"-select_streams a",
|
|
||||||
"-show_entries stream=codec_name",
|
|
||||||
"-of default=noprint_wrappers=1:nokey=1",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
# print(ff.cmd)
|
|
||||||
proc = subprocess.Popen(ff.cmd, shell=True, stdout=subprocess.PIPE)
|
|
||||||
# NOTE read output from previous command
|
|
||||||
# rstrip: remove trailing newline
|
|
||||||
# decode: convert from binary string to normal string
|
|
||||||
format: str = (
|
|
||||||
proc.stdout.read() # pyright: ignore[reportOptionalMemberAccess]
|
|
||||||
.rstrip()
|
|
||||||
.decode("utf8")
|
|
||||||
)
|
|
||||||
# print(format)
|
|
||||||
return format
|
|
||||||
|
|
||||||
|
|
||||||
def remove_picture(inputfile):
|
|
||||||
"""
|
"""
|
||||||
This function makes sure no image is attached to the audio stream.
|
Measure loudness of the given input file
|
||||||
An image might cause problems for the later conversion to opus.
|
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
inputfile (str): Path to file
|
inputfile
|
||||||
|
|
||||||
|
Output:
|
||||||
|
loudness (dict[str, str]): decoded json dictionary containing all loudness information
|
||||||
"""
|
"""
|
||||||
tmpfile = os.path.splitext(inputfile)[0] + ".tmp" + os.path.splitext(inputfile)[1]
|
|
||||||
ff = ffmpy.FFmpeg(
|
|
||||||
inputs={inputfile: None},
|
|
||||||
outputs={tmpfile: "-vn -c:a copy"},
|
|
||||||
global_options=("-v error"),
|
|
||||||
)
|
|
||||||
ff.run()
|
|
||||||
os.remove(inputfile)
|
|
||||||
os.rename(tmpfile, inputfile)
|
|
||||||
|
|
||||||
|
|
||||||
def loudness_info(inputfile) -> dict[str, str]:
|
|
||||||
print("Measuring loudness of ", os.path.basename(inputfile))
|
print("Measuring loudness of ", os.path.basename(inputfile))
|
||||||
# get format from file
|
|
||||||
# inputformat = get_format(inputfile)
|
|
||||||
# NOTE format is actually unnecessary here
|
|
||||||
ff = ffmpy.FFmpeg(
|
ff = ffmpy.FFmpeg(
|
||||||
inputs={inputfile: None},
|
inputs={inputfile: None},
|
||||||
outputs={"/dev/null": "-pass 1 -filter:a loudnorm=print_format=json -f null"},
|
outputs={"/dev/null": "-pass 1 -filter:a loudnorm=print_format=json -f null"},
|
||||||
global_options=("-y"),
|
global_options=("-y"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# print(ff.cmd)
|
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
ff.cmd, shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE
|
ff.cmd, shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE
|
||||||
)
|
)
|
||||||
|
|
||||||
# NOTE get loudness info from subprocess
|
# NOTE get loudness info from subprocess
|
||||||
# rstrip: remove trailing newline
|
# rstrip: remove trailing newline
|
||||||
# decode: convert from binary string to utf8
|
# decode: convert from binary string to utf8
|
||||||
@ -116,17 +68,35 @@ def loudness_info(inputfile) -> dict[str, str]:
|
|||||||
)
|
)
|
||||||
# decode json to dict
|
# decode json to dict
|
||||||
loudness: dict[str, str] = json.loads(loudness_json)
|
loudness: dict[str, str] = json.loads(loudness_json)
|
||||||
# print(loudness_json)
|
|
||||||
# print(ff.cmd)
|
|
||||||
return loudness
|
return loudness
|
||||||
|
|
||||||
|
|
||||||
def convert(inputfile, outputfile, loudness) -> Optional[list[Any]]:
|
def convert(
|
||||||
|
inputfile: str,
|
||||||
|
outputfile: str,
|
||||||
|
codec: str,
|
||||||
|
compression: int,
|
||||||
|
loudness: dict[str, str],
|
||||||
|
bitrate: str = "0k",
|
||||||
|
) -> Optional[list[Any]]:
|
||||||
|
"""
|
||||||
|
Convert the input file to the desired format
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
inputfile (str)
|
||||||
|
outputfile (str)
|
||||||
|
loudness (dict[str, str])
|
||||||
|
|
||||||
|
Output:
|
||||||
|
dynamically normalised files (list)
|
||||||
|
"""
|
||||||
|
|
||||||
print("Working on ", os.path.basename(inputfile))
|
print("Working on ", os.path.basename(inputfile))
|
||||||
# coverpath = os.path.join(os.path.dirname(inputfile), "cover.jpg")
|
|
||||||
# NOTE including covers into ogg/opus containers currently doesn't work
|
# NOTE including covers into ogg/opus containers currently doesn't work
|
||||||
# https://trac.ffmpeg.org/ticket/4448
|
# https://trac.ffmpeg.org/ticket/4448
|
||||||
inputcmd = {inputfile: None}
|
inputcmd = {inputfile: None}
|
||||||
|
# NOTE bitrate is set to 0k when converting to flac. This does not have any effect however and is simply ignored
|
||||||
outputcmd = {
|
outputcmd = {
|
||||||
outputfile: "-pass 2"
|
outputfile: "-pass 2"
|
||||||
" "
|
" "
|
||||||
@ -139,26 +109,31 @@ def convert(inputfile, outputfile, loudness) -> Optional[list[Any]]:
|
|||||||
"measured_tp={input_tp}:measured_thresh={input_thresh}:"
|
"measured_tp={input_tp}:measured_thresh={input_thresh}:"
|
||||||
"print_format=json"
|
"print_format=json"
|
||||||
" "
|
" "
|
||||||
"-c:a libopus"
|
"-c:a {codec}"
|
||||||
" "
|
" "
|
||||||
"-b:a 320k".format(
|
"-b:a {bitrate}"
|
||||||
|
" "
|
||||||
|
"-compression_level {compression}".format(
|
||||||
input_i=loudness["input_i"],
|
input_i=loudness["input_i"],
|
||||||
input_lra=loudness["input_lra"],
|
input_lra=loudness["input_lra"],
|
||||||
input_tp=loudness["input_tp"],
|
input_tp=loudness["input_tp"],
|
||||||
input_thresh=loudness["input_thresh"],
|
input_thresh=loudness["input_thresh"],
|
||||||
|
codec=codec,
|
||||||
|
bitrate=bitrate,
|
||||||
|
compression=compression,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ff = ffmpy.FFmpeg(
|
ff = ffmpy.FFmpeg(
|
||||||
inputs=inputcmd,
|
inputs=inputcmd,
|
||||||
outputs=outputcmd,
|
outputs=outputcmd,
|
||||||
# global_options=("-y", "-v error"),
|
|
||||||
global_options=("-y"),
|
global_options=("-y"),
|
||||||
)
|
)
|
||||||
# ff.run()
|
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
ff.cmd, shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE
|
ff.cmd, shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE
|
||||||
)
|
)
|
||||||
|
|
||||||
# NOTE get loudness info from subprocess
|
# NOTE get loudness info from subprocess
|
||||||
# rstrip: remove trailing newline
|
# rstrip: remove trailing newline
|
||||||
# decode: convert from binary string to utf8
|
# decode: convert from binary string to utf8
|
||||||
@ -167,6 +142,7 @@ def convert(inputfile, outputfile, loudness) -> Optional[list[Any]]:
|
|||||||
loudness_json: str = "\n".join(
|
loudness_json: str = "\n".join(
|
||||||
proc.stdout.read().rstrip().decode("utf8").splitlines()[-12:]
|
proc.stdout.read().rstrip().decode("utf8").splitlines()[-12:]
|
||||||
)
|
)
|
||||||
|
|
||||||
# decode json to dict
|
# decode json to dict
|
||||||
loudness_new: dict[str, str] = json.loads(loudness_json)
|
loudness_new: dict[str, str] = json.loads(loudness_json)
|
||||||
if loudness_new["normalization_type"] != "linear":
|
if loudness_new["normalization_type"] != "linear":
|
||||||
@ -180,9 +156,14 @@ def main(inputfile: str) -> Optional[list[Any]]:
|
|||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
inputfile (str): Path to input file
|
inputfile (str): Path to input file
|
||||||
|
|
||||||
|
Output:
|
||||||
|
dynamically normalised audio files (list)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# set output folder to parent path + "normalized"
|
# set output folder to parent path + "normalized"
|
||||||
outputfolder = os.path.join(os.path.dirname(inputfile), "normalized")
|
outputfolder = os.path.join(os.path.dirname(inputfile), "normalized")
|
||||||
|
|
||||||
# NOTE create output folder
|
# NOTE create output folder
|
||||||
# because multiple parallel processes are at work here,
|
# because multiple parallel processes are at work here,
|
||||||
# there might be conflicts with one trying to create the directory although it already exists
|
# there might be conflicts with one trying to create the directory although it already exists
|
||||||
@ -196,26 +177,41 @@ def main(inputfile: str) -> Optional[list[Any]]:
|
|||||||
time.sleep(randint(0, 4))
|
time.sleep(randint(0, 4))
|
||||||
|
|
||||||
# output file path
|
# output file path
|
||||||
noext_infile: str = os.path.splitext(os.path.basename(inputfile))[0]
|
infile_noextension: str = os.path.splitext(os.path.basename(inputfile))[0]
|
||||||
outputfile: str = os.path.join(outputfolder, noext_infile + ".opus")
|
infile_extension: str = os.path.splitext(os.path.basename(inputfile))[1]
|
||||||
|
|
||||||
# print(inputfile)
|
match infile_extension:
|
||||||
# print(os.path.dirname(inputfile))
|
case ".flac" | ".wav":
|
||||||
# print(os.path.basename(inputfile))
|
outputfile: str = os.path.join(outputfolder, infile_noextension + ".flac")
|
||||||
# print(outputfile)
|
codec: str = "flac"
|
||||||
|
compression: int = 12 # best compression
|
||||||
|
bitrate: str = "0k"
|
||||||
|
case ".mp3" | ".m4a" | ".aac" | ".opus":
|
||||||
|
outputfile: str = os.path.join(outputfolder, infile_noextension + ".opus")
|
||||||
|
codec: str = "libopus"
|
||||||
|
compression: int = 10 # best compression
|
||||||
|
bitrate: str = "192k"
|
||||||
|
case _:
|
||||||
|
print(inputfile, "does not use a known extension. Conversion skipped")
|
||||||
|
return
|
||||||
|
|
||||||
# remove_picture(inputfile=inputfile)
|
loudness: dict[str, str] = loudness_info(inputfile=inputfile)
|
||||||
loudness = loudness_info(inputfile=inputfile)
|
|
||||||
nonlinear: Optional[list[Any]] = convert(
|
nonlinear: Optional[list[Any]] = convert(
|
||||||
inputfile=inputfile,
|
inputfile=inputfile,
|
||||||
outputfile=outputfile,
|
outputfile=outputfile,
|
||||||
|
codec=codec,
|
||||||
|
compression=compression,
|
||||||
loudness=loudness,
|
loudness=loudness,
|
||||||
|
bitrate=bitrate,
|
||||||
)
|
)
|
||||||
|
|
||||||
return nonlinear
|
return nonlinear
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
"""
|
||||||
|
Handle arguments and other details for interactive usage
|
||||||
|
"""
|
||||||
# start time of program
|
# start time of program
|
||||||
starttime = time.time()
|
starttime = time.time()
|
||||||
|
|
||||||
@ -268,8 +264,6 @@ if __name__ == "__main__":
|
|||||||
else:
|
else:
|
||||||
timeprev = 0
|
timeprev = 0
|
||||||
|
|
||||||
# print(timeprev)
|
|
||||||
|
|
||||||
musicfiles: list[str] = []
|
musicfiles: list[str] = []
|
||||||
for root, dirs, files in os.walk(srcfolder):
|
for root, dirs, files in os.walk(srcfolder):
|
||||||
# ignore the "normalized" subfolder
|
# ignore the "normalized" subfolder
|
||||||
@ -281,8 +275,6 @@ if __name__ == "__main__":
|
|||||||
if os.path.getmtime(filepath) >= float(timeprev):
|
if os.path.getmtime(filepath) >= float(timeprev):
|
||||||
musicfiles.append(os.path.join(root, file))
|
musicfiles.append(os.path.join(root, file))
|
||||||
|
|
||||||
# print(musicfiles)
|
|
||||||
|
|
||||||
with Pool(cpu) as p:
|
with Pool(cpu) as p:
|
||||||
nonlinear_all: Optional[list[Any]] = p.map(main, musicfiles)
|
nonlinear_all: Optional[list[Any]] = p.map(main, musicfiles)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user