~ubuntu-branches/ubuntu/wily/karlyriceditor/wily

« back to all changes in this revision

Viewing changes to src/karaokelyricstextkar.cpp

  • Committer: Package Import Robot
  • Author(s): Martin Steghöfer
  • Date: 2014-09-20 12:56:08 UTC
  • Revision ID: package-import@ubuntu.com-20140920125608-qhl2xiq2bkikdp9g
Tags: upstream-1.11
Import upstream version 1.11

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/**************************************************************************
 
2
 *  Karlyriceditor - a lyrics editor and CD+G / video export for Karaoke  *
 
3
 *  songs.                                                                *
 
4
 *  Copyright (C) 2009-2013 George Yunaev, support@ulduzsoft.com          *
 
5
 *                                                                        *
 
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.                                   *
 
10
 *                                                                                                                                            *
 
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.                          *
 
15
 *                                                                        *
 
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
 **************************************************************************/
 
19
 
 
20
#include "karaokelyricstextkar.h"
 
21
 
 
22
 
 
23
// Parsed lyrics
 
24
typedef struct
 
25
{
 
26
        unsigned int    clocks;
 
27
        unsigned int    track;
 
28
        CStdString              text;
 
29
        unsigned int    flags;
 
30
        
 
31
} MidiLyrics;
 
32
 
 
33
 
 
34
// Parsed tempo change structure
 
35
typedef struct
 
36
{
 
37
        unsigned int    clocks;
 
38
        unsigned int    tempo;
 
39
 
 
40
} MidiTempo;
 
41
 
 
42
 
 
43
// Parsed per-channel info
 
44
typedef struct
 
45
{
 
46
        unsigned int    total_lyrics;
 
47
        unsigned int    total_lyrics_space;
 
48
 
 
49
} MidiChannelInfo;
 
50
 
 
51
 
 
52
// Based entirely on class MidiTimestamp from pyKaraoke
 
53
class MidiTimestamp
 
54
{
 
55
        private:
 
56
                const std::vector<MidiTempo>&   m_tempo;
 
57
                double                                                  m_currentMs;
 
58
                unsigned int                                    m_currentClick;
 
59
                unsigned int                                    m_tempoIndex;
 
60
                unsigned int                                    m_division;
 
61
 
 
62
                
 
63
        public:
 
64
                MidiTimestamp( const std::vector<MidiTempo>& tempo, unsigned int division )
 
65
                        : m_tempo (tempo), m_division (division)
 
66
                {
 
67
                        reset();
 
68
                }
 
69
 
 
70
                void reset()
 
71
                {
 
72
                        m_currentMs = 0.0;
 
73
                        m_currentClick = 0;
 
74
                        m_tempoIndex = 0;
 
75
                }
 
76
 
 
77
                double getTimeForClicks( unsigned int click, unsigned int tempo )
 
78
                {
 
79
                double microseconds = ( ( float(click) / m_division ) * tempo );
 
80
                        double ms = microseconds / 1000.0;
 
81
                        return ms;
 
82
                }
 
83
                                
 
84
                // Returns the "advanced" clock value in ms.
 
85
                double advanceClocks( unsigned int click )
 
86
                {
 
87
                        // Moves time forward to the indicated click number.
 
88
                        if ( m_currentClick > click )
 
89
                                throw("Malformed lyrics timing");
 
90
        
 
91
                        unsigned int clicks = click - m_currentClick;
 
92
 
 
93
                        while ( clicks > 0 && m_tempoIndex < m_tempo.size() )
 
94
                        {
 
95
                                // How many clicks remain at the current tempo?
 
96
                                unsigned int clicksRemaining = 0;
 
97
 
 
98
                                if ( m_tempo[ m_tempoIndex ].clocks - m_currentClick > 0 )
 
99
                                        clicksRemaining = m_tempo[ m_tempoIndex ].clocks - m_currentClick;
 
100
 
 
101
                                unsigned int clicksUsed = clicks < clicksRemaining ? clicks : clicksRemaining;
 
102
 
 
103
                                if ( clicksUsed > 0 && m_tempoIndex > 0 )
 
104
                                        m_currentMs += getTimeForClicks( clicksUsed, m_tempo[ m_tempoIndex - 1 ].tempo );
 
105
                                
 
106
                                m_currentClick += clicksUsed;
 
107
                                clicks -= clicksUsed;
 
108
                                clicksRemaining -= clicksUsed;
 
109
 
 
110
                                if ( clicksRemaining == 0 )
 
111
                                        m_tempoIndex++;
 
112
                        }
 
113
 
 
114
                        if ( clicks > 0 )
 
115
                        {
 
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;
 
119
                        }
 
120
 
 
121
                        return m_currentMs;
 
122
                }
 
123
};
 
