ReadMIDILyrics.pas 12.5 KB
// Unit ReadMIDILyrics
//
// This unit takes charge of formatting synchronized lyrics data from raw data of MIDI file.
//
// Author : Silhwan Hyun   (e-mail addr : hyunsh@hanafos.com)
//
//
// Contibutors
//   Emil Weiss : Test & Advice for update/debug
//
//
// Ver 1.0.1                      07 Jun 2012
//   - Modifications to read the lyrics recorded with non-standard method of some Karaoke files.
//
// Ver 1.0                        03 Jun 2012
//   - Initial release
//

unit ReadMIDILyrics;

interface

{$DEFINE MULTI_LANGUAGE}

uses Windows, Messages, Classes, SysUtils, MIDIFile2{$IFDEF MULTI_LANGUAGE}, gnugettext{$ENDIF};

// {$DEFINE USE_KOR_LANG}

const
  MaxSyllablesPerLine = 32;    // 한 라인당 최대 음절 수

// 전주나 간주로 판단하는 시간 간격
  PreInterval = 3000;  // 첫 동기가사 시작시간이 PreInterval 보다 크면 Prelude를 삽입한다.
  MidInterval = 5000;  // 동기가사 휴지시간이 MidInterval 보다 크면 Interlude를 삽입한다.

// 전주나 간주 부분에 대해서 자동으로 삽입할 문자열
// {$IFDEF USE_KOR_LANG}
//  Prelude = '< 전  주 >';
//  Interlude = '< 간  주 >';
// {$ELSE}
//  Prelude = '< Prelude >';
//  Interlude = '< Interlude >';
// {$ENDIF}

type
  TMIDISyncLyric = record
     TimePos : integer;    // position in mili second
   //  Position: DWORD;      // position in ticks
     Lyric: string;
  end;

  TMIDISyncLyrics = array of TMIDISyncLyric;

  TSyllable = record
      StartTime : DWORD;          // in ms
      Duration : DWORD;           // in ms
      WordWidth : Single;
      CharacterStartNo : DWORD;
      aSyllable : UnicodeString;
   end;

   TLyricsLine = record
      StartTime : DWORD;          // in ms
      Duration : DWORD;           // in ms
      NumSyllable : integer;
      LineWidth : Single;
      Syllables : array of TSyllable;
   end;

   TSyncLyrics = record
      NumLine : integer;
      Lines : array of TLyricsLine;
   end;

  function ReadSyncLyrics(MidiFile: TMidiFile2; SyncLyrics: TRawLyrics; DiscardTimeZeroLyrics: boolean) : TSyncLyrics;

implementation


var
  Prelude : string;
  Interlude : string;

// 문자열이 공백문자로만 이루어져 있을 경우 true 값을 얻는다.
function AllSpaces(s : string): boolean;
var
  I : integer;
begin
  result := true;

  for I := 1 to Length(s) do
    if s[I] <> ' ' then
    begin
      result := false;
      break;
    end;

end;

// 문자열 선두의 공백문자수를 얻는다.
function LeftSpaceLen(s: string): integer;
var
  I : integer;
begin
  result := 0;

  for I := 1 to Length(s) do
    if s[I] = ' ' then
      result := I
    else
      break;
end;

// 문자열 후미의 공백문자수를 얻는다.
function RightSpaceLen(s: string): integer;
var
  I : integer;
begin
  result := 0;

  for I := Length(s) downto 1 do
    if s[I] = ' ' then
      inc(result)
    else
      break;

end;

// 가사 중간의 스페이스를 독립된 음절로 분리한다.
// 이 작업을 하는 이유는 동기가사 표시창에서 시간 경과에 따라 동기가사의 색상을 바꾸어 나갈 때
// 가사라인의 중간에 있는 스페이스는 시간적으로 소요시간이 0인 부분으로 처리하기 위해서이다.
procedure SeperateSpace(var SyncLyrics: TSyncLyrics);
var
  K, N, L, I : integer;
  StartTime : DWORD;
  Diff : integer;
  AllSameTime : Boolean;
  tmpLyricsLine : TLyricsLine;
  s : string;
