레이블이 ADT인 게시물을 표시합니다. 모든 게시물 표시
레이블이 ADT인 게시물을 표시합니다. 모든 게시물 표시

2026년 7월 26일 일요일

비정형 박자는 이미 가능했다 — ADT 설계의 재발견과 Nano Ardule·Fluid Ardule의 통섭

오늘부터 Fluid Ardule에서 사용할 드럼 패턴의 비정형 박자를 시험하기 위해 작은 명령행 파이썬 프로그램인 ADX Player를 만들고 있다. 3/4, 5/4, 7/8, 6/8처럼 4/4가 아닌 박자를 실제로 재생해 보려는 것이 이번 작업의 출발점이었다. 시험을 위한 플랫폼은 라즈베리 파이(Fluid Ardule) 그 자체이다. 다시 말해서 Fluid Ardule는 개발 대상이면서 동시에 테스트베드(testbed) 역할도 수행한다.

처음에는 비정형 박자를 지원하려면 ADT 규격과 재생기를 상당히 고쳐야 할 것으로 생각했다. 그러나 기존의 ADT v2.2b 규격을 다시 살펴보고 실제 플레이어를 구현해 보니 뜻밖의 사실이 드러났다.

ADT는 이미 비정형 박자를 표현하고 재생하는 데 필요한 구조를 갖추고 있었다. 여기에서 잠시 ADT를 고안한 이유를 설명하고 넘어가려고 한다. 

Nano Ardule를 개발하던 당시, Arduino Nano에서 SD 카드에 저장된 Standard MIDI File을 별도의 MIDI 재생 라이브러리 없이 직접 스트리밍 방식으로 재생하는 기능을 구현하였다. 메모리가 극히 제한된 환경에서 MIDI 이벤트를 실시간으로 해석하며 재생하는 것은 쉽지 않은 작업이었지만, 결국 원하는 수준의 재생기를 완성할 수 있었다.

그러나 곧 다른 문제가 드러났다. 드럼 반주만을 반복 재생하는 용도로는 MIDI가 지나치게 범용적인 포맷이었다. 특히 Arduino Nano와 같은 제한된 환경에서는 짧은 드럼 패턴을 끊김 없이 반복(loop) 재생하도록 구현하는 일이 생각보다 까다로웠다. 또한 MIDI 이벤트를 텍스트로 살펴보아서는 실제 어떤 리듬인지 직관적으로 이해하기 어려웠고, 패턴을 수정하려면 사실상 DAW와 같은 별도의 MIDI 편집기가 필요했다.

ADT는 MIDI를 대체하기 위해 만든 것이 아니다. 사람이 읽고 편집하기 쉬우면서도, 드럼 패턴을 플랫폼 독립적으로 교환하고 재생하기 위한 경량 텍스트 규격이 필요하다는 판단에서 출발하였다. ADT는 이러한 요구를 충족하는 사람이 읽을 수 있는 소스 형식이며, 이를 빠르게 저장하고 재생하기 위해 컴파일한 바이너리 캐시 형식이 ADP이다.

반복하자면 ADT와 ADP는 원래 자원이 제한된 Arduino Nano 기반의 Nano Ardule을 위해 만들어졌다. 지금 개발을 이어나가는 Fluid Ardule은 Raspberry Pi와 FluidSynth를 기반으로 훨씬 풍부한 음원과 사용자 인터페이스를 제공하는 DIY MIDI sound module이다.

두 시스템은 서로 다른 시대와 하드웨어 환경에서 만들어졌지만, 이번 비정형 박자 구현 작업을 통해 Nano Ardule에서 탄생한 ADT/ADP의 설계가 Fluid Ardule의 새로운 드럼 반주 기능으로 자연스럽게 이어지고 있다.

단순히 예전 파일 형식을 다시 사용하는 정도가 아니다. Nano Ardule에서 축적된 간결한 패턴 표현 방식과 Fluid Ardule의 소프트웨어 음원, MIDI 처리 능력, 확장 가능한 사용자 환경이 하나의 체계 안에서 결합되고 있다.

이제야 Nano Ardule과 Fluid Ardule 사이의 진정한 통섭이 이루어지고 있다고 해도 좋을 것이다.

이번에 한 일은 새로운 포맷을 발명한 것이 아니라, 이미 만들어진 규격 안에 들어 있던 일반성을 다시 확인하고 구현한 것에 가깝다. Nano Ardule 시절의 설계가 사라지지 않고, 더 강력한 Fluid Ardule 안에서 새로운 역할을 얻은 것이다.

