본문 바로가기

이직로그/DSP

Audio DSP: 일렉기타 이펙터 만들기 4 - 딜레이 페달 만들기

오버드라이브 계열은 LTI시스템이 아니기 때문에 전달함수로  구현할수없음을 알아냈다.

그래서 LTI 시스템인 simple delay를 이용하여 전달함수를 만들어 보려고 한다.

 

딜레이는 메아리처럼 일정한 시간적 간격을 가지고 짧게 소리를 되풀이 하는 이펙터이다.

 

  꼴의 FIR 시스템이라고 볼수있다.

이는 z-Transform으로 아래와 같이 표현 할 수 있다.

 

GPT를 활용하여 간단한 딜레이 이펙터를 만들었다.

import numpy as np
from scipy.io import wavfile


def read_wav_mono(path: str):
    fs, x = wavfile.read(path)

    # stereo -> mono
    if x.ndim == 2:
        x = x.mean(axis=1)

    # int -> float32 [-1, 1]
    if np.issubdtype(x.dtype, np.integer):
        max_int = np.iinfo(x.dtype).max
        x = x.astype(np.float32) / max_int
    else:
        x = x.astype(np.float32)

    return fs, x


def write_wav(path: str, fs: int, x: np.ndarray):
    x = np.asarray(x, dtype=np.float32)

    # clipping 방지
    peak = np.max(np.abs(x))
    if peak > 1.0:
        x = x / peak * 0.98

    wavfile.write(path, fs, (x * 32767).astype(np.int16))


def bpm_to_delay_samples(fs: int, bpm: float, note: str = "quarter") -> int:
    """
    BPM과 음표 단위로 delay time 계산

    note options:
    - "whole"
    - "half"
    - "quarter"
    - "eighth"
    - "sixteenth"
    - "dotted_eighth"
    - "triplet_eighth"
    """
    quarter_sec = 60.0 / bpm

    note_map = {
        "whole": 4.0,
        "half": 2.0,
        "quarter": 1.0,
        "eighth": 0.5,
        "sixteenth": 0.25,
        "dotted_eighth": 0.75,
        "triplet_eighth": 1.0 / 3.0,
    }

    if note not in note_map:
        raise ValueError(f"Unsupported note: {note}")

    delay_sec = quarter_sec * note_map[note]
    delay_samples = int(round(delay_sec * fs))
    return delay_samples


def apply_guitar_delay_lti(
    x: np.ndarray,
    fs: int,
    bpm: float,
    note: str = "dotted_eighth",
    mix: float = 0.35,
    feedback: float = 0.40,
    repeats: int = 6,
    dry_gain: float = 1.0,
):
    """
    일렉기타용 LTI delay
    y[n] = dry_gain*x[n] + sum_{k=1..repeats} mix*(feedback^(k-1))*x[n-kD]

    - 고정 계수만 사용 -> LTI
    - BPM 기반 딜레이 시간
    - 멀티탭 echo 형태

    Args:
        x: mono input
        fs: sample rate
        bpm: tempo
        note: delay division
        mix: wet level
        feedback: repeat attenuation (0~1)
        repeats: number of echoes
        dry_gain: direct signal gain
    """
    if not (0.0 <= mix <= 1.0):
        raise ValueError("mix must be between 0 and 1")
    if not (0.0 <= feedback < 1.0):
        raise ValueError("feedback must be in [0, 1)")
    if repeats < 1:
        raise ValueError("repeats must be >= 1")

    delay_samples = bpm_to_delay_samples(fs, bpm, note)

    # 출력 길이 확보
    out_len = len(x) + delay_samples * repeats
    y = np.zeros(out_len, dtype=np.float32)

    # dry
    y[:len(x)] += dry_gain * x

    # echoes
    for k in range(1, repeats + 1):
        gain = mix * (feedback ** (k - 1))
        start = k * delay_samples
        end = start + len(x)
        y[start:end] += gain * x

    return y, delay_samples


if __name__ == "__main__":
    input_path = "DATA/clean1.wav"
    output_path = "OUTPUT/delay_clean1.wav"

    # ===== 사용자 설정 =====
    bpm = 90.0
    note = "eighth"   # "quarter", "eighth", "dotted_eighth" 추천
    mix = 0.32               # wet 양
    feedback = 0.45          # 반복 감쇠
    repeats = 7              # 반복 횟수
    dry_gain = 1.0
    # ======================

    fs, x = read_wav_mono(input_path)

    y, delay_samples = apply_guitar_delay_lti(
        x=x,
        fs=fs,
        bpm=bpm,
        note=note,
        mix=mix,
        feedback=feedback,
        repeats=repeats,
        dry_gain=dry_gain,
    )

    write_wav(output_path, fs, y)

    delay_ms = 1000.0 * delay_samples / fs
    print(f"Saved: {output_path}")
    print(f"BPM: {bpm}")
    print(f"Note: {note}")
    print(f"Delay: {delay_samples} samples ({delay_ms:.2f} ms)")

clean1.wav
0.85MB
delay_clean1.wav
1.04MB

 

두번째 파일을 들어보면 첫번째 파일에서 딜레이가 추가된것을 볼수있다.

 

 

이번에도 

sine

impulse

gaussian

clean

에 적용해서 전달함수를 만들어 보겠다.