Added lyrics UTF8 conversion flag to addLyrics;
[xbmc:xbmc-antiquated.git] / XBMC / xbmc / karaoke / karaokelyricstextlrc.cpp
1 //
2 // C++ Implementation: karaokelyricstextlrc
3 //
4 // Description:
5 //
6 //
7 // Author: Team XBMC <>, (C) 2008
8 //
9 // Copyright: See COPYING file that comes with this distribution
10 //
11 //
12 #include "stdafx.h"
13 #include <math.h>
14
15 #include "Util.h"
16 #include "FileSystem/File.h"
17 #include "Settings.h"
18
19 #include "karaokelyricstextlrc.h"
20
21
22 CKaraokeLyricsTextLRC::CKaraokeLyricsTextLRC( const CStdString & lyricsFile )
23   : CKaraokeLyricsText()
24 {
25   m_lyricsFile = lyricsFile;
26 }
27
28
29 CKaraokeLyricsTextLRC::~CKaraokeLyricsTextLRC()
30 {
31 }
32
33 bool CKaraokeLyricsTextLRC::Load()
34 {
35   enum ParserState
36   {
37     PARSER_INIT,    // looking for time
38     PARSER_IN_TIME,    // processing time
39     PARSER_IN_LYRICS  // processing lyrics
40   };
41
42   XFILE::CFile file;
43
44   // Clear the lyrics array
45   clearLyrics();
46
47   if ( !file.Open( m_lyricsFile, TRUE ) )
48     return false;
49
50   unsigned int lyricSize = (unsigned int) file.GetLength();
51
52   if ( !lyricSize )
53   {
54     CLog::Log( LOGERROR, "LRC lyric loader: lyric file %s has zero length", m_lyricsFile.c_str() );
55     return false;
56   }
57
58   // Read the file into memory array
59   std::vector<char> lyricData( lyricSize );
60
61   file.Seek( 0, SEEK_SET );
62
63   // Read the whole file
64   if ( file.Read( &lyricData[0], lyricSize) != lyricSize )
65     return false; // disk error?
66
67   file.Close();
68
69   // Parse the correction value
70   int timing_correction = MathUtils::round_int( g_advancedSettings.m_karaokeSyncDelayLRC * 10 );
71
72   //
73   // A simple state machine to parse the file
74   //
75   ParserState state = PARSER_INIT;
76   unsigned int state_offset = 0;
77   unsigned int lyric_flags = 0;
78   int lyric_time = -1;
79   int start_offset = 0;
80   unsigned int offset = 0;
81
82   CStdString ext, songfilename = getSongFile();
83   CUtil::GetExtension( songfilename, ext );
84
85   // Skip windoze UTF8 file prefix, if any, and reject UTF16 files
86   if ( lyricSize > 3 )
87   {
88     if ( lyricData[0] == 0xFF && lyricData[1] == 0xFE )
89     {
90       CLog::Log( LOGERROR, "LRC lyric loader: lyrics file is in UTF16 encoding, must be in UTF8" );
91       return false;
92     }
93
94     // UTF8 prefix added by some windoze apps
95     if ( lyricData[0] == 0xEF && lyricData[1] == 0xBB && lyricData[1] == 0xBF )
96       offset = 3;
97   }
98
99   for ( char * p = &lyricData[offset]; offset < lyricSize; offset++, p++ )
100   {
101     // Skip \r
102     if ( *p == 0x0D )
103       continue;
104
105     if ( state == PARSER_IN_LYRICS )
106     {
107       // Lyrics are terminated either by \n or by [
108       if ( *p == '\n' || *p == '[' )
109       {
110         // Time must be there
111         if ( lyric_time == -1 )
112         {
113           CLog::Log( LOGERROR, "LRC lyric loader: lyrics file has no time before lyrics" );
114           return false;
115         }
116
117         // Add existing lyrics
118         char current = *p;
119         CStdString text;
120
121         if ( offset > state_offset )
122         {
123           // null-terminate string, we saved current char anyway
124           *p = '\0';
125           text = &lyricData[0] + state_offset;
126         }
127         else
128           text = " "; // add a single space for empty lyric
129
130         // If this was end of line, set the flags accordingly
131         if ( current == '\n' )
132         {
133           // Add space after the trailing lyric in lrc
134           text += " ";
135           addLyrics( text, lyric_time, lyric_flags | LYRICS_CONVERT_UTF8 );
136           state_offset = -1;
137           lyric_flags = CKaraokeLyricsText::LYRICS_NEW_LINE;
138           state = PARSER_INIT;
139         }
140         else
141         {
142           // No conversion needed as the file should be in UTF8 already
143           addLyrics( text, lyric_time, lyric_flags | LYRICS_CONVERT_UTF8 );
144           lyric_flags = 0;
145           state_offset = offset + 1;
146           state = PARSER_IN_TIME;
147         }
148
149         lyric_time = -1;
150       }
151     }
152     else if ( state == PARSER_IN_TIME )
153     {
154       // Time is terminated by ] or >
155       if ( *p == ']' || *p == '>' )
156       {
157         int mins, secs, htenths, ltenths = 0;
158
159         if ( offset == state_offset )
160         {
161           CLog::Log( LOGERROR, "LRC lyric loader: empty time" );
162           return false; // [] - empty time
163         }
164
165         // null-terminate string
166         char * timestr = &lyricData[0] + state_offset;
167         *p = '\0';
168
169         // Now check if this is time field or info tag. Info tags are like [ar:Pink Floyd]
170         char * fieldptr = strchr( timestr, ':' );
171         if ( timestr[0] >= 'a' && timestr[0] <= 'z' && timestr[1] >= 'a' && timestr[1] <= 'z' && fieldptr )
172         {
173           // Null-terminate the field name and switch to the field value
174           *fieldptr = '\0';
175           fieldptr++;
176
177           while ( isspace( *fieldptr ) )
178             fieldptr++;
179
180           // Check the info field
181           if ( !strcmp( timestr, "ar" ) )
182             m_artist += fieldptr;
183           else if ( !strcmp( timestr, "sr" ) )
184           {
185             // m_artist += "[CR]" + CStdString( fieldptr ); // Add source to the artist name as a separate line
186           }
187           else if ( !strcmp( timestr, "ti" ) )
188             m_songName = fieldptr;
189           else if ( !strcmp( timestr, "offset" ) )
190           {
191             if ( sscanf( fieldptr, "%d", &start_offset ) != 1 )
192             {
193               CLog::Log( LOGERROR, "LRC lyric loader: invalid [offset:] value '%s'", fieldptr );
194               return false; // [] - empty time
195             }
196
197             // Offset is in milliseconds; convert to 1/10 seconds
198             start_offset /= 100;
199           }
200
201           state_offset = -1;
202           state = PARSER_INIT;
203           continue;
204         }
205         else if ( sscanf( timestr, "%d:%d.%1d%1d", &mins, &secs, &htenths, &ltenths ) == 4 )
206           lyric_time = mins * 600 + secs * 10 + htenths + MathUtils::round_int( ltenths / 10 );
207         else if ( sscanf( timestr, "%d:%d.%1d", &mins, &secs, &htenths ) == 3 )
208           lyric_time = mins * 600 + secs * 10 + htenths;
209         else if ( sscanf( timestr, "%d:%d", &mins, &secs ) == 2 )
210           lyric_time = mins * 600 + secs * 10;
211         else
212         {
213           // bad time
214           CLog::Log( LOGERROR, "LRC lyric loader: lyrics file has no proper time field: '%s'", timestr );
215           return false;
216         }
217
218         // Correct timing if necessary
219         lyric_time += start_offset;
220         lyric_time += timing_correction;
221
222         if ( lyric_time < 0 )
223           lyric_time = 0;
224
225         // Set to next char
226         state_offset = offset + 1;
227         state = PARSER_IN_LYRICS;
228       }
229     }
230     else if ( state == PARSER_INIT )
231     {
232       // Ignore spaces
233       if ( *p == ' ' || *p == '\t' )
234         continue;
235
236       // We're looking for [ or <
237       if ( *p == '[' || *p == '<' )
238       {
239         // Set to next char
240         state_offset = offset + 1;
241         state = PARSER_IN_TIME;
242
243         lyric_time = -1;
244       }
245       else if ( *p == '\n' )
246       {
247         // If we get a newline and we're not paragraph, set it
248         if ( lyric_flags & CKaraokeLyricsText::LYRICS_NEW_LINE )
249           lyric_flags = CKaraokeLyricsText::LYRICS_NEW_PARAGRAPH;
250       }
251       else
252       {
253         // Everything else is error
254         CLog::Log( LOGERROR, "LRC lyric loader: lyrics file does not start from time" );
255         return false;
256       }
257     }
258   }
259
260   return true;
261 }