2025-11-23 — 정해영 / ChatGPT 공동 기록
Arduino Nano Every 기반 ArdulePlayer v112325에서
SGL (Single Pattern Play) 모드로 2-bar 드럼 패턴을 재생할 때 다음 문제가 관찰되었다.
패턴 재생과 비트 LED는 서로 다른 함수에서 구동된다.
servicePatternPlayback()micros() → nowUspatLoopStartUsevUs = patLoopStartUs + tick * usPerTick;
serviceBeatEngine()millis() → nowbeatIntervalMsnextBeatMsstartBeatEngine()에서uint32_t now = millis(); nextBeatMs = now;
기존에는 다음과 같은 코드가 존재했다.
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();%%'' 를 시도하기도 했음.
loop()의 실행 순서 상,serviceBeatEngine()이 호출되어 nextBeatMs 및 LED가 준비되고,servicePatternPlayback()이 호출되어 패턴 이벤트가 재생된다.patLoopStartUs가 “로딩 시점” 혹은 “버튼 처리 시점”에 설정되어 있어,nowUs와,patLoopStartUs + tick * usPerTick가 미묘하게 어긋난 상태가 된다.그 결과:
if (nowUs < evUs) break;
evUs가 실제 nowUs보다 미래로 계산되기 때문에,while 루프가 바로 break되어 이벤트가 출력되지 않음.startBeatEngine()에서 nextBeatMs = millis()를 기준으로 바로 0번째 비트를 표시 시작.patLoopStartUs가 늦게 설정되어 있어, 실제 노트는 LED보다 늦게 들림.패턴 재생 기준 시점(patLoopStartUs)은 “패턴 엔진이 실제로 첫 번째 루프를 실행하는 순간(nowUs)”에만 설정한다.
즉,
servicePatternPlayback()이 실제로 처음 호출되어 nowUs = micros()를 계산한 그 순간에:patLoopStartUs = nowUs;patEventIndex = 0;이렇게 하면:
ArduleEngine_111925_v2_5_storageSplit.ino 상단에 다음 플래그를 추가하였다.
// 패턴 재생 엔진이 "첫 루프"인지 여부를 나타내는 플래그 static bool patFirstTick = true;
true로 세팅되고,servicePatternPlayback()이 처음 실행되는 순간에만 사용되어,patLoopStartUs와 patEventIndex를 초기화한 뒤,false로 전환된다.
기존에는 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(); }
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가 설정되고,
ArduleStorage_111925_v2_5.ino의 loadCurrentPatternIntoMemory() 마지막 부분에서
다음 두 줄을 제거(또는 주석 처리)하였다.
// 기존 코드 (제거됨) patLoopStartUs = micros(); patEventIndex = 0;
이제 이 함수는 패턴 데이터를 SD에서 읽어와 메모리에 적재하는 일만 담당하며,
실제 재생 타이밍은 엔진(servicePatternPlayback) 쪽에서 책임진다.
아래와 같은 방식으로 테스트를 수행하였다.
이번 수정으로 ArdulePlayer v112325는 다음과 같은 구조적 개선을 얻었다.
즉, 이번 패치는 단순한 버그 수정 그 이상으로,
Ardule Engine v2.5를 보다 안정적인 v2.6 수준의 구조로 끌어올린 의미가 있다.
servicePatternPlayback()이 실제로 첫 루프를 수행하는 순간(nowUs)에만patLoopStartUs와 patEventIndex를 초기화하도록 변경.patFirstTick를 도입하여,
프로그램 버전: 112325 (NanoEvery ArdulePlayer)
이 문서는 해당 버전에 대한 버그 보고 및 해결 방법을 기록한 기술 메모이다.