오늘의 글에서는 복습을 겸하여 중요한 개념을 국문으로 정리하였다. 초안 작성에는 ChatGPT의 도움을 받았다. 인터넷에서 이런 종류의 글은 아마 거의 찾아볼 수 없을 것이다. 이 글은 GitHub의 README도 아니고, 소프트웨어 개발 일지도 아니며, 실용음악 이론도 아니고, 논문도 아니다. '설계 회고(Design Retrospective)'에 해당한다고 보는 것이 옳다.


드럼 패턴은 시간과 악기로 이루어진 매트릭스다

ADT의 기본 개념은 드럼 패턴을 하나의 2차원 매트릭스, 즉 행렬로 표현하는 것이다.

시간을 X축으로 놓으면 각 열(column)은 하나의 Step이 된다. 세로축의 각 행(row)은 하나의 드럼 악기, 즉 Slot이 된다. Step size를 16분음표로 정의할 경우, 아래의 매트릭스는 매우 전형적인 4비트 록 드럼 패턴을 나타낸다.

                         시간: Step →
          
Step        0 1 2 3 | 4 5 6 7 | 8 9 A B | C D E F
----------------------------------------------------
Crash       O . . . | . . . . | . . . . | . . . .
Open HH     . . . . | . . . . | . . . . | . . O .
Closed HH   . . x . | x . O . | O . x . | x . . .
Snare       . . . . | X . . . | . . . . | X . . .
Kick        O . . . | . . O . | O . . . | . O . .

예를 들어 Step 4의 Kick과 Snare 위치에 모두 타격 기호가 있다면, 재생기는 그 순간 킥과 스네어를 동시에 연주한다. 따라서 하나의 Step에는 여러 Slot의 이벤트가 함께 들어갈 수 있다.

ADT의 패턴 본문은 결국 다음과 같은 논리적 구조를 기록한다.

Pattern[step][slot] = accent level

각 셀에는 단순한 ON/OFF뿐 아니라 네 단계의 상대적 액센트가 저장된다. MIDI의 벨로시티(0-127)를 단순화한 것이다.

기호 레벨 의미
. 0 쉼, 타격 없음
- 1 약한 타격
x 2 보통 타격
o 3 강한 타격

ADT는 같은 매트릭스를 두 방향으로 기록할 수 있다

논리적으로는 시간축(X축)에 Step을 배치하고, Slot을 행으로 나열한 형태가 드럼 패턴을 눈으로 읽고 편집하기에 가장 자연스럽다.

ADT에서는 이 매트릭스를 파일에 기록할 때 각 행(row)이 무엇을 나타내느냐에 따라 두 가지 방향을 사용할 수 있다. 두 가지 기록 방식은 완전히 동일하다. 사람이 보기에는 첫 번째 방법(SLOT 방향)이 편하고, 저장 기본값은 두 번째 방법인 STEP이다.

1. SLOT 방향

ORIENTATION=SLOT에서는 한 행이 하나의 악기를 나타낸다. 따라서 본문의 행 수는 Slot 수와 같고, 각 행에는 시간 순서대로 LENGTH개의 기호가 들어간다. 

ORIENTATION=SLOT

O...O...O...O...
....X.......X...
x.x.x.x.x.x.x.x.
................
...

이 형태에서는 시간축이 가로로 진행한다.

  • 한 행 = 하나의 Slot
  • 한 열 = 하나의 Step
  • 본문 행 수 = SLOTS
  • 각 행의 문자 수 = LENGTH

드럼 머신의 스텝 시퀀서 화면과 비슷하므로 사람이 패턴을 살펴보고 편집하기에 편리하다. 위에서 사례로 든 4비트 록 드럼 패턴이 바로 이렇게 표시한 것이다. 

2. STEP 방향(default)

ORIENTATION=STEP에서는 같은 매트릭스를 90도 돌린 것처럼 기록한다. 한 행이 하나의 시간 Step이 되고, 그 행 안의 각 문자가 Slot을 나타낸다.

ORIENTATION=STEP

O.x.........
............
..x.........
............
OXx.........
...

이 형태에서는 시간이 위에서 아래로 진행한다.

  • 한 행 = 하나의 Step
  • 한 열 = 하나의 Slot
  • 본문 행 수 = LENGTH
  • 각 행의 문자 수 = SLOTS

두 표현은 서로 다른 패턴이 아니다. 하나의 동일한 Step × Slot 매트릭스를 서로 90도 회전시켜 기록한 것뿐이다.

파서는 어느 방향으로 기록된 ADT를 읽더라도 내부에서는 보통 다음과 같은 STEP 기준 구조로 정규화할 수 있다.

grid[LENGTH][SLOTS]

최근 테스트 파일에서 발생한 오류도 이 두 방향의 차이에서 비롯되었다. 본문은 SLOT 방향으로 작성했지만 ORIENTATION=SLOT을 명시하지 않아, 파서가 기본값인 STEP 방향으로 해석했던 것이다.


