Skip to content

Commit

Permalink
v2.1.0 – Prepare for third-party pre-processing software
Browse files Browse the repository at this point in the history
  • Loading branch information
Moonbase59 committed Jun 9, 2024
1 parent 643c933 commit 81a76ec
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 31 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# autocue changelog

### 2024-06-09 – v2.1.0

- Prepare for third-party pre-processing software:
- A JSON file (or stdin) can now be used to set or override any of the
known tags (see help). Values from JSON override values read from the
audio file’s tags.
- This can be used, for example, to set values from a database (like
AzuraCast’s cue and fade data from their Visual Cue Editor).
- `cue_file` might _still_ decide a re-analysis being necessary, so it’s
wise to re-consolidate the data after `cue_file` has returned its results.
- More robust variable reading from tags or JSON:
- Booleans can be bool or string
- Typechecking on tags with unit suffixes, especially `liq_true_peak`,
which was a "dbFS"-suffixed string in v1.2.3 and now is a linear float.
- Added `liq_fade_in` & `liq_fade_out` to known tags. We don’t use these,
but pre-processors might want to set them.
- More advance example in `test_autocue.cue_file.liq` that shows how
annotations can be used to play 15-second snippets of songs.

### 2024-06-08 – v2.0.3

- Fix ffmpeg erroneously treating `.ogg` files with cover image as video.
Expand Down
3 changes: 2 additions & 1 deletion autocue.cue_file.liq
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
# add new `liq_true_peak` (linear, like RG)
# 2024-06-05 - Moonbase59 - v2.0.2 Initial display of version, at log level 2.
# 2024-06-08 - Moonbase59 - v2.0.3 Sync version number with cue_file
# 2024-06-09 - Moonbase59 - v2.1.0 Sync version number with cue_file

# Lots of debugging output for AzuraCast in this, will be removed eventually.

Expand All @@ -31,7 +32,7 @@ let settings.autocue.cue_file.version =
settings.make(
description=
"Software version of autocue.cue_file. Should coincide with `cue_file`.",
"2.0.3"
"2.1.0"
)

let settings.autocue.cue_file.path =
Expand Down
100 changes: 71 additions & 29 deletions cue_file
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,16 @@
# contains ` dBFS` from v1.2.3.
# 2024-06-05 Moonbase59 - No change, just version number.
# 2024-06-08 Moonbase59 - v2.0.3 Fix ffmpeg treating `.ogg` with cover as video
# 2024-06-09 Moonbase59 - v2.1.0 Read/override tags from JSON file (can be stdin)
# - Make variable checking more robust (bool & unit suffixes)
# - Add `liq_fade_in` & `liq_fade_out` tags for reading/writing,
# in case a preprocessor needs to set fade durations.
#
# Originally based on an idea and some code by John Warburton (@Warblefly):
# https://github.com/Warblefly/TrackBoundaries

__author__ = 'Matthias C. Hormann'
__version__ = '2.0.3'
__version__ = '2.1.0'

