KU_SW_Academy / audio_processing /effect_chain.py
heybaeheef's picture
Upload 9 files
3cc9d6f verified
raw
history blame
7.67 kB
"""
Audio Effect Chain
==================
์‹ค์ œ ์˜ค๋””์˜ค์— ์ดํŽ™ํŠธ๋ฅผ ์ ์šฉํ•˜๋Š” ์ฒ˜๋ฆฌ ์ฒด์ธ
pedalboard ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ (Spotify์—์„œ ๋งŒ๋“  ์˜ค๋””์˜ค ํ”Œ๋Ÿฌ๊ทธ์ธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ)
- ๊ณ ํ’ˆ์งˆ VST ์ˆ˜์ค€์˜ ์ดํŽ™ํŠธ
- Python์—์„œ ์‰ฝ๊ฒŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
- ์‹ค์‹œ๊ฐ„ ์ฒ˜๋ฆฌ๋„ ๊ฐ€๋Šฅ
"""
import numpy as np
from pathlib import Path
from typing import Dict, Any, List
import soundfile as sf
# pedalboard - ์˜ค๋””์˜ค ์ดํŽ™ํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
from pedalboard import (
Pedalboard,
Compressor,
Gain,
LowShelfFilter,
HighShelfFilter,
PeakFilter,
Delay,
Reverb,
Distortion,
Limiter,
HighpassFilter,
LowpassFilter
)
from pedalboard.io import AudioFile
class EffectChain:
"""์˜ค๋””์˜ค ์ดํŽ™ํŠธ ์ฒ˜๋ฆฌ ์ฒด์ธ"""
AVAILABLE_EFFECTS = [
"eq_lowshelf",
"eq_highshelf",
"eq_peak1",
"eq_peak2",
"compressor",
"distortion",
"delay",
"reverb",
"limiter"
]
def __init__(self):
"""์ดํŽ™ํŠธ ์ฒด์ธ ์ดˆ๊ธฐํ™”"""
pass
def get_available_effects(self) -> List[str]:
"""์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ดํŽ™ํŠธ ๋ชฉ๋ก ๋ฐ˜ํ™˜"""
return self.AVAILABLE_EFFECTS.copy()
def process(
self,
input_path: str,
output_path: str,
parameters: Dict[str, float]
) -> None:
"""
์˜ค๋””์˜ค ํŒŒ์ผ์— ์ดํŽ™ํŠธ ์ฒด์ธ ์ ์šฉ
Args:
input_path: ์ž…๋ ฅ ์˜ค๋””์˜ค ํŒŒ์ผ ๊ฒฝ๋กœ
output_path: ์ถœ๋ ฅ ์˜ค๋””์˜ค ํŒŒ์ผ ๊ฒฝ๋กœ
parameters: ์ดํŽ™ํ„ฐ ํŒŒ๋ผ๋ฏธํ„ฐ ๋”•์…”๋„ˆ๋ฆฌ
"""
# ์˜ค๋””์˜ค ํŒŒ์ผ ์ฝ๊ธฐ
audio, sample_rate = sf.read(input_path)
# ๋ชจ๋…ธ๋ฉด ์Šคํ…Œ๋ ˆ์˜ค๋กœ ๋ณ€ํ™˜ (์ผ๋ถ€ ์ดํŽ™ํŠธ๊ฐ€ ์Šคํ…Œ๋ ˆ์˜ค ํ•„์š”)
if len(audio.shape) == 1:
audio = np.column_stack([audio, audio])
# float32๋กœ ๋ณ€ํ™˜
audio = audio.astype(np.float32)
# ์ดํŽ™ํŠธ ์ฒด์ธ ๊ตฌ์„ฑ
board = self._build_pedalboard(parameters, sample_rate)
# ์ดํŽ™ํŠธ ์ ์šฉ
processed = board(audio, sample_rate)
# Wet/Dry ๋ฏน์Šค ์ ์šฉ
wet_mix = parameters.get("final_wet_mix", 0.5)
final_audio = (1 - wet_mix) * audio + wet_mix * processed
# ํด๋ฆฌํ•‘ ๋ฐฉ์ง€
final_audio = np.clip(final_audio, -1.0, 1.0)
# ์ถœ๋ ฅ ํŒŒ์ผ ์ €์žฅ
sf.write(output_path, final_audio, sample_rate)
print(f"[EffectChain] ์ฒ˜๋ฆฌ ์™„๋ฃŒ: {output_path}")
def _build_pedalboard(
self,
params: Dict[str, float],
sample_rate: int
) -> Pedalboard:
"""
ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ๋ถ€ํ„ฐ pedalboard ์ดํŽ™ํŠธ ์ฒด์ธ ๊ตฌ์„ฑ
"""
effects = []
# === EQ Section ===
# Low Shelf EQ
if params.get("eq_lowshelf_gain", 0) != 0:
effects.append(
LowShelfFilter(
cutoff_frequency_hz=params.get("eq_lowshelf_freq", 200),
gain_db=params.get("eq_lowshelf_gain", 0),
q=0.707
)
)
# High Shelf EQ
if params.get("eq_highshelf_gain", 0) != 0:
effects.append(
HighShelfFilter(
cutoff_frequency_hz=params.get("eq_highshelf_freq", 8000),
gain_db=params.get("eq_highshelf_gain", 0),
q=0.707
)
)
# Peak EQ 1
if params.get("eq_peak1_gain", 0) != 0:
effects.append(
PeakFilter(
cutoff_frequency_hz=params.get("eq_peak1_freq", 1000),
gain_db=params.get("eq_peak1_gain", 0),
q=params.get("eq_peak1_q", 1.0)
)
)
# Peak EQ 2
if params.get("eq_peak2_gain", 0) != 0:
effects.append(
PeakFilter(
cutoff_frequency_hz=params.get("eq_peak2_freq", 3000),
gain_db=params.get("eq_peak2_gain", 0),
q=params.get("eq_peak2_q", 1.0)
)
)
# === Dynamics Section ===
# Compressor
threshold = params.get("compressor_threshold", -24)
ratio = params.get("compressor_ratio", 4.0)
if ratio > 1.0:
effects.append(
Compressor(
threshold_db=threshold,
ratio=ratio,
attack_ms=params.get("compressor_attack", 5),
release_ms=params.get("compressor_release", 50)
)
)
# Makeup Gain
makeup = params.get("compressor_makeup", 0)
if makeup != 0:
effects.append(Gain(gain_db=makeup))
# === Distortion Section ===
distortion_amount = params.get("distortion_amount", 0)
if distortion_amount > 0:
# pedalboard์˜ Distortion์€ 0-100 ๋ฒ”์œ„
effects.append(
Distortion(drive_db=distortion_amount * 40) # 0-1 -> 0-40dB
)
# Distortion ํ›„ ํ†ค ์กฐ์ ˆ (Tone = LPF)
tone = params.get("distortion_tone", 0.5)
lpf_freq = 2000 + tone * 10000 # 2kHz ~ 12kHz
effects.append(
LowpassFilter(cutoff_frequency_hz=lpf_freq)
)
# === Time-based Effects Section ===
# Delay
delay_mix = params.get("delay_mix", 0)
if delay_mix > 0:
delay_time_ms = params.get("delay_time", 250)
effects.append(
Delay(
delay_seconds=delay_time_ms / 1000,
feedback=params.get("delay_feedback", 0.3),
mix=delay_mix
)
)
# Reverb
reverb_wet = params.get("reverb_wet_dry", 0)
if reverb_wet > 0:
effects.append(
Reverb(
room_size=params.get("reverb_room_size", 0.5),
damping=params.get("reverb_damping", 0.5),
wet_level=reverb_wet,
dry_level=1 - reverb_wet,
width=1.0
)
)
# === Output Section ===
# Limiter (ํด๋ฆฌํ•‘ ๋ฐฉ์ง€)
effects.append(
Limiter(
threshold_db=-1.0,
release_ms=100
)
)
return Pedalboard(effects)
def process_realtime(
self,
audio_chunk: np.ndarray,
sample_rate: int,
parameters: Dict[str, float]
) -> np.ndarray:
"""
์‹ค์‹œ๊ฐ„ ์˜ค๋””์˜ค ์ฒญํฌ ์ฒ˜๋ฆฌ (์ŠคํŠธ๋ฆฌ๋ฐ์šฉ)
Args:
audio_chunk: ์˜ค๋””์˜ค ๋ฐ์ดํ„ฐ ๋ฐฐ์—ด
sample_rate: ์ƒ˜ํ”Œ๋ ˆ์ดํŠธ
parameters: ์ดํŽ™ํ„ฐ ํŒŒ๋ผ๋ฏธํ„ฐ
Returns:
์ฒ˜๋ฆฌ๋œ ์˜ค๋””์˜ค ์ฒญํฌ
"""
if len(audio_chunk.shape) == 1:
audio_chunk = np.column_stack([audio_chunk, audio_chunk])
audio_chunk = audio_chunk.astype(np.float32)
board = self._build_pedalboard(parameters, sample_rate)
processed = board(audio_chunk, sample_rate)
wet_mix = parameters.get("final_wet_mix", 0.5)
final = (1 - wet_mix) * audio_chunk + wet_mix * processed
return np.clip(final, -1.0, 1.0)