I am making a small tool to play mp3 file, associate with lrc file.
The main functionality is to show and highlight the current playing paragraph, and loop-playing on current/specific paragraph.
I use MediaElement/MediaPlayer, or even with MediaTimeLine to control the seeking. it show a weird behavior that, after first setting of the position of MediaElement, the the voice/media stream is not sync with the timeline any more. For instance, when seek back to same position, you hear a little ahead of the expected position and when timeline show it should start next paragraph, but the current paragraph is not ended. the delta is arount 500ms ~ 1000ms , depending on the duration of the mp3 file.
I have tried different classes/methods to control it, even on differnt version of .net framework, the problem is same.
Did I misuse the class or there is a bug of it? please help. Thanks.
following the main code files for reference.
LrcTimeline.cs
using Microsoft.Win32; using System; using System.Collections; using System.IO; using System.Text; using System.Text.RegularExpressions; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Threading; namespace LrcPlayer { class LrcTimeline : MediaTimeline { private MediaElement me;// = new MediaElement(); private ArrayList lrcLines = new ArrayList(); private string mp3FileName; private RichTextBox lrc_txt; private int idx; private string pattern = @"^\s*\[(?<time>.*)\](?<content>.*)$"; private string pattern_time = @"^\s*(?<minute>\d*)\:\s*(?<second>\d*)\.\s*(?<ms>\d*)$"; private DispatcherTimer dispatcherTimer = new DispatcherTimer(); private MainWindow mainWindow; public LrcTimeline(RichTextBox rtb, MediaElement m, MainWindow _mw) { lrc_txt = rtb; me = m; mainWindow = _mw; init(); } public Duration GetNaturalDuration() { Duration d = base.GetNaturalDuration(me.Clock); return d; } private void startTimer() { dispatcherTimer.Tick += new EventHandler(dispatcherTimer_Tick); dispatcherTimer.Interval = new TimeSpan(0, 0, 0, 0, 500); dispatcherTimer.Start(); } private void stopTimer() { dispatcherTimer.Stop(); } int repeat_adust = 0; public void repeat() { } private void playNext(TimeSpan? x) { if (x == null) { x = me.Position; } //Console.WriteLine( "player position:["+x+"]"); Lrc l = getCurrentLrc(); if (l == null) { //Console.WriteLine("index out of range:" + idx + "::" + lrcLines.Count); return; } TimeSpan ts = new TimeSpan(0,0, 0,0, repeat_adust); if (x - l.beginTime - l.duration> ts) { //Console.WriteLine("repeat[" + mp.Position + "] -> [" + l.beginTime + "]-> [" + l.duration + "]"); if (mainWindow.repeating) { Console.WriteLine("repeat[" + me.Position + "] -> lrc[" + idx + "]=[" + l.beginTime + "]-> [" + l.duration + "]"); me.Clock.Controller.Seek(l.beginTime, TimeSeekOrigin.BeginTime); Console.WriteLine("repeat[" + me.Position + "] ts="+ts); } else { idx++; l = getCurrentLrc(); if (l !=null) Console.WriteLine("moveNex at[" + me.Position + "] -> lrc[" + idx + "]=[" + l.beginTime + "]-> [" + l.duration + "]"); highlight(); } } } private void dispatcherTimer_Tick(object sender, EventArgs e) { playNext(null); } public void Play() { Lrc l = getCurrentLrc(); if (l == null) { //Console.WriteLine("index out of range:" + idx + "::" + lrcLines.Count); return; } Console.WriteLine("Play: idx=" + idx + "::" + l); if ( me.Clock ==null) me.Clock= CreateClock(); me.Clock.Controller.Begin(); } public void Pause() { Console.WriteLine("Pause at:" + me.Position); me.Clock.Controller.Pause(); //stopTimer(); } public void Resume() { me.Clock.Controller.Resume(); } public void Stop() { me.Clock.Controller.Stop(); } public void Play(int idx) { } public void init() { //CurrentStateInvalidated += LrcTimeline_CurrentStateInvalidated; CurrentTimeInvalidated += LrcTimeline_CurrentTimeInvalidated; base.Completed += LrcTimeline_Completed; } void mp_MediaOpened(object sender, EventArgs e) { updateLastLrc(); } private void updateLastLrc() { try { lrcLines.RemoveAt(lrcLines.Count - 1); lrc_txt.SelectAll(); int end = lrc_txt.Selection.Start.GetOffsetToPosition(lrc_txt.Selection.End); Lrc last = new Lrc("", me.NaturalDuration.HasTimeSpan ? me.NaturalDuration.TimeSpan : new TimeSpan(0), end); addLrc(last); Console.WriteLine("updateLastLrc:" + lrcLines.Count + " =>[" + last + "]"); } catch { } } public void reset() { idx = 0; repeat_adust = 0; me.Clock.Controller.Stop(); me.Clock = null; Source = new Uri(@mp3FileName); } void mp_MediaEnded(object sender, EventArgs e) { Console.WriteLine("MediaEnded"); mainWindow.reset(); } void LrcTimeline_Completed(object sender, EventArgs e) { Console.WriteLine("Completed:" + idx+"/"+lrcLines.Count); mainWindow.reset(); } void LrcTimeline_CurrentTimeInvalidated(object sender, EventArgs e) { Clock clock = (Clock)sender; //Console.WriteLine("TimeInvalidated: clock=" + clock.CurrentTime+"||| me="+me.Position); if (clock.CurrentTime == null) { } else { playNext(clock.CurrentTime); } } //void LrcTimeline_CurrentStateInvalidated(object sender, EventArgs e) //{ // Console.WriteLine("CurrentStateInvalidated:"); //} private Lrc getCurrentLrc() { //Console.WriteLine("currentLrc:" + idx + " of " + lrcLines.Count); if (idx < lrcLines.Count) { Lrc l = (Lrc)lrcLines[idx]; return l; } else { return null; } } private Lrc getLrc(int i) { if (i < lrcLines.Count) { Lrc l = (Lrc)lrcLines[i]; return l; } else { return null; } } public bool load() { OpenFileDialog fileDialog = new OpenFileDialog(); fileDialog.Title = "Select mp3 file"; fileDialog.Filter = "mp3 files (*.mp3)|*.mp3"; fileDialog.FilterIndex = 1; fileDialog.RestoreDirectory = true; if (fileDialog.ShowDialog() == true) { try { String fileName = fileDialog.FileName; mp3FileName = fileDialog.FileName; Source = new Uri(@fileName); fileName = fileName.Substring(0, fileName.Length - 3) + "lrc"; openLrc(fileName); return true; } catch (Exception ex) { Console.WriteLine("Exception:" + ex.Message); return false; } } else { return false; } } private void openLrc(string fn) { try { StreamReader sr1 = new StreamReader(@fn, Encoding.GetEncoding("GBK")); string nextLine = null; Regex rgx = new Regex(pattern, RegexOptions.IgnoreCase); lrcLines = new ArrayList(); idx = 0; repeat_adust = 0; lrcLines.Add(new Lrc("", new TimeSpan(0), 0)); lrc_txt.FontSize = 16; lrc_txt.Document.Blocks.Clear(); while ((nextLine = sr1.ReadLine()) != null) { nextLine = nextLine.Trim(); if (nextLine.Length == 0) continue; MatchCollection matches = rgx.Matches(nextLine); if (matches.Count > 0) { //Console.WriteLine("{0} ({1} matches):", nextLine, matches.Count); foreach (Match match in matches) { TimeSpan beginT = getDuration(match.Groups["time"].Value); string txt = match.Groups["content"].Value.Trim(); if (txt.Length == 0) continue; lrc_txt.SelectAll(); int pos = lrc_txt.Selection.Start.GetOffsetToPosition(lrc_txt.Selection.End); //.Text.Length+2; Lrc s = new Lrc(txt, beginT, pos + 2); addLrc(s); Paragraph p = new Paragraph(); // Run r = new Run(s.txt); // p.Inlines.Add(r); lrc_txt.Document.Blocks.Add(p); } } } lrc_txt.SelectAll(); int end = lrc_txt.Selection.Start.GetOffsetToPosition(lrc_txt.Selection.End); Lrc last = new Lrc("", me.NaturalDuration.HasTimeSpan ? me.NaturalDuration.TimeSpan : new TimeSpan(0), end); addLrc(last); sr1.Close(); } catch (Exception ex) { Console.WriteLine("Read Lrc file failed:" + ex.Message); } } private void addLrc(Lrc l) { if (lrcLines.Count > 0) { Lrc preLrc = (Lrc)lrcLines[lrcLines.Count - 1]; preLrc.duration = l.beginTime - preLrc.beginTime; } lrcLines.Add(l); } private TimeSpan getDuration(string t) { //return TimeSpan.Parse(t); TimeSpan ret = new TimeSpan(); Regex rgx_time = new Regex(pattern_time, RegexOptions.IgnoreCase); MatchCollection matches = rgx_time.Matches(t); foreach (Match match in matches) { int m = int.Parse(match.Groups["minute"].Value); int s = int.Parse(match.Groups["second"].Value); int ms = int.Parse(match.Groups["ms"].Value); ret = new TimeSpan(0, 0, m, s, ms * 10); } return ret; } private void selectSentence(int start, int length) { TextPointer newSelectionStartPointer = lrc_txt.Document.ContentStart.GetPositionAtOffset(start); TextPointer newSelectionEndPointer = newSelectionStartPointer.GetPositionAtOffset(length); lrc_txt.Selection.Select(newSelectionStartPointer, newSelectionEndPointer); Rect screenPos = lrc_txt.Selection.Start.GetCharacterRect(LogicalDirection.Forward); double offset = screenPos.Top + lrc_txt.VerticalOffset; lrc_txt.ScrollToVerticalOffset(offset - lrc_txt.ActualHeight / 2); } public void highlight() { Lrc s; try { if (idx > 0) { s = (Lrc)lrcLines[idx - 1]; if (s != null) { //Console.WriteLine(s.ToString()); selectSentence(s.position, s.length); lrc_txt.Selection.ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.Gray); lrc_txt.Selection.ApplyPropertyValue(TextElement.FontWeightProperty, FontWeights.Normal); lrc_txt.Selection.ApplyPropertyValue(TextElement.FontSizeProperty, 16.0); } } } catch (Exception ex) { Console.WriteLine("except 1:" + ex.StackTrace + "\r\nMessage: " + ex.Message); } try { s = getCurrentLrc();// (Lrc)lrcLines[idx]; if (s != null) { //Console.WriteLine(s.ToString()); selectSentence(s.position, s.length); lrc_txt.Selection.ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.Blue); lrc_txt.Selection.ApplyPropertyValue(TextElement.FontWeightProperty, FontWeights.Bold); lrc_txt.Selection.ApplyPropertyValue(TextElement.FontSizeProperty, 24.0); } } catch (Exception ex) { Console.WriteLine("except 2:" + ex.StackTrace + " \r\nMessage: " + ex.Message); } } } public class Lrc { public string txt; public TimeSpan beginTime; public TimeSpan duration; public int position; public int length; public Lrc(string _t, TimeSpan _b, int _p) { txt = _t; beginTime = _b; duration = new TimeSpan(0); position = _p; length = _t.Length + 2; } public override string ToString() { string ret = ""; ret += "[" + beginTime + "]"; ret += "[" + duration + "]"; ret += ", pos:[" + position + "]"; ret += ", length:" + length; ret += "\t[" + txt + "]"; return ret; } } }
MainWindow.xaml.cs
using System; using System.Collections.Generic; using System.Text.RegularExpressions; using System.IO; using System.Linq; using System.Text; //using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using Microsoft.Win32; using System.Collections; using System.Windows.Media.Animation; namespace LrcPlayer { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); lrctl = new LrcTimeline(lrc_txt, mp3player, this); } private Boolean playing = false; private bool paused = false; private LrcTimeline lrctl = null; private void btn_play_click(object sender, RoutedEventArgs e) { if (playing) { lrctl.Pause(); paused = true; btn_play.Content = "Play"; } else { if (paused) { lrctl.Resume(); } else { lrctl.Play(); } lrctl.highlight(); btn_play.Content = "Pause"; } playing = !playing; } public bool repeating = false; private void btn_repeat_click(object sender, RoutedEventArgs e) { if (repeating) { btn_repeat_last.Content = "Repeat Last"; } else { btn_repeat_last.Content = "Stop Repeat"; } repeating = !repeating; } private void btn_open_mp3_Click(object sender, RoutedEventArgs e) { reset(); if (lrctl.load()) { btn_repeat_last.IsEnabled = true; btn_play.IsEnabled = true; btn_reset.IsEnabled = true; } } public void reset() { try { playing = false; repeating = false; paused = false; btn_repeat_last.Content = "Repeat Last"; //lrctl.Stop(); lrctl.reset(); btn_play.Content = "Play"; lrc_txt.SelectAll(); lrc_txt.Selection.ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.Black); lrc_txt.Selection.ApplyPropertyValue(TextElement.FontWeightProperty, FontWeights.Normal); lrc_txt.Selection.ApplyPropertyValue(TextElement.FontSizeProperty, 16.0); lrc_txt.ScrollToHome(); } catch { } } private void btn_reset_Click(object sender, RoutedEventArgs e) { reset(); } } }
MainWindow.xaml
<Window x:Class="LrcPlayer.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="392" Width="633"><Grid Margin="-1,1,1,-1"><Grid.RowDefinitions><RowDefinition Height="25"/><RowDefinition /></Grid.RowDefinitions><Grid.ColumnDefinitions><ColumnDefinition Width="101*"/><ColumnDefinition Width="24*"/></Grid.ColumnDefinitions><RichTextBox Grid.Row="1" Grid.Column="0" x:Name="lrc_txt" HorizontalAlignment="Stretch" Margin="5,0,5,5" VerticalAlignment="Stretch" IsReadOnly="True" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Grid.ColumnSpan="2" ><FlowDocument><Paragraph><Run Text="please load mp3 file. (lrc file should have same file name in the same folder)"/></Paragraph></FlowDocument></RichTextBox><StackPanel Grid.Row="0" Grid.Column="0" Orientation="Horizontal" Height="24" VerticalAlignment="Center" Width="auto" Grid.ColumnSpan="2" Margin="0,0,0,1"><Button x:Name="btn_open_mp3" Content="Load" HorizontalAlignment="Left" Margin="2" VerticalAlignment="Top" Height="20" Width="75" Click="btn_open_mp3_Click"/><Button x:Name="btn_play" Content="Play" Margin="2" HorizontalAlignment="Left" Height="20" VerticalAlignment="Top" Width="54" Click="btn_play_click" RenderTransformOrigin="0.556,-2.308" IsEnabled="False"/><Button x:Name="btn_repeat_last" Content="Repeat Last" Margin="2" HorizontalAlignment="Left" Height="20" VerticalAlignment="Top" Width="80" Click="btn_repeat_click" IsEnabled="False" /><Button x:Name="btn_reset" Content="Reset" Width="65" Margin="2" Click="btn_reset_Click" IsEnabled="False"/></StackPanel><MediaElement x:Name="mp3player" HorizontalAlignment="Left" Height="100" Margin="-180,75,0,0" Grid.Row="1" VerticalAlignment="Top" Width="100" LoadedBehavior="Manual"/></Grid></Window>