begin
  for K := 0 to (SyncLyrics.NumLine - 1) do
  begin
    if SyncLyrics.Lines[K].NumSyllable = 1 then  // NumSyllable이 1인 경우는 줄단위 처리
      Continue;

    StartTime := SyncLyrics.Lines[K].Syllables[0].StartTime;
    AllSameTime := true;
    for I := 1 to (SyncLyrics.Lines[K].NumSyllable - 1) do
      if SyncLyrics.Lines[K].Syllables[I].StartTime <> StartTime then
      begin
        AllSameTime := false;
        break;
      end;
    if AllSameTime then
      Continue;

    tmpLyricsLine := SyncLyrics.Lines[K];
    for N := 0 to (SyncLyrics.Lines[K].NumSyllable - 1) do
    begin
      s := SyncLyrics.Lines[K].Syllables[N].aSyllable;
      if AllSpaces(s) then
        Continue;
      if (s[1] = ' ') then
      begin
        L := LeftSpaceLen(s);
        SetLength(tmpLyricsLine.Syllables, High(tmpLyricsLine.Syllables) + 2);
        Diff := High(tmpLyricsLine.Syllables) + 1 - SyncLyrics.Lines[K].NumSyllable;
        for I := High(tmpLyricsLine.Syllables) downto (N + Diff + 1) do
          tmpLyricsLine.Syllables[I] := tmpLyricsLine.Syllables[I-1];
        tmpLyricsLine.Syllables[N+Diff-1].aSyllable := copy(s, 1, L);
        s := copy(s, L+1, Length(s) - L);
        tmpLyricsLine.Syllables[N+Diff].aSyllable := s;
        tmpLyricsLine.Syllables[N+Diff].StartTime := tmpLyricsLine.Syllables[N+Diff-1].StartTime;
      end;
      if (s[Length(s)] = ' ') then
      begin
        L := RightSpaceLen(s);
        SetLength(tmpLyricsLine.Syllables, High(tmpLyricsLine.Syllables) + 2);
        Diff := High(tmpLyricsLine.Syllables) + 1 - SyncLyrics.Lines[K].NumSyllable;
        for I := High(tmpLyricsLine.Syllables) downto (N + Diff + 1) do
          tmpLyricsLine.Syllables[I] := tmpLyricsLine.Syllables[I-1];
        tmpLyricsLine.Syllables[N+Diff-1].aSyllable := copy(s, 1, Length(s) - L);
        tmpLyricsLine.Syllables[N+Diff].aSyllable := copy(s, Length(s) - L + 1, L);
        tmpLyricsLine.Syllables[N+Diff].StartTime := tmpLyricsLine.Syllables[N+Diff-1].StartTime;
      end;

    end;

    tmpLyricsLine.NumSyllable := High(tmpLyricsLine.Syllables) + 1;
  // 공백문자(열)의 시작시간은 항상 앞 음절의 시작시간으로 맞춘다.
    for N := 1 to (tmpLyricsLine.NumSyllable - 2) do
      if AllSpaces(tmpLyricsLine.Syllables[N].aSyllable) then
        tmpLyricsLine.Syllables[N].StartTime := tmpLyricsLine.Syllables[N-1].StartTime;
    SyncLyrics.Lines[K] := tmpLyricsLine;
  end;
end;

// 전주나 간주 부분에 대해서 자동으로 표시할 내용을 삽입해 준다.
procedure InsertComment(var SyncLyrics: TSyncLyrics);
var
  I, J : integer;
  M, N, K : integer;
  T1 : DWORD;
