Table of Contents
ArdulePlayer v112325 — SGL 모드 시작 지연 버그 보고서 및 해결 문서
2025-11-23 — 정해영 / ChatGPT 공동 기록
1. 문제 개요
Arduino Nano Every 기반 ArdulePlayer v112325에서
SGL (Single Pattern Play) 모드로 2-bar 드럼 패턴을 재생할 때 다음 문제가 관찰되었다.
🔴 증상 요약
- 패턴 재생 시작 시 첫 2~3 스텝의 노트가 늦게 출력됨
- SGL 모드에서 PLAY를 눌렀을 때,
- 패턴의 맨 앞에 있어야 할 노트들이 즉시 나오지 않고,
- 3스텝 정도 뒤에서야 첫 노트가 들림.
- 그 이후 구간(반복 포함)은 그리드에 맞게 정상적으로 재생됨.
- LED 비트 표시가 소리보다 1박자 정도 늦게 보임
- 비트 LED(LED_A0/A1/A2)가 강박을 표시하지만,
- 실제 들리는 드럼 소리보다 항상 1박자 밀려서 점등되는 것처럼 느껴짐.
- PreviewAll(자동 순환 프리뷰) 모드는 정상
- Preview 모드에서는 패턴 시작 타이밍과 LED가 잘 맞음.
- 문제가 발생하는 것은 SGL 모드에서의 재생 시작 시점으로 국한됨.
2. 문제의 근본 원인 분석
2.1 관련 엔진 구조
패턴 재생과 비트 LED는 서로 다른 함수에서 구동된다.
- 패턴 재생 엔진
- 함수:
servicePatternPlayback() - 시간 기준:
micros()→nowUs - 패턴 루프 기준:
patLoopStartUs - 이벤트 시점 계산:
evUs = patLoopStartUs + tick * usPerTick;
- 비트/LED 엔진
- 함수:
serviceBeatEngine() - 시간 기준:
millis()→now - 비트 간격:
beatIntervalMs - 다음 비트 시각:
nextBeatMs - 시작 시점:
startBeatEngine()에서
uint32_t now = millis(); nextBeatMs = now;
2.2 기존 코드의 문제점
기존에는 다음과 같은 코드가 존재했다.
- 패턴 로딩 시점에서 재생 기준 설정
loadCurrentPatternIntoMemory()의 마지막 부분:
<code cpp>
usPerQuarter = 60000000UL / (uint32_t)previewBpm;
if (usPerQuarter == 0) usPerQuarter = 1;
usPerTick = usPerQuarter / (uint32_t)midiPPQ;
if (usPerTick == 0) usPerTick = 1;
patLoopStartUs = micros();
patEventIndex = 0;
patLoaded = true;
</code>
→ **패턴을 SD에서 읽어올 때** 이미 ''%%patLoopStartUs%%''가 찍혀 버림.
- **SGL 시작 함수에서도 재생 기준을 또 설정 시도**
''%%startSinglePatternPlayback()%%''의 기존 형태(수정 전):
<code cpp>
void startSinglePatternPlayback() {
if (!sdOK) return;
if (!buildCurrentPatternFilePath(currentFilePath, sizeof(currentFilePath))) return;
if (!loadCurrentPatternIntoMemory()) return;
playSource = PLAY_SRC_PATTERN;
startBeatEngine();
}
</code>
일부 버전에서는 여기에서 또 ''%%patLoopStartUs = micros();%%'' 를 시도하기도 했음.
2.3 시간 기준이 어긋나는 방식
loop()의 실행 순서 상,- 먼저
serviceBeatEngine()이 호출되어nextBeatMs및 LED가 준비되고, - 그 다음에
servicePatternPlayback()이 호출되어 패턴 이벤트가 재생된다.
- 그런데 패턴 재생 기준 시간
patLoopStartUs가 “로딩 시점” 혹은 “버튼 처리 시점”에 설정되어 있어,- 실제 재생 루프에서의
nowUs와, patLoopStartUs + tick * usPerTick가 미묘하게 어긋난 상태가 된다.
그 결과:
- 초기 몇 스텝의 이벤트가 “아직 시간이 안 됐다”고 판단됨
if (nowUs < evUs) break;
- 첫 이벤트들의
evUs가 실제nowUs보다 미래로 계산되기 때문에, - 초기 2~3 스텝 동안은
while루프가 바로break되어 이벤트가 출력되지 않음. - 그 뒤부터는 루프 타이밍과 시간이 맞아서 정상 재생되는 것처럼 들림.
- LED는 다른 타이밍 기준을 사용
- LED는
startBeatEngine()에서nextBeatMs = millis()를 기준으로 바로 0번째 비트를 표시 시작. - 패턴은
patLoopStartUs가 늦게 설정되어 있어, 실제 노트는 LED보다 늦게 들림. - 따라서 LED가 1박자 뒤에서 따라오는 것처럼 느껴지는 현상이 발생.
3. 해결 원리
🎯 핵심 아이디어
패턴 재생 기준 시점(patLoopStartUs)은 “패턴 엔진이 실제로 첫 번째 루프를 실행하는 순간(nowUs)”에만 설정한다.
즉,
- 패턴 로딩 시점이나 버튼 입력 시점이 아니라,
servicePatternPlayback()이 실제로 처음 호출되어nowUs = micros()를 계산한 그 순간에:patLoopStartUs = nowUs;patEventIndex = 0;- 를 설정하도록 변경한다.
이렇게 하면:
- 패턴 엔진과 LED 엔진 모두,
- 동일한 loop iteration에서
- 각자의 시작 기준(0박자)을 설정하게 되고,
- 초기 스텝 딜레이와 LED 1박자 지연이 동시에 사라진다.
4. 실제 패치 내용 (v112325)
4.1 전역 플래그 추가
ArduleEngine_111925_v2_5_storageSplit.ino 상단에 다음 플래그를 추가하였다.
// 패턴 재생 엔진이 "첫 루프"인지 여부를 나타내는 플래그 static bool patFirstTick = true;
- 이 플래그는 패턴 재생을 시작할 때
true로 세팅되고, servicePatternPlayback()이 처음 실행되는 순간에만 사용되어,patLoopStartUs와patEventIndex를 초기화한 뒤,- 다시
false로 전환된다.
4.2 startSinglePatternPlayback() 수정
기존에는 SGL 시작 시점에서 patLoopStartUs를 건드릴 수도 있는 구조였으나,
이제는 패턴 엔진 초기화 플래그만 세우고, 실제 시간 기준 설정은 엔진에게 맡긴다.
void startSinglePatternPlayback() { if (!sdOK) return; if (!buildCurrentPatternFilePath(currentFilePath, sizeof(currentFilePath))) return; if (!loadCurrentPatternIntoMemory()) return; // SGL 모드에서 자동 순환 프리뷰는 끈다 previewAllMode = false; previewLoopCount = 0; // 패턴 이벤트 인덱스 리셋 patEventIndex = 0; // 🔴 다음 servicePatternPlayback() 첫 호출 때 // patLoopStartUs를 nowUs로 맞추도록 플래그 세팅 patFirstTick = true; // 패턴 재생 + 비트/LED 엔진 시작 playSource = PLAY_SRC_PATTERN; startBeatEngine(); }
4.3 servicePatternPlayback()에서 첫 루프 시점에 기준 설정
servicePatternPlayback()의 상단부는 다음과 같이 변경되었다.
void servicePatternPlayback() { if (playState != PLAYSTATE_PLAYING) return; if (playSource != PLAY_SRC_PATTERN) return; if (!patLoaded) return; uint32_t nowUs = micros(); // 🔴 패턴 재생 엔진이 "처음" 동작하는 순간에만 // 현재 시각을 루프 시작 기준으로 잡는다. if (patFirstTick) { patLoopStartUs = nowUs; // 재생 타임라인의 0 시점 patEventIndex = 0; patFirstTick = false; } uint32_t loopLenUs = (uint32_t)patLoopLenTicks * usPerTick; if (loopLenUs == 0) return; if ((nowUs - patLoopStartUs) >= loopLenUs) { patLoopStartUs += loopLenUs; patEventIndex = 0; if (previewAllMode) { previewLoopCount++; if (previewLoopCount >= PREVIEW_LOOPS_PER_PATTERN) { previewLoopCount = 0; advanceAutoPatternPreview(); return; } } } while (patEventIndex < patEventCount) { uint32_t evUs = patLoopStartUs + (uint32_t)patEvents[patEventIndex].tick * usPerTick; if (nowUs < evUs) break; uint8_t st = patEvents[patEventIndex].status; uint8_t d1 = patEvents[patEventIndex].d1; uint8_t d2 = patEvents[patEventIndex].d2; uint8_t type = st & 0xF0; if (type == 0x80 || type == 0x90 || type == 0xA0 || type == 0xB0 || type == 0xE0) { sendMidiMessage3(st, d1, d2); } else if (type == 0xC0 || type == 0xD0) { sendMidiMessage2(st, d1); } patEventIndex++; } }
이제:
- 패턴 로딩 시점과는 관계없이,
- 실제로
servicePatternPlayback()이 동작하는 첫 순간(nowUs)에patLoopStartUs가 설정되고,- 패턴 이벤트 재생도 0틱에서 시작한다.
4.4 loadCurrentPatternIntoMemory()에서 타임라인 초기화 제거
ArduleStorage_111925_v2_5.ino의 loadCurrentPatternIntoMemory() 마지막 부분에서
다음 두 줄을 제거(또는 주석 처리)하였다.
// 기존 코드 (제거됨) patLoopStartUs = micros(); patEventIndex = 0;
이제 이 함수는 패턴 데이터를 SD에서 읽어와 메모리에 적재하는 일만 담당하며,
실제 재생 타이밍은 엔진(servicePatternPlayback) 쪽에서 책임진다.
5. 수정 후 동작 확인
아래와 같은 방식으로 테스트를 수행하였다.
- 단순 4분 킥 패턴 테스트
- 2-bar 동안 킥만 4분음표로 찍힌 패턴을 사용.
- Preview 모드 / SGL 모드에서 각각 재생.
- 결과:
- 두 모드 모두 첫 킥이 정확히 LED 강박과 일치.
- 더 이상 첫 2~3스텝이 늦게 나오지 않음.
- 일반 드럼 패턴(하이햇+스네어+킥) 테스트
- 실제 사용 중인 2-bar 패턴 여러 개로 검증.
- SGL 모드로 재생 시작 시:
- 첫 마디부터 그리드에 맞게 정확히 재생됨.
- LED도 1박자 지연 없이 정상적으로 따라감.
- PreviewAll 모드 재검증
- PreviewAll 모드에도 동일 엔진이 사용되지만,
- 기존에도 문제가 없었고,
- 이번 수정 이후에도 동작에 변화 없음(정상 유지).
6. 아키텍처적 의미
이번 수정으로 ArdulePlayer v112325는 다음과 같은 구조적 개선을 얻었다.
- 절대시간 기반 엔진의 통일된 시작 기준 확립
- Preview / SGL / SONG 모드 모두,
- “재생 엔진이 실제로 움직이기 시작하는 순간”을 시간 0으로 사용.
- 로딩/버튼 입력 타이밍과 재생 타이밍의 분리
- SD I/O, UI 조작, 모드 전환 등은 재생 타임라인에 영향을 주지 않음.
- 오로지 재생 엔진의 첫 루프만이 기준을 결정.
- 향후 기능 확장에 유리한 토대
- seamless pattern switching,
- SONG ↔ PATTERN 전환,
- 패턴 간 crossfade 등 시간 축에 민감한 기능을 추가하기 쉬워짐.
즉, 이번 패치는 단순한 버그 수정 그 이상으로,
Ardule Engine v2.5를 보다 안정적인 v2.6 수준의 구조로 끌어올린 의미가 있다.
7. 결론
- 버그 요약
- SGL 모드에서 패턴 재생을 시작할 때,
- 패턴 재생 기준 시점(patLoopStartUs)과 LED 비트 시점이 서로 어긋나면서,
- 첫 2~3 스텝이 늦게 재생되고 LED는 1박자 늦게 보이는 문제가 있었다.
- 해결 요약
- 패턴 로딩 시점 및 SGL 시작 함수에서 타임라인을 설정하지 않고,
servicePatternPlayback()이 실제로 첫 루프를 수행하는 순간(nowUs)에만patLoopStartUs와patEventIndex를 초기화하도록 변경.
- 이를 위해 전역 플래그
patFirstTick를 도입하여,- 재생 시작 시 한 번만 초기화가 일어나도록 구현.
- 결과
- SGL 모드 시작 시 딜레이 및 LED 오프셋 문제 완전 해결.
- Preview / SGL / SONG 모드 전체에서 시간 축이 일관되게 동작.
프로그램 버전: 112325 (NanoEvery ArdulePlayer)
이 문서는 해당 버전에 대한 버그 보고 및 해결 방법을 기록한 기술 메모이다.