import os
import tempfile
Expand Down Expand Up @@ -134,42 +138,49 @@ tags_mandatory = set([

# bool() returns True for every nonempty string, so use a function
def is_true(v):
return v.lower() == 'true'
if isinstance(v, str):
return v.lower() == 'true'
elif isinstance(v, bool):
return v
else:
raise ValueError('must be bool or str')


# these are the tags to check when reading/writing tags from/to files
tags_to_check = {
"duration": float,
"liq_amplify_adjustment": float,
"liq_amplify": float, # like replaygain_track_gain
"liq_blankskip": is_true,
"liq_blank_skipped": is_true,
"liq_cross_duration": float,
"liq_cross_start_next": float,
"liq_cue_duration": float,
"liq_cue_in": float,
"liq_cue_out": float,
"liq_cross_start_next": float,
"liq_fade_in": float,
"liq_fade_out": float,
"liq_longtail": is_true,
"liq_cross_duration": float,
"liq_loudness": float,
"liq_loudness_range": float, # like replaygain_track_range
"liq_amplify": float, # like replaygain_track_gain
"liq_amplify_adjustment": float,
"liq_reference_loudness": float, # like replaygain_reference_loudness
"liq_blankskip": is_true,
"liq_blank_skipped": is_true,
"liq_true_peak_db": float,
"liq_true_peak": float,
"r128_track_gain": int,
"replaygain_reference_loudness": float,
"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,
"liq_ramp3": float,
"liq_hook1_in": float,
"liq_hook1_out": float,
"liq_hook2_in": float,
"liq_hook2_out": float,
"liq_hook3_in": float,
"liq_hook3_out": float,
"liq_ramp1": float,
"liq_ramp2": float,
"liq_ramp3": float,
}


Expand All @@ -187,7 +198,12 @@ def amplify_correct(target, loudness, true_peak_dB, noclip):
return amplify, amplify_correction


def read_tags(filename, target=TARGET_LUFS, blankskip=False, noclip=False):
def read_tags(
filename,
tags_json={},
target=TARGET_LUFS,
blankskip=False,
noclip=False):
# NOTE: Older ffmpeg/ffprobe don’t read ID3 tags if RIFF chunk found,
# see https://trac.ffmpeg.org/ticket/9848
# ffprobe -v quiet -show_entries
Expand Down Expand Up @@ -224,15 +240,22 @@ def read_tags(filename, target=TARGET_LUFS, blankskip=False, noclip=False):
except KeyError:
format_items = {}

# get tags in JSON override file
json_items = tags_json.items()

tags_in_stream = {
k.lower(): v for k,
v in stream_items if k.lower() in tags_to_check}
tags_in_format = {
k.lower(): v for k,
v in format_items if k.lower() in tags_to_check}
tags_in_json = {
k.lower(): v for k,
v in json_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 | tags_in_json
tags_found = {**tags_in_stream, **tags_in_format, **tags_in_json}
# print(json.dumps(tags_found, indent=2, sort_keys=True))

# add duration of stream #0
try:
Expand All @@ -257,7 +280,7 @@ def read_tags(filename, target=TARGET_LUFS, blankskip=False, noclip=False):
"liq_true_peak", # in case old " dBFS" values were stored in v1.2.3
]
for tag in suffixed_tags:
if tag in tags:
if tag in tags and isinstance(tags[tag], str):
if tags[tag].endswith(
(" dB", " LU", " dBFS", " dBTP", " LUFS")):
number, _, _ = tags[tag].rpartition(" ")
Expand Down Expand Up @@ -330,25 +353,25 @@ def read_tags(filename, target=TARGET_LUFS, blankskip=False, noclip=False):
tags_found["liq_loudness"],
tags_found["liq_true_peak_db"],
noclip
)
)
else:
skip_analysis = False

# if liq_blankskip different from requested, we need a re-analysis
if (skip_analysis
and "liq_blankskip" in tags_found
and (tags_found["liq_blankskip"] != blankskip)
):
and "liq_blankskip" in tags_found
and (tags_found["liq_blankskip"] != blankskip)
):
skip_analysis = False

# liq_loudness_range is only informational but we want to show correct values
# can’t blindly take replaygain_track_range—it might be in different unit
if (skip_analysis
and "liq_loudness_range" not in tags_found
):
and "liq_loudness_range" not in tags_found
):
skip_analysis = False

# print(skip_analysis, json.dumps(tags_found, indent=2))
# print(skip_analysis, json.dumps(tags_found, indent=2, sort_keys=True))
return skip_analysis, tags_found


Expand Down Expand Up @@ -650,7 +673,8 @@ def write_tags(filename, tags={}, replaygain=False):
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"])
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"]
Expand Down Expand Up @@ -940,12 +964,30 @@ parser.add_argument(
help="Linux/MacOS only: Use nice? Will run analysis at nice level 18.",
default=False,
action='store_true')
parser.add_argument(
"-j",
"--json",
help="Read/override tags from a JSON file. Use - to read from stdin. "
"Intended for pre-processing software which can, for instance, fill in "
"values from their database here.",
type=argparse.FileType('r'),
)

args = parser.parse_args()
args.target = float(args.target)
# args.target = float(args.target)

# read JSON from stdin or file, containing "overriding" or missing tags
# intended for pre-processing software
tags_json = {}
if args.json:
try:
tags_json = json.load(args.json)
except json.decoder.JSONDecodeError:
pass
args.json.close()

skip_analysis, tags_found = read_tags(
args.file, args.target, args.blankskip, args.noclip)
args.file, tags_json, args.target, args.blankskip, args.noclip)

if args.force or not skip_analysis:
result = analyse(
Expand Down
4 changes: 3 additions & 1 deletion test_autocue.cue_file.liq
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ enable_autocue_metadata()
#uri = "/home/matthias/Musik/Playlists/Radio/Classic Rock.m3u"
uri = "/home/matthias/media/videostream/yyy"
#songs = playlist(prefix="autocue2:", uri)
songs = playlist(prefix='annotate:liq_dummy="DUMMY":', uri)
#songs = playlist(prefix='annotate:liq_dummy="DUMMY":', uri)
# Test: Play 15s snippets by overriding some settings!
songs = playlist(prefix='annotate:liq_dummy="DUMMY",liq_cue_in=30.0,liq_cue_out=45.0,liq_cross_start_next=44.0,liq_fade_in=1.0,liq_fade_out=2.5:', uri)

# --- Use YOUR playlist here! ---
uri = "/home/matthias/Musik/Other/Jingles/Short"
Expand Down

0 comments on commit 81a76ec

Please sign in to comment.