Step: 시간축의 최소 칸

Step은 ADT 시간축의 최소 단위이다.

Step 자체가 언제나 16분음표를 뜻하는 것은 아니다. Step 하나의 실제 시간 길이는 GRID가 정한다.

예를 들어 다음과 같이 정의되어 있다면, Step 하나는 16분음표 하나에 해당한다.

GRID=16

Step 0, Step 1, Step 2는 각각 연속된 16분음표 시간 칸이다.

플레이어는 각 Step에 들어 있는 모든 Slot을 조사하여 필요한 MIDI 드럼 노트를 동시에 출력한 뒤, GRID가 정한 시간만큼 기다리고 다음 Step으로 이동한다.


Slot: 동시에 연주할 수 있는 악기 레인

Slot은 드럼 패턴의 악기축이다.

ADT의 기본 드럼 구성에서는 12개의 Slot을 사용하며, 각 Slot은 MIDI 드럼 노트 하나와 연결된다.

SLOTS=12

SLOT0=KK@36,KICK
SLOT1=SN@38,SNARE
SLOT2=CH@42,HH_CLOSED
SLOT3=OH@46,HH_OPEN
...

KK@36은 이 Slot의 약어가 KK이고 MIDI 노트 번호가 36이라는 뜻이다.

Slot은 시간의 길이를 결정하지 않는다. 단지 각 Step에서 어떤 악기가 울리는지를 결정한다.


GRID: 시간축의 해상도

GRID는 4분음표를 기준으로 Step이 얼마나 촘촘하게 나뉘는지를 정의한다.

GRID Step 하나의 의미 4분음표당 Step 수
16 일반 16분음표 4
8T 8분음표 셋잇단음표 3
16T 16분음표 셋잇단음표 6

따라서 GRID는 패턴의 박자를 정하는 값이라기보다, 시간축을 어느 정도의 해상도로 나눌 것인지를 정하는 값이다.


8T와 16T: 셋잇단음표 그리드

GRID 이름 뒤의 T는 Triplet, 즉 셋잇단음표를 뜻한다.

GRID=8T

4분음표 하나를 3개의 동일한 Step으로 나눈다.

4분음표 1개 = 3 Step

4/4 한 마디에는 4분음표가 4개 있으므로 12 Step, 두 마디에는 24 Step이 필요하다.

TIME_SIG=4/4
GRID=8T
LENGTH=24

GRID=16T

4분음표 하나를 6개의 동일한 Step으로 나눈다.

4분음표 1개 = 6 Step

따라서 4/4 두 마디는 48 Step이 된다.

TIME_SIG=4/4
GRID=16T
LENGTH=48

이 구조 덕분에 셔플이나 트리플렛 계열의 리듬도 별도의 예외적인 재생 알고리즘 없이 표현할 수 있다.


LENGTH: 패턴 전체의 Step 수

LENGTH는 패턴에 들어 있는 전체 Step의 수이다.

ADT 패턴은 관례적으로 두 마디 길이이지만, 두 마디가 언제나 32 Step인 것은 아니다. 실제 Step 수는 박자와 GRID에 따라 달라진다.

플레이어는 Step 0부터 LENGTH - 1까지 재생한 뒤 다시 Step 0으로 돌아간다.

for step in range(LENGTH):
    play_all_slots(step)
    wait_for_grid_interval()

repeat

따라서 실제 재생 엔진에서 가장 권위 있는 패턴 길이 값은 LENGTH이다. 나의 설계에서는 패턴 길이는 의도적으로 2 bar를 가정하고 있다. Player는 두 마디 중 어느 하나만 선택하여 재생할 수 있다.


TIME_SIG: 패턴의 박자

TIME_SIG는 Time Signature, 즉 패턴의 박자를 나타낸다.

TIME_SIG=4/4
TIME_SIG=3/4
TIME_SIG=5/4
TIME_SIG=7/8
TIME_SIG=6/8

분자는 한 마디에 들어 있는 기준 음표의 수를 나타내고, 분모는 그 기준 음표의 종류를 나타낸다.

예를 들어 7/8은 한 마디가 8분음표 일곱 개 길이라는 뜻이다.

TIME_SIG는 음악적 의미, 문서화, 마디 구분, 에디터 표시 및 LENGTH 검증에 사용된다.

그러나 현재의 ADT 재생 구조에서는 매 Step의 실제 간격은 GRID가 정하고, 루프의 끝은 LENGTH가 정한다. 따라서 플레이어는 TIME_SIG를 직접 참조하지 않고도 패턴을 정확한 길이로 재생할 수 있다.