124
 
 
125
 
 
126
 
 
127
CKaraokeLyricsTextKAR::CKaraokeLyricsTextKAR( const CStdString & midiFile )
 
128
        : CKaraokeLyricsText()
 
129
{
 
130
        m_midiData = 0;
 
131
        m_midiFile = midiFile;
 
132
}
 
133
 
 
134
 
 
135
CKaraokeLyricsTextKAR::~CKaraokeLyricsTextKAR()
 
136
{
 
137
        delete[] m_midiData;
 
138
}
 
139
 
 
140
/*
 
141
bool CKaraokeLyricsTextKAR::Load()
 
142
{
 
143
        XFILE::CFile file;
 
144
        bool succeed = true;
 
145
 
 
146
        // Clear the lyrics array
 
147
        clearLyrics();
 
148
                        
 
149
        if ( !file.Open( m_midiFile, TRUE ) )
 
150
                return false;
 
151
        
 
152
        m_midiSize = (unsigned int) file.GetLength();
 
153
 
 
154
        if ( !m_midiSize )
 
155
                return false;   // shouldn't happen, but
 
156
        
 
157
        file.Seek( 0, SEEK_SET );
 
158
 
 
159
        m_midiData = new unsigned char [ m_midiSize ];
 
160
 
 
161
        // Read the whole file
 
162
        if ( !m_midiData || file.Read( m_midiData, m_midiSize) != m_midiSize )
 
163
                return false;
 
164
        
 
165
        file.Close();
 
166
 
 
167
        // Parse MIDI
 
168
        try
 
169
        {
 
170
                parseMIDI();
 
171
        }
 
172
        catch ( const char * p )
 
173
        {
 
174
                CLog::Log( LOGDEBUG, "KAR lyric loader: cannot load file: %s", p );
 
175
                succeed = false;
 
176
        }
 
177
                                
 
178
        delete [] m_midiData;
 
179
        m_midiData = 0;
 
180
        return succeed;
 
181
}
 
182
*/
 
183
 
 
184
//
 
185
// Got a lot of good ideas from pykaraoke by Kelvin Lawson (kelvinl@users.sf.net). Thanks!
 
186
//
 
187
void CKaraokeLyricsTextKAR::parseMIDI()
 
