본문 바로가기
  • 우당탕탕속의 잔잔함
Programming/Sound Analysis

[Sound Cropper] 사운드 파일 속, 소리 구간 추출

by zpstls 2023. 3. 24.
반응형

이번 포스트에서는 음성 데이터를 다룰 예정입니다.

음성 파일이 있을 때 해당 파일에서 소리가 나는 구간만 추출하는 프로그램을 만들어 보고자 합니다.

 

 

녹음된 파일 또는 음악 파일 등을 사용할 때, 빈 소리가 중간에 포함되지 않았으면 하는 경우가 있을 것입니다.

예를 들면, 1번 곡에서 2번 곡으로 넘어갈 때의 공백이 없어졌으면 좋겠다던지...?

실제 음악 플레이어에서 해당 기능을 제공하는 경우도 많고 개발하기 어려운 주제도 아니기에 의미가 없을 수도 있겠습니다만, 이러한 기초적인 기능이 다른 큰 프로젝트에서 활용되는 경우도 종종 있어서 다뤄볼 예정입니다.

 

구현 방식은 여러가지가 있겠지만, 정말 간편하고 쉽게 Waveform을 이용해 구현해 볼 것입니다.

우선, 전반적인 아이디어는 다음과 같습니다.

위와 같은 사운드 파일, 특히 Amplitude Data의 양상을 보았을 때, , 빨간 박스 부분만 Crop하고 싶은 것입니다. 빨간 박스의 시작점과 끝점을 보면 빨간 박스의 바깥 부분보다 값이 크다는 것을 확인할 수 있습니다.

오디오 데이터는 +, -를 왔다갔다하고 의미 있는 소리는 절대 값이 크기에 모든 Amplitude를 절댓값으로 바꾸고 Amplitude의 추세를 기반으로 Threshold를 지정하여 해당 Threshold를 넘는 구산을 Crop 하고자 하는 소리의 구간으로 판단하면 될 것 같습니다.

따라서 아래와 같이, 모든 Amplitude의 값을 절댓값화 하고 소리의 구간을 대략 samplerate의 8분의 1 정도로 잘게 자릅니다. 이 구간들의 평균 값을 계산하고 각 구간들과의 Distance를 계산하고 평균을 내어 Threshold 값을 정하도록 합니다.

이때 이 Threshold는 주어진 음성 데이터의 크기와 변화가 작은 값들을 걸러내기 위한 값입니다. 이 Threshold 이상의 Amplitude를 분리하고 이 값들만 이용하여 다시 한번 변화 값의 Distance를 구하고 평균을 냅니다. 이것이 새로운 Threshold이며, 최종적으로 이 값을 초과하는 구간을 의미 있는 소리 구간이라고 판단합니다.

 

위 내용을 코드로 구현하면 다음과 같습니다.

import librosa
import soundfile as sf
import numpy as np

# original audio data
audio, sr = librosa.load(audio_file, sr=16000)

term = int(sr/8)
Amplitude_ABS = np.abs(audio)
Amplitude_ABS_mean = []

for t in range(0, len(Amplitude_ABS), term):
    Amplitude_ABS_mean.append(Amplitude_ABS[t:t + term].mean())

Amplitude_Distance = []
for i in range(0, len(Amplitude_ABS_mean) - 1) :
    distance = 1 + ((Amplitude_ABS_mean[i+1] - Amplitude_ABS_mean[i]) ** 2)
    Amplitude_Distance.append(distance ** 0.5)

Amplitude_Distance_array = np.array(Amplitude_Distance)
threshold1 = Amplitude_Distance_array.mean()
print("Average of Distance1 = ", threshold1)

Threshold1_Data = []
for i in range(0, len(Amplitude_Distance)) :
    if Amplitude_Distance[i] > threshold1 :
        Threshold1_Data.append(Amplitude_Distance[i])

threshold2 = np.array(Threshold1_Data).mean()
print("Average of Distance2 = ", threshold2)

valid_index = []
for i in range(0, len(Amplitude_Distance)) :
    if Amplitude_Distance[i] > threshold2 :
        valid_index.append(i)

startP = valid_index[0] * term
endP = valid_index[len(valid_index) - 1] * term
print("Start Point = ", startP)
print("End Point = ", endP)

# Final Result
sf.write('result.wav', audio[startP:(endP+sr)], sr)

코드가 수행된 결과를 그래프로 확인해보면 다음과 같습니다.

위 이미지는 2개의 음성 데이터에 대한 그래프입니다. 빨간 줄은 최종적으로 사용된 Threshold 값이고요.

전반적으로 데이터의 패턴은 유지된 채 값이 강조되었고 의미 없는 데이터를 제외하여 계산하였기 때문에 전체 데이터의 개수도 줄어들었습니다.

처음 의미 있는 음성이라고 생각되는 시점과 마지막으로 의미있는 음성이라고 생각되는 시점 사이의 음성들만 추출하면 공백이 없어질 것입니다. 결과 음성에서 중간에 공백이 있다면 또 잘게 쪼개서 앞서 수행한 방식을 적용시키면 또 다른 의미 있는 데이터만 따로 분리될 것입니다.

 

이렇게 단 코드 몇 줄만을 이용해 사운드 데이터를 Crop하는 기능을 만들어 보았습니다.

위 방식의 단점은 잡음이 심한 경우 사용할 수 없다는 것입니다. 또한 범용적으로 활용되기보다는 적용하고자 하는 데이터의 패턴에 따라 결과의 질이 달라질 것이고요.

여기서 중요한 것은 결국엔 다루고자 하는 데이터가 어떠한 특징을 가지고 있는지 파악하는 것입니다. 가장 최소한의 노력으로 최대한의 결과를 내면 좋으니까요. 뭐... 위 방식으로도 충분히 해결되는데 굳이 딥러닝 방식까지 꺼낼 필요는 없다고 생각하는 것처럼...?? ㅎㅎ

 

이번 포스트는 여기서 마무리하도록 하겠습니다.

반응형

댓글