# ArdulePlayer v112325 — SGL 모드 시작 지연 버그 보고서 및 해결 문서 *2025-11-23 — 정해영 / ChatGPT 공동 기록* --- ## 1. 문제 개요 Arduino Nano Every 기반 **ArdulePlayer v112325**에서 **SGL (Single Pattern Play) 모드**로 2-bar 드럼 패턴을 재생할 때 다음 문제가 관찰되었다. ### 🔴 증상 요약 1. **패턴 재생 시작 시 첫 2~3 스텝의 노트가 늦게 출력됨** - SGL 모드에서 PLAY를 눌렀을 때, - 패턴의 맨 앞에 있어야 할 노트들이 즉시 나오지 않고, - **3스텝 정도 뒤에서야 첫 노트가 들림**. - 그 이후 구간(반복 포함)은 그리드에 맞게 정상적으로 재생됨. 2. **LED 비트 표시가 소리보다 1박자 정도 늦게 보임** - 비트 LED(LED_A0/A1/A2)가 강박을 표시하지만, - **실제 들리는 드럼 소리보다 항상 1박자 밀려서** 점등되는 것처럼 느껴짐. 3. **PreviewAll(자동 순환 프리뷰) 모드는 정상** - Preview 모드에서는 패턴 시작 타이밍과 LED가 잘 맞음. - 문제가 발생하는 것은 **SGL 모드에서의 재생 시작 시점**으로 국한됨. --- ## 2. 문제의 근본 원인 분석 ### 2.1 관련 엔진 구조 패턴 재생과 비트 LED는 서로 다른 함수에서 구동된다. - **패턴 재생 엔진** - 함수: `servicePatternPlayback()` - 시간 기준: `micros()` → `nowUs` - 패턴 루프 기준: `patLoopStartUs` - 이벤트 시점 계산: ```cpp evUs = patLoopStartUs + tick * usPerTick; ``` - **비트/LED 엔진** - 함수: `serviceBeatEngine()` - 시간 기준: `millis()` → `now` - 비트 간격: `beatIntervalMs` - 다음 비트 시각: `nextBeatMs` - 시작 시점: `startBeatEngine()`에서 ```cpp uint32_t now = millis(); nextBeatMs = now; ``` ### 2.2 기존 코드의 문제점 기존에는 다음과 같은 코드가 존재했다. 1. **패턴 로딩 시점에서 재생 기준 설정** `loadCurrentPatternIntoMemory()`의 마지막 부분: ```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; ``` → **패턴을 SD에서 읽어올 때** 이미 `patLoopStartUs`가 찍혀 버림. 2. **SGL 시작 함수에서도 재생 기준을 또 설정 시도** `startSinglePatternPlayback()`의 기존 형태(수정 전): ```cpp void startSinglePatternPlayback() { if (!sdOK) return; if (!buildCurrentPatternFilePath(currentFilePath, sizeof(currentFilePath))) return; if (!loadCurrentPatternIntoMemory()) return; playSource = PLAY_SRC_PATTERN; startBeatEngine(); } ``` 일부 버전에서는 여기에서 또 `patLoopStartUs = micros();` 를 시도하기도 했음. ### 2.3 시간 기준이 어긋나는 방식 - `loop()`의 실행 순서 상, - 먼저 `serviceBeatEngine()`이 호출되어 `nextBeatMs` 및 LED가 준비되고, - 그 다음에 `servicePatternPlayback()`이 호출되어 패턴 이벤트가 재생된다. - 그런데 **패턴 재생 기준 시간 `patLoopStartUs`가 “로딩 시점” 혹은 “버튼 처리 시점”에 설정**되어 있어, - 실제 재생 루프에서의 `nowUs`와, - `patLoopStartUs + tick * usPerTick`가 **미묘하게 어긋난 상태**가 된다. 그 결과: 1. **초기 몇 스텝의 이벤트가 “아직 시간이 안 됐다”고 판단됨** ```cpp if (nowUs < evUs) break; ``` - 첫 이벤트들의 `evUs`가 실제 `nowUs`보다 미래로 계산되기 때문에, - 초기 2~3 스텝 동안은 `while` 루프가 바로 `break`되어 이벤트가 출력되지 않음. - 그 뒤부터는 루프 타이밍과 시간이 맞아서 정상 재생되는 것처럼 들림. 2. **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` 상단에 다음 플래그를 추가하였다. ```cpp // 패턴 재생 엔진이 "첫 루프"인지 여부를 나타내는 플래그 static bool patFirstTick = true; ``` - 이 플래그는 패턴 재생을 시작할 때 `true`로 세팅되고, - `servicePatternPlayback()`이 처음 실행되는 순간에만 사용되어, - `patLoopStartUs`와 `patEventIndex`를 초기화한 뒤, - 다시 `false`로 전환된다. --- ### 4.2 startSinglePatternPlayback() 수정 기존에는 SGL 시작 시점에서 patLoopStartUs를 건드릴 수도 있는 구조였으나, 이제는 **패턴 엔진 초기화 플래그만 세우고, 실제 시간 기준 설정은 엔진에게 맡긴다.** ```cpp 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()`의 상단부는 다음과 같이 변경되었다. ```cpp 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()` 마지막 부분에서 다음 두 줄을 제거(또는 주석 처리)하였다. ```cpp // 기존 코드 (제거됨) patLoopStartUs = micros(); patEventIndex = 0; ``` 이제 이 함수는 **패턴 데이터를 SD에서 읽어와 메모리에 적재하는 일만 담당**하며, 실제 재생 타이밍은 엔진(`servicePatternPlayback`) 쪽에서 책임진다. --- ## 5. 수정 후 동작 확인 아래와 같은 방식으로 테스트를 수행하였다. 1. **단순 4분 킥 패턴 테스트** - 2-bar 동안 킥만 4분음표로 찍힌 패턴을 사용. - Preview 모드 / SGL 모드에서 각각 재생. - 결과: - **두 모드 모두 첫 킥이 정확히 LED 강박과 일치**. - 더 이상 첫 2~3스텝이 늦게 나오지 않음. 2. **일반 드럼 패턴(하이햇+스네어+킥) 테스트** - 실제 사용 중인 2-bar 패턴 여러 개로 검증. - SGL 모드로 재생 시작 시: - 첫 마디부터 그리드에 맞게 정확히 재생됨. - LED도 1박자 지연 없이 정상적으로 따라감. 3. **PreviewAll 모드 재검증** - PreviewAll 모드에도 동일 엔진이 사용되지만, - 기존에도 문제가 없었고, - 이번 수정 이후에도 동작에 변화 없음(정상 유지). --- ## 6. 아키텍처적 의미 이번 수정으로 ArdulePlayer v112325는 다음과 같은 구조적 개선을 얻었다. 1. **절대시간 기반 엔진의 통일된 시작 기준 확립** - Preview / SGL / SONG 모드 모두, - “재생 엔진이 실제로 움직이기 시작하는 순간”을 시간 0으로 사용. 2. **로딩/버튼 입력 타이밍과 재생 타이밍의 분리** - SD I/O, UI 조작, 모드 전환 등은 재생 타임라인에 영향을 주지 않음. - 오로지 재생 엔진의 첫 루프만이 기준을 결정. 3. **향후 기능 확장에 유리한 토대** - 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)** 이 문서는 해당 버전에 대한 버그 보고 및 해결 방법을 기록한 기술 메모이다.