이것은 TIME_SIG가 중요하지 않다는 뜻이 아니다. TIME_SIG는 패턴의 음악적 구조를 설명하고, GRID 및 LENGTH가 서로 일치하는지를 확인하는 중요한 메타데이터이다.


박자와 GRID에 따른 LENGTH 계산

분모가 4인 박자만 생각한다면 다음과 같이 간단하게 계산할 수 있다.

LENGTH
= 마디 수 × 한 마디의 박 수 × 한 박당 Step 수

그러나 7/8이나 6/8처럼 분모가 4가 아닌 박자까지 일반화하려면 박자의 분모도 계산에 반영해야 한다.

LENGTH
= 마디 수
× TIME_SIG 분자
× (4 / TIME_SIG 분모)
× GRID의 4분음표당 Step 수

ADT의 표준 패턴 길이인 두 마디를 기준으로 몇 가지 예를 계산하면 다음과 같다.

박자 GRID 한 마디 Step 두 마디 LENGTH
4/4 16 16 32
3/4 16 12 24
5/4 16 20 40
7/8 16 14 28
3/4 8T 9 18
6/8 8T 9 18

여기에서 3/4와 6/8은 마디의 전체 시간 길이는 같을 수 있지만, 음악적인 강세 구조는 다르다. 이 차이는 TIME_SIG가 설명하고, 실제 시간축의 세분화는 GRID가 담당한다.


왜 v2.2b만으로 비정형 박자가 가능한가

5/4를 예로 들어 보자.

GRID=16에서는 4분음표 한 박이 4 Step이다. 5/4 한 마디는 20 Step이고, ADT의 기본 길이인 두 마디는 40 Step이다.

TIME_SIG=5/4
GRID=16
LENGTH=40

플레이어는 40개의 Step을 재생하고 루프하면 된다. 5/4 전용 재생 루틴은 필요하지 않다.

7/8도 마찬가지다. GRID=16에서는 8분음표 하나가 2 Step이므로 한 마디는 14 Step, 두 마디는 28 Step이다.

TIME_SIG=7/8
GRID=16
LENGTH=28

재생기는 28 Step을 순서대로 재생한 뒤 처음으로 돌아가면 된다.

결국 비정형 박자를 가능하게 하는 핵심은 특정 박자별 예외 처리 코드가 아니라 다음 세 요소의 분리이다.

  • TIME_SIG: 음악적인 박자
  • GRID: 시간축의 해상도
  • LENGTH: 패턴 전체의 Step 수

이번 정리 작업의 의미

이번 문서 정리와 ADX Player 제작은 ADT에 새로운 기능을 억지로 추가하기 위한 작업이 아니다.

목적은 3/4, 5/4, 7/8, 6/8과 같은 비정형 박자를 실제로 구현하고 검증하는 것이다.

그 과정에서 확인한 것은 기존 ADT v2.2b 규격의 완성도가 예상보다 높았다는 사실이다.

ADT는 이미 다음 요소를 서로 독립적으로 정의하고 있었다.

  • 박자를 나타내는 TIME_SIG
  • 시간 해상도를 나타내는 GRID
  • 전체 재생 길이를 나타내는 LENGTH
  • 악기축을 나타내는 SLOT
  • STEP 방향과 SLOT 방향의 두 가지 본문 표현
  • 직선 계열과 트리플렛 계열의 GRID

특히 재생 엔진이 TIME_SIG에 종속되지 않고 LENGTH를 권위 있는 루프 길이로 사용하는 구조 덕분에, 기존의 동일한 재생 루프로 다양한 박자를 처리할 수 있었다.

과거에는 주로 4/4 패턴만 제작하고 사용했기 때문에 이 일반성이 눈에 잘 띄지 않았을 뿐이다.

비정형 박자는 새로 가능해진 것이 아니다. 이미 가능했으며, 이번 작업을 통해 그 사실을 명시적으로 확인한 것이다.

다만 7/8이나 6/8처럼 분모가 4가 아닌 박자까지 문서에서 완전히 일반화하려면 LENGTH 계산식에 TIME_SIG의 분모를 분명하게 반영할 필요가 있다. 이것은 파일 구조나 재생 방식의 변경이 아니라, 이미 가능한 동작을 더 정확하게 설명하기 위한 문서상의 보완이다.

ADT의 핵심 철학은 다음과 같이 요약할 수 있다.

ADT는 드럼 패턴을 Step과 Slot으로 이루어진 매트릭스로 표현한다. GRID는 각 Step의 시간 간격을 정하고, LENGTH는 패턴의 전체 길이를 정하며, TIME_SIG는 그 시간 구조의 음악적 박자를 설명한다.

SLOT 방향에서는 시간이 가로로 흐르고 악기가 세로로 배열된다. STEP 방향에서는 같은 매트릭스를 90도 돌려 시간이 세로로 흐르고 악기가 가로로 배열된다.

