2929#include " ../lyrics-common/lyrics.h"
3030#include " ../lyrics-common/preferences.h"
3131
32+ #include < algorithm> // For std::sort
33+ #include < clocale>
34+ #include < regex> // For std::regex and std::smatch
35+ #include < sstream> // For std::istringstream
36+ #include < vector>
37+
38+ struct TimedLyricLine
39+ {
40+ int timestamp_ms; // Timestamp in milliseconds
41+ String text; // Lyric text at this timestamp
42+ };
43+
44+ std::vector<TimedLyricLine>
45+ timed_lyrics; // Stores parsed lyrics with timestamps
46+
3247class LyricsGtk : public GeneralPlugin
3348{
3449public:
@@ -88,8 +103,100 @@ void update_lyrics_window (const char * title, const char * artist, const char *
88103 gtk_text_buffer_insert (textbuffer, & iter, " \n\n " , -1 );
89104 gtk_text_buffer_insert (textbuffer, & iter, lyrics, -1 );
90105
91- gtk_text_buffer_get_start_iter (textbuffer, & iter);
92- gtk_text_view_scroll_to_iter (textview, & iter, 0 , true , 0 , 0 );
106+ // Parse the lyrics and populate timed_lyrics
107+ timed_lyrics.clear ();
108+
109+ // Add a dummy timestamp for the first line (at -1ms to ensure it's always
110+ // before the actual lyrics)
111+ TimedLyricLine title_line;
112+ title_line.timestamp_ms = -1 ;
113+ title_line.text = String (title);
114+ timed_lyrics.push_back (title_line);
115+
116+ std::istringstream iss (lyrics);
117+ std::string line;
118+
119+ int global_offset = 0 ;
120+ std::regex time_re (R"( \[\s*(\d+)\s*:\s*(\d+(?:\.\d+)?)\s*\])" );
121+ std::regex offset_re (R"( \[\s*offset\s*:\s*([+-]?\d+)\s*\])" ,
122+ std::regex_constants::icase);
123+
124+ while (std::getline (iss, line))
125+ {
126+ // Sanitize the line: remove leading/trailing spaces and carriage return
127+ line.erase (0 , line.find_first_not_of (" \t\r " )); // Remove leading whitespace and \r
128+ line.erase (line.find_last_not_of (" \t\r " ) + 1 ); // Remove trailing whitespace and \r
129+
130+ if (line.empty ())
131+ continue ;
132+
133+ // 1. Check for the global offset tag
134+ std::smatch offset_match;
135+ if (std::regex_search (line, offset_match, offset_re))
136+ {
137+ global_offset = std::stoi (offset_match[1 ].str ());
138+ continue ;
139+ }
140+
141+ // 2. Extract MULTIPLE time tags from a single line
142+ std::sregex_iterator it (line.begin (), line.end (), time_re);
143+ std::sregex_iterator end;
144+
145+ std::vector <int > timestamps;
146+ size_t last_pos = 0 ;
147+
148+ for (; it != end; ++it)
149+ {
150+ int minutes = std::stoi (it->str (1 ));
151+ float seconds = std::stof (it->str (2 ));
152+ int timestamp_ms =
153+ static_cast <int > ((minutes * 60 + seconds) * 1000 );
154+
155+ timestamps.push_back (timestamp_ms);
156+ last_pos = it->position () + it->length ();
157+ }
158+
159+ // 3. If timestamps were found, attach the remaining text to all of them
160+ if (!timestamps.empty ())
161+ {
162+ std::string text = line.substr (last_pos);
163+ text.erase (0 , text.find_first_not_of (" \t " )); // Trim leading spaces
164+
165+ for (int ts : timestamps)
166+ {
167+ TimedLyricLine timed_line;
168+ timed_line.timestamp_ms = ts;
169+ timed_line.text = String (text.c_str ());
170+
171+ timed_lyrics.push_back (timed_line);
172+ }
173+ }
174+ }
175+
176+ // 4. Apply offsets and sort chronologically (skipping the dummy title line
177+ // at index 0)
178+ if (timed_lyrics.size () > 1 )
179+ {
180+ for (size_t i = 1 ; i < timed_lyrics.size (); ++i)
181+ {
182+ timed_lyrics[i].timestamp_ms -= global_offset;
183+ }
184+
185+ std::sort (timed_lyrics.begin () + 1 , timed_lyrics.end (),
186+ [](const TimedLyricLine & a, const TimedLyricLine & b) {
187+ return a.timestamp_ms < b.timestamp_ms ;
188+ });
189+
190+ // Ensure the dummy title line stays chronologically before the first
191+ // actual lyric
192+ if (timed_lyrics[1 ].timestamp_ms <= timed_lyrics[0 ].timestamp_ms )
193+ {
194+ timed_lyrics[0 ].timestamp_ms = timed_lyrics[1 ].timestamp_ms - 1000 ;
195+ }
196+ }
197+
198+ // gtk_text_buffer_get_start_iter (textbuffer, & iter);
199+ // gtk_text_view_scroll_to_iter (textview, & iter, 0, true, 0, 0);
93200}
94201
95202bool try_parse_json (const Index<char > & buf, const char * key, String & output)
@@ -224,7 +331,7 @@ static GtkWidget * build_widget ()
224331 gtk_box_pack_start ((GtkBox *) vbox, scrollview, true , true , 0 );
225332
226333 gtk_widget_show_all (vbox);
227-
334+ gtk_text_buffer_create_tag (textbuffer, " highlight " , " foreground " , " yellow " , nullptr );
228335 gtk_text_buffer_create_tag (textbuffer, " weight_bold" , " weight" , PANGO_WEIGHT_BOLD, nullptr );
229336 gtk_text_buffer_create_tag (textbuffer, " scale_large" , " scale" , PANGO_SCALE_LARGE, nullptr );
230337 gtk_text_buffer_create_tag (textbuffer, " style_italic" , " style" , PANGO_STYLE_ITALIC, nullptr );
@@ -235,6 +342,103 @@ static GtkWidget * build_widget ()
235342 return vbox;
236343}
237344
345+ void highlight_lyrics (int current_time_ms)
346+ {
347+ if (!textbuffer)
348+ return ;
349+
350+ // Check if lyrics synchronization is enabled
351+ if (!aud_get_bool (" lyricwiki" , " sync_lyrics" ))
352+ return ;
353+
354+ // Clear the text buffer
355+ gtk_text_buffer_set_text (textbuffer, " " , -1 );
356+
357+ // Find the 4 lines closest to the current timestamp (current + 3 neighbors)
358+ std::vector <TimedLyricLine> lines_to_display;
359+
360+ // Store up to 4 lines starting from the current one
361+ for (size_t i = 0 ; i < timed_lyrics.size (); ++i)
362+ {
363+
364+ if (timed_lyrics[i].timestamp_ms >= current_time_ms)
365+ {
366+ // Ensure we have the current line and its neighbors (3 more lines)
367+ size_t start_index = (i > 1 ) ? i - 2 : 0 ; // Start 2 lines before the current line
368+ size_t end_index = std::min (i + 2 , timed_lyrics.size () - 1 ); // Limit to 3 more lines after
369+
370+ // Ensure no more than 4 lines are selected
371+ size_t line_count = 0 ;
372+
373+ for (size_t j = start_index; j <= end_index && line_count < 4 ; ++j)
374+ {
375+ lines_to_display.push_back (timed_lyrics[j]);
376+ line_count++;
377+ }
378+ break ;
379+ }
380+ }
381+
382+ // Retrieve the tag table
383+ GtkTextTagTable * tag_table = gtk_text_buffer_get_tag_table (textbuffer);
384+
385+ // Check if the enlarge tag exists, and create it if not
386+ GtkTextTag * enlarge_tag =
387+ gtk_text_tag_table_lookup (tag_table, " enlarge_tag" );
388+ if (!enlarge_tag)
389+ {
390+ enlarge_tag = gtk_text_tag_new (" enlarge_tag" );
391+ g_object_set (enlarge_tag, " scale" , 1.5 , NULL ); // Enlarge by 1.5 times (adjust as needed)
392+ gtk_text_tag_table_add (tag_table, enlarge_tag);
393+ }
394+
395+ // Insert the selected lines into the text buffer
396+ GtkTextIter iter;
397+ gtk_text_buffer_get_start_iter (textbuffer, &iter);
398+
399+ for (size_t i = 0 ; i < lines_to_display.size (); ++i)
400+ {
401+ const TimedLyricLine & line = lines_to_display[i];
402+ std::string text_with_newline = std::string (line.text );
403+
404+ // Special handling for the title line (dummy timestamp)
405+ if (line.timestamp_ms < 0 && i == 0 &&
406+ text_with_newline ==
407+ static_cast <const char *> (timed_lyrics[0 ].text ))
408+ {
409+ gtk_text_buffer_insert_with_tags_by_name (
410+ textbuffer, &iter, text_with_newline.c_str (), -1 , " weight_bold" ,
411+ " scale_large" , nullptr );
412+ }
413+ else if (i == 1 && line.timestamp_ms >= 0 )
414+ { // Current line (but not title)
415+ gtk_text_buffer_insert_with_tags_by_name (textbuffer, &iter,
416+ text_with_newline.c_str (),
417+ -1 , " highlight" , nullptr );
418+ }
419+ else
420+ {
421+ gtk_text_buffer_insert (textbuffer, &iter, text_with_newline.c_str (), -1 );
422+ }
423+
424+ gtk_text_buffer_insert (textbuffer, &iter, " \n " , -1 );
425+ }
426+
427+ // After inserting lines, force scroll to the last line
428+ GtkTextIter end_iter;
429+ gtk_text_buffer_get_end_iter (textbuffer, &end_iter);
430+ gtk_text_view_scroll_to_iter (textview, &end_iter, 0 , TRUE , 0 , 0 );
431+ }
432+
433+ gboolean update_lyrics_display (gpointer data)
434+ {
435+ int current_time_ms =
436+ aud_drct_get_time (); // Get current time from player in ms
437+ highlight_lyrics (current_time_ms);
438+
439+ return G_SOURCE_CONTINUE; // Continue calling this function
440+ }
441+
238442void * LyricsGtk::get_gtk_widget ()
239443{
240444 GtkWidget * vbox = build_widget ();
@@ -246,6 +450,6 @@ void * LyricsGtk::get_gtk_widget ()
246450 lyrics_playback_began ();
247451
248452 g_signal_connect (vbox, " destroy" , (GCallback) destroy_cb, nullptr );
249-
453+ g_timeout_add ( 100 , update_lyrics_display, nullptr );
250454 return vbox;
251455}
0 commit comments