def get_system_info(): python_version_info = "Python " + str(sys.version_info[0]) + "." + str(sys.version_info[1])+ "." + str(sys.version_info[2]) ffmpeg_cmd_version = 'cmd /c "' + ConfigGetter.get_configs("ffmpeg_path") + '" -version' java_cmd_version = 'cmd /c "' + ConfigGetter.get_configs("java_path") + '" -version' pipeline_version = re.sub(r".+org\.daisy\.pipeline_", "Daisy Pipeline v. ", ConfigGetter.get_configs("pipeline_path")) ffmpeg_version = "" raw_output_ffmpeg = ExternalProgramCaller.run_external_command(ffmpeg_cmd_version).splitlines() for line in raw_output_ffmpeg: if re.search(r" Copyright.+", line): ffmpeg_version = re.sub(r' Copyright.+', '', line) ffmpeg_version = re.sub(r'ffmpeg', 'FFmpeg', ffmpeg_version) break java_version = "" raw_output_java = ExternalProgramCaller.run_external_command(java_cmd_version).splitlines() for line in raw_output_java: if re.search(r".+version.+", line): java_version = "Java " + re.sub(r'"', '', line) break system_info_list = [python_version_info, ffmpeg_version, java_version, pipeline_version] #print(system_info_list) return system_info_list
def encode_audio(ncc, output_path): # pipeline-cli.bat scripts\modify_improve\dtb\DTBAudioEncoder.taskScript "--input=path\ncc.html" # "--output=outpath" "--bitrate=48" pipeline_path = pathlib.Path( ConfigGetter.get_configs("pipeline_path")).absolute() if pipeline_path.exists(): cmd_encode = ".\\pipeline-cli.bat scripts\\modify_improve\\dtb\\DTBAudioEncoder.taskScript" target_kbps = ConfigGetter.get_configs("target_kbps") if not target_kbps == "32" and not target_kbps == "48" and not target_kbps == "64" and not target_kbps == "128": print( " !Error while reading target_kbps from config.txt, using default value!" ) target_kbps = "48" cmd_encode_audio = 'C: & cd ' + str(pipeline_path) + ' & ' + cmd_encode \ + ' "--input=' + ncc + '" "--output=' + output_path + '" "--bitrate=' + target_kbps + '"' os.system(cmd_encode_audio) print("Validating encoded Daisy book...") output_ncc = pathlib.Path.joinpath(pathlib.Path(output_path), "ncc.html") validator_checks = DaisyScrutinizer.validate_daisy(str(output_ncc)) if len(validator_checks[0]) == 0 and len(validator_checks[1]) == 0: print("No errors or warning reported.") else: for err in validator_checks[0]: print(err) for warn in validator_checks[1]: print(warn) else: print("Daisy pipeline not found! Skipping audio encoding!")
def compare_lufs(audio_files): max_volume_level_flux = ConfigGetter.get_configs( "max_volume_level_flux") try: max_volume_level_flux = int(max_volume_level_flux) except: print( " !Error while reading max_volume_level_flux from config.txt, using default value!" ) max_volume_level_flux = 1 lufs_flux = [] i = 1 while i < len(audio_files): audio_f_comp = audio_files[i - 1] if (audio_f_comp.lufs * -1) - (audio_files[i].lufs * -1) > (1 * max_volume_level_flux) \ or (audio_f_comp.lufs * -1) - (audio_files[i].lufs * -1) < (-1 * max_volume_level_flux): lufs_flux.append( "Noticeable change in volume compared to previous audiofile on " + audio_files[i].name + "." + audio_files[i].f_type + "! (Change " + str(audio_f_comp.lufs) + " LUFS -> " + str(audio_files[i].lufs) + " LUFS.)") i += 1 return lufs_flux
def check_audio_ext_paths(): ffmpeg_path = ConfigGetter.get_configs("ffmpeg_path") if ffmpeg_path != "ffmpeg": ffmpeg_path = str(pathlib.Path(ffmpeg_path).absolute()) if which(ffmpeg_path) is None: #print("FFmpeg not found at " + str(ffmpeg_path)) return False else: return True
def check_daisy_ext_paths(): programs_found = True pipeline_path = pathlib.Path(ConfigGetter.get_configs("pipeline_path"), "pipeline-cli.bat").absolute() if not pipeline_path.exists(): programs_found = False #print("Pipeline not found at " + str(pipeline_path)) return programs_found
def validate_daisy(ncc): errors = [] warnings = [] pipeline_path = pathlib.Path( ConfigGetter.get_configs("pipeline_path")).absolute() if not pipeline_path.exists(): print("Daisy validator not found! Skipping validation!") errors.append("Validator not found at location " + str(pipeline_path) + "! CAN NOT VALIDATE DAISY!") results = [errors, warnings] return results cmd_validator = ".\\pipeline-cli.bat scripts\\verify\\Daisy202DTBValidator.taskScript" # NOTE! It seems there is a bug in pipeline, so that the pipelinecli can not be run from any other location, # than from where it is installed. Trying to run it from any other location causes "Could not # find or load main class"-error. As a workaround in this program the location # is changed before any pipeline commands. The workaround does following: # # cmd /c ... # cmd starts in what ever drive it is called from... # C: # ...but cmd has to be on drive 'C:' # cd pipeline_path # ...or "cd pipeline_path" command won't work # pipeline commands cmd_validate = 'cmd /c "C: & cd ' + str( pipeline_path) + ' & ' + cmd_validator + ' "--input=' + ncc + '"" ' validator_output = ExternalProgramCaller.run_external_command( cmd_validate).splitlines() for line in validator_output: # print(line) if re.search(r".ERROR, Validator. ", line): line = re.sub(r".ERROR, Validator. ", "", line) errors.append(line) elif re.search(r".WARNING, Validator. ", line): line = re.sub(r".WARNING, Validator. ", "", line) warnings.append(line) results = [errors, warnings] return results
def write_report(audiobook, report_out_path, critical_errors, errors, warnings): validation_report = open(str(report_out_path), "x", encoding="utf-8") program_versions = get_system_info() skip_daisy_checks = False if not ConfigGetter.get_configs("daisy_validation") == "1": skip_daisy_checks = True skip_audio_checks = False if not ConfigGetter.get_configs("audio_validation") == "1": skip_audio_checks = True validation_report.write("<!DOCTYPE html>\n" + "<html lang=""en"">\n" + "<head>\n" + "<meta charset=""UTF-8"">\n" + "<title>VALIDATION REPORT</title>\n" + "</head>\n" + "<body>\n") if not skip_daisy_checks: validation_report.write("<h1>Validation report for dtb " + audiobook.dc_title + " / " + audiobook.dc_creator + " (" + audiobook.dc_identifier + ")</h1>\n\n") validation_report.write("<p style=""font-size:22px;"">\n" + "<b>Title:</b> " + audiobook.dc_title + "<br />\n" + "<b>Author:</b> " + audiobook.dc_creator + "<br />\n" + "<b>Narrator:</b> " + audiobook.ncc_narrator + "<br />\n" + "<b>Id:</b> " + audiobook.dc_identifier + "<br /></p>\n\n") else: validation_report.write("<h1>Validation report for dtb " + audiobook.path + "</h1>") validation_report.write("<h2>Validation summary:</h2>\n\n") if len(critical_errors) > 0: validation_report.write("<h3>Critical errors:</h3>\n") validation_report.write("<pre style=""font-size:18px;"">\n") for err in critical_errors: validation_report.write(" " + err + "\n") validation_report.write("</pre>\n\n") if len(errors) > 0: validation_report.write("<h3>Errors:</h3>\n") validation_report.write("<pre style=""font-size:18px;"">\n") for err in errors: validation_report.write(" " + err + "\n") validation_report.write("</pre>\n\n") if len(warnings) > 0: validation_report.write("<h3>Warnings:</h3>\n") validation_report.write("<pre style=""font-size:18px;"">\n") for err in warnings: validation_report.write(" " + err + "\n") validation_report.write("</pre>\n\n") if len(critical_errors) == 0 and len(errors) == 0 and len(warnings) == 0: validation_report.write("<p style=""font-size:22px;"">\n" + "No errors or warnings found. Congratulations!</p>\n\n") validation_report.write("<h2>Detailed information</h2>\n") if not skip_audio_checks: validation_report.write("<h3>Audio stats</h3>\n") validation_report.write("<pre style=""font-size:18px;"">\n") validation_report.write(" Avg. LUFS: " + format(audiobook.lufs, ".2f") + " LUFS\n") validation_report.write(" Avg. pkdb: " + format(audiobook.pkdb, ".2f") + " dB\n") validation_report.write(" Avg. tpkdb: " + format(audiobook.tpkdb, ".2f") + " dB\n") validation_report.write(" Avg. snr: " + format(audiobook.snr, ".2f") + " dB\n") validation_report.write(" KBPS: " + format(audiobook.kbps, ".0f") + "\n\n") for audio_f in audiobook.files: if isinstance(audio_f, AudioFile): # format(float(silence_length), ".3f") validation_report.write(" " + audio_f.name + "." + audio_f.f_type + ", " + format(audio_f.lufs, ".2f") + " LUFS, " + "peak " + format(audio_f.pkdb, ".2f") + " dB, tpkdb " + format(audio_f.tpkdb, ".2f") + " dB, snr " + format(audio_f.snr, ".2f") + " dB\n") validation_report.write("</pre>\n\n") if not skip_daisy_checks: validation_report.write("<h3>Filestructure</h3>\n") validation_report.write("<pre style=""font-size:18px;"">\n") validation_report.write(" <b>ncc.html files:</b> " + str(audiobook.ncc_count) + "\n") validation_report.write(" <b>audiofiles:</b> " + str(audiobook.audio_file_count) + "\n") validation_report.write(" <b>smil-files:</b> " + str(audiobook.smil_count) + "\n") validation_report.write(" <b>master.smil files:</b> " + str(audiobook.master_smil_count) + "\n") validation_report.write(" <b>image files:</b> " + str(audiobook.image_count) + "\n") validation_report.write(" <b>css files:</b> " + str(audiobook.css_count) + "\n") validation_report.write(" <b>other files:</b> " + str(audiobook.other_filetype_count) + "\n") validation_report.write("</pre>\n\n") validation_report.write("<h3>Metadata</h3>\n") validation_report.write("<pre style=""font-size:18px;"">\n") validation_report.write(" <b>dc:title:</b> " + audiobook.dc_title + "\n") validation_report.write(" <b>dc:creator:</b> " + audiobook.dc_creator + "\n") validation_report.write(" <b>dc:date:</b> " + audiobook.dc_date + "\n") validation_report.write(" <b>dc:identifier:</b> " + audiobook.dc_identifier + "\n") validation_report.write(" <b>dc:language:</b> " + audiobook.dc_language + "\n") validation_report.write(" <b>dc:publisher:</b> " + audiobook.dc_publisher + "\n") validation_report.write(" <b>dc:source:</b> " + audiobook.dc_source + "\n") validation_report.write(" <b>ncc:narrator:</b> " + audiobook.ncc_narrator + "\n") validation_report.write(" <b>ncc:producer:</b> " + audiobook.ncc_producer + "\n") validation_report.write(" <b>dc:format:</b> " + audiobook.dc_format + "\n") validation_report.write(" <b>ncc:generator:</b> " + audiobook.ncc_generator + "\n") validation_report.write(" <b>prod:prevGenerator:</b> " + audiobook.prod_prev_generator + "\n") validation_report.write(" <b>ncc:pageNormal:</b> " + audiobook.ncc_page_normal + "\n") validation_report.write(" <b>ncc:pageFront:</b> " + audiobook.ncc_page_front + "\n") validation_report.write(" <b>ncc:pageSpecial:</b> " + audiobook.ncc_page_special + "\n") validation_report.write(" <b>ncc:depth:</b> " + audiobook.ncc_depth + "\n") validation_report.write(" <b>ncc:charset:</b> " + audiobook.ncc_charset + "\n") validation_report.write(" <b>ncc:multimediaType:</b> " + audiobook.ncc_multimedia_type + "\n") validation_report.write(" <b>ncc:tocItems:</b> " + audiobook.ncc_toc_items + "\n") validation_report.write(" <b>ncc:totalTime:</b> " + audiobook.ncc_total_time + "\n") validation_report.write(" <b>ncc:files:</b> " + audiobook.ncc_files + "\n") validation_report.write("</pre>\n\n") validation_report.write("<h2>END OF VALIDATION REPORT</h2>\n\n") validation_report.write('<p style="width: 100%; text-align: center; font-size: 12px; font-style: italic; font-weight: bold;">') validation_report.write("CeliaDTBValidator v. 0.9.3 (" + program_versions[0] + "; " + program_versions[1]) validation_report.write("; " + program_versions[2] + "; " + program_versions[3] + ")</p>\n") validation_report.write("</body>\n" + "</html>") validation_report.close()
def __init__(self, path): self.path = path self.kbps = 0.0 self.lufs = 0.0 self.pkdb = 0.0 self.tpkdb = 0.0 self.snr = 0.0 self.files = [] self.filename_prefix = "" self.daisy_validation_errors = [] self.daisy_validation_warnings = [] skip_daisy_checks = False if not ConfigGetter.get_configs("daisy_validation") == "1": skip_daisy_checks = True skip_audio_checks = False if not ConfigGetter.get_configs("audio_validation") == "1": skip_audio_checks = True if skip_audio_checks and skip_daisy_checks: input("Confuration set to skip daisy AND audio checks! Nothing to do...") sys.exit() print("Processing files...") files_in_folder = FileScrutinizer.list_files_in_folder(self.path) i = 0 for f in files_in_folder: i += 1 f_info = FileScrutinizer.find_fname_ftype(f) print("Processing file " + f_info[0] + "." + f_info[1] + " (" + str(i) + "/" + str(len(files_in_folder)) + ")") if (f_info[1].lower() == "wav" or f_info[1].lower() == "mp3") and not skip_audio_checks: new_file = AudioFile(f, f_info[0], f_info[1]) else: new_file = GenericFile(f, f_info[0], f_info[1]) self.files.append(new_file) self.ncc_count = 0 self.audio_file_count = 0 self.smil_count = 0 self.master_smil_count = 0 self.image_count = 0 self.css_count = 0 self.other_filetype_count = 0 self.pagemark_errors = [] file_count = DaisyScrutinizer.count_daisy_files(self.files) self.ncc_count = file_count[0] self.audio_file_count = file_count[1] self.smil_count = file_count[2] self.master_smil_count = file_count[3] self.image_count = file_count[4] self.css_count = file_count[5] self.other_filetype_count = file_count[6] if not skip_audio_checks: for f in self.files: if isinstance(f, AudioFile): self.lufs += f.lufs self.pkdb += f.pkdb self.tpkdb += f.tpkdb self.snr += f.snr self.kbps += f.kbps self.lufs = self.lufs / self.audio_file_count self.pkdb = self.pkdb / self.audio_file_count self.tpkdb = self.tpkdb / self.audio_file_count self.snr = self.snr / self.audio_file_count self.kbps = self.kbps / self.audio_file_count # Daisy Checks... if not skip_daisy_checks: print("Processing book metadata...") charencoding = FileScrutinizer.get_encoding_tag(self.path + "/ncc.html") metadata_list = DaisyScrutinizer.get_metadata(self.path + "/ncc.html", charencoding) self.dc_title = metadata_list[0] self.dc_creator = metadata_list[1] self.dc_date = metadata_list[2] self.dc_identifier = metadata_list[3] self.dc_language = metadata_list[4] self.dc_publisher = metadata_list[5] self.dc_source = metadata_list[6] self.ncc_narrator = metadata_list[7] self.ncc_producer = metadata_list[8] self.dc_format = metadata_list[9] self.ncc_generator = metadata_list[10] self.ncc_page_normal = metadata_list[11] self.ncc_page_front = metadata_list[12] self.ncc_page_special = metadata_list[13] self.ncc_depth = metadata_list[14] self.ncc_charset = metadata_list[15] self.ncc_multimedia_type = metadata_list[16] self.ncc_toc_items = metadata_list[17] self.ncc_total_time = metadata_list[18] self.ncc_files = metadata_list[19] self.prod_prev_generator = "" for prev_gen in metadata_list[20]: if self.prod_prev_generator == "": self.prod_prev_generator = prev_gen else: self.prod_prev_generator = self.prod_prev_generator + ", " + prev_gen self.filename_prefix = FileScrutinizer.get_filename_prefix(self.files) self.pagemark_errors = [] self.pagemark_errors = DaisyScrutinizer.find_pagemark_errors(self.files) self.first_smil_audio_references = DaisyScrutinizer.check_first_smil(self.files) print("Running daisy validator...") daisy_validation = DaisyScrutinizer.validate_daisy(self.path + "/ncc.html") self.daisy_validation_errors = daisy_validation[0] self.daisy_validation_warnings = daisy_validation[1] else: print("Skipping Daisy checks.") self.dc_title = "" self.dc_creator = "" self.dc_date = "" self.dc_identifier = "" self.dc_language = "" self.dc_publisher = "" self.dc_source = "" self.ncc_narrator = "" self.ncc_producer = "" self.dc_format = "" self.ncc_generator = "" self.ncc_page_normal = "" self.ncc_page_front = "" self.ncc_page_special = "" self.ncc_depth = "" self.ncc_charset = "" self.ncc_multimedia_type = "" self.ncc_toc_items = "" self.ncc_total_time = "" self.ncc_files = "" self.prod_prev_generator = "" self.filename_prefix = FileScrutinizer.get_filename_prefix(self.files) self.pagemark_errors = [] self.first_smil_audio_references = 0 self.daisy_validation_errors = [] self.daisy_validation_warnings = []
def find_silence(f_path): print( " Checking for unexpected silences at start, end or middle of file" ) silence_db = ConfigGetter.get_configs("silence_db") if silence_db == "": silence_db = "-26" try: silence_db = int(silence_db) except: print( " !Error while reading silence_db from config.txt, using default value!" ) silence_db = -26 silence_check = [] no_start_silence = True start_silence_max = ConfigGetter.get_configs("start_silence_max") if not re.search(r'^\d:\d\d\.\d\d\d$', start_silence_max.strip()): print( " !Error while reading start_silence_max from config.txt, using default value!" ) start_silence_max = "0:01.001" else: start_silence_max = start_silence_max.strip() #ffmpeg_path = str(pathlib.Path(ConfigGetter.get_configs("ffmpeg_path"))) ffmpeg_path = ConfigGetter.get_configs("ffmpeg_path") if ffmpeg_path != "ffmpeg": ffmpeg_path = str(pathlib.Path(ffmpeg_path).absolute()) # print("FFmpeg at " + ffmpeg_path) start_silence_max_ffmpeg_cmd = 'cmd /c ""' + ffmpeg_path + '" -ss 00:00:00 -nostats -i "' + f_path + '" -to 00:0' + start_silence_max + ' -filter_complex ebur128=peak=true -f null - 2>&1"' raw_output = ExternalProgramCaller.run_external_command( start_silence_max_ffmpeg_cmd).splitlines() if get_peak(raw_output) < silence_db: no_start_silence = False silence_at_end = True end_silence_min = ConfigGetter.get_configs("end_silence_min") if not re.search(r'^\d:\d\d\.\d\d\d$', end_silence_min.strip()): print( " !Error while reading end_silence_min from config.txt, using default value!" ) end_silence_min = "0:01.801" else: end_silence_min = end_silence_min.strip() end_silence_min_ffmpeg_cmd = 'cmd /c ""' + ffmpeg_path + '" -sseof -00:0' + end_silence_min + ' -nostats -i "' + f_path + '" -filter_complex ebur128=peak=true -f null - 2>&1"' raw_output = ExternalProgramCaller.run_external_command( end_silence_min_ffmpeg_cmd).splitlines() if get_peak(raw_output) > silence_db: silence_at_end = False no_unexpected_silence_at_end = True end_silence_max = ConfigGetter.get_configs("end_silence_max") if not re.search(r'^\d:\d\d\.\d\d\d$', end_silence_max.strip()): print( " !Error while reading end_silence_max from config.txt, using default value!" ) end_silence_max = "0:15.001" else: end_silence_max = end_silence_max.strip() end_silence_max_ffmpeg_cmd = 'cmd /c ""' + ffmpeg_path + '" -sseof -00:0' + end_silence_max + ' -nostats -i "' + f_path + '" -filter_complex ebur128=peak=true -f null - 2>&1"' raw_output = ExternalProgramCaller.run_external_command( end_silence_max_ffmpeg_cmd).splitlines() if get_peak(raw_output) < silence_db: no_unexpected_silence_at_end = False mid_silence_max = ConfigGetter.get_configs("mid_silence_max") try: mid_silence_max_int = int(mid_silence_max) mid_silence_max = str(mid_silence_max_int) except: print( " !Error while reading mid_silence_max from config.txt, using default value!" ) mid_silence_max = "7" mid_silences = [] mid_silence_cmd = 'cmd /c ""' + ffmpeg_path + '" -nostats -i "' \ + f_path + '" -af silencedetect=noise=' + str(silence_db) + 'dB:d=' \ + mid_silence_max + ' -f null - 2>&1 "' #print(mid_silence_cmd) raw_output = ExternalProgramCaller.run_external_command( mid_silence_cmd).splitlines() for line in raw_output: #print(line) if re.search(r".+silence_end.+", line): line = re.sub(r".+silence_end: ", r"", line) pieces = line.split(" | ") silence_length = re.sub(r"silence_duration: ", r"", pieces[1]) silence_at = float(pieces[0]) silence_at_hour = int(silence_at // 3600) silence_at_hour_str = str(silence_at_hour) silence_at_min = int( (silence_at - silence_at_hour * 60 * 60) // 60) if silence_at_min < 10: silence_at_min_str = "0" + str(silence_at_min) else: silence_at_min_str = str(silence_at_min) silence_at_sec_ms = silence_at - (silence_at_hour * 60 * 60) - (silence_at_min * 60) silence_at_sec_ms_tmp = str(silence_at_sec_ms).split(".") silence_at_sec = int(silence_at_sec_ms_tmp[0]) silence_at_ms = float("0." + silence_at_sec_ms_tmp[1]) silence_at_ms = float(format(silence_at_ms, ".3f")) silence_at_sec_ms = silence_at_sec + silence_at_ms if silence_at_sec_ms < 10: silence_at_sec_ms_str = "0" + format( silence_at_sec_ms, ".3f") else: silence_at_sec_ms_str = format(silence_at_sec_ms, ".3f") silence_at_fmt = silence_at_hour_str + ":" + silence_at_min_str + ":" + silence_at_sec_ms_str mid_silences.append("Unexpected silence found at " + silence_at_fmt + " (Silence duration: " + format(float(silence_length), ".3f") + " sec)") silence_check.append(no_start_silence) silence_check.append(silence_at_end) silence_check.append(no_unexpected_silence_at_end) silence_check.append(mid_silences) return silence_check
def get_stats(f_path): print(" Calculating lufs & peak levels") stats = [] lufs = 0.0 pkdb = 0.0 tpkdb = 0.0 snr = 0.0 kbps = 0.0 # general stats with ffmpeg ffmpeg_path = ConfigGetter.get_configs("ffmpeg_path") if ffmpeg_path != "ffmpeg": ffmpeg_path = str(pathlib.Path(ffmpeg_path).absolute()) # print("FFmpeg at " + ffmpeg_path) ffmpeg_cmd_lufs = 'cmd /c ""' + ffmpeg_path + '" -nostats -i "' + f_path + '" -filter_complex ebur128=peak=true -f null - 2>&1"' ffmpeg_cmd_stats = 'cmd /c ""' + ffmpeg_path + '" -i "' + f_path + '" -af astats -f null - 2>&1"' #input(ffmpeg_cmd_stats) raw_output_lufs = ExternalProgramCaller.run_external_command( ffmpeg_cmd_lufs).splitlines() raw_output_stats = ExternalProgramCaller.run_external_command( ffmpeg_cmd_stats).splitlines() rms_trough_db = 0.0 rms_peak_db = 0.0 for line in reversed(raw_output_lufs): #input(line) if re.search(r".+Peak", line): line = re.sub(r".+Peak: *", "", line) tpkdb = float(re.sub(r" dBFS", "", line)) if re.search(r" *I: .+ LUFS", line): line = re.sub(r" *I: *", "", line) line = re.sub(r" LUFS", "", line) lufs = round(float(line), 2) break for line in raw_output_lufs: if re.search(r".+bitrate:", line): line = re.sub(r".+bitrate: *", r"", line) line = re.sub(r" kb.+", r"", line) kbps = float(line) break for line in reversed(raw_output_stats): if re.search(r".+Peak level dB", line): line = re.sub(r".+Peak level dB: ", "", line) pkdb = round(float(line), 2) break if re.search(r".+RMS trough dB", line): line = re.sub(r".+RMS trough dB: ", "", line) rms_trough_db = round(float(line), 2) if re.search(r".+RMS peak dB", line): line = re.sub(r".+RMS peak dB: ", "", line) rms_peak_db = round(float(line), 2) snr = rms_peak_db - rms_trough_db stats.append(lufs) stats.append(pkdb) stats.append(tpkdb) stats.append(snr) stats.append(kbps) return stats