[cosmetics] update date in GPL header
[vuplus_xbmc] / xbmc / music / karaoke / karaokelyricstext.cpp
1 /*
2  *      Copyright (C) 2005-2013 Team XBMC
3  *      http://www.xbmc.org
4  *
5  *  This Program is free software; you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation; either version 2, or (at your option)
8  *  any later version.
9  *
10  *  This Program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with XBMC; see the file COPYING.  If not, see
17  *  <http://www.gnu.org/licenses/>.
18  *
19  */
20
21 // C++ Implementation: karaokelyricstext
22
23 #include <math.h>
24
25 #include "utils/CharsetConverter.h"
26 #include "settings/Settings.h"
27 #include "settings/GUISettings.h"
28 #include "guilib/GUITextLayout.h"
29 #include "guilib/GUIFont.h"
30 #include "karaokelyricstext.h"
31 #include "utils/URIUtils.h"
32 #include "filesystem/File.h"
33 #include "guilib/GUIFontManager.h"
34 #include "addons/Skin.h"
35 #include "utils/MathUtils.h"
36 #include "utils/log.h"
37
38 typedef struct
39 {
40   unsigned int   text;
41   unsigned int   active;
42   unsigned int   outline;
43
44 } LyricColors;
45
46 // Must be synchronized with strings.xml and GUISettings.cpp!
47 static LyricColors gLyricColors[] =
48 {
49   // <string id="22040">white/green</string>
50   // First 0xFF is alpha!
51   {  0xFFDADADA,  0xFF00FF00,  0xFF000000  },
52
53   // <string id="22041">white/red</string>
54   {  0xFFDADADA,  0xFFFF0000,  0xFF000000  },
55
56   // <string id="22042">white/blue</string>
57   {  0xFFDADADA,  0xFF0000FF,  0xFF000000  },
58
59   // <string id="22043">black/white</string>
60   {  0xFF000000,  0xFFDADADA,  0xFFFFFFFF  },
61 };
62
63
64 CKaraokeLyricsText::CKaraokeLyricsText()
65   : CKaraokeLyrics()
66 {
67   m_karaokeLayout = 0;
68   m_preambleLayout = 0;
69   m_karaokeFont = 0;
70
71   int coloridx = g_guiSettings.GetInt("karaoke.fontcolors");
72   if ( coloridx < KARAOKE_COLOR_START || coloridx >= KARAOKE_COLOR_END )
73     coloridx = 0;
74
75   m_colorLyrics = gLyricColors[coloridx].text;
76   m_colorLyricsOutline = gLyricColors[coloridx].outline;
77   m_colorSinging.Format( "%08X", gLyricColors[coloridx].active );
78
79   m_delayAfter = 50; // 5 seconds
80   m_showLyricsBeforeStart = 50; // 7.5 seconds
81   m_showPreambleBeforeStart = 35; // 5.5 seconds
82   m_paragraphBreakTime = 50; // 5 seconds; for autodetection paragraph breaks
83   m_mergeLines = true;
84   m_hasPitch = false;
85   m_videoOffset = 0;
86
87   m_lyricsState = STATE_END_SONG;
88 }
89
90
91 CKaraokeLyricsText::~CKaraokeLyricsText()
92 {
93 }
94
95 void CKaraokeLyricsText::clearLyrics()
96 {
97   m_lyrics.clear();
98   m_songName.clear();
99   m_artist.clear();
100   m_hasPitch = false;
101   m_videoFile.clear();
102   m_videoOffset = 0;
103 }
104
105
106 void CKaraokeLyricsText::addLyrics(const CStdString & text, unsigned int timing, unsigned int flags, unsigned int pitch)
107 {
108   Lyric line;
109
110   if ( flags & LYRICS_CONVERT_UTF8 )
111   {
112     // Reset the flag
113     flags &= ~LYRICS_CONVERT_UTF8;
114     g_charsetConverter.unknownToUTF8(text, line.text);
115   }
116   else
117   {
118     line.text = text;
119   }
120
121   line.flags = flags;
122   line.timing = timing;
123   line.pitch = pitch;
124
125   // If this is the first entry, remove LYRICS_NEW_LINE and LYRICS_NEW_PARAGRAPH flags
126   if ( m_lyrics.size() == 0 )
127     line.flags &= ~(LYRICS_NEW_LINE | LYRICS_NEW_PARAGRAPH );
128
129   // 'New paragraph' includes new line as well
130   if ( line.flags & LYRICS_NEW_PARAGRAPH )
131     line.flags &= ~LYRICS_NEW_LINE;
132
133   m_lyrics.push_back( line );
134 }
135
136
137 bool CKaraokeLyricsText::InitGraphics()
138 {
139   if ( m_lyrics.empty() )
140     return false;
141
142   CStdString fontPath = "special://xbmc/media/Fonts/" + g_guiSettings.GetString("karaoke.font");
143   m_karaokeFont = g_fontManager.LoadTTF("__karaoke__", fontPath,
144                   m_colorLyrics, 0, g_guiSettings.GetInt("karaoke.fontheight"), FONT_STYLE_BOLD );
145   CGUIFont *karaokeBorder = g_fontManager.LoadTTF("__karaokeborder__", fontPath,
146                             m_colorLyrics, 0, g_guiSettings.GetInt("karaoke.fontheight"), FONT_STYLE_BOLD, true );
147
148   if ( !m_karaokeFont )
149   {
150     CLog::Log(LOGERROR, "CKaraokeLyricsText::PrepareGraphicsData - Unable to load subtitle font");
151     return false;
152   }
153
154   m_karaokeLayout = new CGUITextLayout( m_karaokeFont, true, 0, karaokeBorder );
155   m_preambleLayout = new CGUITextLayout( m_karaokeFont, true, 0, karaokeBorder );
156
157   if ( !m_karaokeLayout || !m_preambleLayout )
158   {
159     delete m_preambleLayout;
160     delete m_karaokeLayout;
161     m_karaokeLayout = m_preambleLayout = 0;
162
163     CLog::Log(LOGERROR, "CKaraokeLyricsText::PrepareGraphicsData - cannot create layout");
164     return false;
165   }
166
167   rescanLyrics();
168
169   m_indexNextPara = 0;
170
171   // Generate next paragraph
172   nextParagraph();
173
174   m_lyricsState = STATE_WAITING;
175   return true;
176 }
177
178
179 void CKaraokeLyricsText::Shutdown()
180 {
181   CKaraokeLyrics::Shutdown();
182
183   delete m_preambleLayout;
184   m_preambleLayout = 0;
185
186   if ( m_karaokeLayout )
187   {
188     g_fontManager.Unload("__karaoke__");
189     g_fontManager.Unload("__karaokeborder__");
190     delete m_karaokeLayout;
191     m_karaokeLayout = NULL;
192   }
193
194   m_lyricsState = STATE_END_SONG;
195 }
196
197
198 void CKaraokeLyricsText::Render()
199 {
200   if ( !m_karaokeLayout )
201     return;
202
203   // Get the current song timing
204   unsigned int songTime = (unsigned int) MathUtils::round_int( (getSongTime() * 10) );
205
206   bool updatePreamble = false;
207   bool updateText = false;
208
209   // No returns in switch if anything needs to be drawn! Just break!
210   switch ( m_lyricsState )
211   {
212     // the next paragraph lyrics are not shown yet. Screen is clear.
213     // m_index points to the first entry.
214     case STATE_WAITING:
215       if ( songTime + m_showLyricsBeforeStart < m_lyrics[ m_index ].timing )
216         return;
217
218       // Is it time to play already?
219       if ( songTime >= m_lyrics[ m_index ].timing )
220       {
221         m_lyricsState = STATE_PLAYING_PARAGRAPH;
222       }
223       else
224       {
225         m_lyricsState = STATE_PREAMBLE;
226         m_lastPreambleUpdate = songTime;
227       }
228
229       updateText = true;
230       break;
231
232     // the next paragraph lyrics are shown, but the paragraph hasn't start yet.
233     // Using m_lastPreambleUpdate, we redraw the marker each second.
234     case STATE_PREAMBLE:
235       if ( songTime < m_lyrics[ m_index ].timing )
236       {
237         // Time to redraw preamble?
238         if ( songTime + m_showPreambleBeforeStart >= m_lyrics[ m_index ].timing )
239         {
240           if ( songTime - m_lastPreambleUpdate >= 10 )
241           {
242             // Fall through out of switch() to redraw
243             m_lastPreambleUpdate = songTime;
244             updatePreamble = true;
245           }
246         }
247       }
248       else
249       {
250         updateText = true;
251         m_lyricsState = STATE_PLAYING_PARAGRAPH;
252       }
253       break;
254
255     // The lyrics are shown, but nothing is colored or no color is changed yet.
256     // m_indexStart, m_indexEnd and m_index are set, m_index timing shows when to color.
257     case STATE_PLAYING_PARAGRAPH:
258       if ( songTime >= m_lyrics[ m_index ].timing )
259       {
260         m_index++;
261         updateText = true;
262
263         if ( m_index > m_indexEndPara )
264           m_lyricsState = STATE_END_PARAGRAPH;
265       }
266       break;
267
268     // the whole paragraph is colored, but still shown, waiting until it's time to clear the lyrics.
269     // m_index still points to the last entry, and m_indexNextPara points to the first entry of next
270     // paragraph, or to LYRICS_END. When the next paragraph is about to start (which is
271     // m_indexNextPara timing - m_showLyricsBeforeStart), the state switches to STATE_START_PARAGRAPH. When time
272     // goes after m_index timing + m_delayAfter, the state switches to STATE_WAITING,
273     case STATE_END_PARAGRAPH:
274       {
275         unsigned int paraEnd = m_lyrics[ m_indexEndPara ].timing + m_delayAfter;
276
277         // If the next paragraph starts before current ends, use its start time as our end
278         if ( m_indexNextPara != LYRICS_END && m_lyrics[ m_indexNextPara ].timing <= paraEnd + m_showLyricsBeforeStart )
279         {
280           if ( m_lyrics[ m_indexNextPara ].timing > m_showLyricsBeforeStart )
281             paraEnd = m_lyrics[ m_indexNextPara ].timing - m_showLyricsBeforeStart;
282           else
283             paraEnd = 0;
284         }
285
286         if ( songTime >= paraEnd )
287         {
288           // Is the song ended?
289           if ( m_indexNextPara != LYRICS_END )
290           {
291             // Are we still waiting?
292             if ( songTime >= m_lyrics[ m_indexNextPara ].timing )
293               m_lyricsState = STATE_PLAYING_PARAGRAPH;
294             else
295               m_lyricsState = STATE_WAITING;
296
297             // Get next paragraph
298             nextParagraph();
299             updateText = true;
300           }
301           else
302           {
303             m_lyricsState = STATE_END_SONG;
304             return;
305           }
306         }
307       }
308       break;
309
310     case STATE_END_SONG:
311       // the song is completed, there are no more lyrics to show. This state is finita la comedia.
312       return;
313   }
314
315   // Calculate drawing parameters
316   RESOLUTION resolution = g_graphicsContext.GetVideoResolution();
317   g_graphicsContext.SetRenderingResolution(g_graphicsContext.GetResInfo(), false);
318   float maxWidth = (float) g_settings.m_ResInfo[resolution].Overscan.right - g_settings.m_ResInfo[resolution].Overscan.left;
319
320   // We must only fall through for STATE_DRAW_SYLLABLE or STATE_PREAMBLE
321   if ( updateText )
322   {
323     // So we need to update the layout with current paragraph text, optionally colored according to index
324     bool color_used = false;
325     m_currentLyrics = "";
326
327     // Draw the current paragraph test if needed
328     if ( songTime + m_showLyricsBeforeStart >= m_lyrics[ m_indexStartPara ].timing )
329     {
330       for ( unsigned int i = m_indexStartPara; i <= m_indexEndPara; i++ )
331       {
332         if ( m_lyrics[i].flags & LYRICS_NEW_LINE )
333           m_currentLyrics += "[CR]";
334
335         if ( i == m_indexStartPara && songTime >= m_lyrics[ m_indexStartPara ].timing )
336         {
337           color_used = true;
338           m_currentLyrics += "[COLOR " + m_colorSinging + "]";
339         }
340
341         if ( songTime < m_lyrics[ i ].timing && color_used )
342         {
343           color_used = false;
344           m_currentLyrics += "[/COLOR]";
345         }
346
347         m_currentLyrics += m_lyrics[i].text;
348       }
349
350       if ( color_used )
351         m_currentLyrics += "[/COLOR]";
352
353 //      CLog::Log( LOGERROR, "Updating text: state %d, time %d, start %d, index %d (time %d) [%s], text %s",
354 //        m_lyricsState, songTime, m_lyrics[ m_indexStartPara ].timing, m_index, m_lyrics[ m_index ].timing,
355 //        m_lyrics[ m_index ].text.c_str(), m_currentLyrics.c_str());
356     }
357
358     m_karaokeLayout->Update(m_currentLyrics, maxWidth * 0.9f);
359     updateText = false;
360   }
361
362   if ( updatePreamble )
363   {
364     m_currentPreamble = "";
365
366     // Get number of seconds left to the song start
367     if ( m_lyrics[ m_indexStartPara ].timing >= songTime )
368     {
369       unsigned int seconds = (m_lyrics[ m_indexStartPara ].timing - songTime) / 10;
370
371       while ( seconds-- > 0 )
372         m_currentPreamble += "- ";
373     }
374
375     m_preambleLayout->Update( m_currentPreamble, maxWidth * 0.9f );
376   }
377
378   float x = maxWidth * 0.5f + g_settings.m_ResInfo[resolution].Overscan.left;
379   float y = (float)g_settings.m_ResInfo[resolution].Overscan.top +
380       (g_settings.m_ResInfo[resolution].Overscan.bottom - g_settings.m_ResInfo[resolution].Overscan.top) / 8;
381
382   float textWidth, textHeight;
383   m_karaokeLayout->GetTextExtent(textWidth, textHeight);
384   m_karaokeLayout->RenderOutline(x, y, 0, m_colorLyricsOutline, XBFONT_CENTER_X, maxWidth);
385
386   if ( !m_currentPreamble.IsEmpty() )
387   {
388     float pretextWidth, pretextHeight;
389     m_preambleLayout->GetTextExtent(pretextWidth, pretextHeight);
390     m_preambleLayout->RenderOutline(x - textWidth / 2, y - pretextHeight, 0, m_colorLyricsOutline, XBFONT_LEFT, maxWidth);
391   }
392 }
393
394
395 void CKaraokeLyricsText::nextParagraph()
396 {
397   if ( m_indexNextPara == LYRICS_END )
398     return;
399
400   bool new_para_found = false;
401   m_indexStartPara = m_index = m_indexNextPara;
402
403   for ( m_indexEndPara = m_index + 1; m_indexEndPara < m_lyrics.size(); m_indexEndPara++ )
404   {
405     if ( m_lyrics[ m_indexEndPara ].flags & LYRICS_NEW_PARAGRAPH
406     || ( m_lyrics[ m_indexEndPara ].timing - m_lyrics[ m_indexEndPara - 1 ].timing ) > m_paragraphBreakTime )
407     {
408       new_para_found = true;
409       break;
410     }
411   }
412
413   // Is this the end of array?
414   if ( new_para_found )
415     m_indexNextPara = m_indexEndPara;
416   else
417     m_indexNextPara = LYRICS_END;
418
419   m_indexEndPara--;
420 }
421
422
423 typedef struct
424 {
425   float  width;      // total screen width of all lyrics in this line
426   int    timediff;    // time difference between prev line ends and this line starts
427   bool  upper_start;  // true if this line started with a capital letter
428   int    offset_start;  // offset points to a 'new line' flag entry of the current line
429
430 } LyricTimingData;
431
432 void CKaraokeLyricsText::rescanLyrics()
433 {
434   // Rescan fixes the following things:
435   // - lyrics without spaces;
436   // - lyrics without paragraphs
437   std::vector<LyricTimingData> lyricdata;
438   unsigned int spaces = 0, syllables = 0, paragraph_lines = 0, max_lines_per_paragraph = 0;
439
440   // First get some statistics from the lyrics: number of paragraphs, number of spaces
441   // and time difference between one line ends and second starts
442   for ( unsigned int i = 0; i < m_lyrics.size(); i++ )
443   {
444     if ( m_lyrics[i].text.Find( " " ) != -1 )
445       spaces++;
446
447     if ( m_lyrics[i].flags & LYRICS_NEW_LINE )
448       paragraph_lines++;
449
450     if ( m_lyrics[i].flags & LYRICS_NEW_PARAGRAPH )
451     {
452       if ( max_lines_per_paragraph < paragraph_lines )
453         max_lines_per_paragraph = paragraph_lines;
454
455       paragraph_lines = 0;
456     }
457
458     syllables++;
459   }
460
461   // Second, add spaces if less than 5%, and rescan to gather more data.
462   bool add_spaces = (syllables && (spaces * 100 / syllables < 5)) ? true : false;
463   RESOLUTION res = g_graphicsContext.GetVideoResolution();
464   float maxWidth = (float) g_settings.m_ResInfo[res].Overscan.right - g_settings.m_ResInfo[res].Overscan.left;
465
466   CStdString line_text;
467   int prev_line_idx = -1;
468   int prev_line_timediff = -1;
469
470   for ( unsigned int i = 0; i < m_lyrics.size(); i++ )
471   {
472     if ( add_spaces )
473       m_lyrics[i].text += " ";
474
475     // We split the lyric when it is end of line, end of array, or current string is too long already
476     if ( i == (m_lyrics.size() - 1)
477     || (m_lyrics[i+1].flags & (LYRICS_NEW_LINE | LYRICS_NEW_PARAGRAPH)) != 0
478     || getStringWidth( line_text + m_lyrics[i].text ) >= maxWidth )
479     {
480       // End of line, or end of array. Add current string.
481       line_text += m_lyrics[i].text;
482
483       // Reparagraph if we're out of screen width
484       if ( getStringWidth( line_text ) >= maxWidth )
485         max_lines_per_paragraph = 0;
486
487       LyricTimingData ld;
488       ld.width = getStringWidth( line_text );
489       ld.timediff = prev_line_timediff;
490       ld.offset_start = prev_line_idx;
491
492       // This piece extracts the first character of a new string and makes it uppercase in Unicode way
493       CStdStringW temptext;
494       g_charsetConverter.utf8ToW( line_text, temptext );
495
496       // This is pretty ugly upper/lowercase for Russian unicode character set
497       if ( temptext[0] >= 0x410 && temptext[0] <= 0x44F )
498         ld.upper_start = temptext[0] <= 0x42F;
499       else
500       {
501         CStdString lower = m_lyrics[i].text;
502         lower.ToLower();
503         ld.upper_start = (m_lyrics[i].text == lower);
504       }
505
506       lyricdata.push_back( ld );
507
508       // Reset the params
509       line_text = "";
510       prev_line_idx = i + 1;
511       prev_line_timediff = (i == m_lyrics.size() - 1) ? -1 : m_lyrics[i+1].timing - m_lyrics[i].timing;
512     }
513     else
514     {
515       // Handle incorrect lyrics with no line feeds in the condition statement above
516       line_text += m_lyrics[i].text;
517     }
518   }
519
520   // Now see if we need to re-paragraph. Basically we reasonably need a paragraph
521   // to have no more than 8 lines
522   if ( max_lines_per_paragraph == 0 || max_lines_per_paragraph > 8 )
523   {
524     // Reparagraph
525     unsigned int paragraph_lines = 0;
526     float total_width = 0;
527
528     CLog::Log( LOGDEBUG, "CKaraokeLyricsText: lines need to be reparagraphed" );
529
530     for ( unsigned int i = 0; i < lyricdata.size(); i++ )
531     {
532       // Is this the first line?
533       if ( lyricdata[i].timediff == -1 )
534       {
535         total_width = lyricdata[i].width;
536         continue;
537       }
538
539       // Do we merge the current line with previous? We do it if:
540       // - there is a room on the screen for those lines combined
541       // - the time difference between line ends and new starts is less than 1.5 sec
542       // - the first character in the new line is not uppercase (i.e. new logic line)
543       if ( m_mergeLines && total_width + lyricdata[i].width < maxWidth && !lyricdata[i].upper_start && lyricdata[i].timediff < 15 )
544       {
545         // Merge
546         m_lyrics[ lyricdata[i].offset_start ].flags &= ~(LYRICS_NEW_LINE | LYRICS_NEW_PARAGRAPH);
547
548         // Since we merged the line, add the extra space. It will be removed later if not necessary.
549         m_lyrics[ lyricdata[i].offset_start ].text = " " + m_lyrics[ lyricdata[i].offset_start ].text;
550         total_width += lyricdata[i].width;
551
552 //        CLog::Log(LOGERROR, "Line merged; diff %d width %g, start %d, offset %d, max %g",
553 //              lyricdata[i].timediff, lyricdata[i].width, lyricdata[i].upper_start, lyricdata[i].offset_start, maxWidth );
554       }
555       else
556       {
557         // Do not merge; reset width and add counter
558         total_width = lyricdata[i].width;
559         paragraph_lines++;
560
561 //        CLog::Log(LOGERROR, "Line not merged; diff %d width %g, start %d, offset %d, max %g",
562 //              lyricdata[i].timediff, lyricdata[i].width, lyricdata[i].upper_start, lyricdata[i].offset_start, maxWidth );
563       }
564
565       // Set paragraph
566       if ( paragraph_lines > 3 )
567       {
568         m_lyrics[ lyricdata[i].offset_start ].flags &= ~LYRICS_NEW_LINE;
569         m_lyrics[ lyricdata[i].offset_start ].flags |= LYRICS_NEW_PARAGRAPH;
570         paragraph_lines = 0;
571         line_text = "";
572       }
573     }
574   }
575
576   // Prepare a new first lyric entry with song name and artist.
577   if ( m_songName.IsEmpty() )
578   {
579     m_songName = URIUtils::GetFileName( getSongFile() );
580     URIUtils::RemoveExtension( m_songName );
581   }
582
583   // Split the lyrics into per-character array
584   std::vector<Lyric> newlyrics;
585   bool title_entry = false;
586
587   if ( m_lyrics.size() > 0 && m_lyrics[0].timing >= 50 )
588   {
589     // Add a new title/artist entry
590     Lyric ltitle;
591     ltitle.flags = 0;
592     ltitle.timing = 0;
593     ltitle.text = m_songName;
594
595     if ( !m_artist.IsEmpty() )
596       ltitle.text += "[CR][CR]" + m_artist;
597
598     newlyrics.push_back( ltitle );
599     title_entry = true;
600   }
601
602   bool last_was_space = false;
603   bool invalid_timing_reported = false;
604   for ( unsigned int i = 0; i < m_lyrics.size(); i++ )
605   {
606     CStdStringW utf16;
607     g_charsetConverter.utf8ToW( m_lyrics[i].text, utf16 );
608
609     // Skip empty lyrics
610     if ( utf16.size() == 0 )
611       continue;
612
613     // Use default timing for the last note
614     unsigned int next_timing = m_lyrics[ i ].timing + m_delayAfter;
615
616     if ( i < (m_lyrics.size() - 1) )
617     {
618       // Set the lenght for the syllable  to the length of prev syllable if:
619       // - this is not the first lyric (as there is no prev otherwise)
620       // - this is the last lyric on this line (otherwise use next);
621       // - this is not the ONLY lyric on this line (otherwise the calculation is wrong)
622       // - lyrics size is the same as previous (currently removed).
623       if ( i > 0
624       && m_lyrics[ i + 1 ].flags & (LYRICS_NEW_LINE | LYRICS_NEW_PARAGRAPH)
625       && ! (m_lyrics[ i ].flags & (LYRICS_NEW_LINE | LYRICS_NEW_PARAGRAPH) ) )
626 //      && m_lyrics[ i ].text.size() == m_lyrics[ i -1 ].text.size() )
627         next_timing = m_lyrics[ i ].timing + (m_lyrics[ i ].timing - m_lyrics[ i -1 ].timing );
628
629       // Sanity check
630       if ( m_lyrics[ i+1 ].timing < m_lyrics[ i ].timing )
631       {
632         if ( !invalid_timing_reported )
633           CLog::Log( LOGERROR, "Karaoke lyrics normalizer: time went backward, enabling workaround" );
634
635         invalid_timing_reported = true;
636         m_lyrics[ i ].timing = m_lyrics[ i+1 ].timing;
637       }
638
639       if ( m_lyrics[ i+1 ].timing < next_timing )
640         next_timing = m_lyrics[ i+1 ].timing;
641     }
642
643     // Calculate how many 1/10 seconds we have per lyric character
644     double time_per_char = ((double) next_timing - m_lyrics[ i ].timing) / utf16.size();
645
646     // Convert to characters
647     for ( unsigned int j = 0; j < utf16.size(); j++ )
648     {
649       Lyric l;
650
651       // Copy flags only to the first character
652       if ( j == 0 )
653         l.flags = m_lyrics[i].flags;
654       else
655         l.flags = 0;
656       l.timing = (unsigned int) MathUtils::round_int( m_lyrics[ i ].timing + j * time_per_char );
657
658       g_charsetConverter.wToUTF8( utf16.Mid( j, 1 ), l.text );
659
660       if ( l.text == " " )
661       {
662         if ( last_was_space )
663           continue;
664
665         last_was_space = true;
666       }
667       else
668         last_was_space = false;
669
670       newlyrics.push_back( l );
671     }
672   }
673
674   m_lyrics = newlyrics;
675
676   // Set the NEW PARAGRAPH flag on the first real lyric entry since we changed it
677   if ( title_entry )
678     m_lyrics[1].flags |= LYRICS_NEW_PARAGRAPH;
679
680   saveLyrics();
681 }
682
683
684 float CKaraokeLyricsText::getStringWidth(const CStdString & text)
685 {
686   CStdStringW utf16;
687   vecText utf32;
688
689   g_charsetConverter.utf8ToW(text, utf16);
690
691   utf32.resize( utf16.size() );
692   for ( unsigned int i = 0; i < utf16.size(); i++ )
693     utf32[i] = utf16[i];
694
695   return m_karaokeFont->GetTextWidth(utf32);
696 }
697
698 void CKaraokeLyricsText::saveLyrics()
699 {
700   XFILE::CFile file;
701
702   CStdString out;
703
704   for ( unsigned int i = 0; i < m_lyrics.size(); i++ )
705   {
706     CStdString timing;
707     timing.Format( "%02d:%02d.%d", m_lyrics[i].timing / 600, (m_lyrics[i].timing % 600) / 10, (m_lyrics[i].timing % 10) );
708
709     if ( (m_lyrics[i].flags & LYRICS_NEW_PARAGRAPH) != 0 )
710       out += "\n\n";
711
712     if ( (m_lyrics[i].flags & LYRICS_NEW_LINE) != 0 )
713       out += "\n";
714
715     out += "[" + timing + "]" + m_lyrics[i].text;
716   }
717
718   out += "\n";
719
720   if ( !file.OpenForWrite( "special://temp/tmp.lrc", true ) )
721     return;
722
723   file.Write( out, out.size() );
724 }
725
726
727 bool CKaraokeLyricsText::HasBackground()
728 {
729   return false;
730 }
731
732 bool CKaraokeLyricsText::HasVideo()
733 {
734   return m_videoFile.IsEmpty() ? false : true;
735 }
736
737 void CKaraokeLyricsText::GetVideoParameters(CStdString & path, int64_t & offset)
738 {
739   path = m_videoFile;
740   offset = m_videoOffset;
741 }