From 9d08d966e33a874bf54b221378f324ad9a76a98a Mon Sep 17 00:00:00 2001 From: Moonbase59 Date: Tue, 4 Jun 2024 21:53:09 +0200 Subject: [PATCH] =?UTF-8?q?v2.0.1=20=E2=80=93=20See=20CHANGELOG.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 63 ++++++++++++++++++++++++ autocue.cue_file.liq | 49 +++++++++++++++--- cue_file | 101 ++++++++++++++++++++++++++++++++------ test_autocue.cue_file.liq | 15 ++++-- 4 files changed, 201 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f0ebac..ed7b09a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,68 @@ # autocue changelog +### 2024-06-04 - v2.0.1 + +- Fix small bug when reading already stored `liq_true_peak` that contained + a ` dBFS` value from v1.2.3. + +### 2024-06-04 – v2.0.0 + +Version 2.0.0 is out! It offers the chance to write ReplayGain data to +audio files, in addition to the Liquidsoap/Autocue tags. + +This is useful for station owners that can’t or don’t want to use audio tracks +that have been pre-replaygained. You can now add ReplayGain data to songs and +thus ensure the analysis won’t be done over and over again. + +This is an _additional_ option to the existing +write_tags feature and will only work when `write_tags` is enabled. Based on +our file analysis, it will save (and overwrite) ReplayGain Track data, such as +the gain value, the reference loudness, integrated loudness and true peak info. + +The logic is inside `cue_file`, so it can as well be used for pre-processing +your files without the need of other tools. Autocue & ReplayGain—all in one! + +The `cue_file` help now shows the file formats it supports, and the tags it +knows about. `cue_file` will automatically detect if Mutagen is installed and +offer you a whopping **20 more filetypes** to work with! + +#### Breaking Changes: + +- `liq_true_peak` (in dBFS) has been renamed to `liq_true_peak_db`. +- A new `liq_true_peak` has been added that stores the peak value as expected + and needed by ReplayGain. +- Both `cue_file` and `autocue.cue_file.liq` must be updated to v2.0.0 for + everything to work correctly. +- It is advisable to install Mutagen on the system running Liquidsoap, or in + the AzuraCast Docker container. Doing this gets you more file and tag types + you can work with, and safer/more compatible tagging. + Mutagen can usually be installed with `pip3 install mutagen` (preferred), or + `sudo apt install python3-mutagen` (often older versions, distros don’t + update that often). + + +### 2024-06-04 – v1.2.3 + +First public version with a numbering scheme. We use SemVer: + +- MAJOR.MINOR.PATCH +- MAJOR increments when API-breaking changes are released. +- MINOR increments with new functionality compatible with the existing API. +- PATCH increments when API-compatible bugfixes and minor changes are done. + +The version numbers of `autocue.cue_file` and the external `cue_file` binary +should usually be the same. + +Introducing: + +- Mutagen (Pyhon tagging library) should be installed and allows writing tags + to _many_ file and tagging formats. +- ffmpeg is now only used for tagging where we know it’s safe. +- Much better error checking: + - Writing tags only to "known good" file types. + - Gracefully skip writing to write-protected files/folders/drives. + - `cue_file` has got better CLI params checking and much improved help. + ### 2024-04-19 In preparation for later merge with `master` branch: diff --git a/autocue.cue_file.liq b/autocue.cue_file.liq index 9a15e46..96ae642 100644 --- a/autocue.cue_file.liq +++ b/autocue.cue_file.liq @@ -13,6 +13,9 @@ # replaygain_reference_loudness # 2024-05-02 - Moonbase59 - add clipping prevention logic (cue_file -k) # 2024-05-04 - Moonbase59 - Add (informational) liq_loudness_range +# 2024-06-04 - Moonbase59 - v2.0.0 Breaking: Add -r/--replaygain overwrite +# - Changed `liq_true_peak` to `liq_true_peak_db`, +# add new `liq_true_peak` (linear, like RG) # Lots of debugging output for AzuraCast in this, will be removed eventually. @@ -21,6 +24,14 @@ # Initialize settings for cue_file autocue implementation let settings.autocue.cue_file = () +# Internal only! Not a user setting. +let settings.autocue.cue_file.version = + settings.make( + description= + "Software version of autocue.cue_file. Should coincide with `cue_file`.", + "2.0.1" + ) + let settings.autocue.cue_file.path = settings.make( description= @@ -114,12 +125,19 @@ let settings.autocue.cue_file.unify_loudness_correction = let settings.autocue.cue_file.write_tags = settings.make( description= - "Write back `liq_*` tags to original audio file. Use with care, as \ - ffmpeg can't write all tags to all file types! Ensure you have enough \ + "Write back `liq_*` tags to original audio file. Ensure you have enough \ free space to hold a copy of the original file.", false ) +let settings.autocue.cue_file.write_replaygain = + settings.make( + description= + "Write ReplayGain tags to file (track only, no album). Useful if your \ + files have no previous RG tags. Only valid if `write_tags` is also true.", + false + ) + let settings.autocue.cue_file.force_analysis = settings.make( description= @@ -147,6 +165,7 @@ def cue_file(~request_metadata, ~file_metadata, filename) = overlay_longtail = settings.autocue.cue_file.overlay_longtail() blankskip = settings.autocue.cue_file.blankskip() write_tags = settings.autocue.cue_file.write_tags() + write_replaygain = settings.autocue.cue_file.write_replaygain() force_analysis = settings.autocue.cue_file.force_analysis() nice = settings.autocue.cue_file.nice() noclip = settings.autocue.cue_file.noclip() @@ -235,6 +254,12 @@ def cue_file(~request_metadata, ~file_metadata, filename) = "Clipping prevention active: #{noclip}" ) + log( + level=3, + label=label, + "Writing tags: #{write_tags}, including ReplayGain: #{write_replaygain}" + ) + # set up CLI arguments args = ref( @@ -255,6 +280,7 @@ def cue_file(~request_metadata, ~file_metadata, filename) = if noclip then args := list.add('-k', args()) end if blankskip() then args := list.add('-b', args()) end if write_tags then args := list.add('-w', args()) end + if write_replaygain then args := list.add('-r', args()) end if force_analysis then args := list.add('-f', args()) end if nice then args := list.add('-n', args()) end @@ -302,7 +328,8 @@ def cue_file(~request_metadata, ~file_metadata, filename) = liq_reference_loudness, liq_blankskip, liq_blank_skipped, - liq_true_peak + liq_true_peak, + liq_true_peak_db } : { @@ -320,7 +347,8 @@ def cue_file(~request_metadata, ~file_metadata, filename) = liq_reference_loudness: string, liq_blankskip: bool, liq_blank_skipped: bool, - liq_true_peak: string + liq_true_peak: float, + liq_true_peak_db: string } ) = res() @@ -341,7 +369,8 @@ def cue_file(~request_metadata, ~file_metadata, filename) = ("liq_reference_loudness", liq_reference_loudness), ("liq_blankskip", string(liq_blankskip)), ("liq_blank_skipped", string(liq_blank_skipped)), - ("liq_true_peak", liq_true_peak) + ("liq_true_peak", string(liq_true_peak)), + ("liq_true_peak_db", liq_true_peak_db) ] ) @@ -384,12 +413,12 @@ def cue_file(~request_metadata, ~file_metadata, filename) = # Override liq_amplify, liq_amplify_adjustment & liq_reference_loudness, # using clipping prevention as requested - # liq_loudness & liq_true_peak are always in the cue_file result + # liq_loudness & liq_true_peak_db are always in the cue_file result let (amp, amp_correction) = amplify_correct( target, list.assoc("liq_loudness", result()), - list.assoc("liq_true_peak", result()), + list.assoc("liq_true_peak_db", result()), noclip ) result := list.assoc.remove("liq_amplify", result()) @@ -463,7 +492,7 @@ def cue_file(~request_metadata, ~file_metadata, filename) = level=3, label=label, 'Clipping prevention: Adjusted liq_amplify by #{amp_correction_dB} \ - because track’s true peak is #{list.assoc("liq_true_peak", result())}.' + because track’s true peak is #{list.assoc("liq_true_peak_db", result())}.' ) end @@ -618,7 +647,10 @@ def cue_file(~request_metadata, ~file_metadata, filename) = ("liq_reference_loudness", list.assoc("liq_reference_loudness", result())), ("liq_blank_skipped", list.assoc("liq_blank_skipped", result())), ("liq_true_peak", list.assoc("liq_true_peak", result())), + ("liq_true_peak_db", list.assoc("liq_true_peak_db", result())), ...optional_meta("replaygain_track_gain", result()), + ...optional_meta("replaygain_track_peak", result()), + ...optional_meta("replaygain_track_range", result()), ...optional_meta("replaygain_reference_loudness", result()) ] @@ -667,6 +699,7 @@ settings.autocue.target_cross_duration := settings.autocue.cue_file.fade_out() # settings.autocue.cue_file.blankskip := false # skip silence in tracks # settings.autocue.cue_file.unify_loudness_correction := true # unify `replaygain_track_gain` & `liq_amplify` # settings.autocue.cue_file.write_tags := false # write liq_* tags back to file +# settings.autocue.cue_file.write_replaygain := false # write ReplayGain tags back to file # settings.autocue.cue_file.force_analysis := false # force re-analysis even if tags found # settings.autocue.cue_file.nice := false # Linux/MacOS only: Use NI=18 for analysis diff --git a/cue_file b/cue_file index 6147d70..7d59503 100755 --- a/cue_file +++ b/cue_file @@ -27,12 +27,17 @@ # - v1.2.1 Much more informative help, nicer formatting. # - v1.2.2 Limit -t/--target input range to -23.0..0.0 # - v1.2.3 Limit all params to sensible ranges +# - v2.0.0 Breaking: Add -r/--replaygain overwrite +# - Changed `liq_true_peak` to `liq_true_peak_db`, +# add new `liq_true_peak` (linear, like RG) +# - v2.0.1 Fix `liq_true_peak` reading when it still +# contains ` dBFS` from v1.2.3. # # Originally based on an idea and some code by John Warburton (@Warblefly): # https://github.com/Warblefly/TrackBoundaries __author__ = 'Matthias C. Hormann' -__version__ = '1.2.3' +__version__ = '2.0.1' import os import tempfile @@ -142,10 +147,12 @@ tags_to_check = { "liq_blankskip": is_true, "liq_blank_skipped": is_true, "replaygain_track_gain": float, + "replaygain_track_peak": float, "replaygain_track_range": float, "replaygain_reference_loudness": float, "r128_track_gain": int, "liq_true_peak": float, + "liq_true_peak_db": float, # reserved for future expansion "liq_ramp1": float, "liq_ramp2": float, @@ -217,7 +224,8 @@ def read_tags(filename, target=TARGET_LUFS, blankskip=False, noclip=False): k.lower(): v for k, v in format_items if k.lower() in tags_to_check} # unify, right overwrites left if key in both - tags_found = tags_in_stream | tags_in_format + #tags_found = tags_in_stream | tags_in_format + tags_found = {**tags_in_stream, **tags_in_format} # add duration of stream #0 try: @@ -238,7 +246,8 @@ def read_tags(filename, target=TARGET_LUFS, blankskip=False, noclip=False): "liq_loudness", "liq_loudness_range", "liq_reference_loudness", "replaygain_track_gain", "replaygain_track_range", "replaygain_reference_loudness", - "liq_true_peak" + "liq_true_peak_db", + "liq_true_peak", # in case old " dBFS" values were stored in v1.2.3 ] for tag in suffixed_tags: if tag in tags: @@ -301,19 +310,20 @@ def read_tags(filename, target=TARGET_LUFS, blankskip=False, noclip=False): # liq_amplify or liq_reference_loudness missing, must re-analyse skip_analysis = False - # we need liq_true_peak if noclip is requested + # we need liq_true_peak_db if noclip is requested if ( skip_analysis - and "liq_true_peak" in tags_found + and "liq_true_peak_db" in tags_found + and "liq_true_peak" in tags_found # for RG tag writing and "liq_loudness" in tags_found ): tags_found["liq_amplify"], tags_found["liq_amplify_adjustment"] = \ amplify_correct( target, tags_found["liq_loudness"], - tags_found["liq_true_peak"], + tags_found["liq_true_peak_db"], noclip - ) + ) else: skip_analysis = False @@ -364,6 +374,16 @@ def add_missing(tags_found, target=TARGET_LUFS, blankskip=False, noclip=False): if "liq_reference_loudness" not in tags_found: tags_found["liq_reference_loudness"] = target + # for RG tag writing + if "replaygain_track_gain" not in tags_found: + tags_found["replaygain_track_gain"] = tags_found["liq_amplify"] + if "replaygain_track_peak" not in tags_found: + tags_found["replaygain_track_peak"] = tags_found["liq_true_peak"] + if "replaygain_track_range" not in tags_found: + tags_found["replaygain_track_range"] = tags_found["liq_loudness_range"] + if "replaygain_reference_loudness" not in tags_found: + tags_found["replaygain_reference_loudness"] = tags_found["liq_reference_loudness"] + return tags_found @@ -584,13 +604,19 @@ def analyse( "liq_reference_loudness": target, "liq_blankskip": blankskip, "liq_blank_skipped": blank_skipped, - "liq_true_peak": true_peak_dB + "liq_true_peak": true_peak, + "liq_true_peak_db": true_peak_dB, + # for RG writing + "replaygain_track_gain": amplify, + "replaygain_track_peak": true_peak, + "replaygain_track_range": loudness_range, + "replaygain_reference_loudness": target }) -def write_tags(filename, tags={}): +def write_tags(filename, tags={}, replaygain=False): # Add the liq_* tags (and only these) - # Don’t touch replaygain_track_gain or R128_TRACK_GAIN. + # Only touch replaygain_track_gain or R128_TRACK_GAIN if so requested. # Only write tags to files if we can safely do so. filename = Path(filename) @@ -602,19 +628,55 @@ def write_tags(filename, tags={}): temp = filename.with_suffix('.tmp' + filename.suffix) # print(temp) + rg_tags = [ + "replaygain_track_gain", + "replaygain_track_peak", + "replaygain_track_range", + "replaygain_reference_loudness" + ] # copy only `liq_*`, float with 2 decimals, bools and strings lowercase tags_new = {k: "{:.2f}".format(v) if isinstance(v, float) else str(v).lower() - for k, v in tags.items() if k.startswith("liq_") + for k, v in tags.items() if k.startswith("liq_") or k in rg_tags } + # liq_true_peak & replaygain_track_peak have 6 decimals, fix it + if "liq_true_peak" in tags_new: + tags_new["liq_true_peak"] = "{:.6f}".format(tags["liq_true_peak"]) + if "replaygain_track_peak" in tags_new: + tags_new["replaygain_track_peak"] = "{:.6f}".format(tags["replaygain_track_peak"]) + + # pre-calculate Opus R128_TRACK_GAIN (ref: -23 LUFS), just in case + target = tags["liq_reference_loudness"] + og = str(int((tags["liq_amplify"] - (target - -23.0)) * 256)) + tags_new["R128_TRACK_GAIN"] = og + # add the units tags_new["liq_amplify"] += " dB" tags_new["liq_amplify_adjustment"] += " dB" tags_new["liq_loudness"] += " LUFS" tags_new["liq_loudness_range"] += " LU" tags_new["liq_reference_loudness"] += " LUFS" - tags_new["liq_true_peak"] += " dBFS" - # print(temp, json.dumps(tags_new, indent=2)) + tags_new["liq_true_peak_db"] += " dBFS" + tags_new["replaygain_track_gain"] += " dB" + tags_new["replaygain_track_range"] += " dB" + tags_new["replaygain_reference_loudness"] += " LUFS" + + if replaygain: + # delete unwanted tags + if filename.suffix.casefold() == ".opus": + # for Opus, delete the `replaygain_*` tags + for k in rg_tags: + tags_new.pop(k, None) + else: + # for all others, delete Opus Track Gain tag + del tags_new["R128_TRACK_GAIN"] + else: + # no ReplayGain override, remove all "gain" type tags + for k in rg_tags: + tags_new.pop(k, None) + del tags_new["R128_TRACK_GAIN"] + + # print(replaygain, temp, json.dumps(tags_new, indent=2)) if MUTAGEN_AVAILABLE and filename.suffix.casefold() in mp4_ext: # MP4-like files using Apple iTunes type tags @@ -842,6 +904,14 @@ parser.add_argument( "free space to hold a copy of the original file.", default=False, action='store_true') +parser.add_argument( + "-r", + "--replaygain", + help="Write ReplayGain tags to file (track only, no album). Useful if " + "your files have no previous RG tags. Only valid if -w/--write is also " + "specified.", + default=False, + action='store_true') parser.add_argument( "-f", "--force", @@ -879,7 +949,7 @@ else: # print(result) if args.write: - write_tags(args.file, result) + write_tags(args.file, result, args.replaygain) # prepare JSON result # we use "dB" instead of "LU" units, because LS & others don’t understand "LU" @@ -898,7 +968,8 @@ liq_result = { "liq_reference_loudness": f"{result['liq_reference_loudness']:.2f} LUFS", "liq_blankskip": result['liq_blankskip'], "liq_blank_skipped": result['liq_blank_skipped'], - "liq_true_peak": f"{result['liq_true_peak']:.2f} dBFS", + "liq_true_peak": result['liq_true_peak'], + "liq_true_peak_db": f"{result['liq_true_peak_db']:.2f} dBFS", } # output compact (one line) JSON, for use in Liquidsoap "autocue:" protocol diff --git a/test_autocue.cue_file.liq b/test_autocue.cue_file.liq index 0e6cca4..e8b96c4 100644 --- a/test_autocue.cue_file.liq +++ b/test_autocue.cue_file.liq @@ -34,7 +34,7 @@ settings.autocue.cue_file.path := "./cue_file" # settings.autocue.cue_file.fade_in := 0.1 # settings.autocue.cue_file.fade_out := 2.5 # settings.autocue.cue_file.timeout := 60.0 -settings.autocue.cue_file.target := -14.0 # not recommended +# settings.autocue.cue_file.target := -14.0 # not recommended # settings.autocue.cue_file.silence := -42.0 # settings.autocue.cue_file.overlay := -8.0 # settings.autocue.cue_file.longtail := 15.0 @@ -42,8 +42,9 @@ settings.autocue.cue_file.target := -14.0 # not recommended settings.autocue.cue_file.noclip := true # clipping prevention settings.autocue.cue_file.blankskip := true # settings.autocue.cue_file.unify_loudness_correction := true -# settings.autocue.cue_file.write_tags := false -# settings.autocue.cue_file.force_analysis := false +settings.autocue.cue_file.write_tags := true +settings.autocue.cue_file.write_replaygain := true +settings.autocue.cue_file.force_analysis := true settings.autocue.cue_file.nice := true # Linux/MacOS only! # `enable_autocue_metadata()` will autocue ALL files Liquidsoap processes. @@ -105,7 +106,13 @@ def show_meta(m) # show `liq_` & other metadata in level 3 def fl(k, _) = - tags = ["duration", "replaygain_track_gain", "replaygain_reference_loudness"] + tags = [ + "duration", + "replaygain_track_gain", + "replaygain_track_peak", + "replaygain_track_range", + "replaygain_reference_loudness" + ] string.contains(prefix="liq_", k) or list.mem(k, tags) end liq = list.assoc.filter((fl), l)