diff --git a/pikaraoke/app.py b/pikaraoke/app.py index 45dc99d..8ac871a 100644 --- a/pikaraoke/app.py +++ b/pikaraoke/app.py @@ -174,6 +174,7 @@ def main(): bg_video_path=args.bg_video_path, disable_score=args.disable_score, limit_user_songs_by=args.limit_user_songs_by, + avsync=args.avsync, config_file_path=args.config_file_path, ) diff --git a/pikaraoke/karaoke.py b/pikaraoke/karaoke.py index 8089416..8102069 100644 --- a/pikaraoke/karaoke.py +++ b/pikaraoke/karaoke.py @@ -108,6 +108,7 @@ def __init__( disable_bg_video=False, disable_score=False, limit_user_songs_by=0, + avsync=0, config_file_path="config.ini", ): logging.basicConfig( @@ -161,6 +162,7 @@ def __init__( self.limit_user_songs_by = ( self.get_user_preference("limit_user_songs_by") or limit_user_songs_by ) + self.avsync = self.get_user_preference("avsync") or avsync self.url_override = url self.url = self.get_url() @@ -458,7 +460,10 @@ def play_file(self, file_path, semitones=0): logging.info(f"Playing file: {file_path} transposed {semitones} semitones") requires_transcoding = ( - semitones != 0 or self.normalize_audio or is_transcoding_required(file_path) + semitones != 0 + or self.normalize_audio + or is_transcoding_required(file_path) + or self.avsync != 0 ) logging.debug(f"Requires transcoding: {requires_transcoding}") @@ -493,7 +498,11 @@ def play_file(self, file_path, semitones=0): else: self.kill_ffmpeg() ffmpeg_cmd = build_ffmpeg_cmd( - fr, semitones, self.normalize_audio, self.complete_transcode_before_play + fr, + semitones, + self.normalize_audio, + self.complete_transcode_before_play, + self.avsync, ) self.ffmpeg_process = ffmpeg_cmd.run_async(pipe_stderr=True, pipe_stdin=True) diff --git a/pikaraoke/lib/args.py b/pikaraoke/lib/args.py index df190f8..e6bd8f6 100644 --- a/pikaraoke/lib/args.py +++ b/pikaraoke/lib/args.py @@ -237,6 +237,12 @@ def parse_pikaraoke_args(): default="0", required=False, ), + parser.add_argument( + "--avsync", + help="Use avsync (in seconds) if the audio and video streams are out of sync. (negative = advances audio | positive = delays audio)", + default="0", + required=False, + ), parser.add_argument( "--config-file-path", help=f"Path to a config file to load settings from. Config file settings are set in the web interface or manually edited and will override command line arguments. Default {default_config_file_path}", diff --git a/pikaraoke/lib/ffmpeg.py b/pikaraoke/lib/ffmpeg.py index 397e37e..f99b4b7 100644 --- a/pikaraoke/lib/ffmpeg.py +++ b/pikaraoke/lib/ffmpeg.py @@ -12,7 +12,10 @@ def get_media_duration(file_path): return None -def build_ffmpeg_cmd(fr, semitones=0, normalize_audio=True, buffer_fully_before_playback=False): +def build_ffmpeg_cmd( + fr, semitones=0, normalize_audio=True, buffer_fully_before_playback=False, avsync=0 +): + avsync = float(avsync) # use h/w acceleration on pi default_vcodec = "h264_v4l2m2m" if supports_hardware_h264_encoding() else "libx264" # just copy the video stream if it's an mp4 or webm file, since they are supported natively in html5 @@ -24,13 +27,21 @@ def build_ffmpeg_cmd(fr, semitones=0, normalize_audio=True, buffer_fully_before_ # copy the audio stream if no transposition/normalization, otherwise reincode with the aac codec is_transposed = semitones != 0 - acodec = "aac" if is_transposed or normalize_audio else "copy" + acodec = "aac" if is_transposed or normalize_audio or avsync != 0 else "copy" + input = ffmpeg.input(fr.file_path) + audio = input.audio + + # If avsync is set, delay or trim the audio stream + if avsync > 0: + audio = audio.filter("adelay", f"{avsync * 1000}|{avsync * 1000}") # delay + elif avsync < 0: + audio = audio.filter("atrim", start=-avsync) # trim # The pitch value is (2^x/12), where x represents the number of semitones pitch = 2 ** (semitones / 12) - audio = input.audio.filter("rubberband", pitch=pitch) if is_transposed else input.audio + audio = audio.filter("rubberband", pitch=pitch) if is_transposed else audio # normalize the audio audio = audio.filter("loudnorm", i=-16, tp=-1.5, lra=11) if normalize_audio else audio diff --git a/pikaraoke/routes/info.py b/pikaraoke/routes/info.py index 7bacf5e..4697da8 100644 --- a/pikaraoke/routes/info.py +++ b/pikaraoke/routes/info.py @@ -73,6 +73,7 @@ def info(): disable_score=k.disable_score, hide_url=k.hide_url, limit_user_songs_by=k.limit_user_songs_by, + avsync=k.avsync, hide_notifications=k.hide_notifications, hide_overlay=k.hide_overlay, normalize_audio=k.normalize_audio, diff --git a/pikaraoke/templates/info.html b/pikaraoke/templates/info.html index 689423c..d4eadac 100644 --- a/pikaraoke/templates/info.html +++ b/pikaraoke/templates/info.html @@ -241,6 +241,13 @@
{# MSG: Help text explaining when videos will be transcoded #} {% trans %}* Videos are only transcoded when: normalization is on, a song is transposed, playing a CDG/MOV/AVI/MKV file. Most unmodified MP4 files will not need to be transcoded.{% endtrans %}
+{# MSG: Help text explaining when videos will be transcoded #} {% trans %}* Videos are only transcoded when: normalization is on, audio/video synchronization not zero, a song is transposed, playing a CDG/MOV/AVI/MKV file. Most unmodified MP4 files will not need to be transcoded.{% endtrans %}