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>