목표: 클래식 MIDI 29곡 → YouTube-ready 고품질 오디오/비디오 자동 생산 파이프라인
기간: 2025.02 ~ 2026.02 (약 12개월, 간헐적)
핵심 산출물: quartet_pipeline.py (40KB, 코어 엔진) + run_everything.py (19KB, 오케스트레이션)
총 코드량: 87개 Python 파일, 13,098 라인
작업 데이터: ~20GB 중간 결과물 (sq_work/mass_test/)
- LUFS (0.20) / LRA (0.25) / True Peak (0.15) / Freq Balance (0.20) / DynStd (0.20)
4가지 적응형 규칙 도입:
1. Phrase dynamics: CC11 기반 프레이즈 다이내믹 자동 삽입
2. LUFS normalization: 장르별 타겟 (-17/-18 dB)
3. Warm EQ: 주파수 밸런스 보정 (body↑, bright↓)
4. dynaudnorm: 극단적 LRA 대응 (f=100, g=7)
핵심 발견:
| 트랙 | 초기 점수 | 문제 | 해결 | 최종 점수 | ||||
| `nocturne_13` | ~86 | DynStd 극단적 변동 | skip_humanize + vel_compress(60,85) + sec_norm(sn5.0) + dn(f=10,m=50) | **99.8** | ||||
|---|---|---|---|---|---|---|---|---|
| `bruckner_sq` | ~97 | Freq balance (bright 과다) | warm EQ (bright=-1, air=-1, presence=3, body=4, comp=3.5) | **99.8~100.0** |
nocturne_13 — 가장 난해했던 트랙:
bruckner_sq — EQ 미세 조정:
29/29 트랙: ≥ 99.5 (120초 테스트 렌더 기준)
- Orchestral 11곡: avg 99.9, min 99.5
- Piano 18곡: avg 99.7, min 99.5 (nocturne_13=99.8)
- 예: prelude_103_9 — 120초: ~100점, full: 86.2점 (LRA 15.5)
- dynaudnorm(f=100, g=7) 적용 시 99.2까지 회복
- 더 aggressive한 설정 (f=50 or f=30) 필요할 수 있음
1. Full-length 적응형 렌더링 로직 완성
2. 29곡 전체 렌더링 실행 (~1-2시간)
3. YouTube MP4 생성
4. 중간 파일 정리 (~20GB)
MIDI (29곡)
│
├─ detect_families() → CONFIG 자동 선택
│ ├─ STRING_CONFIG (현악 4중주 등)
│ ├─ WOODWIND_CONFIG (목관)
│ ├─ BRASS_CONFIG (금관)
│ ├─ ORCHESTRAL_CONFIG (혼합)
│ └─ PIANO_CONFIG (피아노)
│
├─ MIDI 전처리
│ ├─ fix_midi_piano / fix_midi_strings
│ ├─ apply_expression (S-curve k=3.0)
│ ├─ apply_phrase_dynamics (CC11)
│ ├─ compress_velocity_range
│ ├─ normalize_section_velocity
│ └─ humanize (±velocity 랜덤)
│
├─ FluidSynth 렌더링 (SGM-V2.01.sf2)
│
├─ 마스터링 체인 (FFmpeg)
│ ├─ loudnorm (LUFS 타겟)
│ ├─ compand (다이내믹 압축)
│ ├─ EQ (body/presence/bright/air)
│ ├─ dynaudnorm (LRA 정규화)
│ └─ alimiter (True Peak)
│
├─ Score Engine (5차원 평가)
│ └─ LUFS(0.20) + LRA(0.25) + TP(0.15) + Freq(0.20) + Dyn(0.20)
│
└─ try_adaptive() — 점수 < 99.5 시 자동 최적화
├─ Piano: 5-tier (velcomp → secnorm → combined → dynaudnorm → phrase)
└─ Orch: dynaudnorm → phrase → LUFS → warm EQ
| 파일 | 크기 | 역할 | ||
| `quartet_pipeline.py` | 40KB | 코어 엔진: MIDI처리 + 렌더링 + 마스터링 + 스코어링 | ||
|---|---|---|---|---|
| `run_everything.py` | 19KB | 29곡 오케스트레이션 + adaptive search | ||
| `score_engine.py` | 8KB | 5차원 스코어 엔진 | ||
| `optimizer.py` | 14KB | S-curve 파라미터 최적화 | ||
| `render_full_and_video.py` | 12KB | Full-length WAV + YouTube MP4 | ||
| `verify_fixes.py` | 2KB | bruckner_sq + nocturne_13 검증 | ||
| `fix_round4~6.py` | ~31KB | nocturne_13/bruckner_sq 해결 과정 |
5차원 Score Engine 없이는 이 프로젝트가 불가능했다. "좋은 소리"라는 주관적 기준을 LUFS/LRA/TP/Freq/DynStd 숫자로 바꿔놓으니, A/B 테스트의 승패가 명확해졌고, 파라미터 튜닝이 과학이 됐다. 매 실험마다 점수가 올라가는 걸 보면서 방향을 잡을 수 있었다.
"일단 기본값으로 돌리고, 점수 안 나오면 자동으로 보정" — 이 패턴이 11개 오케스트랄 + 18개 피아노 전부를 커버했다. 한 곡에 맞춰 하드코딩하는 대신 규칙 기반 적응형으로 가니 새 곡이 들어와도 파이프라인이 돌아간다.
6라운드에 걸친 체계적 디버깅은 이 프로젝트의 하이라이트다. vel_compress → sec_norm → dynaudnorm → 조합 → skip_humanize 순서로 가설을 세우고 검증하면서, 결국 "humanize 랜덤성 자체가 문제"라는 비직관적 답을 찾았다. 자동화의 모든 단계를 의심하고 하나씩 제거한 접근이 효과적이었다.
SSO, VSCO2 등 여러 SFZ를 테스트했지만 SGM 하나로 통일한 건 좋은 판단이었다. 오케스트라와 피아노 모두에서 일관된 품질을 얻었고, 배포/관리가 단순해졌다.
fix_round4.py, fix_round5.py, fix_round6.py, run_fix_piano_v3.py, run_fix_piano_v4.py...
실험할 때마다 새 파일을 만들어서 D:\tmp에 87개 스크립트가 쌓였다. 어떤 게 최종인지, 어떤 게 폐기된 건지 파일명만으로는 구분이 안 된다. 실험 브랜치를 쓰거나, experiments/ 하위 폴더로 관리했어야 했다.
29곡 120초 테스트에서 전부 99.5+ 찍고 "다 됐다" 했는데, full-length로 가니 LRA가 터졌다 (86.2점). 처음부터 full-length로 테스트했으면 이 문제를 일찍 잡았을 텐데, 120초 테스트에 안주한 측면이 있다. 다만 120초 기준으로 피드백 루프가 빨라서 파라미터 탐색이 가능했던 것도 사실이라 trade-off이긴 하다.
sq_work/mass_test/에 676개 WAV 파일, 20GB. adaptive search가 시도하는 모든 변형을 저장해두니 디스크가 차올랐다. 최선 결과만 남기고 나머지는 삭제하는 로직이 있었어야 했다.
마지막 세션에서 render_full_and_video.py를 만들었지만 adaptive 로직 없이 기본 config만 적용했다. 결국 full-length 렌더는 아직 미완. dynaudnorm 기본 적용 + 재시도 로직을 추가해야 하는데, 이건 다음 세션 숙제로 남았다.
1. 스코어 엔진을 먼저 만들어라 — 측정 없이 최적화는 불가능. 점수가 있으니 "이건 좋다/나쁘다"를 코드가 판단할 수 있었다.
2. 120초 프록시 테스트의 한계 — 빠른 반복엔 좋지만, 최종 산출물과 동일 조건에서 검증해야 한다.
3. 적응형 > 하드코딩 — 곡마다 특수 config를 만들면 관리가 불가능. 규칙 기반 자동 적응이 확장성 있다.
4. 하나씩 제거하며 디버깅 — nocturne_13처럼 복잡한 문제는 변수를 하나씩 끄면서 범인을 좁혀야 한다.
120초 테스트 Full-length (예상)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Orchestral 11곡 avg 99.9 dynaudnorm 적용 필요
Piano 18곡 avg 99.7 dynaudnorm 적용 필요
전체 29곡 29/29 ≥ 99.5 미완 (적응형 로직 보완 후)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
nocturne_13 (최난곡) 99.8 (확정) TBD
bruckner_sq 99.8~100.0 TBD
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
코드 87파일 / 13K줄
중간 데이터 ~20GB (정리 필요)
1. render_full_and_video.py에 적응형 dynaudnorm 로직 추가
2. 29곡 full-length 렌더링 + 스코어 검증
3. YouTube MP4 전곡 생성
4. 실험 파일 정리 (87개 → 핵심 5~6개만 보존)
5. sq_work/mass_test/ 20GB 정리