diff --git a/pymusiclooper/cli.py b/pymusiclooper/cli.py index b5284b9..63aa9fe 100644 --- a/pymusiclooper/cli.py +++ b/pymusiclooper/cli.py @@ -191,9 +191,10 @@ def split_audio(**kwargs): @common_loop_options @common_export_options @click.option('--format', type=click.Choice(("WAV", "FLAC", "OGG", "MP3"), case_sensitive=False), default="MP3", show_default=True, help="Audio format to use for the output audio file.") -@click.option('--extended-length', type=float, required=True, help="Desired length of the extended looped track in seconds. [Must be longer than the audio's original length.]") +@click.option('--extended-length', type=float, required=False, help="Desired length of the extended looped track in seconds. [Must be longer than the audio's original length.] [dim cyan]\[mutually exclusive with --extended-count][/] [dim red]\[at least one required][/]") +@click.option('--extended-count', type=float, required=False, help="Desired number of loops. [Must be more than 1.] [dim cyan]\[mutually exclusive with --extended-length][/] [dim red]\[at least one required][/]") @click.option('--fade-length', type=float, default=5, show_default=True, help="Desired length of the loop fade out in seconds.") -@click.option('--disable-fade-out', is_flag=True, default=False, help="Extend the track with all its sections (intro/loop/outro) without fading out. --extended-length will be treated as an 'at least' constraint.") +@click.option('--disable-fade-out', is_flag=True, default=False, help="Extend the track with all its sections (intro/loop/outro) without fading out. --extended-length and --extended-count will be treated as an 'at least' constraint.") def extend(**kwargs): """Create an extended version of the input audio by looping it to a specific length.""" run_handler(**kwargs) diff --git a/pymusiclooper/console.py b/pymusiclooper/console.py index 20e1642..7902ae0 100644 --- a/pymusiclooper/console.py +++ b/pymusiclooper/console.py @@ -48,7 +48,7 @@ def _option_groups(additional_basic_options=None): "pymusiclooper split-audio": _common_option_groups, "pymusiclooper tag": _option_groups(["--tag-names", "--tag-offset"]), "pymusiclooper export-points": _option_groups(["--export-to", "--alt-export-top", "--fmt"]), - "pymusiclooper extend": _option_groups(["--extended-length", "--fade-length", "--disable-fade-out"]), + "pymusiclooper extend": _option_groups(["--extended-length", "--extended-count", "--fade-length", "--disable-fade-out"]), } _COMMAND_GROUPS = { "pymusiclooper": [ diff --git a/pymusiclooper/core.py b/pymusiclooper/core.py index 82819d2..9c5fe94 100644 --- a/pymusiclooper/core.py +++ b/pymusiclooper/core.py @@ -154,7 +154,8 @@ def extend( self, loop_start: int, loop_end: int, - extended_length: float, + extended_length: Optional[float], + extended_count: Optional[float], fade_length: float = 5, disable_fade_out: bool = False, format: str = "WAV", @@ -177,16 +178,21 @@ def extend( else: out_path = os.path.abspath(self.mlaudio.filepath) - if extended_length < self.mlaudio.total_duration: - raise ValueError( - "Extended length must be greater than the audio's original length." - ) - intro = self.mlaudio.playback_audio[:loop_start] loop = self.mlaudio.playback_audio[loop_start:loop_end] outro = self.mlaudio.playback_audio[loop_end:] - loop_extended_length = self.mlaudio.seconds_to_samples(extended_length) - intro.shape[0] + if extended_length is not None and extended_length < self.mlaudio.total_duration: + raise ValueError( + "Extended length must be greater than the audio's original length." + ) + + if extended_count is not None: + if extended_length is not None: + raise ValueError("Must not specify extended length and extended count simultaneously.") + loop_extended_length = round(extended_count * loop.shape[0]) + else: + loop_extended_length = self.mlaudio.seconds_to_samples(extended_length) - intro.shape[0] # If the outro will be included, account for its length when calculating the new loop duration if disable_fade_out: @@ -212,23 +218,28 @@ def extend( ) # Format extended file name with its duration suffixed - extended_loop_length = final_loop.shape[0] + ( - loop.shape[0] * (int(loop_factor)) - ) - extended_audio_length = ( - intro.shape[0] - + extended_loop_length - + (outro.shape[0] if disable_fade_out else 0) - ) - total_length_seconds = self.mlaudio.samples_to_seconds(extended_audio_length) - duration_sec = ceil(total_length_seconds%60) - duration_mins = int(total_length_seconds//60) - if duration_sec == 60: - duration_sec = 0 - duration_mins += 1 - extended_audio_length_fmt = ( - f"{duration_mins:d}m{duration_sec:02d}s" - ) + if extended_length is not None: + extended_loop_length = final_loop.shape[0] + ( + loop.shape[0] * (int(loop_factor)) + ) + extended_audio_length = ( + intro.shape[0] + + extended_loop_length + + (outro.shape[0] if disable_fade_out else 0) + ) + total_length_seconds = self.mlaudio.samples_to_seconds(extended_audio_length) + duration_sec = ceil(total_length_seconds%60) + duration_mins = int(total_length_seconds//60) + if duration_sec == 60: + duration_sec = 0 + duration_mins += 1 + extended_audio_length_fmt = ( + f"{duration_mins:d}m{duration_sec:02d}s" + ) + else: + extended_audio_length_fmt = ( + f"{extended_count:g}lp" + ) output_file_path = ( f"{out_path}-extended-{extended_audio_length_fmt}.{format.lower()}" ) diff --git a/pymusiclooper/handler.py b/pymusiclooper/handler.py index 2d4c148..5165ba4 100644 --- a/pymusiclooper/handler.py +++ b/pymusiclooper/handler.py @@ -197,7 +197,8 @@ def __init__( tag_names: Optional[Tuple[str, str]] = None, tag_offset: Optional[bool] = None, batch_mode: bool = False, - extended_length: float = 0, + extended_length: Optional[float] = None, + extended_count: Optional[float] = None, fade_length: float = 0, disable_fade_out: bool = False, **kwargs, @@ -222,7 +223,10 @@ def __init__( self.tag_names = tag_names self.tag_offset = tag_offset self.batch_mode = batch_mode + if (extended_length is not None) and (extended_count is not None): + raise ValueError("Must not specify --extended-length and --extended_count simultaneously") self.extended_length = extended_length + self.extended_count = extended_count self.disable_fade_out = disable_fade_out self.fade_length = fade_length self._is_autocreated_outdir = False @@ -245,6 +249,7 @@ def run(self): or self.to_txt or self.split_audio or self.extended_length + or self.extended_count is not None ) and not os.path.exists(self.output_directory): os.mkdir(self.output_directory) self._is_autocreated_outdir = True @@ -258,7 +263,7 @@ def run(self): if self.split_audio: self.split_audio_runner(loop_start, loop_end) - if self.extended_length: + if self.extended_length or self.extended_count is not None: self.extend_track_runner(loop_start, loop_end) finally: if ( @@ -305,6 +310,7 @@ def extend_track_runner(self, loop_start: int, loop_end: int): format=self.format, output_dir=self.output_directory, extended_length=self.extended_length, + extended_count=self.extended_count, disable_fade_out=self.disable_fade_out, fade_length=self.fade_length, )