diff --git a/DereTore.Applications.StarlightDirector/DereTore.Applications.StarlightDirector.csproj b/DereTore.Applications.StarlightDirector/DereTore.Applications.StarlightDirector.csproj index 4d6ecbf..01308c9 100644 --- a/DereTore.Applications.StarlightDirector/DereTore.Applications.StarlightDirector.csproj +++ b/DereTore.Applications.StarlightDirector/DereTore.Applications.StarlightDirector.csproj @@ -158,10 +158,6 @@ Designer MSBuild:Compile - - Designer - MSBuild:Compile - Designer MSBuild:Compile @@ -203,6 +199,8 @@ + + LineLayer.xaml @@ -235,9 +233,6 @@ - - SimpleScoreNote.xaml - diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/Models/DrawingNote.cs b/DereTore.Applications.StarlightDirector/UI/Controls/Models/DrawingNote.cs new file mode 100644 index 0000000..601bc8e --- /dev/null +++ b/DereTore.Applications.StarlightDirector/UI/Controls/Models/DrawingNote.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using DereTore.Applications.StarlightDirector.Entities; + +namespace DereTore.Applications.StarlightDirector.UI.Controls.Models +{ + /// + /// Internal representation of notes for drawing + /// + public class DrawingNote + { + public Note Note { get; set; } + public DrawingNote HoldTarget { get; set; } + public DrawingNote SyncTarget { get; set; } + public DrawingNote GroupTarget { get; set; } + public int Timing { get; set; } + public bool Done { get; set; } + public int Duration { get; set; } + public bool IsHoldStart { get; set; } + public double LastT { get; set; } + public double X { get; set; } + public double Y { get; set; } + public int DrawType { get; set; } + public int HitPosition { get; set; } + public bool EffectShown { get; set; } + } +} diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs b/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs new file mode 100644 index 0000000..94dde75 --- /dev/null +++ b/DereTore.Applications.StarlightDirector/UI/Controls/PreviewCanvas.cs @@ -0,0 +1,384 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using DereTore.Applications.StarlightDirector.Extensions; +using DereTore.Applications.StarlightDirector.UI.Controls.Models; + +namespace DereTore.Applications.StarlightDirector.UI.Controls +{ + public class PreviewCanvas : Canvas + { + // constants + private const double NoteMarginTop = 20; + private const double NoteMarginBottom = 60; + private const double NoteXBetween = 80; + private const double NoteSize = 30; + private const double NoteRadius = NoteSize / 2; + + // dimensions + private double _noteStartY; + private double _noteEndY; + private readonly List _noteX = new List(); + private readonly List _startPoints = new List(); + private readonly List _endPoints = new List(); + + // computation + private List _notes; + private int _notesHead; + private int _notesTail; + private double _approachTime; + + // rendering + private volatile bool _isPreviewing; + private List _hitEffectStartTime; + private List _hitEffectT; + private readonly EventWaitHandle _renderCompleteHandle = new EventWaitHandle(false, EventResetMode.AutoReset); + + public double HitEffectMilliseconds { get; set; } + + public PreviewCanvas() + { + + } + + public void Initialize(List notes, double approachTime) + { + _notes = notes; + _approachTime = approachTime; + _notesHead = 0; + + // compute positions + + _noteStartY = NoteMarginTop; + _noteEndY = ActualHeight - NoteMarginBottom; + + _noteX.Clear(); + _noteX.Add((ActualWidth - 4 * NoteXBetween - 5 * NoteSize) / 2); + for (int i = 1; i < 5; ++i) + { + _noteX.Add(_noteX.Last() + NoteXBetween + NoteSize); + } + + for (int i = 0; i < 5; ++i) + { + _startPoints.Add(new Point(_noteX[i], _noteStartY)); + _endPoints.Add(new Point(_noteX[i], _noteEndY)); + } + + // initialize note positions + + foreach (var note in _notes) + { + note.X = _noteX[note.HitPosition]; + note.Y = _noteStartY; + } + + // hit effect timing + + _hitEffectStartTime = new List(); + _hitEffectT = new List(); + for (int i = 0; i < 5; ++i) + { + _hitEffectStartTime.Add(0); + _hitEffectT.Add(0); + } + + _isPreviewing = true; + } + + public void RenderFrameBlocked(int songTime) + { + ComputeFrame(songTime); + + Dispatcher.Invoke(new Action(InvalidateVisual)); + _renderCompleteHandle.WaitOne(); + } + + public void Stop() + { + _isPreviewing = false; + } + + private void NoteHit(int position, int songTime) + { + _hitEffectStartTime[position] = songTime; + } + + #region Brushes and Pens + + private static readonly SolidColorBrush NoteStroke = + new SolidColorBrush(Color.FromRgb(0x22, 0x22, 0x22)); + + private static readonly SolidColorBrush NoteShapeOutlineFill = Brushes.White; + + private static readonly SolidColorBrush NormalNoteShapeStroke = + new SolidColorBrush(Color.FromRgb(0xFF, 0x33, 0x66)); + + private static readonly LinearGradientBrush NormalNoteShapeFill = + new LinearGradientBrush(Color.FromRgb(0xFF, 0x33, 0x66), Color.FromRgb(0xFF, 0x99, 0xBB), 90.0); + + private static readonly SolidColorBrush HoldNoteShapeStroke = + new SolidColorBrush(Color.FromRgb(0xFF, 0xBB, 0x22)); + + private static readonly LinearGradientBrush HoldNoteShapeFillOuter = + new LinearGradientBrush(Color.FromRgb(0xFF, 0xBB, 0x22), Color.FromRgb(0xFF, 0xDD, 0x66), 90.0); + + private static readonly SolidColorBrush HoldNoteShapeFillInner = Brushes.White; + + private static readonly SolidColorBrush FlickNoteShapeStroke = + new SolidColorBrush(Color.FromRgb(0x22, 0x55, 0xBB)); + + private static readonly LinearGradientBrush FlickNoteShapeFillOuter = + new LinearGradientBrush(Color.FromRgb(0x22, 0x55, 0xBB), Color.FromRgb(0x88, 0xBB, 0xFF), 90.0); + + private static readonly SolidColorBrush FlickNoteShapeFillInner = Brushes.White; + + private static readonly Pen NoteStrokePen = new Pen(NoteStroke, 1.5); + + private static readonly Pen NormalNoteShapeStrokePen = new Pen(NormalNoteShapeStroke, 1); + + private static readonly Pen HoldNoteShapeStrokePen = new Pen(HoldNoteShapeStroke, 1); + + private static readonly Pen FlickNoteShapeStrokePen = new Pen(FlickNoteShapeStroke, 1); + + private static readonly Pen GridPen = new Pen(Brushes.DarkGray, 1); + + private static readonly Brush RelationBrush = Application.Current.FindResource(App.ResourceKeys.RelationBorderBrush); + + private static readonly Pen[] LinePens = + { + new Pen(RelationBrush, 16), // hold line + new Pen(RelationBrush, 2), // sync line + new Pen(RelationBrush, 8) // group line + }; + + private static readonly Geometry LeftNoteOuterGeometry = + Geometry.Parse("M -6,15 A 24,24 0 0 1 15,0 A 15,15 0 0 1 15,30 A 24,24 0 0 1 -6,15 Z"); + private static readonly Geometry LeftNoteInnerGeometry = + Geometry.Parse("M -6,15 C 6,-2 20,-2 15,15 C 21,32 6,32 -6,15 Z"); + + private static readonly Geometry RightNoteOuterGeometry = + Geometry.Parse("M 36,15 A 24,24 0 0 0 15,0 A 15,15 0 0 0 15,30 A 24,24 0 0 0 36,15 Z"); + private static readonly Geometry RightNoteInnerGeometry = + Geometry.Parse("M 36,15 C 24,-2 16,-2 15,15 C 16,32 24,32 36,15 Z"); + + private static readonly ScaleTransform FlickNoteOuterScale = new ScaleTransform(0.722, 0.722, 15, 15); + private static readonly ScaleTransform FlickNoteInnerScale = new ScaleTransform(0.5714, 0.5714, 15, 15); + + private static readonly SolidColorBrush HitEffectBrush = Brushes.Gold; + + #endregion + + #region Computation and Positions + + private void SetNotePosition(DrawingNote note, double t) + { + note.Y = _noteStartY + t * (_noteEndY - _noteStartY); + } + + private void ComputeFrame(int songTime) + { + var headUpdated = false; + for (int i = _notesHead; i < _notes.Count; ++i) + { + var note = _notes[i]; + + /* + * diff < 0 --- not shown + * diff == 0 --- begin to show + * diff == approachTime --- arrive at end + * diff > approachTime --- ended + */ + var diff = songTime - note.Timing + _approachTime; + if (diff < 0) + break; + if (note.Done) + continue; + + _notesTail = i; + + var t = diff / _approachTime; + if (t > 1) + t = 1; + + note.LastT = t; + SetNotePosition(note, t); + + // note arrive at bottom + if (diff > _approachTime) + { + if (!note.EffectShown) + { + NoteHit(note.HitPosition, songTime); + note.EffectShown = true; + } + + // Hit and flick notes end immediately + // Hold note heads end after its duration + if (!note.IsHoldStart || diff > _approachTime + note.Duration) + { + note.Done = true; + } + } + + // update head to be the first note that is not done + if (!note.Done && !headUpdated) + { + _notesHead = i; + headUpdated = true; + } + } + + // Update hit effects + for (int i = 0; i < 5; ++i) + { + if (_hitEffectStartTime[i] == 0) + continue; + + var diff = songTime - _hitEffectStartTime[i]; + if (diff <= HitEffectMilliseconds) + { + _hitEffectT[i] = 1 - diff/HitEffectMilliseconds; + } + else + { + _hitEffectT[i] = 0; + } + } + } + + #endregion + + #region Rendering + + private void DrawGrid(DrawingContext dc) + { + for (int i = 0; i < 5; ++i) + { + dc.DrawLine(GridPen, _startPoints[i], _endPoints[i]); + } + } + + private void DrawLines(DrawingContext dc) + { + for (int i = _notesHead; i <= _notesTail; ++i) + { + var note = _notes[i]; + + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (note.LastT == 0 || note.Done) + continue; + + // Hold line + if (note.IsHoldStart) + { + dc.DrawLine(LinePens[0], new Point(note.X, note.Y), new Point(note.HoldTarget.X, note.HoldTarget.Y)); + } + + // Sync line + // check LastT so that when HoldNote arrives the line is gone + if (note.SyncTarget != null && note.LastT < 1) + { + dc.DrawLine(LinePens[1], new Point(note.X, note.Y), new Point(note.SyncTarget.X, note.SyncTarget.Y)); + } + + // Flicker line + if (note.GroupTarget != null && note.LastT < 1) + { + dc.DrawLine(LinePens[2], new Point(note.X, note.Y), new Point(note.GroupTarget.X, note.GroupTarget.Y)); + } + } + } + + private void DrawNotes(DrawingContext dc) + { + for (int i = _notesHead; i <= _notesTail; ++i) + { + var note = _notes[i]; + if (note.Done) + continue; + + var center = new Point(note.X, note.Y); + + + switch (note.DrawType) + { + case 0: + dc.DrawEllipse(NoteShapeOutlineFill, NoteStrokePen, center, NoteRadius, NoteRadius); + dc.DrawEllipse(NormalNoteShapeFill, NormalNoteShapeStrokePen, center, NoteRadius - 4, NoteRadius - 4); + break; + case 1: + dc.PushTransform(new TranslateTransform(note.X - 15, note.Y - 15)); + dc.DrawGeometry(NoteShapeOutlineFill, NoteStrokePen, LeftNoteOuterGeometry); + dc.PushTransform(FlickNoteOuterScale); + dc.DrawGeometry(FlickNoteShapeFillOuter, FlickNoteShapeStrokePen, LeftNoteOuterGeometry); + dc.Pop(); + dc.PushTransform(FlickNoteInnerScale); + dc.DrawGeometry(FlickNoteShapeFillInner, null, LeftNoteInnerGeometry); + dc.Pop(); + dc.Pop(); + break; + case 2: + dc.PushTransform(new TranslateTransform(note.X - 15, note.Y - 15)); + dc.DrawGeometry(NoteShapeOutlineFill, NoteStrokePen, RightNoteOuterGeometry); + dc.PushTransform(FlickNoteOuterScale); + dc.DrawGeometry(FlickNoteShapeFillOuter, FlickNoteShapeStrokePen, RightNoteOuterGeometry); + dc.Pop(); + dc.PushTransform(FlickNoteInnerScale); + dc.DrawGeometry(FlickNoteShapeFillInner, null, RightNoteInnerGeometry); + dc.Pop(); + dc.Pop(); + break; + case 3: + dc.DrawEllipse(NoteShapeOutlineFill, NoteStrokePen, center, NoteRadius, NoteRadius); + dc.DrawEllipse(HoldNoteShapeFillOuter, HoldNoteShapeStrokePen, center, NoteRadius - 4, NoteRadius - 4); + dc.DrawEllipse(HoldNoteShapeFillInner, null, center, NoteRadius - 10, NoteRadius - 10); + break; + } + } + } + + private void DrawEffect(DrawingContext dc) + { + for (int i = 0; i < 5; ++i) + { + var t = _hitEffectT[i]; + + // ReSharper disable once CompareOfFloatsByEqualityOperator + if (t == 0) + continue; + + dc.PushOpacity(t); + dc.DrawEllipse(HitEffectBrush, null, _endPoints[i], NoteSize, NoteSize); + dc.Pop(); + } + } + + protected override void OnRender(DrawingContext dc) + { + _renderCompleteHandle.Reset(); + + base.OnRender(dc); + + if (!_isPreviewing) + { + _renderCompleteHandle.Set(); + return; + } + + DrawGrid(dc); + DrawLines(dc); + DrawEffect(dc); + DrawNotes(dc); + + _renderCompleteHandle.Set(); + } + + #endregion + } +} diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/Primitives/SimpleScoreNote.xaml b/DereTore.Applications.StarlightDirector/UI/Controls/Primitives/SimpleScoreNote.xaml deleted file mode 100644 index 54d8da1..0000000 --- a/DereTore.Applications.StarlightDirector/UI/Controls/Primitives/SimpleScoreNote.xaml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/Primitives/SimpleScoreNote.xaml.cs b/DereTore.Applications.StarlightDirector/UI/Controls/Primitives/SimpleScoreNote.xaml.cs deleted file mode 100644 index e9ceb87..0000000 --- a/DereTore.Applications.StarlightDirector/UI/Controls/Primitives/SimpleScoreNote.xaml.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -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 DereTore.Applications.StarlightDirector.Entities; - -namespace DereTore.Applications.StarlightDirector.UI.Controls.Primitives -{ - /// - /// Interaction logic for SimpleScoreNote.xaml - /// - public partial class SimpleScoreNote : UserControl - { - public static readonly DependencyProperty NoteProperty = DependencyProperty.Register("Note", typeof(Note), typeof(SimpleScoreNote), new PropertyMetadata(default(Note))); - - public SimpleScoreNote() - { - InitializeComponent(); - } - - public Note Note - { - get { return (Note) GetValue(NoteProperty); } - set { SetValue(NoteProperty, value); } - } - } -} diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml b/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml index 8c0db31..92ec8f9 100644 --- a/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml +++ b/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml @@ -7,7 +7,6 @@ mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300" Background="Transparent"> - - + diff --git a/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs b/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs index d584a5d..e15e7a9 100644 --- a/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs +++ b/DereTore.Applications.StarlightDirector/UI/Controls/ScorePreviewer.xaml.cs @@ -4,15 +4,10 @@ using System.Threading; using System.Threading.Tasks; using System.Windows; -using System.Windows.Controls; -using System.Windows.Media; -using System.Windows.Shapes; -using DereTore.Applications.StarlightDirector.Extensions; using DereTore.Applications.StarlightDirector.Entities; -using DereTore.Applications.StarlightDirector.UI.Controls.Primitives; using System.Linq; +using DereTore.Applications.StarlightDirector.UI.Controls.Models; using DereTore.Applications.StarlightDirector.UI.Windows; -using LineTuple = System.Tuple; namespace DereTore.Applications.StarlightDirector.UI.Controls { @@ -21,69 +16,24 @@ namespace DereTore.Applications.StarlightDirector.UI.Controls /// public partial class ScorePreviewer { - /// - /// Internal representation of notes for drawing - /// - private class SimpleNote - { - public Note Note { get; set; } - public SimpleNote HoldTarget { get; set; } - public SimpleNote SyncTarget { get; set; } - public SimpleNote GroupTarget { get; set; } - public int NoteId { get; set; } - public int Timing { get; set; } - public bool Done { get; set; } - public int Duration { get; set; } - public bool IsHoldStart { get; set; } - public double LastT { get; set; } - public double X { get; set; } - public double Y { get; set; } - public SimpleScoreNote ScoreNote { get; set; } - public Line HoldLine { get; set; } - public Line GroupLine { get; set; } - public Line SyncLine { get; set; } - } - - private const double NoteMarginTop = 20; - private const double NoteMarginBottom = 60; - private const double NoteXBetween = 80; - private const double NoteSize = 30; - private const double NoteRadius = NoteSize / 2; - private const double HoldLineThickness = 16; - private const double SyncLineThickness = 8; - private const double GridLineThickness = 1; // used by frame update thread private Score _score; private volatile bool _isPreviewing; private Task _task; - private readonly List _notes = new List(); + private readonly List _notes = new List(); private double _targetFps; private int _startTime; - private double _approachTime; - // WPF new object performance is horrible, so we use pools to reuse them - private readonly Queue _scoreNotePool = new Queue(); - private readonly Queue _linePool = new Queue(); - - // screen positions - private double _noteStartY; - private double _noteEndY; - private List _noteX; - private double _lineOffset; - - private readonly Brush _relationBrush = Application.Current.FindResource(App.ResourceKeys.RelationBorderBrush); - private readonly Brush _gridBrush = Brushes.DarkGray; + // music time fixing + private int _lastMusicTime; + private int _lastComputedSongTime; + private DateTime _lastFrameEndtime; // window-related private readonly MainWindow _window; private bool _shouldPlayMusic; - // note hit effect! - private int _noteHitCounter; - private int _noteHitMax; - private Line _effectedLine; - public ScorePreviewer() { InitializeComponent(); @@ -93,18 +43,14 @@ public ScorePreviewer() public void BeginPreview(Score score, double targetFps, int startTime, double approachTime) { // setup parameters + _score = score; _isPreviewing = true; _targetFps = targetFps; _startTime = startTime; - _approachTime = approachTime; - - _noteX = new List(); - for (int i = 0; i < 5; ++i) - _noteX.Add(0); - ComputePositions(); // prepare notes + foreach (var note in _score.Notes) { // can I draw it? @@ -119,17 +65,16 @@ public void BeginPreview(Score score, double targetFps, int startTime, double ap if (pos == 0) continue; - var snote = new SimpleNote + var snote = new DrawingNote { Note = note, - NoteId = note.ID, Done = false, Duration = 0, IsHoldStart = note.IsHoldStart, Timing = (int) (note.HitTiming*1000), LastT = 0, - X = _noteX[pos - 1], - Y = _noteStartY + HitPosition = pos - 1, + DrawType = (note.IsTap && !note.IsHoldEnd) || note.IsFlick ? (int)note.FlickType : 3 }; if (note.IsHoldStart) @@ -143,99 +88,63 @@ public void BeginPreview(Score score, double targetFps, int startTime, double ap _notes.Sort((a, b) => a.Timing - b.Timing); // prepare note relationships + foreach (var snote in _notes) { if (snote.IsHoldStart) { - snote.HoldTarget = _notes.FirstOrDefault(note => note.NoteId == snote.Note.HoldTargetID); + snote.HoldTarget = _notes.FirstOrDefault(note => note.Note.ID == snote.Note.HoldTargetID); } if (snote.Note.HasNextSync) { - snote.SyncTarget = _notes.FirstOrDefault(note => note.NoteId == snote.Note.NextSyncTarget.ID); + snote.SyncTarget = _notes.FirstOrDefault(note => note.Note.ID == snote.Note.NextSyncTarget.ID); } if (snote.Note.HasNextFlick) { - snote.GroupTarget = _notes.FirstOrDefault(note => note.NoteId == snote.Note.NextFlickNoteID); + snote.GroupTarget = _notes.FirstOrDefault(note => note.Note.ID == snote.Note.NextFlickNoteID); } } - // draw some grid lines - DrawGrid(); - // music - _shouldPlayMusic = ShouldPlayMusic(); - _task = new Task(DrawPreviewFrame); - _task.Start(); - } + _shouldPlayMusic = _window != null && _window.MusicLoaded; - public void EndPreview() - { - if (_shouldPlayMusic) - { - StopMusic(); - } + // fix start time - _isPreviewing = false; - } + _startTime -= (int)approachTime; + if (_startTime < 0) + _startTime = 0; - /// - /// Compute where the notes should be according to current window size - /// - private void ComputePositions() - { - _noteStartY = NoteMarginTop; - _noteEndY = PreviewCanvas.ActualHeight - NoteMarginBottom; - _noteX[0] = (PreviewCanvas.ActualWidth - 4*NoteXBetween - 5*NoteSize)/2; - for (int i = 1; i < 5; ++i) - { - _noteX[i] = _noteX[i - 1] + NoteXBetween + NoteSize; - } + // prepare canvas - _lineOffset = NoteRadius; - } + MainCanvas.Initialize(_notes, approachTime); - private void DrawGrid() - { - var lines = new List(); + // go - foreach (var x in _noteX) + if (_shouldPlayMusic) { - lines.Add(new LineTuple(x, _noteStartY, x, _noteEndY)); + StartMusic(_startTime); } - lines.Add(new LineTuple(_noteX[0], _noteStartY, _noteX[4], _noteStartY)); - lines.Add(new LineTuple(_noteX[0], _noteEndY, _noteX[4], _noteEndY)); - lines.Add(new LineTuple(_noteX[0], _noteEndY, _noteX[4], _noteEndY)); - foreach (var line in lines) + _task = new Task(DrawPreviewFrame); + _task.Start(); + } + + public void EndPreview() + { + if (_shouldPlayMusic) { - // after the loop, _effectedLine will be the LAST line - _effectedLine = new Line - { - Stroke = _gridBrush, - StrokeThickness = GridLineThickness, - X1 = line.Item1 + _lineOffset, - Y1 = line.Item2 + _lineOffset, - X2 = line.Item3 + _lineOffset, - Y2 = line.Item4 + _lineOffset - }; - LineCanvas.Children.Add(_effectedLine); + StopMusic(); } - _effectedLine.Stroke = Brushes.Gold; - _effectedLine.Opacity = 0; + _isPreviewing = false; } // These methods invokes the main thread and perform the tasks #region Multithreading Invoke - private bool ShouldPlayMusic() - { - return (bool)Dispatcher.Invoke(new Func(() => _window != null && _window.MusicLoaded)); - } - private void StartMusic(double milliseconds) { Dispatcher.Invoke(new Action(() => _window.PlayMusic(milliseconds))); @@ -246,285 +155,85 @@ private void StopMusic() Dispatcher.Invoke(new Action(() => _window.StopMusic())); } - private readonly Action _setPositionInCanvasAction = - (elem, x, y) => - { - Canvas.SetLeft(elem, x); - Canvas.SetTop(elem, y); - }; - - private SimpleScoreNote CreateScoreNote(SimpleNote note) - { - return Dispatcher.Invoke(new Func(() => - { - if (_scoreNotePool.Count == 0) - { - var scoreNote = new SimpleScoreNote {Note = note.Note}; - PreviewCanvas.Children.Add(scoreNote); - Canvas.SetTop(scoreNote, 0); - Canvas.SetLeft(scoreNote, 0); - return scoreNote; - } - else - { - var scoreNote = _scoreNotePool.Dequeue(); - scoreNote.Note = note.Note; - Canvas.SetTop(scoreNote, 0); - Canvas.SetLeft(scoreNote, 0); - scoreNote.Visibility = Visibility.Visible; - return scoreNote; - } - })) as SimpleScoreNote; - } - - private void SetPositionInCanvas(UIElement elem, double x, double y) - { - Dispatcher.Invoke(_setPositionInCanvasAction, elem, x, y); - } - - private void ReleaseScoreNote(SimpleScoreNote scoreNote) - { - Dispatcher.Invoke(new Action(() => - { - scoreNote.Visibility = Visibility.Collapsed; - _scoreNotePool.Enqueue(scoreNote); - })); - } - - private void ReleaseLine(Line line) - { - if (line == null) - return; - - Dispatcher.Invoke(new Action(() => - { - line.Visibility = Visibility.Collapsed; - _linePool.Enqueue(line); - })); - } - - private void ClearCanvases() - { - Dispatcher.Invoke(new Action(() => - { - PreviewCanvas.Children.Clear(); - LineCanvas.Children.Clear(); - })); - } - #endregion - /// - /// Helper used by DrawLines. - /// - private Line CreateLineOnCurrentThread(double thickness) - { - Line line; - if (_linePool.Count == 0) - { - line = new Line - { - Stroke = _relationBrush, - StrokeThickness = thickness - }; - LineCanvas.Children.Add(line); - } - else - { - line = _linePool.Dequeue(); - line.Visibility = Visibility.Visible; - line.StrokeThickness = thickness; - } - - return line; - } - - /// - /// Draw lines. MUST BE CALLED ON MAIN THREAD - /// - private void DrawLines() - { - foreach (var note in _notes) - { - // ReSharper disable once CompareOfFloatsByEqualityOperator - if (note.LastT == 0 || note.Done) - continue; - - // Hold line - if (note.IsHoldStart) - { - if (note.HoldLine == null) - { - note.HoldLine = CreateLineOnCurrentThread(HoldLineThickness); - } - - note.HoldLine.X1 = note.HoldLine.X2 = note.X + _lineOffset; - note.HoldLine.Y1 = note.Y + _lineOffset; - note.HoldLine.Y2 = note.HoldTarget.Y + _lineOffset; - } - - // Sync line - // check LastT so that when HitNote arrives the line is gone - if (note.SyncTarget != null && note.LastT < 1) - { - if (note.SyncLine == null) - { - note.SyncLine = CreateLineOnCurrentThread(SyncLineThickness); - } - - note.SyncLine.X1 = note.X + _lineOffset; - note.SyncLine.Y1 = note.SyncLine.Y2 = note.Y + _lineOffset; - note.SyncLine.X2 = note.SyncTarget.X + _lineOffset; - } - - // Flicker line - if (note.GroupTarget != null && note.LastT < 1) - { - if (note.GroupLine == null) - { - note.GroupLine = CreateLineOnCurrentThread(SyncLineThickness); - } - - note.GroupLine.X1 = note.X + _lineOffset; - note.GroupLine.Y1 = note.Y + _lineOffset; - note.GroupLine.X2 = note.GroupTarget.X + _lineOffset; - note.GroupLine.Y2 = note.GroupTarget.Y + _lineOffset; - } - } - } - /// /// Running in a background thread, refresh the locations of notes periodically. It tries to keep the target frame rate. /// private void DrawPreviewFrame() { - // computation parameters - var targetFrameTime = 1000 / _targetFps; - _noteHitMax = (int) (_targetFps / 5); // effect lasts for 0.2 second + // frame rate + double targetFrameTime = 0; + if (_targetFps < Double.MaxValue) + { + targetFrameTime = 1000/_targetFps; + } - // fix start time - _startTime -= (int)_approachTime; - if (_startTime < 0) - _startTime = 0; + MainCanvas.HitEffectMilliseconds = 200; // drawing and timing var startTime = DateTime.UtcNow; - var notesHead = 0; - - if (_shouldPlayMusic) - { - StartMusic(_startTime); - } while (true) { if (!_isPreviewing) { - ClearCanvases(); - - _linePool.Clear(); + MainCanvas.Stop(); _notes.Clear(); - _scoreNotePool.Clear(); return; } var frameStartTime = DateTime.UtcNow; + // compute time + int songTime; if (_shouldPlayMusic) { - songTime = (int)_window.MusicTime().TotalMilliseconds; + var time = _window.MusicTime(); + if (time == Double.MaxValue) + { + EndPreview(); + continue; + } + + songTime = (int)time; + if (songTime > 0 && songTime == _lastMusicTime) + { + // music time not updated, add frame time + _lastComputedSongTime += (int) (frameStartTime - _lastFrameEndtime).TotalMilliseconds; + songTime = _lastComputedSongTime; + } + else + { + // music time updated + _lastComputedSongTime = songTime; + _lastMusicTime = songTime; + } } else { songTime = (int)(frameStartTime - startTime).TotalMilliseconds + _startTime; } - bool headUpdated = false; - for (int i = notesHead; i < _notes.Count; ++i) - { - var note = _notes[i]; - - /* - * diff < 0 --- not shown - * diff == 0 --- begin to show - * diff == approachTime --- arrive at end - * diff > approachTime --- ended - */ - var diff = songTime - note.Timing + _approachTime; - if (diff < 0) - break; - if (note.Done) - continue; - - if (note.ScoreNote == null) - { - note.ScoreNote = CreateScoreNote(note); - } - - var t = diff/ _approachTime; - if (t > 1) - t = 1; + // wait for rendering - note.LastT = t; + MainCanvas.RenderFrameBlocked(songTime); - // change this line if you want game-like approaching path - note.Y = _noteStartY + t*(_noteEndY - _noteStartY); - SetPositionInCanvas(note.ScoreNote, note.X, note.Y); + // wait for next frame - // note arrive at bottom - if (diff > _approachTime) + _lastFrameEndtime = DateTime.UtcNow; + if (targetFrameTime > 0) + { + var frameEllapsedTime = (_lastFrameEndtime - frameStartTime).TotalMilliseconds; + if (frameEllapsedTime < targetFrameTime) { - ReleaseLine(note.SyncLine); - ReleaseLine(note.GroupLine); - note.SyncLine = null; - note.GroupLine = null; - - // Hit and flick notes end immediately - // Hold note heads end after its duration - if (!note.IsHoldStart || diff > _approachTime + note.Duration) - { - ReleaseScoreNote(note.ScoreNote); - ReleaseLine(note.HoldLine); - note.ScoreNote = null; - note.HoldLine = null; - - note.Done = true; - - // note hit effect - _noteHitCounter = _noteHitMax; - } - - if (!headUpdated) - { - notesHead = i; - headUpdated = true; - } + Thread.Sleep((int)(targetFrameTime - frameEllapsedTime)); } - } - - // Draw sync, group, hold lines - Dispatcher.Invoke(new Action(DrawLines)); - - // Do note hit effect - if (_noteHitCounter >= 0) - { - Dispatcher.Invoke(new Action(() => + else { - double et = _noteHitCounter/(double) _noteHitMax; // from 1 to 0 - _effectedLine.Opacity = et; - })); - --_noteHitCounter; - } - - var frameEllapsedTime = (DateTime.UtcNow - frameStartTime).TotalMilliseconds; - if (frameEllapsedTime < targetFrameTime) - { - Thread.Sleep((int) (targetFrameTime - frameEllapsedTime)); - } - else - { - Debug.WriteLine($"[Warning] Frame ellapsed time {frameEllapsedTime:N2} exceeds target."); + Debug.WriteLine($"[Warning] Frame ellapsed time {frameEllapsedTime:N2} exceeds target."); + } } } } diff --git a/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.Commands.Music.cs b/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.Commands.Music.cs index 9840527..1423088 100644 --- a/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.Commands.Music.cs +++ b/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.Commands.Music.cs @@ -108,9 +108,9 @@ internal void StopMusic() CmdMusicStop.RaiseCanExecuteChanged(); } - internal TimeSpan MusicTime() + internal double MusicTime() { - return _waveReader.CurrentTime; + return _waveReader?.CurrentTime.TotalMilliseconds ?? Double.MaxValue; } private AudioOut _selectedWaveOut; diff --git a/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.EventHandlers.cs b/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.EventHandlers.cs index 464c9bf..f49ce11 100644 --- a/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.EventHandlers.cs +++ b/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.EventHandlers.cs @@ -110,6 +110,13 @@ private void PreviewFpsComboBoxItem_Selected(object sender, System.Windows.Route { var item = e.OriginalSource as ComboBoxItem; double fps; + var s = item?.Content?.ToString(); + if (s == "Unlimited") + { + PreviewFps = Double.MaxValue; + return; + } + if (Double.TryParse(item?.Content?.ToString(), out fps)) { PreviewFps = fps; diff --git a/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.xaml b/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.xaml index 161f4bc..6260111 100644 --- a/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.xaml +++ b/DereTore.Applications.StarlightDirector/UI/Windows/MainWindow.xaml @@ -178,9 +178,12 @@ - + + + +