Photo by Jake Kaminski on Unsplash
사람 목소리에서 가장 중요한 요소 중 하나인 피치(pitch) 를 조절하는 방법에 대해서 알아보겠습니다.
제일 간단한 방법으로 음원파일을 다운샘플링 하면 목소리 톤이 낮아지고 반대로 업샘플링을 하면 톤이 올라갑니다.
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) 알고리즘을 사용해야 합니다.
말하는 속도는 같고 피치만 바꾸는 방법으로 먼저 음원파일을 프레임 (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 가 생기는데 이를 해결하기 위해서 프레임 간격을 추가적으로 조절해 단절된 정도를 최소화 시킵니다.
피치를 어느정도 이상 올리면 목소리가 헬륨 소리로 변형됩니다. 이걸 완화시켜주기 위해 해당 음원 주파수를 비율로 늘려주면 더 자연스로운 톤으로 변형됩니다. 여기서 는 pitch factor 값이고 는 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) 다른 특성들도 같이 조절해 주면 더더욱 좋습니다. 그 파트는 다음시간에 알아보겠습니다.
글 잘 읽었습니다. 혹시 특정 화자에게서 pitch를 추출해서 다른 목소리에 적용시키는 코드 구현하신 거 있으실까요?