2 * Copyright (C) 2005-2013 Team XBMC
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)
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.
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/>.
21 // C++ Implementation: karaokelyricstext
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"
46 // Must be synchronized with strings.xml and GUISettings.cpp!
47 static LyricColors gLyricColors[] =
49 // <string id="22040">white/green</string>
50 // First 0xFF is alpha!
51 { 0xFFDADADA, 0xFF00FF00, 0xFF000000 },
53 // <string id="22041">white/red</string>
54 { 0xFFDADADA, 0xFFFF0000, 0xFF000000 },
56 // <string id="22042">white/blue</string>
57 { 0xFFDADADA, 0xFF0000FF, 0xFF000000 },
59 // <string id="22043">black/white</string>
60 { 0xFF000000, 0xFFDADADA, 0xFFFFFFFF },
64 CKaraokeLyricsText::CKaraokeLyricsText()
71 int coloridx = g_guiSettings.GetInt("karaoke.fontcolors");
72 if ( coloridx < KARAOKE_COLOR_START || coloridx >= KARAOKE_COLOR_END )
75 m_colorLyrics = gLyricColors[coloridx].text;
76 m_colorLyricsOutline = gLyricColors[coloridx].outline;
77 m_colorSinging.Format( "%08X", gLyricColors[coloridx].active );
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
87 m_lyricsState = STATE_END_SONG;
91 CKaraokeLyricsText::~CKaraokeLyricsText()
95 void CKaraokeLyricsText::clearLyrics()
106 void CKaraokeLyricsText::addLyrics(const CStdString & text, unsigned int timing, unsigned int flags, unsigned int pitch)
110 if ( flags & LYRICS_CONVERT_UTF8 )
113 flags &= ~LYRICS_CONVERT_UTF8;
114 g_charsetConverter.unknownToUTF8(text, line.text);
122 line.timing = timing;
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 );
129 // 'New paragraph' includes new line as well
130 if ( line.flags & LYRICS_NEW_PARAGRAPH )
131 line.flags &= ~LYRICS_NEW_LINE;
133 m_lyrics.push_back( line );
137 bool CKaraokeLyricsText::InitGraphics()
139 if ( m_lyrics.empty() )
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 );
148 if ( !m_karaokeFont )
150 CLog::Log(LOGERROR, "CKaraokeLyricsText::PrepareGraphicsData - Unable to load subtitle font");
154 m_karaokeLayout = new CGUITextLayout( m_karaokeFont, true, 0, karaokeBorder );
155 m_preambleLayout = new CGUITextLayout( m_karaokeFont, true, 0, karaokeBorder );
157 if ( !m_karaokeLayout || !m_preambleLayout )
159 delete m_preambleLayout;
160 delete m_karaokeLayout;
161 m_karaokeLayout = m_preambleLayout = 0;
163 CLog::Log(LOGERROR, "CKaraokeLyricsText::PrepareGraphicsData - cannot create layout");
171 // Generate next paragraph
174 m_lyricsState = STATE_WAITING;
179 void CKaraokeLyricsText::Shutdown()
181 CKaraokeLyrics::Shutdown();
183 delete m_preambleLayout;
184 m_preambleLayout = 0;
186 if ( m_karaokeLayout )
188 g_fontManager.Unload("__karaoke__");
189 g_fontManager.Unload("__karaokeborder__");
190 delete m_karaokeLayout;
191 m_karaokeLayout = NULL;
194 m_lyricsState = STATE_END_SONG;
198 void CKaraokeLyricsText::Render()
200 if ( !m_karaokeLayout )
203 // Get the current song timing
204 unsigned int songTime = (unsigned int) MathUtils::round_int( (getSongTime() * 10) );
206 bool updatePreamble = false;
207 bool updateText = false;
209 // No returns in switch if anything needs to be drawn! Just break!
210 switch ( m_lyricsState )
212 // the next paragraph lyrics are not shown yet. Screen is clear.
213 // m_index points to the first entry.
215 if ( songTime + m_showLyricsBeforeStart < m_lyrics[ m_index ].timing )
218 // Is it time to play already?
219 if ( songTime >= m_lyrics[ m_index ].timing )
221 m_lyricsState = STATE_PLAYING_PARAGRAPH;
225 m_lyricsState = STATE_PREAMBLE;
226 m_lastPreambleUpdate = songTime;
232 // the next paragraph lyrics are shown, but the paragraph hasn't start yet.
233 // Using m_lastPreambleUpdate, we redraw the marker each second.
235 if ( songTime < m_lyrics[ m_index ].timing )
237 // Time to redraw preamble?
238 if ( songTime + m_showPreambleBeforeStart >= m_lyrics[ m_index ].timing )
240 if ( songTime - m_lastPreambleUpdate >= 10 )
242 // Fall through out of switch() to redraw
243 m_lastPreambleUpdate = songTime;
244 updatePreamble = true;
251 m_lyricsState = STATE_PLAYING_PARAGRAPH;
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 )
263 if ( m_index > m_indexEndPara )
264 m_lyricsState = STATE_END_PARAGRAPH;
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:
275 unsigned int paraEnd = m_lyrics[ m_indexEndPara ].timing + m_delayAfter;
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 )
280 if ( m_lyrics[ m_indexNextPara ].timing > m_showLyricsBeforeStart )
281 paraEnd = m_lyrics[ m_indexNextPara ].timing - m_showLyricsBeforeStart;
286 if ( songTime >= paraEnd )
288 // Is the song ended?
289 if ( m_indexNextPara != LYRICS_END )
291 // Are we still waiting?
292 if ( songTime >= m_lyrics[ m_indexNextPara ].timing )
293 m_lyricsState = STATE_PLAYING_PARAGRAPH;
295 m_lyricsState = STATE_WAITING;
297 // Get next paragraph
303 m_lyricsState = STATE_END_SONG;
311 // the song is completed, there are no more lyrics to show. This state is finita la comedia.
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;
320 // We must only fall through for STATE_DRAW_SYLLABLE or STATE_PREAMBLE
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 = "";
327 // Draw the current paragraph test if needed
328 if ( songTime + m_showLyricsBeforeStart >= m_lyrics[ m_indexStartPara ].timing )
330 for ( unsigned int i = m_indexStartPara; i <= m_indexEndPara; i++ )
332 if ( m_lyrics[i].flags & LYRICS_NEW_LINE )
333 m_currentLyrics += "[CR]";
335 if ( i == m_indexStartPara && songTime >= m_lyrics[ m_indexStartPara ].timing )
338 m_currentLyrics += "[COLOR " + m_colorSinging + "]";
341 if ( songTime < m_lyrics[ i ].timing && color_used )
344 m_currentLyrics += "[/COLOR]";
347 m_currentLyrics += m_lyrics[i].text;
351 m_currentLyrics += "[/COLOR]";
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());
358 m_karaokeLayout->Update(m_currentLyrics, maxWidth * 0.9f);
362 if ( updatePreamble )
364 m_currentPreamble = "";
366 // Get number of seconds left to the song start
367 if ( m_lyrics[ m_indexStartPara ].timing >= songTime )
369 unsigned int seconds = (m_lyrics[ m_indexStartPara ].timing - songTime) / 10;
371 while ( seconds-- > 0 )
372 m_currentPreamble += "- ";
375 m_preambleLayout->Update( m_currentPreamble, maxWidth * 0.9f );
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;
382 float textWidth, textHeight;
383 m_karaokeLayout->GetTextExtent(textWidth, textHeight);
384 m_karaokeLayout->RenderOutline(x, y, 0, m_colorLyricsOutline, XBFONT_CENTER_X, maxWidth);
386 if ( !m_currentPreamble.IsEmpty() )
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);
395 void CKaraokeLyricsText::nextParagraph()
397 if ( m_indexNextPara == LYRICS_END )
400 bool new_para_found = false;
401 m_indexStartPara = m_index = m_indexNextPara;
403 for ( m_indexEndPara = m_index + 1; m_indexEndPara < m_lyrics.size(); m_indexEndPara++ )
405 if ( m_lyrics[ m_indexEndPara ].flags & LYRICS_NEW_PARAGRAPH
406 || ( m_lyrics[ m_indexEndPara ].timing - m_lyrics[ m_indexEndPara - 1 ].timing ) > m_paragraphBreakTime )
408 new_para_found = true;
413 // Is this the end of array?
414 if ( new_para_found )
415 m_indexNextPara = m_indexEndPara;
417 m_indexNextPara = LYRICS_END;
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
432 void CKaraokeLyricsText::rescanLyrics()
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;
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++ )
444 if ( m_lyrics[i].text.Find( " " ) != -1 )
447 if ( m_lyrics[i].flags & LYRICS_NEW_LINE )
450 if ( m_lyrics[i].flags & LYRICS_NEW_PARAGRAPH )
452 if ( max_lines_per_paragraph < paragraph_lines )
453 max_lines_per_paragraph = paragraph_lines;
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;
466 CStdString line_text;
467 int prev_line_idx = -1;
468 int prev_line_timediff = -1;
470 for ( unsigned int i = 0; i < m_lyrics.size(); i++ )
473 m_lyrics[i].text += " ";
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 )
480 // End of line, or end of array. Add current string.
481 line_text += m_lyrics[i].text;
483 // Reparagraph if we're out of screen width
484 if ( getStringWidth( line_text ) >= maxWidth )
485 max_lines_per_paragraph = 0;
488 ld.width = getStringWidth( line_text );
489 ld.timediff = prev_line_timediff;
490 ld.offset_start = prev_line_idx;
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 );
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;
501 CStdString lower = m_lyrics[i].text;
503 ld.upper_start = (m_lyrics[i].text == lower);
506 lyricdata.push_back( ld );
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;
515 // Handle incorrect lyrics with no line feeds in the condition statement above
516 line_text += m_lyrics[i].text;
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 )
525 unsigned int paragraph_lines = 0;
526 float total_width = 0;
528 CLog::Log( LOGDEBUG, "CKaraokeLyricsText: lines need to be reparagraphed" );
530 for ( unsigned int i = 0; i < lyricdata.size(); i++ )
532 // Is this the first line?
533 if ( lyricdata[i].timediff == -1 )
535 total_width = lyricdata[i].width;
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 )
546 m_lyrics[ lyricdata[i].offset_start ].flags &= ~(LYRICS_NEW_LINE | LYRICS_NEW_PARAGRAPH);
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;
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 );
557 // Do not merge; reset width and add counter
558 total_width = lyricdata[i].width;
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 );
566 if ( paragraph_lines > 3 )
568 m_lyrics[ lyricdata[i].offset_start ].flags &= ~LYRICS_NEW_LINE;
569 m_lyrics[ lyricdata[i].offset_start ].flags |= LYRICS_NEW_PARAGRAPH;
576 // Prepare a new first lyric entry with song name and artist.
577 if ( m_songName.IsEmpty() )
579 m_songName = URIUtils::GetFileName( getSongFile() );
580 URIUtils::RemoveExtension( m_songName );
583 // Split the lyrics into per-character array
584 std::vector<Lyric> newlyrics;
585 bool title_entry = false;
587 if ( m_lyrics.size() > 0 && m_lyrics[0].timing >= 50 )
589 // Add a new title/artist entry
593 ltitle.text = m_songName;
595 if ( !m_artist.IsEmpty() )
596 ltitle.text += "[CR][CR]" + m_artist;
598 newlyrics.push_back( ltitle );
602 bool last_was_space = false;
603 bool invalid_timing_reported = false;
604 for ( unsigned int i = 0; i < m_lyrics.size(); i++ )
607 g_charsetConverter.utf8ToW( m_lyrics[i].text, utf16 );
610 if ( utf16.size() == 0 )
613 // Use default timing for the last note
614 unsigned int next_timing = m_lyrics[ i ].timing + m_delayAfter;
616 if ( i < (m_lyrics.size() - 1) )
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).
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 );
630 if ( m_lyrics[ i+1 ].timing < m_lyrics[ i ].timing )
632 if ( !invalid_timing_reported )
633 CLog::Log( LOGERROR, "Karaoke lyrics normalizer: time went backward, enabling workaround" );
635 invalid_timing_reported = true;
636 m_lyrics[ i ].timing = m_lyrics[ i+1 ].timing;
639 if ( m_lyrics[ i+1 ].timing < next_timing )
640 next_timing = m_lyrics[ i+1 ].timing;
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();
646 // Convert to characters
647 for ( unsigned int j = 0; j < utf16.size(); j++ )
651 // Copy flags only to the first character
653 l.flags = m_lyrics[i].flags;
656 l.timing = (unsigned int) MathUtils::round_int( m_lyrics[ i ].timing + j * time_per_char );
658 g_charsetConverter.wToUTF8( utf16.Mid( j, 1 ), l.text );
662 if ( last_was_space )
665 last_was_space = true;
668 last_was_space = false;
670 newlyrics.push_back( l );
674 m_lyrics = newlyrics;
676 // Set the NEW PARAGRAPH flag on the first real lyric entry since we changed it
678 m_lyrics[1].flags |= LYRICS_NEW_PARAGRAPH;
684 float CKaraokeLyricsText::getStringWidth(const CStdString & text)
689 g_charsetConverter.utf8ToW(text, utf16);
691 utf32.resize( utf16.size() );
692 for ( unsigned int i = 0; i < utf16.size(); i++ )
695 return m_karaokeFont->GetTextWidth(utf32);
698 void CKaraokeLyricsText::saveLyrics()
704 for ( unsigned int i = 0; i < m_lyrics.size(); i++ )
707 timing.Format( "%02d:%02d.%d", m_lyrics[i].timing / 600, (m_lyrics[i].timing % 600) / 10, (m_lyrics[i].timing % 10) );
709 if ( (m_lyrics[i].flags & LYRICS_NEW_PARAGRAPH) != 0 )
712 if ( (m_lyrics[i].flags & LYRICS_NEW_LINE) != 0 )
715 out += "[" + timing + "]" + m_lyrics[i].text;
720 if ( !file.OpenForWrite( "special://temp/tmp.lrc", true ) )
723 file.Write( out, out.size() );
727 bool CKaraokeLyricsText::HasBackground()
732 bool CKaraokeLyricsText::HasVideo()
734 return m_videoFile.IsEmpty() ? false : true;
737 void CKaraokeLyricsText::GetVideoParameters(CStdString & path, int64_t & offset)
740 offset = m_videoOffset;