188
{
 
189
  m_midiOffset = 0;
 
190
 
 
191
  // Bytes 0-4: header
 
192
  unsigned int header = readDword();
 
193
 
 
194
  // If we get MS RIFF header, skip it
 
195
  if ( header == 0x52494646 )
 
196
  {
 
197
    setPos( currentPos() + 16 );
 
198
    header = readDword();
 
199
  }
 
200
 
 
201
  // MIDI header
 
202
  if ( header != 0x4D546864 )
 
203
    throw( "Not a MIDI file" );
 
204
 
 
205
  // Bytes 5-8: header length
 
206
  unsigned int header_length = readDword();
 
207
 
 
208
  // Bytes 9-10: format
 
209
  unsigned short format = readWord();
 
210
 
 
211
  if ( format > 2 )
 
212
    throw( "Unsupported format" );
 
213
 
 
214
  // Bytes 11-12: tracks
 
215
  unsigned short tracks = readWord();
 
216
 
 
217
  // Bytes 13-14: divisious
 
218
  unsigned short divisions = readWord();
 
219
 
 
220
  if ( divisions > 32768 )
 
221
    throw( "Unsupported division" );
 
222
 
 
223
  // Number of tracks is always 1 if format is 0
 
224
  if ( format == 0 )
 
225
    tracks = 1;
 
226
 
 
227
  // Parsed per-channel info
 
228
  std::vector<MidiLyrics> lyrics;
 
229
  std::vector<MidiTempo> tempos;
 
230
  std::vector<MidiChannelInfo> channels;
 
231
 
 
232
  channels.resize( tracks );
 
233
 
 
234
  // Set up default tempo
 
235
  MidiTempo te;
 
236
  te.clocks = 0;
 
237
  te.tempo = 500000;
 
238
  tempos.push_back( te );
 
239
 
 
240
  int preferred_lyrics_track = -1;
 
241
  int lastchannel = 0;
 
242
  int laststatus = 0;
 
243
  unsigned int firstNoteClocks = 1000000000; // arbitrary large value
 
244
  unsigned int next_line_flag = 0;
 
245
 
 
246
  // Point to first byte after MIDI header
 
247
  setPos( 8 + header_length );
 
248
 
 
249
  // Parse all tracks
 
250
  for ( int track = 0; track < tracks; track++ )
 
251
  {
 
252
    char tempbuf[1024];
 
253
    unsigned int clocks = 0;
 
254
 
 
255
    channels[track].total_lyrics = 0;
 
256
    channels[track].total_lyrics_space = 0;
 
257
 
 
258
    // Skip malformed files
 
259
    if ( readDword() != 0x4D54726B )
 
260
      throw( "Malformed track header" );
 
261
 
 
262
    // Next track position
 
263
    int tracklen = readDword();
 
264
    unsigned int nexttrackstart = tracklen + currentPos();
 
265
 
 
266
    // Parse track until end of track event
 
267
    while ( currentPos() < nexttrackstart )
 
268
    {
 
269
      // field length
 
270
      clocks += readVarLen();
 
271
      unsigned char msgtype = readByte();
 
272
 
 
273
      //
 
274
      // Meta event
 
275
      //
 
276
      if ( msgtype == 0xFF )
 
277
      {
 
278
        unsigned char metatype = readByte();
 
279
        unsigned int metalength = readVarLen();
 
280
 
 
281
        if ( metatype == 3 )
 
282
        {
 
283
          // Track title metatype
 
284
          if ( metalength > sizeof( tempbuf ) )
 
285
            throw( "Meta event too long" );
 
286
 
 
287
          readData( tempbuf, metalength );
 
288
          tempbuf[metalength] = '\0';
 
289
 
 
290
          if ( !strcmp( tempbuf, "Words" ) )
 
291
            preferred_lyrics_track = track;
 
292
        }
 
293
        else if ( metatype == 5 || metatype == 1 )
 
294
        {
 
295
          // Lyrics metatype
 
296
          if ( metalength > sizeof( tempbuf ) )
 
297
            throw( "Meta event too long" );
 
298
 
 
299
          readData( tempbuf, metalength );
 
300
          tempbuf[metalength] = '\0';
 
301
 
 
302
          if ( (tempbuf[0] == '@' && tempbuf[1] >= 'A' && tempbuf[1] <= 'Z')
 
303
          || strstr( tempbuf, " SYX" ) || strstr( tempbuf, "Track-" )
 
304
          || strstr( tempbuf, "%-" ) || strstr( tempbuf, "%+" ) )
 
305
          {
 
306
            // Keywords
 
307
          }
 
308
          else
 
309
          {
 
310
            MidiLyrics lyric;
 
311
            lyric.clocks = clocks;
 
312
            lyric.track = track;
 
313
            lyric.flags = next_line_flag;
 
314
 
 
315
            if ( tempbuf[0] == '\\' )
 
316
            {
 
317
              lyric.flags = CKaraokeLyricsText::LYRICS_NEW_PARAGRAPH;
 
318
              lyric.text = tempbuf + 1;
 
319
            }
 
320
            else if ( tempbuf[0] == '/' )
 
321
            {
 
322
              lyric.flags = CKaraokeLyricsText::LYRICS_NEW_LINE;
 
323
              lyric.text = tempbuf + 1;
 
324
            }
 
325
            else if ( tempbuf[1] == '\0' && (tempbuf[0] == '\n' || tempbuf[0] == '\r' ) )
 
326
            {
 
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;
 
330
              else
 
331
                next_line_flag = CKaraokeLyricsText::LYRICS_NEW_LINE;
 
332
            }
 
333
            else
 
334
            {
 
335
              next_line_flag = (strchr(tempbuf, '\n') || strchr(tempbuf, '\r')) ? CKaraokeLyricsText::LYRICS_NEW_LINE : 0;
 
336
              lyric.text = tempbuf;
 
337
            }
 
338
 
 
339
            lyrics.push_back( lyric );
 
340
 
 
341
            // Calculate the number of spaces in current syllable
 
342
            for ( unsigned int j = 0; j < metalength; j++ )
 
343
            {
 
344
              channels[ track ].total_lyrics++;
 
345
 
 
346
              if ( tempbuf[j] == 0x20 )
 
347
                channels[ track ].total_lyrics_space++;
 
348
            }
 
349
          }
 
350
        }
 
351
        else if ( metatype == 0x51 )
 
352
        {
 
353
          // Set tempo event
 
354
          if ( metalength != 3 )
 
355
            throw( "Invalid tempo" );
 
356
 
 
357
          unsigned char a1 = readByte();
 
358
          unsigned char a2 = readByte();
 
359
          unsigned char a3 = readByte();
 
360
          unsigned int tempo = (a1 << 16) | (a2 << 8) | a3;
 
361
 
 
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
 
364
          //if ( track != 0 )
 
365
          //  throw( "Invalid tempo track" );
 
366
 
 
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" );
 
370
 
 
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;
 
374
          else
 
375
          {
 
376
            MidiTempo mt;
 
377
            mt.clocks = clocks;
 
378
            mt.tempo = tempo;
 
379
 
 
380
            tempos.push_back( mt );
 
381
          }
 
382
        }
 
383
        else
 
384
        {
 
385
          // Skip the event completely
 
386
          setPos( currentPos() + metalength );
 
387
        }
 
388
      }
 
389
      else if ( msgtype== 0xF0 || msgtype == 0xF7 )
 
390
      {
 
391
        // SysEx event
 
392
        unsigned int length = readVarLen();
 
393
        setPos( currentPos() + length );
 
394
      }
 
395
      else
 
396
      {
 
397
        // Regular MIDI event
 
398
        if ( msgtype & 0x80 )
 
399
        {
 
400
          // Status byte
 
401
          laststatus = ( msgtype >> 4) & 0x07;
 
402
          lastchannel = msgtype & 0x0F;
 
403
 
 
404
          if ( laststatus != 0x07 )
 
405
            msgtype = readByte() & 0x7F;
 
406
        }
 
407
 
 
408
        switch ( laststatus )
 
409
        {
 
410
          case 0:  // Note off
 
411
            readByte();
 
412
            break;
 
413
 
 
414
          case 1: // Note on
 
415
            if ( (readByte() & 0x7F) != 0 ) // this would be in fact Note off
 
416
            {
 
417
              // Remember the time the first note played
 
418
              if ( firstNoteClocks > clocks )
 
419
                firstNoteClocks = clocks;
 
420
            }
 
421
            break;
 
422
 
 
423
          case 2: // Key Pressure
 
424
          case 3: // Control change
 
425
          case 6: // Pitch wheel
 
426
            readByte();
 
427
            break;
 
428
 
 
429
          case 4: // Program change
 
430
          case 5: // Channel pressure
 
431
            break;
 
432
 
 
433
          default: // case 7: Ignore this event
 
434
            if ( (lastchannel & 0x0F) == 2 ) // Sys Com Song Position Pntr
 
435
              readWord();
 
436
            else if ( (lastchannel & 0x0F) == 3 ) // Sys Com Song Select(Song #)
 
437
              readByte();
 
438
            break;
 
439
        }
 
440
      }
 
441
    }
 
442
  }
 
443
 
 
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 )
 
