aboutsummaryrefslogtreecommitdiff
path: root/src/music/karaoke/karaokelyricstextlrc.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/music/karaoke/karaokelyricstextlrc.cpp')
-rw-r--r--src/music/karaoke/karaokelyricstextlrc.cpp542
1 files changed, 542 insertions, 0 deletions
diff --git a/src/music/karaoke/karaokelyricstextlrc.cpp b/src/music/karaoke/karaokelyricstextlrc.cpp
new file mode 100644
index 0000000000..ba1780434c
--- /dev/null
+++ b/src/music/karaoke/karaokelyricstextlrc.cpp
@@ -0,0 +1,542 @@
+/*
+ * Copyright (C) 2005-2013 Team XBMC
+ * http://xbmc.org
+ *
+ * This Program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This Program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with XBMC; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// C++ Implementation: karaokelyricstextlrc
+
+#include <math.h>
+
+#include "filesystem/File.h"
+#include "settings/AdvancedSettings.h"
+#include "utils/MathUtils.h"
+#include "utils/log.h"
+#include "utils/URIUtils.h"
+
+#include "karaokelyricstextlrc.h"
+
+enum ParserState
+{
+ PARSER_INIT, // looking for time
+ PARSER_IN_TIME, // processing time
+ PARSER_IN_LYRICS // processing lyrics
+};
+
+// Used in multi-time lyric loader
+typedef struct
+{
+ CStdString text;
+ unsigned int timing;
+ unsigned int flags;
+} MtLyric;
+
+CKaraokeLyricsTextLRC::CKaraokeLyricsTextLRC( const CStdString & lyricsFile )
+ : CKaraokeLyricsText()
+{
+ m_lyricsFile = lyricsFile;
+}
+
+
+CKaraokeLyricsTextLRC::~CKaraokeLyricsTextLRC()
+{
+}
+
+bool CKaraokeLyricsTextLRC::Load()
+{
+ XFILE::CFile file;
+
+ // Clear the lyrics array
+ clearLyrics();
+
+ XFILE::auto_buffer buf;
+ if (file.LoadFile(m_lyricsFile, buf) <= 0)
+ {
+ CLog::Log(LOGERROR, "%s: can't load \"%s\" file", __FUNCTION__, m_lyricsFile.c_str());
+ return false;
+ }
+
+ file.Close();
+
+ // Parse the correction value
+ int timing_correction = MathUtils::round_int( g_advancedSettings.m_karaokeSyncDelayLRC * 10 );
+
+ unsigned int offset = 0;
+
+ CStdString songfilename = getSongFile();
+
+ // Skip windoze UTF8 file prefix, if any, and reject UTF16 files
+ if (buf.size() > 3)
+ {
+ if ((unsigned char)buf.get()[0] == 0xFF && (unsigned char)buf.get()[1] == 0xFE)
+ {
+ CLog::Log( LOGERROR, "LRC lyric loader: lyrics file is in UTF16 encoding, must be in UTF8" );
+ return false;
+ }
+
+ // UTF8 prefix added by some windoze apps
+ if ((unsigned char)buf.get()[0] == 0xEF && (unsigned char)buf.get()[1] == 0xBB && (unsigned char)buf.get()[2] == 0xBF)
+ offset = 3;
+ }
+
+ if (checkMultiTime(buf.get() + offset, buf.size() - offset))
+ return ParserMultiTime(buf.get() + offset, buf.size() - offset, timing_correction);
+ else
+ return ParserNormal(buf.get() + offset, buf.size() - offset, timing_correction);
+}
+
+bool CKaraokeLyricsTextLRC::checkMultiTime(char *lyricData, unsigned int lyricSize)
+{
+ // return true only when find lines like:
+ // [02:24][01:40][00:51][00:05]I'm a big big girl
+ // but not like:
+ // [00:01.10]I [00:01.09]just [00:01.50]call
+ bool inTime = false;
+ bool newLine = true;
+ bool maybeMultiTime = false;
+ unsigned int i = 0;
+ for ( char * p = lyricData; i < lyricSize; i++, p++ )
+ {
+ if (inTime)
+ {
+ if (*p == ']')
+ inTime = false;
+ }
+ else
+ {
+ if (*p == '[')
+ {
+ inTime = true;
+ if (newLine)
+ {
+ newLine = false;
+ }
+ else
+ {
+ if (*(p - 1) != ']')
+ return false;
+ else
+ maybeMultiTime = true;
+ }
+ }
+ if (*p == '\n')
+ newLine = true;
+ }
+ }
+ return maybeMultiTime;
+}
+
+bool CKaraokeLyricsTextLRC::ParserNormal(char *lyricData, unsigned int lyricSize, int timing_correction)
+{
+ CLog::Log( LOGDEBUG, "LRC lyric loader: parser normal lyrics file" );
+ //
+ // A simple state machine to parse the file
+ //
+ ParserState state = PARSER_INIT;
+ unsigned int state_offset = 0;
+ unsigned int lyric_flags = 0;
+ int lyric_time = -1;
+ int start_offset = 0;
+ unsigned int offset = 0;
+
+ for ( char * p = lyricData; offset < lyricSize; offset++, p++ )
+ {
+ // Skip \r
+ if ( *p == 0x0D )
+ continue;
+
+ if ( state == PARSER_IN_LYRICS )
+ {
+ // Lyrics are terminated either by \n or by [
+ if ( *p == '\n' || *p == '[' || *p == '<' )
+ {
+ // Time must be there
+ if ( lyric_time == -1 )
+ {
+ CLog::Log( LOGERROR, "LRC lyric loader: lyrics file has no time before lyrics" );
+ return false;
+ }
+
+ // Add existing lyrics
+ char current = *p;
+ CStdString text;
+
+ if ( offset > state_offset )
+ {
+ // null-terminate string, we saved current char anyway
+ *p = '\0';
+ text = &lyricData[0] + state_offset;
+ }
+ else
+ text = " "; // add a single space for empty lyric
+
+ // If this was end of line, set the flags accordingly
+ if ( current == '\n' )
+ {
+ // Add space after the trailing lyric in lrc
+ text += " ";
+ addLyrics( text, lyric_time, lyric_flags | LYRICS_CONVERT_UTF8 );
+ state_offset = -1;
+ lyric_flags = CKaraokeLyricsText::LYRICS_NEW_LINE;
+ state = PARSER_INIT;
+ }
+ else
+ {
+ // No conversion needed as the file should be in UTF8 already
+ addLyrics( text, lyric_time, lyric_flags | LYRICS_CONVERT_UTF8 );
+ lyric_flags = 0;
+ state_offset = offset + 1;
+ state = PARSER_IN_TIME;
+ }
+
+ lyric_time = -1;
+ }
+ }
+ else if ( state == PARSER_IN_TIME )
+ {
+ // Time is terminated by ] or >
+ if ( *p == ']' || *p == '>' )
+ {
+ int mins, secs, htenths, ltenths = 0;
+
+ if ( offset == state_offset )
+ {
+ CLog::Log( LOGERROR, "LRC lyric loader: empty time" );
+ return false; // [] - empty time
+ }
+
+ // null-terminate string
+ char * timestr = &lyricData[0] + state_offset;
+ *p = '\0';
+
+ // Now check if this is time field or info tag. Info tags are like [ar:Pink Floyd]
+ char * fieldptr = strchr( timestr, ':' );
+ if ( timestr[0] >= 'a' && timestr[0] <= 'z' && timestr[1] >= 'a' && timestr[1] <= 'z' && fieldptr )
+ {
+ // Null-terminate the field name and switch to the field value
+ *fieldptr = '\0';
+ fieldptr++;
+
+ while ( isspace( *fieldptr ) )
+ fieldptr++;
+
+ // Check the info field
+ if ( !strcmp( timestr, "ar" ) )
+ m_artist += fieldptr;
+ else if ( !strcmp( timestr, "sr" ) )
+ {
+ // m_artist += "[CR]" + CStdString( fieldptr ); // Add source to the artist name as a separate line
+ }
+ else if ( !strcmp( timestr, "ti" ) )
+ m_songName = fieldptr;
+ else if ( !strcmp( timestr, "offset" ) )
+ {
+ if ( sscanf( fieldptr, "%d", &start_offset ) != 1 )
+ {
+ CLog::Log( LOGERROR, "LRC lyric loader: invalid [offset:] value '%s'", fieldptr );
+ return false; // [] - empty time
+ }
+
+ // Offset is in milliseconds; convert to 1/10 seconds
+ start_offset /= 100;
+ }
+
+ state_offset = -1;
+ state = PARSER_INIT;
+ continue;
+ }
+ else if ( sscanf( timestr, "%d:%d.%1d%1d", &mins, &secs, &htenths, &ltenths ) == 4 )
+ lyric_time = mins * 600 + secs * 10 + htenths + MathUtils::round_int( ltenths / 10 );
+ else if ( sscanf( timestr, "%d:%d.%1d", &mins, &secs, &htenths ) == 3 )
+ lyric_time = mins * 600 + secs * 10 + htenths;
+ else if ( sscanf( timestr, "%d:%d", &mins, &secs ) == 2 )
+ lyric_time = mins * 600 + secs * 10;
+ else
+ {
+ // bad time
+ CLog::Log( LOGERROR, "LRC lyric loader: lyrics file has no proper time field: '%s'", timestr );
+ return false;
+ }
+
+ // Correct timing if necessary
+ lyric_time += start_offset;
+ lyric_time += timing_correction;
+
+ if ( lyric_time < 0 )
+ lyric_time = 0;
+
+ // Set to next char
+ state_offset = offset + 1;
+ state = PARSER_IN_LYRICS;
+ }
+ }
+ else if ( state == PARSER_INIT )
+ {
+ // Ignore spaces
+ if ( *p == ' ' || *p == '\t' )
+ continue;
+
+ // We're looking for [ or <
+ if ( *p == '[' || *p == '<' )
+ {
+ // Set to next char
+ state_offset = offset + 1;
+ state = PARSER_IN_TIME;
+ lyric_time = -1;
+ }
+ else if ( *p == '\n' )
+ {
+ // If we get a newline and we're not paragraph, set it
+ if ( lyric_flags & CKaraokeLyricsText::LYRICS_NEW_LINE )
+ lyric_flags = CKaraokeLyricsText::LYRICS_NEW_PARAGRAPH;
+ }
+ else
+ {
+ // Everything else is error
+ CLog::Log( LOGERROR, "LRC lyric loader: lyrics file does not start from time" );
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+bool CKaraokeLyricsTextLRC::ParserMultiTime(char *lyricData, unsigned int lyricSize, int timing_correction)
+{
+ CLog::Log( LOGDEBUG, "LRC lyric loader: parser mult-time lyrics file" );
+ ParserState state = PARSER_INIT;
+ unsigned int state_offset = 0;
+ unsigned int lyric_flags = 0;
+ std::vector<int> lyric_time(1, -1);
+ int time_num = 0;
+ std::vector<MtLyric> mtline;
+ MtLyric line;
+ int start_offset = 0;
+ unsigned int offset = 0;
+
+ for ( char * p = lyricData; offset < lyricSize; offset++, p++ )
+ {
+ // Skip \r
+ if ( *p == 0x0D )
+ continue;
+
+ if ( state == PARSER_IN_LYRICS )
+ {
+ // Lyrics are terminated either by \n or by [
+ if ( *p == '\n' || *p == '[' )
+ {
+ // Time must be there
+ if ( lyric_time[0] == -1 )
+ {
+ CLog::Log( LOGERROR, "LRC lyric loader: lyrics file has no time before lyrics" );
+ return false;
+ }
+
+ // Add existing lyrics
+ char current = *p;
+ CStdString text;
+
+ if ( offset > state_offset )
+ {
+ // null-terminate string, we saved current char anyway
+ *p = '\0';
+ text = &lyricData[0] + state_offset;
+ }
+ else
+ text = " "; // add a single space for empty lyric
+
+ // If this was end of line, set the flags accordingly
+ if ( current == '\n' )
+ {
+ // Add space after the trailing lyric in lrc
+ text += " ";
+ for ( int i = 0; i <= time_num; i++ )
+ {
+ line.text = text;
+ line.flags = lyric_flags | LYRICS_CONVERT_UTF8;
+ line.timing = lyric_time[i];
+ mtline.push_back( line );
+ }
+ state_offset = -1;
+ lyric_flags = CKaraokeLyricsText::LYRICS_NEW_LINE;
+ state = PARSER_INIT;
+ }
+ else
+ {
+ // No conversion needed as the file should be in UTF8 already
+ for ( int i = 0; i <= time_num; i++ )
+ {
+ line.text = text;
+ line.flags = lyric_flags | LYRICS_CONVERT_UTF8;
+ line.timing = lyric_time[i];
+ mtline.push_back( line );
+ }
+ lyric_flags = 0;
+ state_offset = offset + 1;
+ state = PARSER_IN_TIME;
+ }
+
+ time_num = 0;
+ lyric_time.resize(1);
+ lyric_time[0] = -1;
+ }
+ }
+ else if ( state == PARSER_IN_TIME )
+ {
+ // Time is terminated by ] or >
+ if ( *p == ']' || *p == '>' )
+ {
+ int mins, secs, htenths, ltenths = 0;
+
+ if ( offset == state_offset )
+ {
+ CLog::Log( LOGERROR, "LRC lyric loader: empty time" );
+ return false; // [] - empty time
+ }
+
+ // null-terminate string
+ char * timestr = &lyricData[0] + state_offset;
+ *p = '\0';
+
+ // Now check if this is time field or info tag. Info tags are like [ar:Pink Floyd]
+ char * fieldptr = strchr( timestr, ':' );
+ if ( timestr[0] >= 'a' && timestr[0] <= 'z' && timestr[1] >= 'a' && timestr[1] <= 'z' && fieldptr )
+ {
+ // Null-terminate the field name and switch to the field value
+ *fieldptr = '\0';
+ fieldptr++;
+
+ while ( isspace( *fieldptr ) )
+ fieldptr++;
+
+ // Check the info field
+ if ( !strcmp( timestr, "ar" ) )
+ m_artist += fieldptr;
+ else if ( !strcmp( timestr, "sr" ) )
+ {
+ // m_artist += "[CR]" + CStdString( fieldptr ); // Add source to the artist name as a separate line
+ }
+ else if ( !strcmp( timestr, "ti" ) )
+ m_songName = fieldptr;
+ else if ( !strcmp( timestr, "offset" ) )
+ {
+ if ( sscanf( fieldptr, "%d", &start_offset ) != 1 )
+ {
+ CLog::Log( LOGERROR, "LRC lyric loader: invalid [offset:] value '%s'", fieldptr );
+ return false; // [] - empty time
+ }
+
+ // Offset is in milliseconds; convert to 1/10 seconds
+ start_offset /= 100;
+ }
+
+ state_offset = -1;
+ state = PARSER_INIT;
+ continue;
+ }
+ else if ( sscanf( timestr, "%d:%d.%1d%1d", &mins, &secs, &htenths, &ltenths ) == 4 )
+ lyric_time[time_num] = mins * 600 + secs * 10 + htenths + MathUtils::round_int( ltenths / 10 );
+ else if ( sscanf( timestr, "%d:%d.%1d", &mins, &secs, &htenths ) == 3 )
+ lyric_time[time_num] = mins * 600 + secs * 10 + htenths;
+ else if ( sscanf( timestr, "%d:%d", &mins, &secs ) == 2 )
+ lyric_time[time_num] = mins * 600 + secs * 10;
+ else
+ {
+ // bad time
+ CLog::Log( LOGERROR, "LRC lyric loader: lyrics file has no proper time field: '%s'", timestr );
+ return false;
+ }
+
+ // Correct timing if necessary
+ lyric_time[time_num] += start_offset;
+ lyric_time[time_num] += timing_correction;
+
+ if ( lyric_time[time_num] < 0 )
+ lyric_time[time_num] = 0;
+
+ // Multi-time line
+ if ( *(p + 1) == '[' )
+ {
+ offset++;
+ p++;
+ state_offset = offset + 1;
+ state = PARSER_IN_TIME;
+ time_num++;
+ lyric_time.push_back(-1);
+ }
+ else
+ {
+ // Set to next char
+ state_offset = offset + 1;
+ state = PARSER_IN_LYRICS;
+ }
+ }
+ }
+ else if ( state == PARSER_INIT )
+ {
+ // Ignore spaces
+ if ( *p == ' ' || *p == '\t' )
+ continue;
+
+ // We're looking for [ or <
+ if ( *p == '[' || *p == '<' )
+ {
+ // Set to next char
+ state_offset = offset + 1;
+ state = PARSER_IN_TIME;
+
+ time_num = 0;
+ lyric_time.resize(1);
+ lyric_time[0] = -1;
+ }
+ else if ( *p == '\n' )
+ {
+ // If we get a newline and we're not paragraph, set it
+ if ( lyric_flags & CKaraokeLyricsText::LYRICS_NEW_LINE )
+ lyric_flags = CKaraokeLyricsText::LYRICS_NEW_PARAGRAPH;
+ }
+ else
+ {
+ // Everything else is error
+ CLog::Log( LOGERROR, "LRC lyric loader: lyrics file does not start from time" );
+ return false;
+ }
+ }
+ }
+
+ unsigned int lyricsNum = mtline.size();
+ if ( lyricsNum >= 2 )
+ {
+ for ( unsigned int i = 0; i < lyricsNum - 1; i++ )
+ {
+ for ( unsigned int j = i + 1; j < lyricsNum; j++ )
+ {
+ if ( mtline[i].timing > mtline[j].timing )
+ {
+ line = mtline[i];
+ mtline[i] = mtline[j];
+ mtline[j] = line;
+ }
+ }
+ }
+ }
+ for ( unsigned int i=0; i < lyricsNum; i++ )
+ addLyrics( mtline[i].text, mtline[i].timing, mtline[i].flags );
+
+ return true;
+}
+