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