446
  {
 
447
    unsigned int max_lyrics = 0;
 
448
 
 
449
    for ( unsigned int t = 0; t < tracks; t++ )
 
450
    {
 
451
      if ( channels[t].total_lyrics > max_lyrics )
 
452
      {
 
453
        preferred_lyrics_track = t;
 
454
        max_lyrics = channels[t].total_lyrics;
 
455
      }
 
456
    }
 
457
  }
 
458
 
 
459
  if ( preferred_lyrics_track == -1 )
 
460
    throw( "No lyrics found" );
 
461
 
 
462
  // We found the lyrics track. Dump some debug information.
 
463
  MidiTimestamp mts( tempos, divisions );
 
464
  double firstNoteTime = mts.advanceClocks( firstNoteClocks );
 
465
 
 
466
  // Now go through all lyrics on this track, convert them into time.
 
467
  mts.reset();
 
468
 
 
469
  for ( unsigned int i = 0; i < lyrics.size(); i++ )
 
470
  {
 
471
    if ( (int) lyrics[i].track != preferred_lyrics_track )
 
472
      continue;
 
473
 
 
474
    double lyrics_timing = mts.advanceClocks( lyrics[i].clocks );
 
475
 
 
476
    // Skip lyrics which start before the first note
 
477
    if ( lyrics_timing < firstNoteTime )
 
478
      continue;
 
479
 
 
480
    unsigned int mstime = (unsigned int)ceil( (lyrics_timing - firstNoteTime) / 100);
 
481
    addLyrics( lyrics[i].text, mstime, lyrics[i].flags );
 
482
  }
 
