Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Options page and time to next note indicator added #55

Merged
merged 11 commits into from
Nov 6, 2020
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ public MarkablePlayback(IEnumerable<ITimedObject> timedObjects, TempoMap tempoMa
/// </summary>
public bool IsRunning => this.clock.IsRunning;

/// <summary>
/// Gets the current playback time.
/// </summary>
public TimeSpan CurrentTime => this.clock.CurrentTime;

/// <summary>
/// Gets or sets a value indicating whether currently playing notes must be stopped on playback stop or not.
/// </summary>
Expand Down
54 changes: 46 additions & 8 deletions ProjectCoimbra.UWP/Project.Coimbra.Midi/MidiEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace Coimbra.Midi
using System.Threading;
using System.Threading.Tasks;
using Coimbra.DryWetMidiIntegration;
using Coimbra.Midi.Models;
using Coimbra.OSIntegration;
using Melanchall.DryWetMidi.Common;
using Melanchall.DryWetMidi.Core;
Expand Down Expand Up @@ -74,8 +75,9 @@ private MidiEngine()
/// The render current notes delegate.
/// </summary>
/// <param name="queue">The queue of markable playback events.</param>
/// <param name="currentTime">The current time of playback.</param>
/// <returns>An asynchronous task.</returns>
public delegate Task RenderCurrentNotesAsync(ConcurrentQueue<MarkablePlaybackEvent>[] queue);
public delegate Task RenderCurrentNotesAsync(ConcurrentQueue<MarkablePlaybackEvent>[] queue, TimeSpan currentTime);

/// <summary>
/// The output device sent event.
Expand All @@ -95,7 +97,7 @@ private MidiEngine()
/// <summary>
/// The playback finished.
/// </summary>
public event EventHandler<EventArgs> PlaybackFinished;
public event EventHandler<EventArgs> PlaybackFinished;

/// <summary>
/// Gets or sets the MIDI file.
Expand All @@ -115,8 +117,8 @@ private MidiEngine()
/// <summary>
/// Gets the set of instruments associated with the MIDI file.
/// </summary>
public IDictionary<FourBitNumber, ICollection<SevenBitNumber>> Instruments { get; } =
new Dictionary<FourBitNumber, ICollection<SevenBitNumber>>();
public IDictionary<FourBitNumber, InstrumentInfo> Instruments { get; } =
new Dictionary<FourBitNumber, InstrumentInfo>();

/// <summary>
/// Gets the set of pitches associated with the selected instrument.
Expand All @@ -130,6 +132,35 @@ public List<string> RetrievePitchesForInstrument(FourBitNumber instrument) =>
.Distinct()
.ToList();

/// <summary>
/// Gets the play times of notes associated with the selected instrument.
/// </summary>
/// <param name="instrument">Instrument to retrieve note times for.</param>
/// <returns>Note times for instrument.</returns>
public List<long> RetrieveNoteTimesForInstrument(FourBitNumber instrument)
{
var midiMap = this.midi.GetTempoMap();
return this.midi.GetNotes()
.Where(note => note.Channel == instrument)
.Select(note => ((MetricTimeSpan)note.TimeAs(TimeSpanType.Metric, midiMap)).TotalMicroseconds / 1000)
.Distinct()
.ToList();
}

/// <summary>
/// Gets how many notes are there for each instrument.
/// </summary>
/// <returns>Note counts of each instrument.</returns>
public Dictionary<FourBitNumber, int> RetrieveNoteCountsOfInstruments() =>
this.midi.GetNotes()
.GroupBy(note => note.Channel)
.Select(group => new
{
Channel = group.Key,
Count = group.Count()
})
.ToDictionary(channelAndNoteCount => channelAndNoteCount.Channel, channelAndNoteCount => channelAndNoteCount.Count);

/// <summary>
/// Called when parsing a file.
/// </summary>
Expand Down Expand Up @@ -165,6 +196,7 @@ public void Start()
this.startThread = new Task(() => this.playback.Start(), this.startThreadCancellationToken.Token);

this.startTimer = new Timer(_ => this.startThread.Start(), null, TimeSpan.Zero, TimeSpan.Zero);
this.isStopped = false;
}

/// <summary>
Expand Down Expand Up @@ -248,6 +280,9 @@ private static string GetTrackDisplayName(StorageFile file, MidiFile midi)