이 단순하고 분리된 구조 덕분에 ADT는 4/4에 갇혀 있지 않다. 3/4, 5/4, 7/8, 6/8과 트리플렛 계열 패턴도 동일한 자료 구조와 동일한 재생 엔진으로 처리할 수 있다.

이번 작업은 ADT를 새로 설계하는 작업이 아니라, 이미 잘 만들어져 있던 설계의 잠재력을 발견하고 실제로 증명하는 작업이다.

ADT는 오래된 포맷이 아니다. ADP의 장점을 계승하면서 사람 친화적인 텍스트 규격으로 새롭게 설계되었다. 그리고 불과 1년 만에 ADX Player를 통해 4/4를 넘어 다양한 박자를 자연스럽게 수용할 수 있음을 확인했다. 결과적으로 Nano Ardule에서 시작된 ADP와 Fluid Ardule에서 발전한 ADT는 이제 하나의 패턴 생태계를 이루게 되었고, 이것이야말로 두 프로젝트의 진정한 통섭이라 할 수 있다.


(회고) 왜 ADT v2.3 Final Draft를 만들었었나?

ADT v2.2b는 이미 충분히 완성도가 높은 규격이었다. 실제로 이번 작업을 진행하면서 가장 놀라웠던 점은 새로운 기능을 추가하지 않고도 3/4, 5/4, 6/8, 7/8과 같은 다양한 박자를 자연스럽게 표현할 수 있었다는 사실이다.

그렇다면 굳이 ADT v2.3 Final Draft를 만들 필요가 있었던 이유는 무엇일까?

결론부터 말하면, 규격을 바꾸기 위해서가 아니라 규격이 원래 지니고 있던 일반성을 더욱 정확하게 문서화하기 위해서였다.

이번에 ADX Player를 구현하면서 4/4가 아닌 여러 박자를 실제로 재생해 보았다. 그 과정에서 ADT의 핵심 요소인 TIME_SIG, GRID, LENGTH가 특정 박자에 종속되지 않는 일반적인 구조임을 다시 확인할 수 있었다.

v2.2b에도 이러한 개념은 이미 포함되어 있었다. 다만 설명과 예제가 주로 4/4 박자를 중심으로 작성되어 있었기 때문에, 독자가 ADT를 4/4 전용 규격으로 오해할 여지가 있었다. 또한 LENGTH를 계산하는 설명도 분모가 4가 아닌 박자까지 포괄하도록 더욱 일반화할 필요가 있었다.

따라서 v2.3 Final Draft에서는 다음 사항을 명확히 하거나 보완하였다.

  • ADT가 4/4뿐 아니라 다양한 박자를 표현할 수 있는 일반적인 패턴 규격임을 명시하였다.
  • TIME_SIG, GRID, LENGTH 사이의 관계를 더욱 정확하게 설명하였다.
  • 박자표의 분모까지 반영할 수 있도록 LENGTH 계산 원리를 일반화하였다.
  • 8T16T 등 트리플렛 그리드의 의미를 보완하였다.
  • STEPSLOT 표현이 서로 90도 회전한 관계임을 분명히 하였다.
  • 3/4, 5/4, 6/8, 7/8 등의 예제를 추가하여 규격의 일반성을 실제로 확인할 수 있게 하였다.

즉, v2.3은 새로운 기능을 추가하거나 파일 구조를 바꾼 버전이라기보다, v2.2b에 이미 들어 있던 설계 의도를 구현 결과에 맞추어 더 정확하게 설명하고 문서의 완성도를 높인 버전이라고 할 수 있다.

Final Draft라는 표현도 이러한 성격을 나타낸다. 규격의 핵심 구조는 사실상 확정되었지만, ADX Player와 실제 패턴을 통해 마지막 검증을 거친 뒤 정식 v2.3으로 확정하기 위한 최종 검토본이라는 뜻이다.

특히 이번 작업에서는 Nano Ardule에서 출발한 ADP의 간결한 패턴 구조와, 이후 사람이 읽고 편집할 수 있도록 설계한 ADT가 Raspberry Pi 기반의 Fluid Ardule에서 다시 활용될 수 있음이 확인되었다. 과거의 설계를 단순히 보존하는 데 그치지 않고, 새로운 재생 환경에서 그 일반성과 확장성을 실제로 증명한 것이다.

그런 의미에서 v2.3 Final Draft는 단순한 문서 개정판이 아니다. Nano Ardule과 Fluid Ardule의 진정한 통섭이 이루어지고 있음을 보여 주는 하나의 이정표이기도 하다.

