178 lines
6.4 KiB
C#
178 lines
6.4 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
// 표준 MIDI 파일(.mid)을 파싱해 Note 리스트로 변환하는 미니 파서.
|
|
// 단일 템포 가정. Note-On(velocity>0) 이벤트만 사용한다.
|
|
public static class MidiChartParser
|
|
{
|
|
// data: .mid 원본 바이트 / offset: 곡 시작 보정(초)
|
|
// bpmOverride: 0이면 MIDI 내장 템포 사용, >0이면 이 BPM으로 강제
|
|
// lanePitches: 비우면 모든 노트를 레인 0으로, 채우면 해당 음높이만 레인으로 매핑
|
|
public static List<Note> Parse(byte[] data, float offset, float bpmOverride, List<int> lanePitches)
|
|
{
|
|
var notes = new List<Note>();
|
|
if (data == null || data.Length < 14)
|
|
{
|
|
Debug.LogWarning("[MidiChartParser] MIDI 데이터가 비었거나 너무 짧습니다.");
|
|
return notes;
|
|
}
|
|
|
|
int p = 0;
|
|
|
|
// ---- 헤더 청크 (MThd) ----
|
|
if (!MatchId(data, p, 'M', 'T', 'h', 'd'))
|
|
{
|
|
Debug.LogWarning("[MidiChartParser] MThd 헤더가 아닙니다. (TextAsset이 .mid가 맞는지 확인)");
|
|
return notes;
|
|
}
|
|
p += 4;
|
|
ReadUInt32(data, ref p); // 헤더 길이(=6), 사용 안 함
|
|
ReadUInt16(data, ref p); // 포맷
|
|
int trackCount = ReadUInt16(data, ref p);
|
|
int division = ReadUInt16(data, ref p);
|
|
|
|
if ((division & 0x8000) != 0)
|
|
Debug.LogWarning("[MidiChartParser] SMPTE 타임코드는 미지원입니다. PPQN으로 가정합니다.");
|
|
int ticksPerQuarter = division & 0x7FFF;
|
|
if (ticksPerQuarter <= 0) ticksPerQuarter = 480;
|
|
|
|
// (절대틱, 음높이) 수집 + 첫 템포
|
|
var rawNotes = new List<(int tick, int pitch)>();
|
|
int tempoMicros = -1; // 4분음표당 마이크로초 (미발견 시 -1)
|
|
|
|
// ---- 트랙 청크들 (MTrk) ----
|
|
for (int t = 0; t < trackCount && p + 8 <= data.Length; t++)
|
|
{
|
|
if (!MatchId(data, p, 'M', 'T', 'r', 'k'))
|
|
break;
|
|
p += 4;
|
|
long len = ReadUInt32(data, ref p);
|
|
int trackEnd = p + (int)len;
|
|
if (trackEnd > data.Length) trackEnd = data.Length;
|
|
|
|
int absTicks = 0;
|
|
int runningStatus = 0;
|
|
|
|
while (p < trackEnd)
|
|
{
|
|
absTicks += ReadVarLen(data, ref p);
|
|
|
|
int status = data[p];
|
|
if (status < 0x80)
|
|
{
|
|
status = runningStatus; // 러닝 스테이터스: 상태 바이트 생략
|
|
if (status == 0) { p = trackEnd; break; } // 손상된 데이터 방어
|
|
}
|
|
else
|
|
{
|
|
p++;
|
|
runningStatus = status;
|
|
}
|
|
|
|
if (status == 0xFF) // 메타 이벤트
|
|
{
|
|
int metaType = data[p++];
|
|
int mlen = ReadVarLen(data, ref p);
|
|
if (metaType == 0x51 && mlen == 3 && tempoMicros < 0)
|
|
tempoMicros = (data[p] << 16) | (data[p + 1] << 8) | data[p + 2];
|
|
p += mlen;
|
|
runningStatus = 0;
|
|
}
|
|
else if (status == 0xF0 || status == 0xF7) // SysEx
|
|
{
|
|
int slen = ReadVarLen(data, ref p);
|
|
p += slen;
|
|
runningStatus = 0;
|
|
}
|
|
else // 채널 메시지
|
|
{
|
|
int type = status & 0xF0;
|
|
switch (type)
|
|
{
|
|
case 0x90: // Note-On
|
|
int pitch = data[p++];
|
|
int velocity = data[p++];
|
|
if (velocity > 0) rawNotes.Add((absTicks, pitch));
|
|
break;
|
|
case 0x80: // Note-Off
|
|
case 0xA0: // 폴리 애프터터치
|
|
case 0xB0: // 컨트롤 체인지
|
|
case 0xE0: // 피치 벤드
|
|
p += 2;
|
|
break;
|
|
case 0xC0: // 프로그램 체인지
|
|
case 0xD0: // 채널 애프터터치
|
|
p += 1;
|
|
break;
|
|
default:
|
|
p = trackEnd; // 알 수 없는 상태 → 트랙 종료(무한루프 방지)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
p = trackEnd; // 트랙 경계 보정
|
|
}
|
|
|
|
// ---- 틱 → 초 변환 ----
|
|
double usPerQuarter = bpmOverride > 0f
|
|
? 60000000.0 / bpmOverride
|
|
: (tempoMicros > 0 ? tempoMicros : 500000.0); // 기본 120 BPM
|
|
double secPerTick = usPerQuarter / 1000000.0 / ticksPerQuarter;
|
|
|
|
bool useLanes = lanePitches != null && lanePitches.Count > 0;
|
|
|
|
foreach (var raw in rawNotes)
|
|
{
|
|
int lane = 0;
|
|
if (useLanes)
|
|
{
|
|
lane = lanePitches.IndexOf(raw.pitch);
|
|
if (lane < 0) continue; // 레인에 매핑 안 된 음높이는 무시
|
|
}
|
|
|
|
notes.Add(new Note
|
|
{
|
|
Time = offset + (float)(raw.tick * secPerTick),
|
|
Lane = lane
|
|
});
|
|
}
|
|
|
|
notes.Sort((a, b) => a.Time.CompareTo(b.Time));
|
|
return notes;
|
|
}
|
|
|
|
// ---- 바이트 읽기 헬퍼 (MIDI는 빅엔디안) ----
|
|
|
|
private static bool MatchId(byte[] d, int p, char a, char b, char c, char e)
|
|
=> p + 4 <= d.Length && d[p] == a && d[p + 1] == b && d[p + 2] == c && d[p + 3] == e;
|
|
|
|
private static uint ReadUInt32(byte[] d, ref int p)
|
|
{
|
|
uint v = (uint)((d[p] << 24) | (d[p + 1] << 16) | (d[p + 2] << 8) | d[p + 3]);
|
|
p += 4;
|
|
return v;
|
|
}
|
|
|
|
private static int ReadUInt16(byte[] d, ref int p)
|
|
{
|
|
int v = (d[p] << 8) | d[p + 1];
|
|
p += 2;
|
|
return v;
|
|
}
|
|
|
|
// 가변 길이 수치(Variable-Length Quantity)
|
|
private static int ReadVarLen(byte[] d, ref int p)
|
|
{
|
|
int value = 0;
|
|
byte b;
|
|
do
|
|
{
|
|
b = d[p++];
|
|
value = (value << 7) | (b & 0x7F);
|
|
}
|
|
while ((b & 0x80) != 0 && p < d.Length);
|
|
return value;
|
|
}
|
|
}
|