User Tools

Site Tools


nano_ardule_midi_controller:internal_emergency_pattern_playback_postmortem

내부 비상 패턴 재생 사후 분석

  • 작성일: 2026-01-16 (Asia/Seoul)

요약: 성공적으로 구현 완료

본 문서는 펌웨어 내부에 상시 탑재된 비상용(내장) 드럼 패턴을 안정적으로 재생하는 기능을 성공적으로 구현한 과정과 그 기술적 사후 분석을 정리한 것이다.
해당 비상 패턴은 SD 카드가 없거나 인식 실패 시에도 재생 가능하며, 다음과 같은 목표를 달성하였다.

  • INTERNAL(내부) 패턴은 SD 카드 패턴과 동일한 Pattern Play UI 흐름을 사용한다.
  • SD 카드 유무와 무관하게 패턴 선택 → READY → PLAY의 사용자 경험이 일관된다.
  • 인코더 동작은 기존 규칙을 그대로 유지한다.
    • 회전: BPM 조절
    • 롱 프레스: 기존 파라미터 편집 진입 기능 유지 (신규 기능 추가 없음)

왜 이렇게 어려웠는가

이번 작업에서 문제는 크게 두 갈래로 나뉘었다.

  1. 컴파일 단계의 불안정성
    • .ino 파일이 많아지면서 발생한 예측 불가능한 빌드 오류
  2. 런타임 재생 품질 문제 (INTERNAL 패턴 한정)
    • 재생 시작 시 MIDI 이벤트가 한꺼번에 쏟아지는 현상(burst, ‘와르르’)
    • 재생 시작 시 LED 박자와 실제 소리가 어긋남
    • 동일 엔진을 사용하는 SD 카드 패턴에서는 문제가 발생하지 않음

이 차이는 중요한 단서였다.
문제는 재생 엔진 자체가 아니라, INTERNAL 패턴의 “재생 시작 경로”에 있었다.


시스템 배경: INTERNAL 패턴이 특별한 이유

SD 카드 패턴의 경우

  • 패턴 데이터는 SD 카드 파일에서 로드된다.
  • 재생은 오래 검증된 경로를 따른다.
    • 기존 재생 정지
    • 이벤트 인덱스 초기화
    • 박자 기준점 재설정
    • 비트 엔진 시작
  • 그 결과, 시작 시 타이밍과 LED가 정확히 일치한다.

INTERNAL(비상) 패턴의 경우

  • 패턴 데이터는 펌웨어 내부 플래시(PROGMEM) 에 상주한다.
  • INTERNAL 모드 진입 시 다음과 같은 추가 단계가 존재한다.
    • INTERNAL 모드 선택
    • 내장 패턴 인덱스 선택
    • 페이로드 디코딩 또는 오프셋 계산
    • UI 전환

이 과정 중 재생 시작 시점이 엔진 상태 초기화보다 앞서거나, 일부 상태가 리셋되지 않으면 문제가 발생한다.


핵심 문제 1: 컴파일 오류 — “not declared in this scope”

대표적인 오류 메시지

error: 'startBeatEngine' was not declared in this scope

발생 원인

Arduino 빌드 시스템은 다음과 같이 동작한다.

  1. 여러 .ino 파일을 하나의 파일로 결합
  2. 자동으로 함수 프로토타입을 생성하려 시도

하지만 다중 .ino 파일 구조에서는 이 자동 프로토타입 생성이 신뢰할 수 없다.

특히 다음 상황에서 문제가 발생한다.

  • 파일 상단에 새 헬퍼 함수가 추가되었고
  • 그 함수가 다른 .ino 파일에 정의된 함수를 호출하며
  • 해당 함수의 정의가 결합 순서상 뒤에 오는 경우

이번 작업에서 새로 도입한 공통 시작 함수
beginLoadedPatternPlayback() 이 내부에서 startBeatEngine() 를 호출했지만,
컴파일러는 그 시점에 해당 함수의 존재를 알지 못했다.

해결 방법

명시적인 전방 선언을 추가하였다.

void startBeatEngine();

이 한 줄로 컴파일 순서 문제는 완전히 해결되었다.


핵심 문제 2: INTERNAL 패턴 재생 시작 시 burst + LED 불일치

