1
/**************************************************************************
2
* Karlyriceditor - a lyrics editor and CD+G / video export for Karaoke *
4
* Copyright (C) 2009-2013 George Yunaev, support@ulduzsoft.com *
6
* This program is free software: you can redistribute it and/or modify *
7
* it under the terms of the GNU General Public License as published by *
8
* the Free Software Foundation, either version 3 of the License, or *
9
* (at your option) any later version. *
11
* This program is distributed in the hope that it will be useful, *
12
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
13
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14
* GNU General Public License for more details. *
16
* You should have received a copy of the GNU General Public License *
17
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
18
**************************************************************************/
20
#include "karaokelyricstextkar.h"
34
// Parsed tempo change structure
43
// Parsed per-channel info
46
unsigned int total_lyrics;
47
unsigned int total_lyrics_space;
52
// Based entirely on class MidiTimestamp from pyKaraoke
56
const std::vector<MidiTempo>& m_tempo;
58
unsigned int m_currentClick;
59
unsigned int m_tempoIndex;
60
unsigned int m_division;
64
MidiTimestamp( const std::vector<MidiTempo>& tempo, unsigned int division )
65
: m_tempo (tempo), m_division (division)
77
double getTimeForClicks( unsigned int click, unsigned int tempo )
79
double microseconds = ( ( float(click) / m_division ) * tempo );
80
double ms = microseconds / 1000.0;
84
// Returns the "advanced" clock value in ms.
85
double advanceClocks( unsigned int click )
87
// Moves time forward to the indicated click number.
88
if ( m_currentClick > click )
89
throw("Malformed lyrics timing");
91
unsigned int clicks = click - m_currentClick;
93
while ( clicks > 0 && m_tempoIndex < m_tempo.size() )
95
// How many clicks remain at the current tempo?
96
unsigned int clicksRemaining = 0;
98
if ( m_tempo[ m_tempoIndex ].clocks - m_currentClick > 0 )
99
clicksRemaining = m_tempo[ m_tempoIndex ].clocks - m_currentClick;
101
unsigned int clicksUsed = clicks < clicksRemaining ? clicks : clicksRemaining;
103
if ( clicksUsed > 0 && m_tempoIndex > 0 )
104
m_currentMs += getTimeForClicks( clicksUsed, m_tempo[ m_tempoIndex - 1 ].tempo );
106
m_currentClick += clicksUsed;
107
clicks -= clicksUsed;
108
clicksRemaining -= clicksUsed;
110
if ( clicksRemaining == 0 )
116
// We have reached the last tempo mark of the song, so this tempo holds forever.
117
m_currentMs += getTimeForClicks( clicks, m_tempo[ m_tempoIndex - 1 ].tempo );
118
m_currentClick += clicks;
127
CKaraokeLyricsTextKAR::CKaraokeLyricsTextKAR( const CStdString & midiFile )
128
: CKaraokeLyricsText()
131
m_midiFile = midiFile;
135
CKaraokeLyricsTextKAR::~CKaraokeLyricsTextKAR()
141
bool CKaraokeLyricsTextKAR::Load()
146
// Clear the lyrics array
149
if ( !file.Open( m_midiFile, TRUE ) )
152
m_midiSize = (unsigned int) file.GetLength();
155
return false; // shouldn't happen, but
157
file.Seek( 0, SEEK_SET );
159
m_midiData = new unsigned char [ m_midiSize ];
161
// Read the whole file
162
if ( !m_midiData || file.Read( m_midiData, m_midiSize) != m_midiSize )
172
catch ( const char * p )
174
CLog::Log( LOGDEBUG, "KAR lyric loader: cannot load file: %s", p );
178
delete [] m_midiData;
185
// Got a lot of good ideas from pykaraoke by Kelvin Lawson (kelvinl@users.sf.net). Thanks!
187
void CKaraokeLyricsTextKAR::parseMIDI()
192
unsigned int header = readDword();
194
// If we get MS RIFF header, skip it
195
if ( header == 0x52494646 )
197
setPos( currentPos() + 16 );
198
header = readDword();
202
if ( header != 0x4D546864 )
203
throw( "Not a MIDI file" );
205
// Bytes 5-8: header length
206
unsigned int header_length = readDword();
208
// Bytes 9-10: format
209
unsigned short format = readWord();
212
throw( "Unsupported format" );
214
// Bytes 11-12: tracks
215
unsigned short tracks = readWord();
217
// Bytes 13-14: divisious
218
unsigned short divisions = readWord();
220
if ( divisions > 32768 )
221
throw( "Unsupported division" );
223
// Number of tracks is always 1 if format is 0
227
// Parsed per-channel info
228
std::vector<MidiLyrics> lyrics;
229
std::vector<MidiTempo> tempos;
230
std::vector<MidiChannelInfo> channels;
232
channels.resize( tracks );
234
// Set up default tempo
238
tempos.push_back( te );
240
int preferred_lyrics_track = -1;
243
unsigned int firstNoteClocks = 1000000000; // arbitrary large value
244
unsigned int next_line_flag = 0;
246
// Point to first byte after MIDI header
247
setPos( 8 + header_length );
250
for ( int track = 0; track < tracks; track++ )
253
unsigned int clocks = 0;
255
channels[track].total_lyrics = 0;
256
channels[track].total_lyrics_space = 0;
258
// Skip malformed files
259
if ( readDword() != 0x4D54726B )
260
throw( "Malformed track header" );
262
// Next track position
263
int tracklen = readDword();
264
unsigned int nexttrackstart = tracklen + currentPos();
266
// Parse track until end of track event
267
while ( currentPos() < nexttrackstart )
270
clocks += readVarLen();
271
unsigned char msgtype = readByte();
276
if ( msgtype == 0xFF )
278
unsigned char metatype = readByte();
279
unsigned int metalength = readVarLen();
283
// Track title metatype
284
if ( metalength > sizeof( tempbuf ) )
285
throw( "Meta event too long" );
287
readData( tempbuf, metalength );
288
tempbuf[metalength] = '\0';
290
if ( !strcmp( tempbuf, "Words" ) )
291
preferred_lyrics_track = track;
293
else if ( metatype == 5 || metatype == 1 )
296
if ( metalength > sizeof( tempbuf ) )
297
throw( "Meta event too long" );
299
readData( tempbuf, metalength );
300
tempbuf[metalength] = '\0';
302
if ( (tempbuf[0] == '@' && tempbuf[1] >= 'A' && tempbuf[1] <= 'Z')
303
|| strstr( tempbuf, " SYX" ) || strstr( tempbuf, "Track-" )
304
|| strstr( tempbuf, "%-" ) || strstr( tempbuf, "%+" ) )
311
lyric.clocks = clocks;
313
lyric.flags = next_line_flag;
315
if ( tempbuf[0] == '\\' )
317
lyric.flags = CKaraokeLyricsText::LYRICS_NEW_PARAGRAPH;
318
lyric.text = tempbuf + 1;
320
else if ( tempbuf[0] == '/' )
322
lyric.flags = CKaraokeLyricsText::LYRICS_NEW_LINE;
323
lyric.text = tempbuf + 1;
325
else if ( tempbuf[1] == '\0' && (tempbuf[0] == '\n' || tempbuf[0] == '\r' ) )
327
// An empty line; do not add it but set the flag
328
if ( next_line_flag == CKaraokeLyricsText::LYRICS_NEW_LINE )
329
next_line_flag = CKaraokeLyricsText::LYRICS_NEW_PARAGRAPH;
331
next_line_flag = CKaraokeLyricsText::LYRICS_NEW_LINE;
335
next_line_flag = (strchr(tempbuf, '\n') || strchr(tempbuf, '\r')) ? CKaraokeLyricsText::LYRICS_NEW_LINE : 0;
336
lyric.text = tempbuf;
339
lyrics.push_back( lyric );
341
// Calculate the number of spaces in current syllable
342
for ( unsigned int j = 0; j < metalength; j++ )
344
channels[ track ].total_lyrics++;
346
if ( tempbuf[j] == 0x20 )
347
channels[ track ].total_lyrics_space++;
351
else if ( metatype == 0x51 )
354
if ( metalength != 3 )
355
throw( "Invalid tempo" );
357
unsigned char a1 = readByte();
358
unsigned char a2 = readByte();
359
unsigned char a3 = readByte();
360
unsigned int tempo = (a1 << 16) | (a2 << 8) | a3;
362
// MIDI spec says tempo could only be on the first track...
363
// but some MIDI editors still put it on second. Shouldn't break anything anyway, but let's see
365
// throw( "Invalid tempo track" );
367
// Check tempo array. If previous tempo has higher clocks, abort.
368
if ( tempos.size() > 0 && tempos[ tempos.size() - 1 ].clocks > clocks )
369
throw( "Invalid tempo" );
371
// If previous tempo has the same clocks value, override it. Otherwise add new.
372
if ( tempos.size() > 0 && tempos[ tempos.size() - 1 ].clocks == clocks )
373
tempos[ tempos.size() - 1 ].tempo = tempo;
380
tempos.push_back( mt );
385
// Skip the event completely
386
setPos( currentPos() + metalength );
389
else if ( msgtype== 0xF0 || msgtype == 0xF7 )
392
unsigned int length = readVarLen();
393
setPos( currentPos() + length );
397
// Regular MIDI event
398
if ( msgtype & 0x80 )
401
laststatus = ( msgtype >> 4) & 0x07;
402
lastchannel = msgtype & 0x0F;
404
if ( laststatus != 0x07 )
405
msgtype = readByte() & 0x7F;
408
switch ( laststatus )
415
if ( (readByte() & 0x7F) != 0 ) // this would be in fact Note off
417
// Remember the time the first note played
418
if ( firstNoteClocks > clocks )
419
firstNoteClocks = clocks;
423
case 2: // Key Pressure
424
case 3: // Control change
425
case 6: // Pitch wheel
429
case 4: // Program change
430
case 5: // Channel pressure
433
default: // case 7: Ignore this event
434
if ( (lastchannel & 0x0F) == 2 ) // Sys Com Song Position Pntr
436
else if ( (lastchannel & 0x0F) == 3 ) // Sys Com Song Select(Song #)
444
// The MIDI file is parsed. Now try to find the preferred lyric track
445
if ( preferred_lyrics_track == -1 || channels[preferred_lyrics_track].total_lyrics == 0 )
447
unsigned int max_lyrics = 0;
449
for ( unsigned int t = 0; t < tracks; t++ )
451
if ( channels[t].total_lyrics > max_lyrics )
453
preferred_lyrics_track = t;
454
max_lyrics = channels[t].total_lyrics;
459
if ( preferred_lyrics_track == -1 )
460
throw( "No lyrics found" );
462
// We found the lyrics track. Dump some debug information.
463
MidiTimestamp mts( tempos, divisions );
464
double firstNoteTime = mts.advanceClocks( firstNoteClocks );
466
// Now go through all lyrics on this track, convert them into time.
469
for ( unsigned int i = 0; i < lyrics.size(); i++ )
471
if ( (int) lyrics[i].track != preferred_lyrics_track )
474
double lyrics_timing = mts.advanceClocks( lyrics[i].clocks );
476
// Skip lyrics which start before the first note
477
if ( lyrics_timing < firstNoteTime )
480
unsigned int mstime = (unsigned int)ceil( (lyrics_timing - firstNoteTime) / 100);
481
addLyrics( lyrics[i].text, mstime, lyrics[i].flags );
486
unsigned char CKaraokeLyricsTextKAR::readByte()
488
if ( m_midiOffset >= m_midiSize )
489
throw( "Cannot read byte: premature end of file" );
491
const unsigned char * p = m_midiData + m_midiOffset;
496
unsigned short CKaraokeLyricsTextKAR::readWord()
498
if ( m_midiOffset + 1 >= m_midiSize )
499
throw( "Cannot read word: premature end of file" );
501
const unsigned char * p = m_midiData + m_midiOffset;
503
return p[0] << 8 | p[1];
507
unsigned int CKaraokeLyricsTextKAR::readDword()
509
if ( m_midiOffset + 3 >= m_midiSize )
510
throw( "Cannot read dword: premature end of file" );
512
const unsigned char * p = m_midiData + m_midiOffset;
514
return p[0] << 24 | p[1] << 16 | p[2] << 8 | p[3];
517
int CKaraokeLyricsTextKAR::readVarLen()
526
l = (l | (c & 0x7f)) << 7;
532
l = (l | (c & 0x7f)) << 7;
538
l = (l | (c & 0x7f)) << 7;
544
l = (l | (c & 0x7f)) << 7;
550
throw( "Cannot read variable field" );
553
unsigned int CKaraokeLyricsTextKAR::currentPos() const
558
void CKaraokeLyricsTextKAR::setPos(unsigned int offset)
560
m_midiOffset = offset;
563
void CKaraokeLyricsTextKAR::readData(void * buf, unsigned int length)
565
for ( unsigned int i = 0; i < length; i++ )
566
*((char*)buf + i) = readByte();