Table of Contents

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는 서로 다른 함수에서 구동된다.

    evUs = patLoopStartUs + tick * usPerTick;
 
    uint32_t now = millis();
    nextBeatMs = now;
 

2.2 기존 코드의 문제점

기존에는 다음과 같은 코드가 존재했다.

  1. 패턴 로딩 시점에서 재생 기준 설정

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 시간 기준이 어긋나는 방식

그 결과:

  1. 초기 몇 스텝의 이벤트가 “아직 시간이 안 됐다”고 판단됨
  if (nowUs < evUs) break;
 
  1. LED는 다른 타이밍 기준을 사용

3. 해결 원리

🎯 핵심 아이디어

패턴 재생 기준 시점(patLoopStartUs)은 “패턴 엔진이 실제로 첫 번째 루프를 실행하는 순간(nowUs)”에만 설정한다.

즉,

이렇게 하면:


4. 실제 패치 내용 (v112325)

4.1 전역 플래그 추가

ArduleEngine_111925_v2_5_storageSplit.ino 상단에 다음 플래그를 추가하였다.

// 패턴 재생 엔진이 "첫 루프"인지 여부를 나타내는 플래그
static bool patFirstTick = true;

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++;
  }
}

이제:


4.4 loadCurrentPatternIntoMemory()에서 타임라인 초기화 제거

ArduleStorage_111925_v2_5.inoloadCurrentPatternIntoMemory() 마지막 부분에서
다음 두 줄을 제거(또는 주석 처리)하였다.

// 기존 코드 (제거됨)
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. 결론

프로그램 버전: 112325 (NanoEvery ArdulePlayer)
이 문서는 해당 버전에 대한 버그 보고 및 해결 방법을 기록한 기술 메모이다.