Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/amplify/cli.py
Original file line number Diff line number Diff line change
@@ -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)

Expand Down Expand Up @@ -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()
Expand Down
25 changes: 25 additions & 0 deletions src/amplify/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"]
60 changes: 60 additions & 0 deletions src/amplify/render.py
Original file line number Diff line number Diff line change
@@ -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)