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/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"
47 // Must be synchronized with strings.xml and GUISettings.cpp!
48 static LyricColors gLyricColors[] =
50 // <string id="22040">white/green</string>
51 // First 0xFF is alpha!
52 { 0xFFDADADA, 0xFF00FF00, 0xFF000000 },
54 // <string id="22041">white/red</string>
55 { 0xFFDADADA, 0xFFFF0000, 0xFF000000 },
57 // <string id="22042">white/blue</string>
58 { 0xFFDADADA, 0xFF0000FF, 0xFF000000 },
60 // <string id="22043">black/white</string>
61 { 0xFF000000, 0xFFDADADA, 0xFFFFFFFF },
65 CKaraokeLyricsText::CKaraokeLyricsText()
72 int coloridx = CSettings::Get().GetInt("karaoke.fontcolors");
73 if ( coloridx < KARAOKE_COLOR_START || coloridx >= KARAOKE_COLOR_END )
76 m_colorLyrics = gLyricColors[coloridx].text;
77 m_colorLyricsOutline = gLyricColors[coloridx].outline;
78 m_colorSinging = StringUtils::Format("%08X", gLyricColors[coloridx].active);
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
88 m_lyricsState = STATE_END_SONG;
92 CKaraokeLyricsText::~CKaraokeLyricsText()
96 void CKaraokeLyricsText::clearLyrics()
107 void CKaraokeLyricsText::addLyrics(const CStdString & text, unsigned int timing, unsigned int flags, unsigned int pitch)
111 if ( flags & LYRICS_CONVERT_UTF8 )
114 flags &= ~LYRICS_CONVERT_UTF8;
115 g_charsetConverter.unknownToUTF8(text, line.text);
123 line.timing = timing;
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 );
130 // 'New paragraph' includes new line as well
131 if ( line.flags & LYRICS_NEW_PARAGRAPH )
132 line.flags &= ~LYRICS_NEW_LINE;
134 m_lyrics.push_back( line );
138 bool CKaraokeLyricsText::InitGraphics()
140 if ( m_lyrics.empty() )
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 );
151 if ( !m_karaokeFont )
153 CLog::Log(LOGERROR, "CKaraokeLyricsText::PrepareGraphicsData - Unable to load subtitle font");
157 m_karaokeLayout = new CGUITextLayout( m_karaokeFont, true, 0, karaokeBorder );
158 m_preambleLayout = new CGUITextLayout( m_karaokeFont, true, 0, karaokeBorder );
160 if ( !m_karaokeLayout || !m_preambleLayout )
162 delete m_preambleLayout;
163 delete m_karaokeLayout;
164 m_karaokeLayout = m_preambleLayout = 0;
166 CLog::Log(LOGERROR, "CKaraokeLyricsText::PrepareGraphicsData - cannot create layout");
174 // Generate next paragraph
177 m_lyricsState = STATE_WAITING;
182 void CKaraokeLyricsText::Shutdown()
184 CKaraokeLyrics::Shutdown();
186 delete m_preambleLayout;
187 m_preambleLayout = 0;
189 if ( m_karaokeLayout )
191 g_fontManager.Unload("__karaoke__");
192 g_fontManager.Unload("__karaokeborder__");
193 delete m_karaokeLayout;
194 m_karaokeLayout = NULL;
197 m_lyricsState = STATE_END_SONG;
201 void CKaraokeLyricsText::Render()
203 if ( !m_karaokeLayout )
206 // Get the current song timing
207 unsigned int songTime = (unsigned int) MathUtils::round_int( (getSongTime() * 10) );
209 bool updatePreamble = false;
210 bool updateText = false;
212 // No returns in switch if anything needs to be drawn! Just break!
213 switch ( m_lyricsState )
215 // the next paragraph lyrics are not shown yet. Screen is clear.
216 // m_index points to the first entry.
218 if ( songTime + m_showLyricsBeforeStart < m_lyrics[ m_index ].timing )
221 // Is it time to play already?
222 if ( songTime >= m_lyrics[ m_index ].timing )
224 m_lyricsState = STATE_PLAYING_PARAGRAPH;
228 m_lyricsState = STATE_PREAMBLE;
229 m_lastPreambleUpdate = songTime;
235 // the next paragraph lyrics are shown, but the paragraph hasn't start yet.
236 // Using m_lastPreambleUpdate, we redraw the marker each second.
238 if ( songTime < m_lyrics[ m_index ].timing )
240 // Time to redraw preamble?
241 if ( songTime + m_showPreambleBeforeStart >= m_lyrics[ m_index ].timing )
243 if ( songTime - m_lastPreambleUpdate >= 10 )
245 // Fall through out of switch() to redraw
246 m_lastPreambleUpdate = songTime;
247 updatePreamble = true;
254 m_lyricsState = STATE_PLAYING_PARAGRAPH;
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 )
266 if ( m_index > m_indexEndPara )
267 m_lyricsState = STATE_END_PARAGRAPH;
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:
278 unsigned int paraEnd = m_lyrics[ m_indexEndPara ].timing + m_delayAfter;
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 )
283 if ( m_lyrics[ m_indexNextPara ].timing > m_showLyricsBeforeStart )
284 paraEnd = m_lyrics[ m_indexNextPara ].timing - m_showLyricsBeforeStart;
289 if ( songTime >= paraEnd )
291 // Is the song ended?
292 if ( m_indexNextPara != LYRICS_END )
294 // Are we still waiting?
295 if ( songTime >= m_lyrics[ m_indexNextPara ].timing )
296 m_lyricsState = STATE_PLAYING_PARAGRAPH;
298 m_lyricsState = STATE_WAITING;
300 // Get next paragraph
306 m_lyricsState = STATE_END_SONG;
314 // the song is completed, there are no more lyrics to show. This state is finita la comedia.
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;
323 // We must only fall through for STATE_DRAW_SYLLABLE or STATE_PREAMBLE
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 = "";
330 // Draw the current paragraph test if needed
331 if ( songTime + m_showLyricsBeforeStart >= m_lyrics[ m_indexStartPara ].timing )
333 for ( unsigned int i = m_indexStartPara; i <= m_indexEndPara; i++ )
335 if ( m_lyrics[i].flags & LYRICS_NEW_LINE )
336 m_currentLyrics += "[CR]";
338 if ( i == m_indexStartPara && songTime >= m_lyrics[ m_indexStartPara ].timing )
341 m_currentLyrics += "[COLOR " + m_colorSinging + "]";
344 if ( songTime < m_lyrics[ i ].timing && color_used )
347 m_currentLyrics += "[/COLOR]";
350 m_currentLyrics += m_lyrics[i].text;
354 m_currentLyrics += "[/COLOR]";
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());
361 m_karaokeLayout->Update(m_currentLyrics, maxWidth * 0.9f);
365 if ( updatePreamble )
367 m_currentPreamble = "";
369 // Get number of seconds left to the song start
370 if ( m_lyrics[ m_indexStartPara ].timing >= songTime )
372 unsigned int seconds = (m_lyrics[ m_indexStartPara ].timing - songTime) / 10;
374 while ( seconds-- > 0 )
375 m_currentPreamble += "- ";
378 m_preambleLayout->Update( m_currentPreamble, maxWidth * 0.9f );
381 float x = maxWidth * 0.5f + info.Overscan.left;
382 float y = (float)info.Overscan.top +
383 (info.Overscan.bottom - info.Overscan.top) / 8;
385 float textWidth, textHeight;
386 m_karaokeLayout->GetTextExtent(textWidth, textHeight);
387 m_karaokeLayout->RenderOutline(x, y, 0, m_colorLyricsOutline, XBFONT_CENTER_X, maxWidth);
389 if ( !m_currentPreamble.IsEmpty() )
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);
398 void CKaraokeLyricsText::nextParagraph()
400 if ( m_indexNextPara == LYRICS_END )
403 bool new_para_found = false;
404 m_indexStartPara = m_index = m_indexNextPara;
406 for ( m_indexEndPara = m_index + 1; m_indexEndPara < m_lyrics.size(); m_indexEndPara++ )
408 if ( m_lyrics[ m_indexEndPara ].flags & LYRICS_NEW_PARAGRAPH
409 || ( m_lyrics[ m_indexEndPara ].timing - m_lyrics[ m_indexEndPara - 1 ].timing ) > m_paragraphBreakTime )
411 new_para_found = true;
416 // Is this the end of array?
417 if ( new_para_found )
418 m_indexNextPara = m_indexEndPara;
420 m_indexNextPara = LYRICS_END;
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
435 void CKaraokeLyricsText::rescanLyrics()
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;
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++ )
447 if ( m_lyrics[i].text.Find( " " ) != -1 )
450 if ( m_lyrics[i].flags & LYRICS_NEW_LINE )
453 if ( m_lyrics[i].flags & LYRICS_NEW_PARAGRAPH )
455 if ( max_lines_per_paragraph < paragraph_lines )
456 max_lines_per_paragraph = paragraph_lines;
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;
469 CStdString line_text;
470 int prev_line_idx = -1;
471 int prev_line_timediff = -1;
473 for ( unsigned int i = 0; i < m_lyrics.size(); i++ )
476 m_lyrics[i].text += " ";
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 )
483 // End of line, or end of array. Add current string.
484 line_text += m_lyrics[i].text;
486 // Reparagraph if we're out of screen width
487 if ( getStringWidth( line_text ) >= maxWidth )
488 max_lines_per_paragraph = 0;
491 ld.width = getStringWidth( line_text );
492 ld.timediff = prev_line_timediff;
493 ld.offset_start = prev_line_idx;
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 );
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;
504 CStdString lower = m_lyrics[i].text;
506 ld.upper_start = (m_lyrics[i].text == lower);
509 lyricdata.push_back( ld );
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;
518 // Handle incorrect lyrics with no line feeds in the condition statement above
519 line_text += m_lyrics[i].text;
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 )
528 unsigned int paragraph_lines = 0;
529 float total_width = 0;
531 CLog::Log( LOGDEBUG, "CKaraokeLyricsText: lines need to be reparagraphed" );
533 for ( unsigned int i = 0; i < lyricdata.size(); i++ )
535 // Is this the first line?
536 if ( lyricdata[i].timediff == -1 )
538 total_width = lyricdata[i].width;
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 )
549 m_lyrics[ lyricdata[i].offset_start ].flags &= ~(LYRICS_NEW_LINE | LYRICS_NEW_PARAGRAPH);
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;
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 );
560 // Do not merge; reset width and add counter
561 total_width = lyricdata[i].width;
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 );
569 if ( paragraph_lines > 3 )
571 m_lyrics[ lyricdata[i].offset_start ].flags &= ~LYRICS_NEW_LINE;
572 m_lyrics[ lyricdata[i].offset_start ].flags |= LYRICS_NEW_PARAGRAPH;
579 // Prepare a new first lyric entry with song name and artist.
580 if ( m_songName.IsEmpty() )
582 m_songName = URIUtils::GetFileName( getSongFile() );
583 URIUtils::RemoveExtension( m_songName );
586 // Split the lyrics into per-character array
587 std::vector<Lyric> newlyrics;
588 bool title_entry = false;
590 if ( m_lyrics.size() > 0 && m_lyrics[0].timing >= 50 )
592 // Add a new title/artist entry
596 ltitle.text = m_songName;
598 if ( !m_artist.IsEmpty() )
599 ltitle.text += "[CR][CR]" + m_artist;
601 newlyrics.push_back( ltitle );
605 bool last_was_space = false;
606 bool invalid_timing_reported = false;
607 for ( unsigned int i = 0; i < m_lyrics.size(); i++ )
610 g_charsetConverter.utf8ToW( m_lyrics[i].text, utf16 );
613 if ( utf16.size() == 0 )
616 // Use default timing for the last note
617 unsigned int next_timing = m_lyrics[ i ].timing + m_delayAfter;
619 if ( i < (m_lyrics.size() - 1) )
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).
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 );
633 if ( m_lyrics[ i+1 ].timing < m_lyrics[ i ].timing )
635 if ( !invalid_timing_reported )
636 CLog::Log( LOGERROR, "Karaoke lyrics normalizer: time went backward, enabling workaround" );
638 invalid_timing_reported = true;
639 m_lyrics[ i ].timing = m_lyrics[ i+1 ].timing;
642 if ( m_lyrics[ i+1 ].timing < next_timing )
643 next_timing = m_lyrics[ i+1 ].timing;
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();
649 // Convert to characters
650 for ( unsigned int j = 0; j < utf16.size(); j++ )
654 // Copy flags only to the first character
656 l.flags = m_lyrics[i].flags;
659 l.timing = (unsigned int) MathUtils::round_int( m_lyrics[ i ].timing + j * time_per_char );
661 g_charsetConverter.wToUTF8( utf16.Mid( j, 1 ), l.text );
665 if ( last_was_space )
668 last_was_space = true;
671 last_was_space = false;
673 newlyrics.push_back( l );
677 m_lyrics = newlyrics;
679 // Set the NEW PARAGRAPH flag on the first real lyric entry since we changed it
681 m_lyrics[1].flags |= LYRICS_NEW_PARAGRAPH;
687 float CKaraokeLyricsText::getStringWidth(const CStdString & text)
692 g_charsetConverter.utf8ToW(text, utf16);
694 utf32.resize( utf16.size() );
695 for ( unsigned int i = 0; i < utf16.size(); i++ )
698 return m_karaokeFont->GetTextWidth(utf32);
701 void CKaraokeLyricsText::saveLyrics()
707 for ( unsigned int i = 0; i < m_lyrics.size(); i++ )
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));
714 if ( (m_lyrics[i].flags & LYRICS_NEW_PARAGRAPH) != 0 )
717 if ( (m_lyrics[i].flags & LYRICS_NEW_LINE) != 0 )
720 out += "[" + timing + "]" + m_lyrics[i].text;
725 if ( !file.OpenForWrite( "special://temp/tmp.lrc", true ) )
728 file.Write( out, out.size() );
732 bool CKaraokeLyricsText::HasBackground()
737 bool CKaraokeLyricsText::HasVideo()
739 return m_videoFile.IsEmpty() ? false : true;
742 void CKaraokeLyricsText::GetVideoParameters(CStdString & path, int64_t & offset)
745 offset = m_videoOffset;