diff --git a/src/libs/autocue.liq b/src/libs/autocue.liq index 1f9c8fb83e..e124a61b46 100644 --- a/src/libs/autocue.liq +++ b/src/libs/autocue.liq @@ -184,13 +184,12 @@ end # Get frames from ffmpeg.filter.ebur128 # @flag hidden -def autocue.internal.ebur128(~ratio=50., ~timeout=10., filename) = +def autocue.internal.ebur128(~duration, ~ratio=50., ~timeout=10., filename) = ignore(ratio) ignore(timeout) ignore(filename) + ignore(duration) %ifdef ffmpeg.filter.ebur128 - duration = - null.get(default=0., request.duration(resolve_metadata=false, filename)) estimated_processing_time = duration / ratio if @@ -297,408 +296,441 @@ def autocue.internal.implementation( "Starting to process #{filename}" ) - frames = autocue.internal.ebur128(ratio=ratio, timeout=timeout, filename) +%ifdef request.duration.ffmpeg + duration = request.duration.ffmpeg(resolve_metadata=false, filename) +%else + duration = null() +%endif if - list.length(frames) < 2 + duration == null() then log( level=2, label="autocue.internal", - "Autocue computation failed!" + "Could not get request duration, internal autocue disabled!" ) null() else - # Get the 2nd last frame which is the last with loudness data - frame = list.nth(frames, list.length(frames) - 2) + duration = null.get(duration) - # Get the Integrated Loudness from the last frame (overall loudness) - lufs = - float_of_string( - list.assoc(default=string(lufs_target), "lavfi.r128.I", frame) + frames = + autocue.internal.ebur128( + duration=duration, ratio=ratio, timeout=timeout, filename ) - # Calc LUFS difference to target for liq_amplify - lufs_correction = lufs_target - lufs + if + list.length(frames) < 2 + then + log( + level=2, + label="autocue.internal", + "Autocue computation failed!" + ) + null() + else + # Get the 2nd last frame which is the last with loudness data + frame = list.nth(frames, list.length(frames) - 2) - # Create dB thresholds relative to LUFS target - lufs_cue_in_threshold = lufs + cue_in_threshold - lufs_cue_out_threshold = lufs + cue_out_threshold - lufs_cross_threshold = lufs + cross_threshold + # Get the Integrated Loudness from the last frame (overall loudness) + lufs = + float_of_string( + list.assoc(default=string(lufs_target), "lavfi.r128.I", frame) + ) - log( - level=4, - label="autocue.internal", - "Processing results for #{filename}" - ) + # Calc LUFS difference to target for liq_amplify + lufs_correction = lufs_target - lufs - log( - level=4, - label="autocue.internal", - "lufs_correction: #{lufs_correction}" - ) - log( - level=4, - label="autocue.internal", - "lufs_cue_in_threshold: #{lufs_cue_in_threshold}" - ) - log( - level=4, - label="autocue.internal", - "lufs_cue_out_threshold: #{lufs_cue_out_threshold}" - ) - log( - level=4, - label="autocue.internal", - "lufs_cross_threshold: #{lufs_cross_threshold}" - ) + # Create dB thresholds relative to LUFS target + lufs_cue_in_threshold = lufs + cue_in_threshold + lufs_cue_out_threshold = lufs + cue_out_threshold + lufs_cross_threshold = lufs + cross_threshold - # Set cue/fade defaults - cue_in = ref(0.) - cue_out = ref(0.) - cross_cue = ref(0.) - fade_in = ref(0.) - fade_out = ref(0.) - - # Extract timestamps for cue points - # Iterate over loudness data frames and set cue points based on db thresholds - last_ts = ref(0.) - current_ts = ref(0.) - cue_found = ref(false) - cross_start_idx = ref(0.) - cross_stop_idx = ref(0.) - cross_mid_idx = ref(0.) - cross_frame_length = ref(0.) - ending_fst_db = ref(0.) - ending_snd_db = ref(0.) - reset_iter_values = ref(true) - - frames_rev = list.rev(frames) - total_frames_length = float_of_int(list.length(frames)) - frame_idx = ref(total_frames_length - 1.) - lufs_cross_threshold_sustained = ref(lufs_cross_threshold) - lufs_cue_out_threshold_sustained = ref(lufs_cue_out_threshold) - - err = error.register("assoc") - def find_cues( - frame, - ~reverse_order=false, - ~sustained_ending_check=false, - ~sustained_ending_recalc=false - ) = - if - reset_iter_values() - then - last_ts := 0. - current_ts := 0. - cue_found := false - end + log( + level=4, + label="autocue.internal", + "Processing results for #{filename}" + ) - # Get current frame loudness level and timestamp - db_level = list.assoc(default="nan", string("lavfi.r128.M"), frame) - current_ts := - float_of_string(list.assoc(default="0.", "lavfi.liq.pts_time", frame)) + log( + level=4, + label="autocue.internal", + "lufs_correction: #{lufs_correction}" + ) + log( + level=4, + label="autocue.internal", + "lufs_cue_in_threshold: #{lufs_cue_in_threshold}" + ) + log( + level=4, + label="autocue.internal", + "lufs_cue_out_threshold: #{lufs_cue_out_threshold}" + ) + log( + level=4, + label="autocue.internal", + "lufs_cross_threshold: #{lufs_cross_threshold}" + ) - # Process only valid level values - if - db_level != "nan" - then - db_level = float_of_string(db_level) + # Set cue/fade defaults + cue_in = ref(0.) + cue_out = ref(0.) + cross_cue = ref(0.) + fade_in = ref(0.) + fade_out = ref(0.) + + # Extract timestamps for cue points + # Iterate over loudness data frames and set cue points based on db thresholds + last_ts = ref(0.) + current_ts = ref(0.) + cue_found = ref(false) + cross_start_idx = ref(0.) + cross_stop_idx = ref(0.) + cross_mid_idx = ref(0.) + cross_frame_length = ref(0.) + ending_fst_db = ref(0.) + ending_snd_db = ref(0.) + reset_iter_values = ref(true) + + frames_rev = list.rev(frames) + total_frames_length = float_of_int(list.length(frames)) + frame_idx = ref(total_frames_length - 1.) + lufs_cross_threshold_sustained = ref(lufs_cross_threshold) + lufs_cue_out_threshold_sustained = ref(lufs_cue_out_threshold) + + err = error.register("assoc") + def find_cues( + frame, + ~reverse_order=false, + ~sustained_ending_check=false, + ~sustained_ending_recalc=false + ) = + if + reset_iter_values() + then + last_ts := 0. + current_ts := 0. + cue_found := false + end + # Get current frame loudness level and timestamp + db_level = list.assoc(default="nan", string("lavfi.r128.M"), frame) + current_ts := + float_of_string( + list.assoc(default="0.", "lavfi.liq.pts_time", frame) + ) + + # Process only valid level values if - not sustained_ending_check and not sustained_ending_recalc + db_level != "nan" then - # Run regular cue point calc - reset_iter_values := false + db_level = float_of_string(db_level) + if - not reverse_order + not sustained_ending_check and not sustained_ending_recalc then - # Search for cue in + # Run regular cue point calc + reset_iter_values := false if - db_level > lufs_cue_in_threshold + not reverse_order then - # First time exceeding threshold - cue_in := last_ts() + # Search for cue in + if + db_level > lufs_cue_in_threshold + then + # First time exceeding threshold + cue_in := last_ts() - # Break - error.raise( - err, - "break list.iter" - ) + # Break + error.raise( + err, + "break list.iter" + ) + end + else + # Search for cue out and crossfade point starting from the end (reversed) + if + db_level > lufs_cue_out_threshold and not cue_found() + then + # Cue out + cue_out := last_ts() + cross_stop_idx := frame_idx() + cue_found := true + elsif + db_level > lufs_cross_threshold + then + # Absolute crossfade cue + cross_cue := last_ts() + cross_start_idx := frame_idx() + + # Break + error.raise( + err, + "break list.iter" + ) + end + frame_idx := frame_idx() - 1. end - else - # Search for cue out and crossfade point starting from the end (reversed) + elsif + sustained_ending_check + then + # Check regular crossfade data for sustained ending if - db_level > lufs_cue_out_threshold and not cue_found() + reset_iter_values() then - # Cue out - cue_out := last_ts() - cross_stop_idx := frame_idx() - cue_found := true - elsif - db_level > lufs_cross_threshold - then - # Absolute crossfade cue - cross_cue := last_ts() - cross_start_idx := frame_idx() + frame_idx := total_frames_length - 1. + cross_start_idx := cross_start_idx() + 5. + cross_stop_idx := cross_stop_idx() - 5. + cross_frame_length := cross_stop_idx() - cross_start_idx() + cross_mid_idx := cross_stop_idx() - (cross_frame_length() / 2.) + end + reset_iter_values := false - # Break + if + frame_idx() < cross_start_idx() + or + cross_frame_length() < sustained_endings_min_duration * 10. + then error.raise( err, "break list.iter" ) end - frame_idx := frame_idx() - 1. - end - elsif - sustained_ending_check - then - # Check regular crossfade data for sustained ending - if - reset_iter_values() - then - frame_idx := total_frames_length - 1. - cross_start_idx := cross_start_idx() + 5. - cross_stop_idx := cross_stop_idx() - 5. - cross_frame_length := cross_stop_idx() - cross_start_idx() - cross_mid_idx := cross_stop_idx() - (cross_frame_length() / 2.) - end - reset_iter_values := false - if - frame_idx() < cross_start_idx() - or - cross_frame_length() < sustained_endings_min_duration * 10. - then - error.raise( - err, - "break list.iter" - ) - end - - if - frame_idx() < cross_stop_idx() and frame_idx() > cross_mid_idx() - then if - ending_snd_db() < 0. + frame_idx() < cross_stop_idx() and frame_idx() > cross_mid_idx() then - ending_snd_db := (ending_snd_db() + db_level) / 2. - else - ending_snd_db := db_level + if + ending_snd_db() < 0. + then + ending_snd_db := (ending_snd_db() + db_level) / 2. + else + ending_snd_db := db_level + end end - end - if - frame_idx() > cross_start_idx() and frame_idx() < cross_mid_idx() - then if - ending_fst_db() < 0. + frame_idx() > cross_start_idx() + and + frame_idx() < cross_mid_idx() then - ending_fst_db := (ending_fst_db() + db_level) / 2. - else - ending_fst_db := db_level + if + ending_fst_db() < 0. + then + ending_fst_db := (ending_fst_db() + db_level) / 2. + else + ending_fst_db := db_level + end end - end - frame_idx := frame_idx() - 1. - elsif - sustained_ending_recalc - then - # Recalculate crossfade on sustained ending - if - reset_iter_values() - then - cue_out := 0. - cross_cue := 0. - end - reset_iter_values := false - if - db_level > lufs_cue_out_threshold_sustained() and not cue_found() - then - # Cue out - cue_out := last_ts() - cue_found := true - end - if - db_level > lufs_cross_threshold_sustained() + frame_idx := frame_idx() - 1. + elsif + sustained_ending_recalc then - # Absolute crossfade cue - cross_cue := current_ts() - error.raise( - err, - "break list.iter" - ) + # Recalculate crossfade on sustained ending + if + reset_iter_values() + then + cue_out := 0. + cross_cue := 0. + end + reset_iter_values := false + if + db_level > lufs_cue_out_threshold_sustained() + and + not cue_found() + then + # Cue out + cue_out := last_ts() + cue_found := true + end + if + db_level > lufs_cross_threshold_sustained() + then + # Absolute crossfade cue + cross_cue := current_ts() + error.raise( + err, + "break list.iter" + ) + end end - end - # Update last timestamp value with current - last_ts := current_ts() + # Update last timestamp value with current + last_ts := current_ts() + end end - end - - # Search for cue_in first - reset_iter_values := true - def cue_iter_fwd(frame) = - find_cues(frame) - end - try - list.iter(cue_iter_fwd, frames) - catch _ do - log( - level=4, - label="autocue.internal", - "cue_iter_fwd completed." - ) - end - # Reverse frames and search in reverse order for cross_cue and cue_out - reset_iter_values := true - def cue_iter_rev(frame) = - find_cues(frame, reverse_order=true) - end - try - list.iter(cue_iter_rev, frames_rev) - catch _ do - log( - level=4, - label="autocue.internal", - "cue_iter_rev completed." - ) - end - - if - sustained_endings_enabled - then - # Check for sustained ending + # Search for cue_in first reset_iter_values := true - def sustained_ending_check_iter(frame) = - find_cues(frame, sustained_ending_check=true) + def cue_iter_fwd(frame) = + find_cues(frame) end try - list.iter(sustained_ending_check_iter, frames_rev) + list.iter(cue_iter_fwd, frames) catch _ do log( level=4, - label="autocue.internal.sustained_ending", - "sustained_ending_check_iter completed." + label="autocue.internal", + "cue_iter_fwd completed." ) end - log( - level=4, - label="autocue.internal.sustained_ending", - "Analysis frame length: #{cross_frame_length()}" - ) - log( - level=4, - label="autocue.internal.sustained_ending", - "Avg. ending loudness: #{ending_fst_db()} => #{ending_snd_db()}" - ) + # Reverse frames and search in reverse order for cross_cue and cue_out + reset_iter_values := true + def cue_iter_rev(frame) = + find_cues(frame, reverse_order=true) + end + try + list.iter(cue_iter_rev, frames_rev) + catch _ do + log( + level=4, + label="autocue.internal", + "cue_iter_rev completed." + ) + end - # Check whether data indicate a sustained ending if - ending_fst_db() < 0. + sustained_endings_enabled then - slope = ref(0.) - dropoff = lufs_cross_threshold / ending_fst_db() - - if - ending_snd_db() < 0. - then - slope := ending_fst_db() / ending_snd_db() + # Check for sustained ending + reset_iter_values := true + def sustained_ending_check_iter(frame) = + find_cues(frame, sustained_ending_check=true) + end + try + list.iter(sustained_ending_check_iter, frames_rev) + catch _ do + log( + level=4, + label="autocue.internal.sustained_ending", + "sustained_ending_check_iter completed." + ) end log( level=4, label="autocue.internal.sustained_ending", - "Drop off: #{(1. - dropoff) * 100.}%" + "Analysis frame length: #{cross_frame_length()}" ) log( level=4, label="autocue.internal.sustained_ending", - "Slope: #{(1. - slope()) * 100.}%" + "Avg. ending loudness: #{ending_fst_db()} => #{ending_snd_db()}" ) - detect_slope = slope() > 1. - sustained_endings_slope / 100. - detect_dropoff = - ending_fst_db() > - lufs_cross_threshold * (sustained_endings_dropoff / 100. + 1.) + # Check whether data indicate a sustained ending if - detect_slope or detect_dropoff + ending_fst_db() < 0. then - log( - level=3, - label="autocue.internal.sustained_ending", - "Sustained ending detected (drop off: #{detect_dropoff} / slope: #{ - detect_slope - })" - ) + slope = ref(0.) + dropoff = lufs_cross_threshold / ending_fst_db() if - detect_slope + ending_snd_db() < 0. then - lufs_cross_threshold_sustained := - max( - lufs_cross_threshold * sustained_endings_threshold_limit, - ending_snd_db() - 0.5 - ) - else - lufs_cross_threshold_sustained := - max( - lufs_cross_threshold * sustained_endings_threshold_limit, - ending_fst_db() - 0.5 - ) + slope := ending_fst_db() / ending_snd_db() end - lufs_cue_out_threshold_sustained = - ref( - max( - lufs_cue_out_threshold * sustained_endings_threshold_limit, - lufs_cue_out_threshold + - (lufs_cross_threshold_sustained() - lufs_cross_threshold) - ) - ) log( level=4, label="autocue.internal.sustained_ending", - "Changed crossfade threshold: #{lufs_cross_threshold} => #{ - lufs_cross_threshold_sustained() - }" + "Drop off: #{(1. - dropoff) * 100.}%" ) log( level=4, label="autocue.internal.sustained_ending", - "Changed cue out threshold: #{lufs_cue_out_threshold} => #{ - lufs_cue_out_threshold_sustained() - }" + "Slope: #{(1. - slope()) * 100.}%" ) - cross_cue_init = cross_cue() - cue_out_init = cue_out() + detect_slope = slope() > 1. - sustained_endings_slope / 100. + detect_dropoff = + ending_fst_db() > + lufs_cross_threshold * (sustained_endings_dropoff / 100. + 1.) + if + detect_slope or detect_dropoff + then + log( + level=3, + label="autocue.internal.sustained_ending", + "Sustained ending detected (drop off: #{detect_dropoff} / slope: \ + #{detect_slope})" + ) + + if + detect_slope + then + lufs_cross_threshold_sustained := + max( + lufs_cross_threshold * sustained_endings_threshold_limit, + ending_snd_db() - 0.5 + ) + else + lufs_cross_threshold_sustained := + max( + lufs_cross_threshold * sustained_endings_threshold_limit, + ending_fst_db() - 0.5 + ) + end + lufs_cue_out_threshold_sustained = + ref( + max( + lufs_cue_out_threshold * sustained_endings_threshold_limit, + lufs_cue_out_threshold + + (lufs_cross_threshold_sustained() - lufs_cross_threshold) + ) + ) - reset_iter_values := true - def sustained_ending_recalc_iter(frame) = - find_cues(frame, sustained_ending_recalc=true) - end - try - list.iter(sustained_ending_recalc_iter, frames_rev) - catch _ do log( level=4, - label="autocue.internal", - "sustained_ending_recalc_iter completed." + label="autocue.internal.sustained_ending", + "Changed crossfade threshold: #{lufs_cross_threshold} => #{ + lufs_cross_threshold_sustained() + }" + ) + log( + level=4, + label="autocue.internal.sustained_ending", + "Changed cue out threshold: #{lufs_cue_out_threshold} => #{ + lufs_cue_out_threshold_sustained() + }" ) - end - log( - level=4, - label="autocue.internal.sustained_ending", - "Changed crossfade point: #{cross_cue_init} => #{cross_cue()}" - ) - log( - level=4, - label="autocue.internal.sustained_ending", - "Changed cue out point: #{cue_out_init} => #{cue_out()}" - ) + cross_cue_init = cross_cue() + cue_out_init = cue_out() + + reset_iter_values := true + def sustained_ending_recalc_iter(frame) = + find_cues(frame, sustained_ending_recalc=true) + end + try + list.iter(sustained_ending_recalc_iter, frames_rev) + catch _ do + log( + level=4, + label="autocue.internal", + "sustained_ending_recalc_iter completed." + ) + end + + log( + level=4, + label="autocue.internal.sustained_ending", + "Changed crossfade point: #{cross_cue_init} => #{cross_cue()}" + ) + log( + level=4, + label="autocue.internal.sustained_ending", + "Changed cue out point: #{cue_out_init} => #{cue_out()}" + ) + else + log( + level=3, + label="autocue.internal.sustained_ending", + "No sustained ending detected." + ) + end else log( level=3, @@ -706,129 +738,74 @@ def autocue.internal.implementation( "No sustained ending detected." ) end - else - log( - level=3, - label="autocue.internal.sustained_ending", - "No sustained ending detected." - ) end - end - - # Finalize cue/cross/fade values now... - if - cue_out() == 0. - then - file_duration = - begin - # Duration from the filter frames can be tricky so we first try a proper duration: - request_duration = -%ifdef request.duration.ffmpeg - request.duration.ffmpeg(resolve_metadata=false, filename) -%else - null() -%endif - if - null.defined(request_duration) - then - null.get(request_duration) - else - # Get very last frame for precise track duration - frame_duration = - float_of_string( - list.assoc(default="0.", "lavfi.liq.duration_time", frame) - ) + # Finalize cue/cross/fade values now... + if cue_out() == 0. then cue_out := duration end - frame_duration = - if - frame_duration != 0.1 - then - log.important( - label="autocue", - "Warning: reported frame duration should be 100ms. Either \ - the FFmpeg ebur128 filter has changed its internals or the \ - version/build of FFmpeg you are using is buggy. We \ - recommend using a fairly recent distribution with FFmpeg \ - version 7 or above. Backported packages can be tricky." - ) - 0.1 - else - frame_duration - end - - float_of_string( - list.assoc(default="0.", "lavfi.liq.pts_time", frame) - ) + - frame_duration - end - end - - cue_out := file_duration - end + # Calc cross/overlap duration + if + cross_cue() + 0.1 < cue_out() + then + fade_out := cue_out() - cross_cue() + end - # Calc cross/overlap duration - if - cross_cue() + 0.1 < cue_out() - then - fade_out := cue_out() - cross_cue() - end + # Add some margin to cue in + cue_in := cue_in() - 0.1 - # Add some margin to cue in - cue_in := cue_in() - 0.1 + # Avoid hard cuts on cue in + if + cue_in() > 0.2 + then + fade_in := 0.2 + cue_in := cue_in() - 0.2 + end - # Avoid hard cuts on cue in - if - cue_in() > 0.2 - then - fade_in := 0.2 - cue_in := cue_in() - 0.2 - end + # Ignore super short cue in + if + cue_in() <= 0.2 + then + fade_in := 0. + cue_in := 0. + end - # Ignore super short cue in - if - cue_in() <= 0.2 - then - fade_in := 0. - cue_in := 0. - end + # Limit overlap duration to maximum + if max_overlap < fade_in() then fade_in := max_overlap end - # Limit overlap duration to maximum - if max_overlap < fade_in() then fade_in := max_overlap end + if + max_overlap < fade_out() + then + cue_shift = fade_out() - max_overlap + cue_out := cue_out() - cue_shift + fade_out := max_overlap + fade_out := max_overlap + end - if - max_overlap < fade_out() - then - cue_shift = fade_out() - max_overlap - cue_out := cue_out() - cue_shift - fade_out := max_overlap - fade_out := max_overlap + ( + { + amplify= + "#{lufs_correction} dB", + cue_in=cue_in(), + cue_out=cue_out(), + fade_in=fade_in(), + fade_out=fade_out() + } + : + { + amplify?: string, + cue_in: float, + cue_out: float, + fade_in: float, + fade_in_type?: string, + fade_in_curve?: float, + fade_out: float, + fade_out_type?: string, + fade_out_curve?: float, + start_next?: float, + extra_metadata?: [(string*string)] + } + ) end - - ( - { - amplify= - "#{lufs_correction} dB", - cue_in=cue_in(), - cue_out=cue_out(), - fade_in=fade_in(), - fade_out=fade_out() - } - : - { - amplify?: string, - cue_in: float, - cue_out: float, - fade_in: float, - fade_in_type?: string, - fade_in_curve?: float, - fade_out: float, - fade_out_type?: string, - fade_out_curve?: float, - start_next?: float, - extra_metadata?: [(string*string)] - } - ) end end end