begin
// 시작시점에 "< 전 주 >" 추가
  if SyncLyrics.NumLine >= 2 then
    if SyncLyrics.Lines[0].StartTime > PreInterval then  // 가사 시작시점이 PreInterval 이후일 경우
    begin
      SetLength(SyncLyrics.Lines, SyncLyrics.NumLine + 1);
      inc(SyncLyrics.NumLine, 1);
      for I := (SyncLyrics.NumLine - 1) downto 1 do
        SyncLyrics.Lines[i] := SyncLyrics.Lines[i-1];
      SyncLyrics.Lines[0].StartTime := 0;
      SyncLyrics.Lines[0].NumSyllable := 1;
      SetLength(SyncLyrics.Lines[0].Syllables, 1);
      SyncLyrics.Lines[0].Syllables[0].StartTime := 0;
      SyncLyrics.Lines[0].Syllables[0].aSyllable := Prelude;
    end;

  if SyncLyrics.NumLine < 5 then
    exit;

 // 중간 휴지 부분에 "<간 주 >" 삽입
  K := 3;    // 간주는 최소한 2 줄 연주 이후에  나타나는 것으로 본다.
  repeat
    M := (SyncLyrics.NumLine - 1);
    for I := K to M do
    begin
      N := SyncLyrics.Lines[I-1].NumSyllable - 1;
      if (SyncLyrics.Lines[I].StartTime - SyncLyrics.Lines[I-1].Syllables[N].StartTime) >
         MidInterval then     // MidInterval 이상 가사가 없는 상태이면 간주 부분으로 판단한다.
      begin
        SetLength(SyncLyrics.Lines, SyncLyrics.NumLine + 1);
        inc(SyncLyrics.NumLine, 1);
        for J := (SyncLyrics.NumLine - 1) downto (I+1) do
          SyncLyrics.Lines[J] := SyncLyrics.Lines[J-1];
        T1 := SyncLyrics.Lines[I-1].StartTime
                      + (SyncLyrics.Lines[I-1].StartTime - SyncLyrics.Lines[I-2].StartTime);
        if T1 > SyncLyrics.Lines[I-1].Syllables[N].StartTime then
          SyncLyrics.Lines[I].StartTime := T1
        else
          SyncLyrics.Lines[I].StartTime := SyncLyrics.Lines[I-1].Syllables[N].StartTime + 1000;
        SyncLyrics.Lines[I].NumSyllable := 1;
        SetLength(SyncLyrics.Lines[I].Syllables, 1);
        SyncLyrics.Lines[I].Syllables[0].StartTime := SyncLyrics.Lines[I].StartTime;
        SyncLyrics.Lines[I].Syllables[0].aSyllable := Interlude;
        break;
      end;
    end;

    K := I + 2;
  until I >= M;
end;

function ReadSyncLyrics(MidiFile: TMidiFile2; SyncLyrics: TRawLyrics; DiscardTimeZeroLyrics: boolean) : TSyncLyrics;
var
  N, J, K: integer;
  SyllableLen: integer;
  MIDISyncLyrics: TMIDISyncLyrics;
  SubNum: integer;

begin
  Result.NumLine := 0;
  SubNum := 0;

