from __future__ import annotations import uuid from pathlib import Path from tempfile import gettempdir import gradio as gr import soundfile as sf try: import spaces except ImportError: class spaces: class GPU: def __init__(self, func=None, duration=60): self.func = func def __call__(self, *args, **kwargs): if self.func is not None: return self.func(*args, **kwargs) return args[0] from pyharp import ModelCard, build_endpoint, SampleRate, Channels from pyharp.tags import Subcategory from microtcn_runtime import process_audio MIN_AUDIO_SECONDS = 0.5 MAX_AUDIO_SECONDS = 30 OUTPUT_ROOT = Path(gettempdir()) / "microtcn_outputs" model_card = ModelCard( name="micro-TCN", description=( "Emulate a Teletronix LA-2A optical compressor/limiter using a " "lightweight temporal convolutional network. The model is fixed-rate " "and mono, so the result is resampled back to the input's rate and " "copied across its channels: the output has the format it came in with, " "but no stereo image. Repository: " "https://github.com/csteinmetz1/micro-tcn Paper: " "https://arxiv.org/abs/2102.06200" ), author="Christian J. Steinmetz & Joshua D. Reiss", tags=[Subcategory.NEURAL_ANALOG_EFFECTS, SampleRate(44100), Channels(1), 'compressor', 'limiter', 'analog-emulation'], ) @spaces.GPU(duration=120) def match_input_format(output_path: str, input_path: str) -> str: """ Rewrite an output file at the sample rate and channel count it came in with. The models here are fixed-rate and mono, and hand the writing to vendored upstream code, so the format is corrected afterwards rather than threaded through inference. Duplicated channels are copies, not a recovered stereo image; the model card says so. Args: output_path (str): File the model wrote. Overwritten in place. input_path (str): File the request supplied. Returns: output_path (str): The same path, for use in a return statement. """ import librosa import numpy as np import soundfile want = soundfile.info(input_path) have = soundfile.info(output_path) if have.samplerate == want.samplerate and have.channels == want.channels: return output_path data, rate = soundfile.read(output_path, always_2d=True) if rate != want.samplerate: data = librosa.resample( data.T, orig_sr=rate, target_sr=want.samplerate ).T if data.shape[1] == 1 and want.channels > 1: data = np.tile(data, (1, want.channels)) elif data.shape[1] > 1 and want.channels == 1: data = data.mean(axis=1, keepdims=True) soundfile.write(output_path, data, want.samplerate) return output_path def process_fn( audio_path: str | None, mode: str, peak_reduction: float, ) -> str: if not audio_path: raise gr.Error("Please upload an audio file.") try: duration = sf.info(audio_path).duration except Exception as exc: raise gr.Error(f"Could not read the audio file: {exc}") from exc if duration < MIN_AUDIO_SECONDS: raise gr.Error("Audio must be at least 0.5 seconds long.") if duration > MAX_AUDIO_SECONDS: raise gr.Error( f"Audio must be no longer than {MAX_AUDIO_SECONDS} seconds. " f"Received {duration:.1f} seconds." ) limit_mode = 1.0 if mode == "Limit" else 0.0 try: audio, sample_rate = process_audio( audio_path=audio_path, limit_mode=limit_mode, peak_reduction=peak_reduction, ) except Exception as exc: raise gr.Error(f"micro-TCN inference failed: {exc}") from exc output_dir = OUTPUT_ROOT / uuid.uuid4().hex output_dir.mkdir(parents=True, exist_ok=True) output_path = output_dir / "microtcn_output.wav" sf.write(str(output_path), audio, sample_rate) return match_input_format(str(output_path), audio_path) if __name__ == "__main__": with gr.Blocks(title="micro-TCN LA-2A Emulation") as demo: input_components = [ gr.Audio(type="filepath", label="Input Audio") .harp_required(True) .set_info("Audio clip to process, up to 30 seconds."), gr.Dropdown( choices=["Compress", "Limit"], value="Compress", label="Mode", ).set_info( "Compress for gentle gain reduction, Limit for harder peak control." ), gr.Slider( minimum=0.0, maximum=1.0, value=0.42, step=0.01, label="Peak Reduction", ).set_info("Normalized peak-reduction control, from 0 to 1."), ] output_components = [ gr.Audio( type="filepath", label="Processed Audio", ).set_info("Audio processed through the LA-2A emulation."), ] build_endpoint( model_card=model_card, input_components=input_components, output_components=output_components, process_fn=process_fn, ) demo.queue(default_concurrency_limit=1).launch(show_error=True, pwa=True)