야, 너도 드럼 칠 수 있어 : 2. 처리 과정 구현
드럼 악보 채보 프로젝트
- 본 글은 제 대학교 3학년 여름 방학이었던 2019년 6월부터 3개월 간 진행했던, “야, 너도 드럼 칠 수 있어” 프로젝트에 관련된 설명 글입니다.
- 편의상 설명 과정은 ‘~했다.’ 로 작성하였습니다.
- 소스 코드 는 다음과 같습니다. (Source)
채보의 사전적 정의 : 음악을 듣고 악보로 옮겨 적는 것
이전 글 “(야, 너도 드럼 칠 수 있어 : 1. 드럼 비트 샘플 파일 입력)” 에서 이어지는 글입니다.
1. Sliding Window 구현
1. Sliding Window 란?
Sliding Window : TCP/IP 의 TCP 통신과정에서 흐름제어 와 혼잡제어 를 위해 활용
통신 과정에서는 송신측에서 수신측으로부터 수신한 ACK를 통해 Window Size 를 조절하는 일련의 과정이 포함되지만, 우리의 프로젝트에서는 송수신 과정이 필요하지 않았다..
따라서 프로젝트에서는 전체 구간 을 일정 Window Size의 구간 으로 나누어 순차적으로 처리 하는 방법으로써 활용했다.
사실 당시엔 Sliding Window 알고리즘이 이미 존재하는지 몰랐다..만들고 보니 괜찮았던 것
2. Sliding Window를 활용한 이유
우리의 샘플은 44100Hz, 48000Hz 의 두 샘플링레이트 를 가졌다.
덕분에 1초에 최대 48000개의 샘플이 존재했다.
FFT의 시간 복잡도 는 O(Nlog2N) 이다.
물론 FFT 알고리즘 자체는 위와 같은 복잡도를 갖는 매우 빠른 연산 이지만, 초기엔 드럼 샘플 파일을 처음부터 끝까지 통연산을 하다 보니 매우 느렸다.
(10초의 파일을 연산하는데 무려 30초가 걸렸다)
따라서 다음과 같은 두 가지 활용 가능성을 찾았다.
1. FFT 연산 최적화
굳이 처음부터 끝까지 연산할 필요가 없었다.
따라서 Window Size를 매우 짧게 설정한 후, FFT를 반복적으로 수행했다. 또한, 결과값을 바로바로 그 다음 단계인 분류 과정에 대입하고자 했다.
2. 잡음 제거
구간을 나누어 계산하면, 잡음의 제거도 비교적 간편했다.
각 Window 구간의 평균을 구한 후, 일정 진폭 이하의 샘플들은 값을 0으로 두었다. 일정 진폭은 일일히 실험을 통해 얻어진 최적값을 적용했다.
일정 dB 이하의 신호는 제거하는, 일종의 Noise Gate 를 구현했다.
3. Sliding Window 구현
- 다음과 같이 구현했다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | avgdata = [] slicerange = 300 data00 = np.full(np.alen(data0), 0) for i in range(0, np.alen(data0) - (slicerange * 2), int(slicerange / 4)): j = i datasum = 0 for j in range(j, j + (slicerange - 1), 1): datasum = datasum + np.abs(int(data1[j])) avgdata.append(datasum / int(slicerange / 4)) for i in range(0, np.alen(data0) - (slicerange * 2), int(slicerange / 4)): j = i for j in range(j, j + (slicerange - 1), 1): data00[j] = avgdata[int(i / (slicerange / 4))].copy() | cs |
- Window Size = 300, Range = 75
- Window Size를 300으로 두고, 75샘플 만큼 이동하면서 연산을 수행한다.
- (0, np.alen(data0) - (slicerange * 2), int(slicerange / 4))
- range는 0부터 전체 샘플의 크기 - 75샘플 의 구간으로 설정한 후, for loop을 활용했다.
- avgdata.append(datasum/int(slicerange/4))
- 75샘플 만큼 평균을 구한 후, 계속 avgdata 배열에 붙여주었다.
- 위의 과정을 한번 더 반복
- .copy()를 통해 한번 더 붙였다. 아마 python에 익숙하지 않아서 그랬었던 듯..
- 이후, 다음과 같이 FFT를 진행할 구간 을 저장한 배열을 할당했다.
1 | ccount = [[0 for j in range(len(data00))] for i in range(300)] | cs |
- 특정 진폭 이하의 샘플들을 제거했다.
1 2 3 | for num in range(0, len(data00)): if data00[num] < 374: data00[num] = 0 | cs |
실험을 통해 374 라는 특정한 값을 계산했다. 사실 이 과정에서 KNN 등 머신 러닝 알고리즘을 도입하면 어떨까 생각했지만 마감의 압박으로 포기했던 것 같다.
여기까지 읽었으면 알 수 있다시피, 실험을 통해 구해진 값들 이 많다. 만약 이후에 알고리즘을 개선할 기회가 있다면, 머신 러닝을 적용해보고 싶었다.
2. 리듬의 유지시간 계산
총 마디 수를 계산하기 위해 먼저 리듬의 유지 시간 을 계산해야 했다.
- 리듬의 유지시간을 계산하기 위한 배열을 생성했다.
1 2 3 4 5 6 7 8 9 10 11 12 13 | starttime = [] endtime = [] for num in range(0, len(data1)): if data00[num] != 0: ccount[i][j] = data1[num] j = j + 1 if data00[num-1] == 0: starttime.append(num) elif (data00[num] == 0) & (data00[num-1] != 0): i = i + 1 j = 0 endtime.append(num) starttime.append(len(times) - 1) | cs |
times = np.arange(len(data0)) / float(rate)
샘플 파일 입력 과정의 times 배열을 활용했다.
times 배열 크기 만큼의 배열을 만들어주었다.
starttime 엔 리듬이 시작된 시각, endtime 엔 리듬이 종료된 시각을 저장했다.
단위는 모두 [샘플 수] 이다.
- 이후 리듬의 유지 시간을 계산했다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | # 2차원 배열 r 생성 (행 : 리듬 갯수, 열 : 최대악기 3개니까 3) rhythmcount = i r = [[] for i in range(rhythmcount)] # 리듬 사이의 유지시간 탐지 defaulttime = [[] for i in range(rhythmcount + 1)] playtime = [[] for i in range(rhythmcount)] for j in range(1, rhythmcount + 1): defaulttime[j - 1] = round(times[starttime[j]]-times[endtime[j - 1]], 3) for j in range(0, rhythmcount): playtime[j] = round(times[endtime[j]]-times[starttime[j]], 3) # ccount 배열의 0 제거 for num in range(0, 300): for number in range(0, ccount[num].count(0)): ccount[num].pop() | cs |
- 리듬의 갯수 와, 동시에 연주된 악기의 갯수
- rhythmcount 를 생성하여 2차원 배열 을 만들었다.
- 행 은 리듬의 갯수를 의미하고, 열 은 동시에 연주된 악기의 갯수를 의미한다.
- 리듬 사이의 유지시간 탐지
- defaulttime 엔 리듬이 끝나고, 다음 리듬이 시작되기 전 까지의 구간을 저장했다.
- playtime 엔 리듬이 실질적으로 유지되는 구간인, endtime - starttime 을 저장했다.
- 배열의 0 제거
- 리듬이 비어있는 구간 이 존재할 수 있으므로, 그 구간에서는 FFT를 진행하지 않기 위해 0 을 제거 했다.
이렇게 Sliding Window 등 필요한 처리 과정을 구현했다.
이제 FFT를 진행할 차례이다.
다음 글 “(야, 너도 드럼 칠 수 있어 : 3. FFT 과정)” 에서 이어집니다.
댓글남기기