private void AddInstruments(MidiFile midi)
{
this.Instruments.Clear();
var noteCountsOfInstrument = this.RetrieveNoteCountsOfInstruments();

var timedEvents = midi.GetTimedEvents();
var events = timedEvents.Select(e => e.Event).OfType<ProgramChangeEvent>().ToList<ChannelEvent>();

Expand All @@ -270,14 +305,16 @@ private void AddInstruments(MidiFile midi)

if (this.Instruments.ContainsKey(currentEvent.Channel))
{
if (!this.Instruments[currentEvent.Channel].Contains(programNumber))
if (!this.Instruments[currentEvent.Channel].ProgramNumbers.Contains(programNumber))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might be able to use TryAdd here although I'm not sure it's available for the version of .NET we're using.

{
this.Instruments[currentEvent.Channel].Add(programNumber);
this.Instruments[currentEvent.Channel].ProgramNumbers.Add(programNumber);
}
}
else
{
this.Instruments[currentEvent.Channel] = new List<SevenBitNumber> { programNumber };
this.Instruments[currentEvent.Channel] =
new InstrumentInfo(currentEvent.Channel, new List<SevenBitNumber> { programNumber },
noteCountsOfInstrument.ContainsKey(currentEvent.Channel) ? noteCountsOfInstrument[currentEvent.Channel] : 0);
}
}
}
Expand Down Expand Up @@ -323,7 +360,7 @@ private void RenderCurrentNotes()
{
while (!this.isStopped)
{
var task = this.RenderCurrentNotesAsyncEvent?.Invoke(this.notesOnDisplay);
var task = this.RenderCurrentNotesAsyncEvent?.Invoke(this.notesOnDisplay, playback.CurrentTime);
task?.ConfigureAwait(true).GetAwaiter().GetResult();
Thread.Sleep(TimeSpan.FromMilliseconds(200));
}
Expand All @@ -343,6 +380,7 @@ private void Playback_Finished(object sender, EventArgs e)
this.startThreadCancellationToken.Cancel();
this.playback?.Dispose();
this.outputDevice?.Dispose();
this.isStopped = true;

PlaybackFinished?.Invoke(this, null);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Licensed under the MIT License.

namespace Coimbra.Midi.Models
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using Melanchall.DryWetMidi.Common;
using Melanchall.DryWetMidi.Standards;

/// <summary>
/// A class representing an instrument.
/// </summary>
public class InstrumentInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="InstrumentInfo"/> class.
/// </summary>
/// <param name="channel">Channel of the instrument</param>
/// <param name="programNumbers">Program numbers of the instrument</param>
/// <param name="noteCount">Note count of the instrument</param>
public InstrumentInfo(FourBitNumber channel, ICollection<SevenBitNumber> programNumbers, int noteCount)
{
Channel = channel;
ProgramNumbers = programNumbers;
NoteCount = noteCount;
}

/// <summary>
/// A string that contains the name and the note count of the instrument
/// </summary>
public string NameAndNoteCount => string.Format(CultureInfo.CurrentUICulture, "{0} - {1} notes", string.Join(
", ",
ProgramNumbers.Select(d =>
RegularExpression.Replace(
Enum.GetName(typeof(GeneralMidi2Program), (int)d) ?? throw new InvalidOperationException(),
" $1"))), string.Format(CultureInfo.CurrentUICulture, "{0:#,0}", NoteCount));

/// <summary>
/// Channel of the instrument
/// </summary>
public FourBitNumber Channel { get; set; }

/// <summary>
/// Program number of the instrument
/// </summary>
public ICollection<SevenBitNumber> ProgramNumbers { get; set; }

/// <summary>
/// Note count of the instrument
/// </summary>
public int NoteCount { get; set; }

private static readonly Regex RegularExpression = new Regex("(\\B([A-Z]|[0-9]))", RegexOptions.Compiled);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
<Compile Include="DryWetMidiIntegration\NotePlaybackEventMetadata.cs" />
<Compile Include="DryWetMidiIntegration\TrackNotificationEventArgs.cs" />
<Compile Include="MidiEngine.cs" />
<Compile Include="Models\InstrumentInfo.cs" />
<Compile Include="OSIntegration\MediaControls.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<EmbeddedResource Include="Properties\Project.Coimbra.Midi.rd.xml" />
Expand Down
5 changes: 5 additions & 0 deletions ProjectCoimbra.UWP/Project.Coimbra.Model/UserData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,10 @@ public enum Duration
/// Gets or sets the pitchmapper.
/// </summary>
public static PitchMap PitchMap { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the user is changing an option.
/// </summary>
public static bool IsOptionChangeMode { get; set; }
}
}
24 changes: 24 additions & 0 deletions ProjectCoimbra.UWP/Project.Coimbra/Controls/InputControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ public sealed class InputControl : Control, IDisposable
public static readonly DependencyProperty SongTitleProperty =
DependencyProperty.Register("SongTitle", typeof(string), typeof(InputControl), new PropertyMetadata("Title"));

/// <summary>
/// A <see cref="DependencyProperty"/> forming the backing store for the time to next note counter, to enable animation,
/// styling, binding, etc.
/// </summary>
public static readonly DependencyProperty TimeToNextNoteProperty =
DependencyProperty.Register("TimeToNextNote", typeof(string), typeof(InputControl), new PropertyMetadata("TimeToNextNote"));

private readonly IList<Gamepad> gamepads = new List<Gamepad>(1);

private readonly object lockObject = new object();
Expand Down Expand Up @@ -100,6 +107,23 @@ public string SongTitle
set => this.SetValue(SongTitleProperty, value);
}

/// <summary>
/// Gets the song title.
/// </summary>
public string TimeToNextNote
{
get => (string)this.GetValue(TimeToNextNoteProperty);
}

/// <summary>
/// Sets the song title.
/// </summary>
public async void SetTimeToNextNote(string value) => await CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
() =>
{
this.SetValue(TimeToNextNoteProperty, value);
});

/// <summary>
/// Gets or sets the name of the pitch rows.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,14 @@ private void NextButton_Click(object sender, RoutedEventArgs e)
UserData.ActiveDuration = UserData.Duration.ShortDuration;
}

_ = this.Frame.Navigate(typeof(InstrumentsPage), null, new DrillInNavigationTransitionInfo());
if (UserData.IsOptionChangeMode)
{
_ = this.Frame.Navigate(typeof(GamePage), null, new DrillInNavigationTransitionInfo());
}
else
{
_ = this.Frame.Navigate(typeof(InstrumentsPage), null, new DrillInNavigationTransitionInfo());
}
}
}
}
Loading