Source code for brownify.runners

from typing import Dict, List

import soundfile as sf
from pydub import AudioSegment
from pydub.exceptions import PydubException
from tqdm import tqdm

from brownify.errors import (
    InvalidInputError,
    MergingError,
    NoPipelineSourceError,
)
from brownify.models import Pipeline, Track
from brownify.splitters import AudioSplitter


[docs]class PipelineProcessor: """Class for processing a series of Pipeline objects""" def __init__(self, target: str, splitter: AudioSplitter): self.tracks: Dict[str, Track] = {} self.target = target self._splitter = splitter self._saved_files: List[str] = [] for channel in tqdm( splitter.get_channels(), "Loading split sources for processing..." ): filename = f"./{target}/{channel}.wav" self.tracks[channel] = self._load_track(filename) @staticmethod def _load_track(filename: str) -> Track: audio, sample_rate = sf.read(filename) is_stereo = True if len(audio.shape) == 1: is_stereo = False num_channels = 2 if is_stereo else 1 return Track( audio=audio, num_channels=num_channels, sample_rate=sample_rate, save=False, ) @staticmethod def _save_track(filename: str, track: Track) -> None: sf.write(filename, track.audio, track.sample_rate) def _process(self, pipelines: List[Pipeline]) -> None: for pipeline in tqdm(pipelines, desc="Brownifying..."): if pipeline.source not in self.tracks: raise NoPipelineSourceError( f"No track has been loaded with the name {pipeline.source}" f" yet. Is your recipe correct? Pipeline: {pipeline}" ) track = self.tracks[pipeline.source].clone() for action in pipeline.actions: track = action(track) # It is OK to overwrite sources with sinks, so no need to check self.tracks[pipeline.sink] = Track( audio=track.audio, num_channels=track.num_channels, sample_rate=track.sample_rate, save=pipeline.save, ) def _save(self) -> None: for name in tqdm(self.tracks, desc="Preparing to merge tracks..."): track = self.tracks[name] if track.save: filename = f"./{self.target}/{name}.wav" self._save_track(filename, track) self._saved_files.append(filename) def _merge(self, output_file: str) -> None: merged_audio = AudioMerger.merge(self._saved_files) AudioMerger.save_file(output_file, merged_audio)
[docs] def process(self, pipelines: List[Pipeline], output_file: str) -> None: """Process a series of pipelines and output a processed audio file Args: pipelines: List of pipelines to apply to generate an audio track output_file: Path to output the track to """ self._process(pipelines) self._save() self._merge(output_file)
[docs]class AudioMerger:
[docs] @staticmethod def merge(files: List[str]) -> AudioSegment: """Merge the list of audio sources into an AudioSegment Args: files: List of track sources to merge Raises: InvalidInputError: If no files were provided to merge, then this is likely the result of a bad input recipe. MergingError: If the tracks are unabled to be merged Returns: The overlaid tracks merged into an AudioSegment """ if len(files) == 0: raise InvalidInputError( "No files were provided to merge. There is likely an issue " "with the input recipe." ) try: merged = AudioSegment.from_wav(files[0]) if len(files) > 1: for f in tqdm(files[1:], "Merging tracks..."): audio = AudioSegment.from_wav(f) merged = merged.overlay(audio) return merged except PydubException: raise MergingError( f"Unable to combine tracks backed by files: {files}" )
[docs] @staticmethod def save_file(filename: str, audio: AudioSegment) -> None: """Save the merged audio file Args: filename: Path to use for the merged file audio: AudioSegment to save Raises: MergingError: If unable to save the merged track """ try: audio.export(filename, format="mp3") except PydubException: raise MergingError( f"Unable to save merged track to file {filename}" )