483
}
 
484
 
 
485
 
 
486
unsigned char CKaraokeLyricsTextKAR::readByte()
 
487
{
 
488
  if ( m_midiOffset >= m_midiSize )
 
489
    throw( "Cannot read byte: premature end of file" );
 
490
 
 
491
  const unsigned char * p = m_midiData + m_midiOffset;
 
492
  m_midiOffset += 1;
 
493
  return p[0];
 
494
}
 
495
 
 
496
unsigned short CKaraokeLyricsTextKAR::readWord()
 
497
{
 
498
  if ( m_midiOffset + 1 >= m_midiSize )
 
499
    throw( "Cannot read word: premature end of file" );
 
500
 
 
501
  const unsigned char * p = m_midiData + m_midiOffset;
 
502
  m_midiOffset += 2;
 
503
  return p[0] << 8 | p[1];
 
504
}
 
505
 
 
506
 
 
507
unsigned int CKaraokeLyricsTextKAR::readDword()
 
508
{
 
509
  if ( m_midiOffset + 3 >= m_midiSize )
 
510
    throw( "Cannot read dword: premature end of file" );
 
511
 
 
512
  const unsigned char * p = m_midiData + m_midiOffset;
 
513
  m_midiOffset += 4;
 
514
  return p[0] << 24 | p[1] << 16 | p[2] << 8 | p[3];
 
515
}
 
516
 
 
517
int CKaraokeLyricsTextKAR::readVarLen()
 
518
{
 
519
  int l = 0, c;
 
520
 
 
521
  c = readByte();
 
522
 
 
523
  if ( !(c & 0x80) )
 
524
    return l | c;
 
525
 
 
526
  l = (l | (c & 0x7f)) << 7;
 
527
  c = readByte();
 
528
 
 
529
  if ( !(c & 0x80) )
 
530
    return l | c;
 
531
 
 
532
  l = (l | (c & 0x7f)) << 7;
 
533
  c = readByte();
 
534
 
 
535
  if ( !(c & 0x80) )
 
536
    return l | c;
 
537
 
 
538
  l = (l | (c & 0x7f)) << 7;
 
539
  c = readByte();
 
540
 
 
541
  if ( !(c & 0x80) )
 
542
    return l | c;
 
543
 
 
544
  l = (l | (c & 0x7f)) << 7;
 
545
  c = readByte();
 
546
 
 
547
  if ( !(c & 0x80) )
 
548
    return l | c;
 
549
 
 
550
  throw( "Cannot read variable field" );
 
551
}
 
552
 
 
553
unsigned int CKaraokeLyricsTextKAR::currentPos() const
 
554
{
 
555
  return m_midiOffset;
 
556
}
 
557
 
 
558
void CKaraokeLyricsTextKAR::setPos(unsigned int offset)
 
559
{
 
560
  m_midiOffset = offset;
 
561
}
 
562
 
 
563
void CKaraokeLyricsTextKAR::readData(void * buf, unsigned int length)
 
564
{
 
565
  for ( unsigned int i = 0; i < length; i++ )
 
566
    *((char*)buf + i) = readByte();
 
567
}