Good day,
over the past few weeks, I have been working on a project to create an advanced metronome. The metronome consists of the following things:
- swivel console
- light flash
- a set of dynamically created user controls that represent bits (4 of them that are either on, accented, or off).
- user control that displays a liquid crystal digital display and calculates the number of milliseconds between bits for the selected BPM (60000 / BPM = milliseconds)
the user selects BPM and presses the start, and the following happens:
- the lever swings between two angles at a speed of n milliseconds per scan
- light flashes at the end of each sweep of the hand
- indicators are created and they flash sequentially (one at the end of each sweep).
Now the problem is the animation "Hand" and "Light flash" are created in the code and added to the panel with repetition forever and are automatically reversed.
indicators are created in the code and should trigger an event at the end of each sweep animation.
So, what I did after multithreading created a timer that works at the same pace as the storyboard.
the problem, for more than 30 seconds, the timer and storyboard fail, so the indicators and the sweep of the hand do not have time (not good for the metronome !!).
I tried to catch the completed animation event and use it as a trigger to stop and restart the timer, that was all I could think of to keep two in perfect sync.
turning off the synchronization is due to the sliding of the storyboard and the fact that the storyboard is called from the beginning on the line before the timer is called using .start, although microseconds, I think, they start incredibly close, but not exactly in the same time.
my question is, when I try to bind to a completed animation event, it never fires. I got the impression that he even completed the fires regardless of the auto-reverse (that is, between each iteration). this is not true?
can anyone think of another (trickier) way to synchronize two things.
Finally, I really looked if I could start the method from the storyboard (which would make my life very easy, but it would turn out that this could not be done).
If there are any suggestions that I donβt appreciate, I just want it to end !!
the end point of interest, bpm can be adjusted while the metronome is running, this is achieved by calculating the millisecond duration on the fly (clicking the mouse button) and scaling the storyboard to the difference between the current speed and the new speed. it is obvious that the timer that starts the indicators must be changed at the same time (using the interval).
below is the code of my project (not XAML only C #)
using System; using System.Collections.Generic; using System.Windows; using System.Windows.Input; using System.Windows.Media.Animation; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Controls; using System.Windows.Threading; namespace MetronomeLibrary { public partial class MetronomeLarge { private bool Running; //Speed and time signature private int _bpm = 60; private int _beats = 4; private int _beatUnit = 4; private int _currentBeat = 1; private readonly int _baseSpeed = 60000 / 60; private readonly DispatcherTimer BeatTimer = new DispatcherTimer(); private Storyboard storyboard = new Storyboard(); public MetronomeLarge() { InitializeComponent(); NumericDisplay.Value = BPM; BeatTimer.Tick += new EventHandler(TimerTick); SetUpAnimation(); SetUpIndicators(); } public int Beats { get { return _beats; } set { _beats = value; SetUpIndicators(); } } public int BPM { get { return _bpm; } set { _bpm = value; //Scale the story board here SetSpeedRatio(); } } public int BeatUnit { get { return _beatUnit; } set { _beatUnit = value; } } private void SetSpeedRatio() { //divide the new speed (bpm by the old speed to get the new ratio) float newMilliseconds = (60000 / BPM); float newRatio = _baseSpeed / newMilliseconds; storyboard.SetSpeedRatio(newRatio); //Set the beat timer according to the beattype (standard is quarter beats for one sweep of the metronome BeatTimer.Interval = TimeSpan.FromMilliseconds(newMilliseconds); } private void TimerTick(object sender, EventArgs e) { MetronomeBeat(_currentBeat); _currentBeat++; if (_currentBeat > Beats) { _currentBeat = 1; } } private void MetronomeBeat(int Beat) { //turnoff all indicators TurnOffAllIndicators(); //Find a control by name MetronomeLargeIndicator theIndicator = (MetronomeLargeIndicator)gridContainer.Children[Beat-1]; //illuminate the control theIndicator.TurnOn(); theIndicator.PlaySound(); } private void TurnOffAllIndicators() { for (int i = 0; i <= gridContainer.Children.Count-1; i++) { MetronomeLargeIndicator theIndicator = (MetronomeLargeIndicator)gridContainer.Children[i]; theIndicator.TurnOff(); } } private void SetUpIndicators() { gridContainer.Children.Clear(); gridContainer.ColumnDefinitions.Clear(); for (int i = 1; i <= _beats; i++) { MetronomeLargeIndicator theNewIndicator = new MetronomeLargeIndicator(); ColumnDefinition newCol = new ColumnDefinition() { Width = GridLength.Auto }; gridContainer.ColumnDefinitions.Add(newCol); gridContainer.Children.Add(theNewIndicator); theNewIndicator.Name = "Indicator" + i.ToString(); Grid.SetColumn(theNewIndicator, i - 1); } } private void DisplayOverlay_MouseDown(object sender, MouseButtonEventArgs e) { ToggleAnimation(); } private void ToggleAnimation() { if (Running) { //stop the animation ((Storyboard)Resources["Storyboard"]).Stop() ; BeatTimer.Stop(); } else { //start the animation BeatTimer.Start(); ((Storyboard)Resources["Storyboard"]).Begin(); SetSpeedRatio(); } Running = !Running; } private void ButtonIncrement_Click(object sender, RoutedEventArgs e) { NumericDisplay.Value++; BPM = NumericDisplay.Value; } private void ButtonDecrement_Click(object sender, RoutedEventArgs e) { NumericDisplay.Value--; BPM = NumericDisplay.Value; } private void ButtonIncrement_MouseEnter(object sender, MouseEventArgs e) { ImageBrush theBrush = new ImageBrush() { ImageSource = new BitmapImage(new Uri(@"pack://application:,,,/MetronomeLibrary;component/Images/pad-metronome-increment-button-over.png")) }; ButtonIncrement.Background = theBrush; } private void ButtonIncrement_MouseLeave(object sender, MouseEventArgs e) { ImageBrush theBrush = new ImageBrush() { ImageSource = new BitmapImage(new Uri(@"pack://application:,,,/MetronomeLibrary;component/Images/pad-metronome-increment-button.png")) }; ButtonIncrement.Background = theBrush; } private void ButtonDecrement_MouseEnter(object sender, MouseEventArgs e) { ImageBrush theBrush = new ImageBrush() { ImageSource = new BitmapImage(new Uri(@"pack://application:,,,/MetronomeLibrary;component/Images/pad-metronome-decrement-button-over.png")) }; ButtonDecrement.Background = theBrush; } private void ButtonDecrement_MouseLeave(object sender, MouseEventArgs e) { ImageBrush theBrush = new ImageBrush() { ImageSource = new BitmapImage(new Uri(@"pack://application:,,,/MetronomeLibrary;component/Images/pad-metronome-decrement-button.png")) }; ButtonDecrement.Background = theBrush; } private void SweepComplete(object sender, EventArgs e) { BeatTimer.Stop(); BeatTimer.Start(); } private void SetUpAnimation() { NameScope.SetNameScope(this, new NameScope()); RegisterName(Arm.Name, Arm); DoubleAnimation animationRotation = new DoubleAnimation() { From = -17, To = 17, Duration = new Duration(TimeSpan.FromMilliseconds(NumericDisplay.Milliseconds)), RepeatBehavior = RepeatBehavior.Forever, AccelerationRatio = 0.3, DecelerationRatio = 0.3, AutoReverse = true, }; Timeline.SetDesiredFrameRate(animationRotation, 90); MetronomeFlash.Opacity = 0; DoubleAnimation opacityAnimation = new DoubleAnimation() { From = 1.0, To = 0.0, AccelerationRatio = 1, BeginTime = TimeSpan.FromMilliseconds(NumericDisplay.Milliseconds - 0.5), Duration = new Duration(TimeSpan.FromMilliseconds(100)), }; Timeline.SetDesiredFrameRate(opacityAnimation, 10); storyboard.Duration = new Duration(TimeSpan.FromMilliseconds(NumericDisplay.Milliseconds * 2)); storyboard.RepeatBehavior = RepeatBehavior.Forever; Storyboard.SetTarget(animationRotation, Arm); Storyboard.SetTargetProperty(animationRotation, new PropertyPath("(UIElement.RenderTransform).(RotateTransform.Angle)")); Storyboard.SetTarget(opacityAnimation, MetronomeFlash); Storyboard.SetTargetProperty(opacityAnimation, new PropertyPath("Opacity")); storyboard.Children.Add(animationRotation); storyboard.Children.Add(opacityAnimation); Resources.Add("Storyboard", storyboard); } } }