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 do loop-playing on current/specific paragraph on demand.
I am using MediaElement/MediaPlayer, or even with MediaTimeLine to control the seeking. it shows a weird behavior that, after first setting position of MediaElement, the voice/media stream is not synchronized with the timeline anymore. For instance, when seek back to same position, you hear a little ahead of the expected position and when timeline shows it should start next paragraph, but the current paragraph is still heard. If we set to "repeat" the paragraph, the last one or two words will be cut and the end words of previor paragraph will be heard. the delta is arount 500ms ~ 1000ms , depending on the duration of the mp3 file. from printed information, the timeline is always the sam and correct.
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>