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:
exu 2024-03-23 18:32:01 +01:00
parent ca183a436a
commit 78ee74ed9d

View File

@ -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)