diff --git a/src/amplify/cli.py b/src/amplify/cli.py index cd572bc..7517a36 100644 --- a/src/amplify/cli.py +++ b/src/amplify/cli.py @@ -1,6 +1,7 @@ import typer from pathlib import Path from amplify.config import load_cfg, save_cfg, ensure_cfg +from amplify.render import export as render_export app = typer.Typer(add_completion=False) @@ -85,7 +86,8 @@ def export(cfg: str, out: str): data = load_cfg(cfg) data["export"]["path"] = out save_cfg(cfg, data) - typer.echo(f"Would export to {out} (render engine not connected).") + render_export(data) + typer.echo(f"Exported to {out}") def main(): app() diff --git a/src/amplify/pyproject.toml b/src/amplify/pyproject.toml new file mode 100644 index 0000000..612c032 --- /dev/null +++ b/src/amplify/pyproject.toml @@ -0,0 +1,25 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "amplify" +version = "0.1.0" +description = "CLI audio manipulation tool" +requires-python = ">=3.10" +dependencies = [ + "typer>=0.12", + "pyyaml>=6.0", + "pydub>=0.25.1", + "librosa>=0.10.1", + "soundfile>=0.12.1" +] + +[project.scripts] +amplify = "amplify.cli:main" + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/src/amplify/render.py b/src/amplify/render.py new file mode 100644 index 0000000..3346a8c --- /dev/null +++ b/src/amplify/render.py @@ -0,0 +1,60 @@ +import numpy as np +import librosa +import soundfile as sf +from pathlib import Path + +def apply_scale(audio, sr, factor, preserve_pitch): + """ + Time-scale audio + Scale factor > 1.0 = faster audio + Scale factor < 1.0 = slower audio + """ + + # if preserving pitch, change duration but keep pitch the same + if preserve_pitch: + return librosa.effects.time_stretch(audio, rate=factor) + + # if not preserving pitch, change sample rate by a factor + else: + new_sr = int(sr * factor) + return librosa.resample(audio, orig_sr = sr, target_sr = new_sr) + +def render(cfg_data): + + # stores tracks + mix_buffer = [] + + # stores sample rate, set with first audio file in track + sample_rate = None + + for item in cfg_data["timeline"]: + # get asset id of current item + asset_id = item["asset"] + + # finds next asset matching id + asset = next(a for a in cfg_data["assets"] if a["id"] == asset_id) + audio, file_sr = librosa.load(asset["path"], sr = None) + + if sample_rate is None: + sample_rate = file_sr + + for op in item["ops"]: + if op["type"] == "scale": + audio = apply_scale(audio, file_sr, op["factor"], op["preserve_pitch"]) + elif op["type"] == "loop": + if op["count"]: + audio = np.tile(audio, op["count"]) + mix_buffer.append(audio) + output = np.sum(mix_buffer, axis = 0) + + if cfg_data["mix"]["normalize"]: + peak = np.max(np.abs(output)) + if peak > 0: + output = output / peak + + return output, sample_rate + +def export(cfg_data): + audio, sr = render(cfg_data) + out_path = Path(cfg_data["export"]["path"]) + sf.write(out_path, audio, sr) \ No newline at end of file