ADT v2.3은 현재의 설계 목표를 대부분 달성하였다. 앞으로는 기존 구조를 유지하면서 표현력을 조금씩 확장해 나갈 예정이다. 현재 검토 중인 기능으로는 Legacy UI의 12슬롯에 제한되지 않는 사용자 정의 드럼 배열(Custom Drum Set) 과, 드럼 연주의 표현력을 높이기 위한 Flam 지원 등이 있다.

반면 Swing이나 Humanize와 같은 기능은 패턴 데이터 자체의 속성이라기보다 재생 엔진의 동작에 가까우므로, 포맷에 포함하기보다는 플레이어의 선택적 재생 옵션으로 다루는 방안을 우선 고려하고 있다.

2026년 7월 25일 토요일

Fluid Ardule에 드럼 패턴 재생 기능을 넣다

Fluid Ardule은 '전원을 넣고 키보드를 연결하면 즉시 연주 가능'한 MIDI 사운드 모듈을 지향한다. 인터넷 라디오, 각종 오디오 음원 파일 재생, 블루투스 오디오 재생 기능은 재미와 활용성을 높이기 위하여 추가한 것이다. 라즈베리파이 3B라는 제한된 하드웨어를 바탕으로 하기 때문에 샘플러라든가 작은 DAW와 같은 기능을 넣으려는 욕심을 철저히 차단하고 있다. 그래서 JACK이나 PortAudio 등을 절대 쓰지 않으며, 오로지 ALSA만을 이용한 단순하고 빠른 처리를 목표로 하였다.

하지만 드럼 재생 기능만큼은 넣어 보고 싶었다. Nano Ardule을 개발하면서 값진 자산으로 남은 드럼 패턴 라이브러리를 이용하면 어렵지 않게 Fluid Ardule의 운영 스크립트에서 드럼 패턴을 반복 재생하고, 연결된 키보드를 이용하여 드럼과 동시 연주도 가능할 것 같았다.



Nano Ardule 개발 당시 공개된 드럼 MID 파일을 분석하여 2-bar 단위의 패턴을 장르별로 정리해 둔 일이 있다. 이를 위한 두 가지의 전용 파일 포맷도 개발해 두었다. ADT는 사람이 읽고 편집할 수 있는 파일이고, 이를 검증하여 작은 바이너리 캐시로 만든 것은 ADP이다. ADP는 아두이노 나노(에브리)의 작은 메모리에도 비상용으로 수십개 정도는 넣을 수 있다.

사실 아두이노 나노보다 성능이 훨씬 좋은 라즈베리 파이에서는 ADT를 그대로 재생할 수 있다. 하지만 이를 위해서는 운영 스크립트 안에 텍스트 파서가 들어가야 하고, 잘못 작성된 패턴을 검사하는 코드도 필요하다. 예외 처리도 늘어났을 것이고, 앞으로 ADT 문법이 바뀔 때마다 Fluid Ardule도 함께 수정해야 했을지 모른다. 

하지만 ADP는 대단히 단순하다. 이미 검증이 다 끝난 상태이기 때문이다. 텍스트 파일을 다룰 때 필요한 파서 같은 것을 요구하지 않는다.

  • ADP 파일을 연다.
  • 헤더를 확인한다.
  • 스텝 데이터를 읽는다.
  • 해당 스텝의 드럼 음을 출력한다.

이것으로 끝이다. "Play with Drums"라는 메뉴를 넣을 위치를 결정하고, 대략의 화면을 구상한 뒤 지시문을 만들어서 ChatGPT에 ADP 파일 사양 문서와 함께 밀어 넣었다. 그것으로 끝. 인코더를 돌려서 BPM을 조절하고, 키보드 입력 신호를 동시에 연주하는 기능도 순식간에 구현하였다. 오히려 화면 안에서 텍스트 배치를 보기 좋게 조절하는 것에 가장 많은 시간을 쏟았다.

Nano Ardule 개발 당시 꽤 신경을 써서 만든 '드럼 패턴 데이터 표준안'이 결국 미래의 나를 위한 좋은 선물이었던 것이다. 물론 앞으로도 ADT는 필요하다. 사람이 읽고 수정할 수 있는 원본이 있어야 하기 때문이다. 그러나 실제 연주를 담당하는 프로그램에게는 검증이 끝난 ADP가 더 적합하다. 만약 2-bar MIDI drum pattern을 재생 대상으로 삼았더라면 타이밍 계산과 MIDI 이벤트 해석, 스케줄링을 처리하느라 개발 과정은 그야말로 재앙이 되었을 것이다. MIDI drum pattern은 악기 사용 빈도를 계산하고 ADT → ADP를 만드는 과정에서 매우 유용한 raw data 역할을 한 것은 솔직하게 시인한다.