관찰된 현상

  • 모든 INTERNAL 패턴에서 재생 시작 시 burst 발생
  • 첫 박자에서 LED와 소리가 맞지 않음
  • SD 카드 패턴에서는 동일 현상이 전혀 발생하지 않음

이는 다음을 의미한다.

  • 재생 엔진 로직 자체는 정상
  • INTERNAL 패턴 데이터도 재생 가능
  • 차이는 재생 시작 시 초기화 순서 뿐이다

기술적 원인 분석

패턴 재생 엔진은 개념적으로 다음과 같이 동작한다.

  • 현재 시각(now)과 다음 이벤트 시각(next)을 비교
  • now >= next 인 동안 이벤트를 계속 출력

만약 재생 시작 시점에:

  • 이벤트 인덱스가 초기화되지 않았거나
  • 기준 시각(origin)이 현재 시각보다 과거에 있거나
  • next 값이 너무 이른 시점으로 설정되어 있다면

엔진은 “지연된 이벤트를 따라잡기(catch-up)” 위해
여러 이벤트를 매우 짧은 시간에 한꺼번에 출력한다.

→ 이것이 바로 와르르(burst) 현상이다.


해결책: SD와 INTERNAL 재생 시작 경로의 완전한 통합

설계 원칙

패턴 데이터의 출처(SD / INTERNAL)와 무관하게
재생 시작 시퀀스는 반드시 동일해야 한다.

이를 위해 공통 시작 함수 하나를 정의하였다.

  • beginLoadedPatternPlayback()

그리고 모든 재생 경로는 이 함수를 반드시 통과하도록 통일하였다.

  • SD 카드 패턴
    startSinglePatternPlayback() → 패턴 로드 → beginLoadedPatternPlayback()
  • INTERNAL 패턴
    내부 패턴 준비 → beginLoadedPatternPlayback()

beginLoadedPatternPlayback()의 역할

이 함수는 다음을 반드시 이 순서로 보장한다.

  1. 기존 재생 완전 정지
  2. 패턴 재생 상태 초기화
    • 이벤트 인덱스 = 0
    • first tick 플래그 리셋
  3. BPM 기반 타이밍 재계산
  4. 기준 시각을 “지금(now)”으로 재정렬
  5. 비트 엔진 시작 (startBeatEngine())
  6. UI 상태를 READY / PLAYING으로 일관되게 전환

이 과정을 INTERNAL 패턴에도 동일하게 적용함으로써
burst와 LED 불일치 문제는 실질적으로 해소되었다.


UI 관련 원칙 (의도적으로 지킨 규칙)

  • 인코더 롱 프레스에 절대 새로운 기능을 추가하지 않는다
  • INTERNAL 패턴과 SD 패턴은 동일한 UI 흐름을 사용한다

그 결과:

  • MENU > INTERNAL → 즉시 패턴 리스트 UI 진입
  • 패턴 선택 → READY 상태
  • A3 버튼으로 PLAY / PAUSE
  • 인코더 회전은 BPM 조절
  • 인코더 롱 프레스는 기존과 동일하게 파라미터 편집 진입

이는 UX 일관성뿐 아니라
패턴 로딩과 재생 시작 시점을 분리하여 타이밍 안정성에도 기여한다.


향후 회귀 테스트 체크리스트

  1. INTERNAL / SD 모두 동일한 재생 시작 함수 사용 여부
  2. 재생 시작 시 burst 발생 여부
  3. 첫 박자에서 LED와 소리 정렬 여부
  4. 인코더 롱 프레스 동작 변화 여부
  5. 다중 .ino 파일에서 새 함수 추가 시 전방 선언 여부

결론

  • 빌드 문제의 핵심 원인
    → Arduino 다중 .ino 환경에서의 불완전한 자동 프로토타입 생성
    → 해결: 명시적 전방 선언
  • 런타임 문제의 핵심 원인
    → INTERNAL 패턴과 SD 패턴의 재생 시작 시퀀스 불일치
    → 해결: 공통 시작 함수로 완전 통합

이로써 비상용 내부 패턴은 안정적으로 재생 가능해졌으며,
UI와 사용자 조작 모델 역시 SD 카드 기반 재생과 완전히 동일하게 유지되었다.

nano_ardule_midi_controller/internal_emergency_pattern_playback_postmortem.txt · Last modified: by hyjeong