// 미디파일의 동기가사는 Carriage return code(#$0D)가 행(라인) 구분자이다.
// Carriage return code(#$0D)는  Line feed code(#$0A)로 변환하고, Carriage return code만
// 있는 Syllable이면서 가사 표시시점이 이전 Syllable과 동일할 경우는 이전 Syllable과 합친다.
  if High(SyncLyrics) <> - 1 then
  begin
    SetLength(MIDISyncLyrics, High(SyncLyrics) + 1);
    for N := 0 to High(SyncLyrics) do
    begin

    // ** Added at 2012-06-07
      if SyncLyrics[N].Position = 0 then
        if DiscardTimeZeroLyrics then
        begin
          inc(SubNum);
          Continue;
        end;

      MIDISyncLyrics[N-SubNum].TimePos := MIDIFile.Tick2TimePos(SyncLyrics[N].Position);
    //  MIDISyncLyrics[N-SubNum].Position := SyncLyrics[N].Position;

    // Carriage return code(#$0D)는 Line feed code(#$0A)로 바꾸어 준다.
      if Length(SyncLyrics[N].Lyric) = 0 then
        MIDISyncLyrics[N-SubNum].Lyric := ' '
      else if (SyncLyrics[N].Lyric[Length(SyncLyrics[N].Lyric)] = chr(13)) then
      begin
        if Length(SyncLyrics[N].Lyric) = 1 then
        begin
           if N > 0 then
             if MIDISyncLyrics[N-SubNum].TimePos = MIDISyncLyrics[N-SubNum-1].TimePos then
             begin
           // 가사 표시시점이 이전 Syllable과 동일할 경우는 이전 Syllable과 합친다.
           // 이 경우 MIDISyncLyrics의 유효한 배열 갯수는 하나 줄어들므로 줄어든 갯수를
           // 나타내는 변수 SubNum를 +1 증가시킨다.
               MIDISyncLyrics[N-SubNum-1].Lyric := MIDISyncLyrics[N-SubNum-1].Lyric + chr(10);
               inc(SubNum);
             end else
               MIDISyncLyrics[N-SubNum].Lyric := ' ' + chr(10)
           else
             MIDISyncLyrics[N-SubNum].Lyric := ' ' + chr(10);
        end else
          MIDISyncLyrics[N-SubNum].Lyric := copy(SyncLyrics[N].Lyric, 1, Length(SyncLyrics[N].Lyric) - 1) + chr(10)
      end
    // ** Soft Karaoke files use '\' for clear screen, '/' for new line, which are pre-converted
    // **  to chr(13) in the MIDIFile2.pas unit
      else if (SyncLyrics[N].Lyric[1] = chr(13)) {or (SyncLyrics[N].Lyric[1] = '\') or (SyncLyrics[N].Lyric[1] = '/')} then
      begin
        if Length(SyncLyrics[N].Lyric) = 1 then
        begin
           if N > 0 then
             if MIDISyncLyrics[N-SubNum].TimePos = MIDISyncLyrics[N-SubNum-1].TimePos then
             begin
           // 가사 표시시점이 이전 Syllable과 동일할 경우는 이전 Syllable과 합친다.
           // 이 경우 MIDISyncLyrics의 유효한 배열 갯수는 하나 줄어들므로 줄어든 갯수를
           // 나타내는 변수 SubNum를 +1 증가시킨다.
               MIDISyncLyrics[N-SubNum-1].Lyric := MIDISyncLyrics[N-SubNum-1].Lyric + chr(10);
               inc(SubNum);
             end else
               MIDISyncLyrics[N-SubNum].Lyric := ' ' + chr(10)
           else
             MIDISyncLyrics[N-SubNum].Lyric := ' ' + chr(10);
        end else
          if (N - SubNum) > 0 then    // ** Changed at 2012-06-07
          begin
            MIDISyncLyrics[N-SubNum - 1].Lyric := MIDISyncLyrics[N-SubNum - 1].Lyric + chr(10);
            MIDISyncLyrics[N-SubNum].Lyric := copy(SyncLyrics[N].Lyric, 2, Length(SyncLyrics[N].Lyric) - 1);
          end else
            MIDISyncLyrics[N-SubNum].Lyric := copy(SyncLyrics[N].Lyric, 2, Length(SyncLyrics[N].Lyric) - 1);
      end else
        MIDISyncLyrics[N-SubNum].Lyric := SyncLyrics[N].Lyric;
    end;

  end else
    exit;

  if SubNum > 0 then
    SetLength(MIDISyncLyrics, High(SyncLyrics) + 1 - SubNum);
  J := 0;  // Syllable counter
  K := 0;  // Line Counter
  SetLength(Result.Lines, 1);
  for N := 0 to High(MIDISyncLyrics) do
  begin
    SetLength(Result.Lines[k].Syllables, J+1);
    if J = 0 then  // Is the first syllable ?
      Result.Lines[k].StartTime := MIDISyncLyrics[N].TimePos;

    Result.Lines[k].Syllables[J].StartTime := MIDISyncLyrics[N].TimePos;
    Result.Lines[k].Syllables[J].aSyllable := MIDISyncLyrics[N].Lyric;
    inc(J);

    SyllableLen := length(MIDISyncLyrics[N].Lyric);
    if (MIDISyncLyrics[N].Lyric[SyllableLen] = chr(10)) or (J = MaxSyllablesPerLine) then
    begin
      Result.Lines[k].NumSyllable := J;
      if N <> High(MIDISyncLyrics) then   // not last data ?
      begin
        inc(K);
        SetLength(Result.Lines, K+1);
        J := 0;
      end;
    end;

  // 마지막 Sync Lyricsa 음절인 경우에 대한 처리
    if N = High(MIDISyncLyrics) then
      if (MIDISyncLyrics[N].Lyric[SyllableLen] <> chr(10)) then
        Result.Lines[k].NumSyllable := J;
  end;

 Result.NumLine := K + 1;

 SeperateSpace(Result);
 InsertComment(Result);
end;

initialization
{$IFDEF MULTI_LANGUAGE}
  Prelude := _('< Prelude >');
  Interlude := _('< Interlude >');
{$ELSE}
  Prelude := '< Prelude >';
  Interlude := '< Interlude >';
{$ENDIF}

end.