파이썬으로 음성변환 프로그램 만들기

paulleelife·2022년 1월 2일
1

Photo by Jake Kaminski on Unsplash

음성 피치 조절

사람 목소리에서 가장 중요한 요소 중 하나인 피치(pitch) 를 조절하는 방법에 대해서 알아보겠습니다.

Resampling 을 사용한 피치 조절

제일 간단한 방법으로 음원파일을 다운샘플링 하면 목소리 톤이 낮아지고 반대로 업샘플링을 하면 톤이 올라갑니다.

import librosa 
import samplerate
import IPython.display as ipd

data, sr = librosa.load('example.wav', sr=None)
out_data1 = samplerate.resample(data, 0.5, 'sinc_best')
out_data2 = samplerate.resample(data, 1.5, 'sinc_best')

print('Higher pitch:')
ipd.display(ipd.Audio(out_data1, rate=sr))
print('Lower pitch:')
ipd.display(ipd.Audio(out_data2, rate=sr))

하지만 이런 방법을 사용하면 말하는 속도도 같이 영향을 받으면서 부자연스럽게 들립니다. 그래서 같은 속도로 말하면서 피치만 바꿀려면 PSOLA (Pitch Synchronous OverLap and Add) 알고리즘을 사용해야 합니다.

PSOLA 알고리즘을 사용한 피치 조절

말하는 속도는 같고 피치만 바꾸는 방법으로 먼저 음원파일을 프레임 (Frame) 단위로 분절해 줍니다. 이 함수를 frameize 으로 만들어 보았습니다.

def frameize(x: np.array, N: int, H_a: int, hfilt: np.array) -> list:
    """Truncate audio sample into frames.
    
    Params
    ------
    x: audio array
    N: segment size
    H_a: analysis hop size
    hfilt: windowing filter
    
    Returns
    -------
    frames: segments of audio sample
    """
    frames = []
    idx = 0 
    
    while True:
        try: frames += [hfilt*x[H_a*idx:H_a*idx+N]]
        except: break   
        idx += 1
    
    return frames

여기서 H_a 는 음원 분절부분들간의 간격이고 N 은 각 분절부분의 길이입니다.

이제 프레임 간격만 조절하면 음원파일의 시간만 바뀌고 프레임 안에있는 피치는 유지가 됩니다. 이걸 time-stretching 이라고 하고 distort_time 함수로 만들어 보았습니다.

def distort_time(x: np.array, N: int, H_a: int,
                 hfilt: np.array, alpha: float) -> np.array:
    """Distort time of audio sample by given ratio.
    
    Params
    ------
    x: audio data
    N: segment size
    H_a: analysis hop size
    hfilt: windowing filter
    alpha: time-scaling factor
    
    Returns
    -------
    out_x: time-scaled data 
    """
    # put into frames
    frames = frameize(x, N, H_a, hfilt)
    
    H_s = int(np.round(H_a*alpha))
    interval = 200 # search area for best match
    out_x = np.zeros(len(frames)*H_s+N)
        
    # time-distorting
    for i, frame in enumerate(frames):
        # end parts
        if i == len(frames) - 1:
            hfilt_norm = find_hfilt_norm(hfilt, H_s)
        # start, middle parts
        else:
            hfilt_norm = find_hfilt_norm(hfilt, H_s)

        out_x[i*H_s:i*H_s+N] += frame/hfilt_norm
    
    return out_x

여기서 find_hfilt_norm 함수는 분절할때 적용된 필터를 다시 없애주는 역할을 합니다.

그 다음 resampling 을 1/(프레임 간격 비율) 만큼 해 주면 시간은 원래대로 돌아가고 피치만 변형됩니다. 이 방법은 함수 synthesize_pitch 에서 보실 수 있습니다.

def synthesize_pitch(x: np.array, sr: int, N: int, H_a: int,
                      hfilt: np.array, alpha: float) -> np.array:
    """Synthesize sound sample into new one with different pitch using PSOLA algorithm.
    
    Params
    ------
    x: audio data
    sr: sampling rate
    N: segment size
    H_a: analysis hop size
    hfilt: windowing filter
    alpha: pitch factor
    
    Returns
    -------
    syn_x: synthesized data
    """
    syn_data = distort_time(x, N, H_a, hfilt, alpha)

    # resampling
    syn_data = samplerate.resample(syn_data, 1/alpha, 'sinc_best')
    syn_data = syn_data/np.max(abs(syn_data))
        
    return syn_data

이때 주위 할 것은 반드시 프레임 간의 곂치는 부분이 있어야 시간을 늘릴 때 끊기는 소리가 안 들리고 프레임 간격을 바꾸면서 곂치는 부분에 discontinuity 가 생기는데 이를 해결하기 위해서 프레임 간격을 추가적으로 조절해 단절된 정도를 최소화 시킵니다.

피치 조절과 함께하는 Frequency stretching

피치를 어느정도 이상 올리면 목소리가 헬륨 소리로 변형됩니다. 이걸 완화시켜주기 위해 해당 음원 주파수를 β=α1/3\beta=\alpha^{1/3} 비율로 늘려주면 더 자연스로운 톤으로 변형됩니다. 여기서 α\alpha 는 pitch factor 값이고 β\beta 는 stretching factor 값입니다. 이 공식은 여러번의 실험을 통해 만들어졌고 이 논문에서 볼 수 있습니다.

N = 1024 # segment size for sampling rate 44100 Hz
H_a = int(N*0.5) # analysis hop size between 0.5 ~ 1
hfilt = np.hanning(N) # filter type

# input 
data, sr = librosa.load('example.wav', sr=None)
ipd.display(ipd.Audio(data, rate=sr, normalize=False))
alpha = 1.6 # pitch

# pitch increase
data = synthesize_pitch(data, sr, N, H_a, hfilt, alpha=alpha)

# frequency stretching
S1 = librosa.stft(data, n_fft=512, hop_length=64)
S2 = warp_spectrum(S1, alpha**(1/3))
data = librosa.istft(S2, hop_length=64, win_length=512)

ipd.display(ipd.Audio(data, rate=sr, normalize=True))

전체 코드는 깃헙에 작성해 보았습니다.

마지막으로 더 자연스럽게 목소리 변형을 할려면 피치뿐만 아니라 팀버 (Timbre) 다른 특성들도 같이 조절해 주면 더더욱 좋습니다. 그 파트는 다음시간에 알아보겠습니다.

profile
Slow and steady

1개의 댓글

comment-user-thumbnail
2024년 3월 12일

글 잘 읽었습니다. 혹시 특정 화자에게서 pitch를 추출해서 다른 목소리에 적용시키는 코드 구현하신 거 있으실까요?

답글 달기