ReadMIDILyrics.pas
12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
// 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.