본문 바로가기
코딩/madmom

[madmom] 피아노 음원을 MIDI note로 변환하기 (python)

by 오늘도 작심삼일 2021. 8. 6.

시간이 별로 없는 프로젝트였기 때문에 1차적 목표를 피아노 소리를 MIDI note로 따내는 것으로 설정했다.

음원은 아이폰/아이패드의 Garage Band 앱으로 피아노를 연주해서 m4a파일을 얻었다. 

비트와 음의 길이를 모두 무시하고 음만 먼저 따내는 일에는 madmom.features.notes 안에있는 함수들만을 이용하면 충분했다.

 

madmom.features.notes — madmom 0.17.dev0 documentation

Track the notes with an HMM based on a model of attack, decay, sustain, release (ADSR) envelopes. Create a CNNPianoNoteProcessor and pass a file through the processor to obtain a note activation function (sampled with 50 frames per second). Track the notes

madmom.readthedocs.io

잘은 모르겠지만 두가지 방법이 있는듯했다.

  1. RNNPianoNoteProcessor + NoteOnsetPeakPickingProcessor
  2. CNNPianoNoteProcessor + ADSRNoteTrackingProcessor

 

RNNPianoNoteProcessor + NoteOnsetPeakPickingProcessor

 

처음에는 MIDI note를 추출할 샘플 음원으로 madmom github에 올라와있는 stereo_sample.wav 파일을 이용했다. 사실 이 음원은 화음이 들어가 있었다. 단음 2개, 2단 화음 2개, 총 6개의 음으로 이루어진 샘플 파일로 추출을 시도했다. 우선 추출 전에 음원 데이터를 array로 변환한 결과를 보고가자. 

from madmom.features import *

proc = RNNPianoNoteProcessor()
act = proc('tests/data/audio/stereo_sample.wav')

print(act.shape)
print(act)

 

실행 결과

(415, 88)
[[ 5.6179601e-04  5.7703583e-04  3.4437981e-04 ...  6.6896435e-04
  -8.4263738e-04  1.6938150e-04]
 [ 9.3447394e-05 -3.6145560e-05  3.0633062e-05 ...  3.6225654e-05
  -7.1371906e-05 -4.8864633e-05]
 [ 1.6996113e-04 -6.8070367e-06 -1.1721067e-04 ... -1.9978732e-05
   8.1769191e-05  4.8941001e-05]
 ...
 [-2.3070141e-05 -8.9600217e-06 -5.0096773e-05 ... -1.4978461e-05
   4.0986575e-05 -9.8720193e-08]
 [-6.0462044e-05 -4.6007801e-05 -3.5163015e-05 ... -1.1020340e-05
   1.3598707e-04 -1.6275793e-05]
 [-4.3501484e-04  5.9543061e-04 -4.8070401e-04 ... -3.1812768e-04
   7.8679062e-05  1.8898398e-05]]

 

act.shape을 출력한 결과로 (415, 88)이 출력되었다. RNNPianoNoteProcessor는 100fps로 음원을 프레임으로 쪼갠다. 415는 그렇게 쪼개진 frame의 개수이다. column 88개는 각 프레임이 피아노 건반 88개 중 하나에 해당할 transition probability인 듯 하다. 그런데 확률이면 왜 음수도 나오는지는 잘 모르겠다. 여튼 이 결과를 우리가 손으로 직접 해석할 일은 없으니 안심하자. 다음의 코드를 실행하면 한번에 실제 MIDI note를 추출한 결과를 얻을 수 있다. pitch_offset=21로 설정해주어야 피아노 음을 정확하게 인식할 수 있다고 한다. 사실 여기까지는 도큐먼트에 있는 대로 따라하면 된다. 

proc = NoteOnsetPeakPickingProcessor(fps=100, pitch_offset=21)
act = RNNPianoNoteProcessor()('tests/data/audio/stereo_sample.wav')
print(proc(act))

 

결과

[[ 0.14 72.  ]
 [ 1.56 41.  ]
 [ 3.37 75.  ]]

0번 column은 각 note의 attack timing인 듯 하고 1번 column은 각 note의 MIDI number이다. 분명 note가 6개 있는 음원 파일을 넘겨줬는데 3개밖에 찾지 못했다. 그래서 필자는 2번 방법도 시도해보았다. 

 

CNNPianoNoteProcessor + ADSRNoteTrackingProcessor

 

proc = CNNPianoNoteProcessor()
adsr = ADSRNoteTrackingProcessor(pitch_offset=21)

act = proc('test/data/audio/stereo_sample.wav')
MIDI_notes = adsr(act)

print(MIDI_notes)

 

ADSRNoteTrackingProcessor의 사용법은 document에 자세히 나와있지는 않지만 1번 방법때처럼 pitch_offset=21로 주어보았다. 

결과

[[ 0.12 72.    1.44]
 [ 1.54 41.    1.84]
 [ 2.5  77.    1.  ]
 [ 2.52 65.    0.96]
 [ 2.54 60.    0.82]
 [ 2.58 56.    0.82]
 [ 3.34 75.    0.82]
 [ 3.42 43.    0.74]]

이번엔 음 6개짜리 샘플 음원에서 8개의 음을 찾아냈다. 나는 차라리 음을 많이 찾고 줄여나가는 것이 쉽겠다고 판단하여 CNN + ADSR 조합을 조금 더 연구해보기로 했다. array의 첫번째 두번째 column은 아까와 같은 듯 하고 세번째 column은 무슨 숫자인지 아직 모르겠다. 

소스코드를 열러본 결과 굉장히 많은 parameter를 조정해줄 수 있었고, 하나씩 다 해본 끝에 onset_threshold=0.9 를 넘겨주었을 때 비로소 올바른 결과값을 출력할 수 있었다. onset_threshold는 생성자에서 0.5로 기본적으로 초기화되고 있던 값이었다. onset이란 음의 시작점을 말하는데 이 한계값을 적절히 높여주었더니 음으로 인식되면 안되는 노이즈들이 제거되어 올바른 결과가 도출된 것 같다. 

출력 결과는 다음과 같다. 

[[ 0.12 72.    1.44]
 [ 1.54 41.    1.84]
 [ 2.5  77.    1.  ]
 [ 2.52 65.    0.96]
 [ 3.34 75.    0.82]
 [ 3.42 43.    0.74]]

 

여기서 주목할 점은 마지막 4개의 음은 사실 2단 화음 2개라는 점이다. 각 화음끼리의 attack timing은 각각 0.02, 0.08 간격이다. 악보를 그릴 때 각 음간의 간격이 0.08이하이면 화음으로 처리한다 등의 규칙으로 활용할 수 있겠다. (필자의 경우 0.03을 기준으로 하였다.) 

 


필자의 경우 위의 과정들을 수행할 때에 stereo_sample.wav가 아닌 다른 wav파일이나 m4a, mp3 파일들을 변환하려고 하면 에러가 떴다. 이를 위해서 ffmpeg를 설치해야했다. pip으로 설치하는 것이 아니라 인터넷에 검색을 해서 컴퓨터에 직접설치를 하고 컴퓨터를 재부팅 했더니 venv임에도 반영이 잘 되었던 것 같은데 기억이 확실하지 않다. 주의하시길 바란다. 


악보 그리기는 vexflow 카테고리에서 이어진다. 

댓글