내가 만든 체계에서는 드럼킷을 구성하는 악기(slot)를 12개로 제한한다. 실용적으로는 큰 문제가 없다. ADP에서는 슬롯 번호마다 다음과 같이 정해진 악기가 할당된다. 반면 ADT는 12개 슬롯이라는 원칙을 지키기만 한다면 다른 악기를 넣을 수 있다. 예를 들어 카우벨이나 탬버린 같은 것. 따라서 새로운 패턴을 만들 때에는 ADT가 필요하다.

노트 넘버 37번은 rim shot이 아니라 side stick이 맞을 것이다.

이러한 타악기 배치는 임의로 정한 것이 아니다. 인터넷에 공개된 다수의 드럼 패턴 파일(.MID)을 분석한 결과물이므로 일종의 (빅)데이터 분석을 통하여 얻은 결론이다. 이를 컴퓨터 키보드(16키) 또는 AKAI MPK MINI의 패드(8키 x 2뱅크)에 할당하여 입력하기 위해 나름대로 머리를 싸매서 고민을 한 결과가 바로 다음의 스펙 문서이다. 

APS Drum Instrument Sets & Pad Mapping - Reference Specification

실은 요즘 며칠 작업을 하면서 이 문서가 조금 잘못된 것을 깨달았고, 조금씩 수정을 가하고 있다. 결론적으로는 Legacy UI 12 Set를 실질적인 표준으로 삼는 것이 더욱 바람직하다. 내가 만들어 놓은 450개 가까운 ADT/ADP는 실제로 Legacy UI 12 Set를 따르고 있었다. 처음부터 목표를 세워서 이를 구현하기 위해 표준을 설계한 것이 아니라, 공개된 데이터를 분석하고 실제로 사용 가능한 형태로 다듬는 과정에서 자연스럽게 하나의 표준이 만들어졌던 것이다. 어쩌면 내가 숱하게 남긴 기록에도 불구하고 개발 과정을 정확하게 기억하지 못했을 수도 있지만.

결국 좋은 데이터 포맷이란 단순히 파일 크기가 작거나 처리 속도가 빠른 것이 아니다. 사람이 이해하고 수정할 수 있는 원본과, 기계가 빠르고 안정적으로 처리할 수 있는 표현을 적절히 분리하는 것이다. ADT와 ADP는 바로 그런 역할 분담을 목표로 만들어졌다. 몇 달이 지난 지금도 거의 수정 없이 새로운 프로젝트에서 다시 활용할 수 있었다는 사실이, 당시의 설계가 나쁘지 않았음을 증명해 주는 것 같다. 미래의 나는, 과거의 내가 남겨 둔 작은 선물을 기분 좋게 다시 꺼내 쓸 수 있었다.

2025년 10월 30일 목요일

[Nano Ardule] MIDI 파일 재생기를 만들기가 이렇게 어렵다니...

그림 자료를 곁들여 꽤 긴 글을 쓰고 있었는데 순식간에 날아가 버렸다. 키보드를 잘못 건드린 것도 없는데 도대체 무슨 일이 벌어진 것일까? 시스템 또는 서비스의 일시적인 문제라고 믿기로 했다. 지금까지 이런 일은 없었기 때문이다.

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 체크로 송신 버퍼 여유 확인.
SD 파일 접근 안정화
  • 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 제거 + 줄바꿈 통일.
