그림 자료를 곁들여 꽤 긴 글을 쓰고 있었는데 순식간에 날아가 버렸다. 키보드를 잘못 건드린 것도 없는데 도대체 무슨 일이 벌어진 것일까? 시스템 또는 서비스의 일시적인 문제라고 믿기로 했다. 지금까지 이런 일은 없었기 때문이다.
Nano Adrule MIDI controller는 키보드 컨트롤러와 사운드 모듈 사이에 위치하여 음색을 바꾸거나 레이어 또는 스플릿을 하기 위한 목적으로 만든 것이다. 여기서 말하는 사운드 모듈은 디스플레이나 조절 기능이 없이 아주 단순한 구조로 된 것을 뜻한다. 즉, 노래방 반주기에서 적출한 도터보드 형태의 것을 포함한다.
내놓기 민망한 수준으로 회로를 꾸미고 아두이노를 제어하기 위한 스케치(.ino) 파일을 만들어 소기의 목적을 달성하는 데에는 성공하였다. 이 과정에서 챗GPT가 큰 기여를 하였다. 처음에는 마이크로SD카드에 저장한 type 0 MIDI 파일을 재생하는 기능까지 넣으려고 했으나 아두이노 나노라는 제한된 '두뇌'에 펌웨어를 다 밀어넣기가 곤란하여 일단 기능을 분리하게 되었다. 아두이노 나노 호환보드와 아두이노 나노 에브리를 하나씩 갖고 있으니 각각에 대해여 서로 다른 기능의 펌웨어를 업로드해 놓은 뒤 필요에 따라 소켓에 바꿔 끼워 가면서 쓸 수도 있을 것이다.
그러나 안정적인 MIDI 파일 재생 기능을 구현하는 것이 키보드 컨트롤러의 보조 역할을 하는 것보다 훨씬 어렵다는 것을 알게 되었다. 마이크로SD카드에서 파일 목록을 얻는 것부터 쉽지 않았다. 실패를 반복하면서 거의 포기 상태로 한동안 손을 놓고 있다가 또다시 열정을 갖고 파고들기 시작하였다. 수없는 시행착오를 거쳐서 SD카드 접근용 라이브러리 SDFat를 선택하고, MIDI 파일명을 기록한 인덱스 파일(INDEX.TXT)을 통해서 개별 파일을 접근하는 등 최적화를 위해 할 일이 너무나 많았다. 단계별로 적용하여 재생 안정화에 기여한 대책을 정리해 보면 다음과 같다.
MIDI 파싱/타이밍(논리) 안정화
- 러닝 스테이터스 규칙 고정: runningStatus는 오직 채널 보이스(0x80–0xEF)에서만 갱신.
- 메타(0xFF)/SysEx(0xF0/0xF7) 이후에는 runningStatus를 건드리지 않음.
- 프리롤 정합성: Δ=0 구간에서 메타·SysEx는 소비하되, 첫 채널 상태를 만나면 firstStatusPending = true, runningStatus = 해당 상태로 초기화 후 메인루프에 인계.
- 데이터바이트 보호: runningStatus 없이 데이터바이트(<0x80) 등장 시 다음 상태바이트(≥0x80)까지 스캔하여 동기화(트랙 경계 내에서만).
- 트랙 경계 가드: trackEndPos를 프리롤/메인루프/메타파서 전 구간에 적용하여 조기 EOT/읽기 초과 방지.
- 절대시간 스케줄러: songStartMicros + tick*micro_per_tick 방식으로 이벤트 스케줄, Δ=0 클러스터도 순차 처리.
“버스트(폭주)” 완화/전송 안정화
- 스타트 가드: 재생 시작 시 2비트(count-in) 지연 + 15ms 프리롤로 초기 버스트 완화.
- TX 페이싱(옵션): parseMidiEvent() 끝에 delayMicroseconds(200~350) 삽입해 UART 과부하 예방.(권장값: 250µs. 문제 파일에서만 필요하면 컴파일 타임 옵션으로 관리)
- 하드웨어 버퍼 대기(대안): 필요 시 Serial.write() 전에 UDRE 체크로 송신 버퍼 여유 확인.
- SPI 속도 보수화: SD.begin(CS, SD_SCK_MHZ(1)) 로 1 MHz로 시작(호환성↑).
- 오픈 재시도 래퍼: sdSafeOpen()으로 최대 3회 재시도 후 실패 판정.
- 고정 버퍼 라인리더: String 대신 char[] 버퍼 기반 readNextLine()로 CR/LF/CRLF, BOM(UTF-8) 처리.→ countIndexLines(), loadIndexPage() 모두 이 라인리더 사용.
- INDEX 포맷 규칙: 8.3 파일명, 빈 줄/주석(#) 무시, 마지막 줄 개행 보장. 필요 시 PC에서 파이썬 스크립트로 BOM 제거 + 줄바꿈 통일.
- 버튼 디바운스: 모든 버튼에 소프트 디바운스(약 18ms + 릴리즈 대기).
- 인코더 스텝 고정: detent=2 기준(두 클릭=1스텝)으로 과다 이동 억제.
- 방향 반전은 #define ENC_DIR_INVERT 한 줄로 토글.
- 표시 정렬: Vol/BPM/Reb/Kit 숫자 3자리 고정(오른쪽 정렬)로 상태 확인 용이.
모듈/보드 특성 대응
- /SONGS vs /DRUMS 분리:
- /SONGS: 원본 그대로 재생(초기 CC/PC 미전송) → 불필요한 설정 충돌 차단
- /DRUMS: 시작 시 CH10에만 Volume/Reverb/DrumKit(PC) 전송(SAM9703는 PC만으로 킷 변경)
- All Notes Off: 인코더 버튼 롱프레스로 긴급 음 끊김 방지.
나노 아두이노 설계를 시작할 때만 하더라도 이렇게 복잡하고 까다로운 난관을 타개해 나가야 할 것이라고는 전혀 생각하지 못하였다. PC에서 .mid 파일을 만나면 단순히 더블클릭만 해서 어쨌든 소리를 들을 수 있다. 하지만 아두이노는 그렇지 못하다. 예를 들어 C:\Windows\Media 폴더에 전통적으로 존재해 오던 ONESTOP.MID라는 약 4분 분량의 MIDI 파일을 보자. html-midi-player("Play and display MIDI files on the web")에 업로드하면 노트 정보의 시각화는 물론 재생까지 이루어진다. 이 는 다양한 악기가 여러 장르를 거쳐 연주되는 매우 수준 높은 MIDI 파일이다.
| html-midi-player("Play and display MIDI files on the web")에 로드한 ONESTOP.MID 파일. |
Nano Ardule은 이 파일을 재생하다가 1분이 되기 전에 타악기 소리를 와르르 쏟아내고는 멈춘다. 그 이유를 알기 위해서 문제가 일어난 구간의 MIDI 파일의 이벤트를 텍스트 형태로 출력한 뒤(챗GPT를 이용하여 이러한 용도의 파이썬 스크립트를 만들었음) 분석을 해 보았다. 이외에도 부수적으로 만든 파이썬 스크립트가 여럿 있다.
00:35:000 ~ 00:45:500 Time(mm:ss:ms) | AbsTick | Δ? | Kind | Detail ----------------+---------+----+--------+-------------------------------------- 00:35:051 | 9125 | 0 | CHAN | NOTE_ON ch= 1 note= 72 vel= 0 00:35:067 | 9129 | 4 | CHAN | NOTE_ON ch= 1 note= 74 vel= 64 00:35:082 | 9133 | 4 | CHAN | NOTE_ON ch= 8 note= 72 vel= 0 00:35:121 | 9143 | 10 | CHAN | NOTE_ON ch=12 note= 38 vel=115 ...
분석 결론은 다음과 같았다. 핵심은 노트보다 컨트롤 메시지(특히 Pitch Bend, CC, Bank/PC)가 폭주하는 구간이 있다는 점이다.
- Pitch Bend 홍수
- Bank Select + Program Change의 동시 발생
- Mode Wheel(CC#1), Volume(CC#7) 연속 변화
- Δ=0/2/4 정도의 촘촘한 이벤트 다발
PC에서는 내부 버퍼·타임스탬프 큐·멀티스레드로 완충되지만, 아두이노는 UART 대역폭과 싱글 루프 때문에 그대로 내보내면 밀립니다.
오늘의 목표는 ONESTOP.MID 파일을 제대로 재생하게 만드는 것이다. 그 다음으로는 드럼 연주 데이터만 들어있는 MIDI 파일을 2-마디 패턴으로 분해한 뒤 반복 재생하거나. 또는 '패턴 체인' 형태를 만들어서 곡 전체에 해당하는 드럼 연주를 제공하는 것.
아두이노를 이용한 드럼 패턴 플레이어를 만들기 위해 많은 아이디어가 스쳐 지나갔다. 실은 구글에서 online drum machine이라고만 입력하여 검색을 해도 꽤 좋은 서비스가 많다. 하지만 나는 인터넷에 연결되지 않은 상태에서도 이동하면서 사용 가능한 드럼 패턴 플레이어를 원한다.
2마디 단위의 드럼 패턴을 그리드 형태로 양자화한 파일(Ardule Drum Pattern System; 바이너리 파일은 .ADP, 사람이 이해하기 쉬운 텍스트 버전은 .APT)을 마이크로SD카드에 저장해 놓은 뒤 원하는 것을 골라서 반복 재생하는 방식을 비롯하여 MIDI 파일을 2마디 단위로 직접 쪼개서 별도의 파일로 저장한 다음 활용하는 것에 이르기까지 여러 방법의 가능성을 따져 보았다. ADP 체계는 마이크로SD카드 의 MIDI 파일 접근이 쉽지 않아 애를 먹던 시절, 아예 메모리에 저용량의 패턴 데이터를 담아 보려고 생각한 것이다. 코드에 넣어 두었다면 Flash(PROGMEM)을, SD에서 불러온 패턴이라면 SRAM(겨우 2KB!)를, 그리고 사용자 커스텀 패턴이라면 EEPROM(이건 겨우 1kb에 불과함)를 쓰려고 했었다.
| 텍스트 형태인 APT(Ardule Pattern Text)의 사례. 이는 설계 및 편집을 위한 저장용 포맷이고, 실제로 프로그램 내부에서는 바이너리 형태(ADP)로 저장하려고 계획하였다. 드럼 전용이라면 ADT라고 쓰는 것이 옳다. |
챗GPT의 해결책에는 이해하기 어려운 전문적인 내용도 많다. 계속되는 질문과 대답 속에서 다음과 같은 방법으로 거의 구체화가 되어가고 있다.
- (PC) 드럼 연주 정보가 담긴 type 0 MIDI 파일을 2마디 단위의 패턴으로 나눈 뒤, 출현빈도 역순으로 정리한다. 하나의 MIDI 파일에서 유래한 각 패턴은 위치 정보와 MIDI 이벤트 형태로 묶어서 하나의 파일에 정리한다. 이를 self-contained multi-ADT(.MAD)라고 부르기로 한다.
- MAD 파일의 뒷부분에는 완성된 곡 형태의 ARR 데이터를 담을 수 있다.
- (Nano Ardule) .MAD 파일을 읽어서 지정한 패턴의 반복 연주, 또는 ARR 데이터 연주를 실시한다.
초창기에는 ADP system을 고려하다가 표현력에 제한이 있으리라는 것을 이내 알게 되었다. 따라서 온전한 형태의 드럼 MIDI 파일을 아두이노에서 로드한 뒤 스캔하여 패턴 위치 및 빈도 정보를 추출한 뒤 이를 반복 재생하는 방법을 생각했었다. 그러나 서로 떨어진 위치의 패턴을 조합하여 체인을 만들기에는 아두이노에게 부담이 된다. 그래서 이러한 방식까지 진화하였다. .MAD의 개념 도입 초기에는 원본 MIDI 파일을 참조하는 방식을 생각했다가 생각을 바꾸었다. 하지만 ADP는 데모용 기본 리듬 등으로 활용할 가치가 있으니 아직 내다 버리지는 말도록 하자.
제안: multi-ADT 포맷 (권장 확장자: .MAD)
-
파일명:
FOO.MAD(8.3 호환) -
목적: 여러 2-bar 패턴 + 어레인지(패턴 번호/반복수) + 메타(템포, 드럼킷, 카운트-인) 모두 한 파일에.
-
재생: 아두이노는 헤더의 오프셋 테이블을 읽어 패턴 위치로 즉시 seek → 끊김 없이 루프/점프.
바이너리 구조(간결 사양)
포인트: tick→byte 인덱스가 이미 파일 안에 내장되어 있어 패턴 시작 시 즉시 seek만 하면 됩니다.
템포가 바뀌는 패턴은 패턴 이벤트 안에 FF 51 03 메타를 넣어도 되고(Flags로 RAW모드), 대부분의 드럼 패턴은 고정 템포로 충분합니다.
텍스트 디버그 버전(선택): .MADT (사람 읽기용)
개발 단계에서 가독성↑ / 최종 배포는 .MAD 사용.
장점 요약
-
무지연 점프/루프: 패턴 오프셋으로 즉시 seek → 카운트-인 동안 준비 끝.
사이드카 정리:
.MID + .PAT + .ARR가 한 파일로 통합.-
버스트 감소: 시작·전환부에 필요한 CC/PC/템포 이벤트를 패턴 전에 삽입해 둠.
-
8.3 안전:
FOO.MAD하나로 끝.
설계를 구체화하기 위해 너무나 많은 문제와 해결책, 그리고 잘 모르던 개념 사이를 방황하고 다녔더니 이렇게라도 문서로 정리하지 않으면 다 잊어버릴 것만 같다.
MIDI와 오픈 사이언스를 연결할 수 있을까? 자동 생성으로 쓴 간단한 글 한편을 소개한다.
댓글 없음:
댓글 쓰기