UI/입력 안정화

  • 버튼 디바운스: 모든 버튼에 소프트 디바운스(약 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의 해결책에는 이해하기 어려운 전문적인 내용도 많다. 계속되는 질문과 대답 속에서 다음과 같은 방법으로 거의 구체화가 되어가고 있다.

  1. (PC) 드럼 연주 정보가 담긴 type 0 MIDI 파일을 2마디 단위의 패턴으로 나눈 뒤, 출현빈도 역순으로 정리한다. 하나의 MIDI 파일에서 유래한 각 패턴은 위치 정보와 MIDI 이벤트 형태로 묶어서 하나의 파일에 정리한다. 이를 self-contained multi-ADT(.MAD)라고 부르기로 한다.
  2. MAD 파일의 뒷부분에는 완성된 곡 형태의 ARR 데이터를 담을 수 있다.
  3. (Nano Ardule) .MAD 파일을 읽어서 지정한 패턴의 반복 연주, 또는 ARR 데이터 연주를 실시한다.

초창기에는 ADP system을 고려하다가 표현력에 제한이 있으리라는 것을 이내 알게 되었다. 따라서 온전한 형태의 드럼 MIDI 파일을 아두이노에서 로드한 뒤 스캔하여 패턴 위치 및 빈도 정보를 추출한 뒤 이를 반복 재생하는 방법을 생각했었다. 그러나 서로 떨어진 위치의 패턴을 조합하여 체인을 만들기에는 아두이노에게 부담이 된다. 그래서 이러한 방식까지 진화하였다. .MAD의 개념 도입 초기에는 원본 MIDI 파일을 참조하는 방식을 생각했다가 생각을 바꾸었다. 하지만 ADP는 데모용 기본 리듬 등으로 활용할 가치가 있으니 아직 내다 버리지는 말도록 하자.

제안: multi-ADT 포맷 (권장 확장자: .MAD)

  • 파일명: FOO.MAD (8.3 호환)

  • 목적: 여러 2-bar 패턴 + 어레인지(패턴 번호/반복수) + 메타(템포, 드럼킷, 카운트-인) 모두 한 파일에.

  • 재생: 아두이노는 헤더의 오프셋 테이블을 읽어 패턴 위치로 즉시 seek끊김 없이 루프/점프.

바이너리 구조(간결 사양)

Offset Size Field 0x00 4 Magic = "MADT" 0x04 1 Version = 1 0x05 1 Flags (bit0=RAW-tick, bit1=GRID, bit2=HasTempoMap ...) 0x06 2 PPQN (e.g., 480) 0x08 2 Default BPM (for count-in; 곡 중 템포맵은 패턴 내 이벤트에 포함 가능) 0x0A 1 DrumKit PC (채널10 전제; 0=무시) 0x0B 1 CountIn Beats (2 or 4) 0x0C 2 PatternCount (N) 0x0E 2 ArrangeCount (M) // (패턴번호, 반복수) 개수 0x10 4*N PatternOffset[i] // 각 패턴 레코드의 파일 오프셋 ... 4*M ArrangePairs[j] // packed: (hi16=patternIndex, lo16=repeats) --- 각 패턴 레코드 --- PattHdr: +0 4 length_ticks (대개 2bar = 8*PPQN) +4 4 event_bytes (아래 이벤트 스트림 길이) +8 ? 이벤트 스트림 (델타-타임 + 미디메시지) Event Stream 인코딩(가볍고 빠르게): [VLQ delta] [Status or 0x00=running] [data1] [data2?] - 메타/시스엑스는 필요 시 스킵 가능 옵션(Flags) - 드럼 전용이면 CH10 메시지 위주로 간소화 가능

포인트: tick→byte 인덱스가 이미 파일 안에 내장되어 있어 패턴 시작 시 즉시 seek만 하면 됩니다.
템포가 바뀌는 패턴은 패턴 이벤트 안에 FF 51 03 메타를 넣어도 되고(Flags로 RAW모드), 대부분의 드럼 패턴은 고정 템포로 충분합니다.

텍스트 디버그 버전(선택): .MADT (사람 읽기용)

MADT, v=1, ppqn=480, bpm=120, kit=26, countin=2 PATTERN 1, len_ticks=3840 dt=0, 90 24 64 dt=120,90 2A 64 ... PATTERN 2, len_ticks=3840 ... ARR: 1x4,2x2,3x4,2x2,1x8

개발 단계에서 가독성↑ / 최종 배포는 .MAD 사용.


장점 요약

  • 무지연 점프/루프: 패턴 오프셋으로 즉시 seek → 카운트-인 동안 준비 끝.

  • 사이드카 정리: .MID + .PAT + .ARR한 파일로 통합.

  • 버스트 감소: 시작·전환부에 필요한 CC/PC/템포 이벤트를 패턴 전에 삽입해 둠.

  • 8.3 안전: FOO.MAD 하나로 끝.

설계를 구체화하기 위해 너무나 많은 문제와 해결책, 그리고 잘 모르던 개념 사이를 방황하고 다녔더니 이렇게라도 문서로 정리하지 않으면 다 잊어버릴 것만 같다.

MIDI와 오픈 사이언스를 연결할 수 있을까? 자동 생성으로 쓴 간단한 글 한편을 소개한다.

MIDI, 오픈 사이언스가 배워야 할 표준의 철학 


2025년 11월 17일 업데이트

ONESTOP.MID가 재생 중 멈춘 것은 이벤트가 많아서 그런 것이 아니었다. 파일 중간에 삽입된 end of track 메시지를 재생기가 너무 민감하게 받아들인 것이다. Type 0 MIDI 파일이므로 이론적으로 track은 하나여야 하고, 파일 맨 끝에 단 하나의 end of track이 있어야 하나 세상에 존재하는 MIDI 파일이 그렇게 깨끗하고 순수하지는 않다. '적당히 지저분한 MIDI 파일'이라 하더라도 중간에 멈추지 않고 무난하게 재생하도록 재생 엔진을 수정하였다. 재생 중단의 원인을 알아내는 것도 결코 쉽지 않았다.