diff --git a/build/staging/version/BundleInfo.wxi b/build/staging/version/BundleInfo.wxi index c89377f3..8bdde358 100644 --- a/build/staging/version/BundleInfo.wxi +++ b/build/staging/version/BundleInfo.wxi @@ -1,4 +1,4 @@ - + diff --git a/src/api/Abstraction/MidiSrvAbstraction/Midi2.MidiSrv.cpp b/src/api/Abstraction/MidiSrvAbstraction/Midi2.MidiSrv.cpp index c5bce63b..56334c3c 100644 --- a/src/api/Abstraction/MidiSrvAbstraction/Midi2.MidiSrv.cpp +++ b/src/api/Abstraction/MidiSrvAbstraction/Midi2.MidiSrv.cpp @@ -21,7 +21,7 @@ CMidi2MidiSrv::Initialize( TraceLoggingPointer(this, "this") ); - MIDISRV_CLIENTCREATION_PARAMS creationParams{ 0 }; + MIDISRV_CLIENTCREATION_PARAMS creationParams{ }; PMIDISRV_CLIENT client{ nullptr }; wil::unique_rpc_binding bindingHandle; @@ -48,8 +48,13 @@ CMidi2MidiSrv::Initialize( creationParams.Flow = Flow; creationParams.DataFormat = CreationParams->DataFormat; + // Todo: client side buffering requests to come from some service setting? + // - See https://github.com/microsoft/MIDI/issues/219 for details + creationParams.BufferSize = PAGE_SIZE; + //creationParams.BufferSize = 512; // Set this for debugging see https://github.com/microsoft/MIDI/issues/182 for all the drama :) + RETURN_IF_FAILED(GetMidiSrvBindingHandle(&bindingHandle)); diff --git a/src/api/Libs/MidiKs/MidiKs.cpp b/src/api/Libs/MidiKs/MidiKs.cpp index ff39318d..d1f2932f 100644 --- a/src/api/Libs/MidiKs/MidiKs.cpp +++ b/src/api/Libs/MidiKs/MidiKs.cpp @@ -156,6 +156,7 @@ KSMidiDevice::PinSetState( return S_OK; } +_Use_decl_annotations_ HRESULT KSMidiDevice::ConfigureLoopedBuffer(ULONG& BufferSize ) diff --git a/src/user-tools/midi-console/Midi/Commands/Endpoint/EndpointMonitorCommand.cs b/src/user-tools/midi-console/Midi/Commands/Endpoint/EndpointMonitorCommand.cs index 02e44936..3e14efba 100644 --- a/src/user-tools/midi-console/Midi/Commands/Endpoint/EndpointMonitorCommand.cs +++ b/src/user-tools/midi-console/Midi/Commands/Endpoint/EndpointMonitorCommand.cs @@ -35,6 +35,13 @@ internal class EndpointMonitorCommand : Command // we have this struct so we can separate the relatively fast received processing // and its calculations from the comparatively slow displays processing private Queue m_receivedMessagesQueue = new Queue(); + private Queue m_displayMessageQueue = new Queue(); + private Queue m_fileWriterMessagesQueue = new Queue(); + + + private static AutoResetEvent m_displayMessageThreadWakeup = new AutoResetEvent(false); + private static AutoResetEvent m_fileMessageThreadWakeup = new AutoResetEvent(false); + private static AutoResetEvent m_terminateMessageListenerThread = new AutoResetEvent(false); @@ -129,7 +136,7 @@ private void MonitorEndpointConnectionStatusInTheBackground(string endpointId) while (_continueWatchingDevice) { - Thread.Sleep(125); + Thread.Sleep(100); } }); @@ -137,6 +144,11 @@ private void MonitorEndpointConnectionStatusInTheBackground(string endpointId) deviceWatcherThread.Start(); } + + + + + public override int Execute(CommandContext context, Settings settings) { MidiMessageTable displayTable = new MidiMessageTable(settings.Verbose); @@ -203,146 +215,226 @@ public override int Execute(CommandContext context, Settings settings) UInt64 startTimestamp = 0; UInt64 lastReceivedTimestamp = 0; - - //MidiMessageStruct msg; - UInt32 index = 0; + MonitorEndpointConnectionStatusInTheBackground(endpointId); bool continueWaiting = true; + UInt64 outOfOrderMessageCount = 0; - var messageListener = new Thread(() => + + // open the connection + if (connection.Open()) { - connection.MessageReceived += (s, e) => - { - // helps prevent any race conditions with main loop and its output - if (!continueWaiting) return; + // Main message listener background thread ----------------------------------------------------- - //Console.WriteLine("DEBUG: MessageReceived"); - index++; + var messageListener = new Thread(() => + { + UInt64 lastMessageTimestamp = 0; - var receivedMessage = new ReceivedMidiMessage() + connection.MessageReceived += (s, e) => { - Index = index, - ReceivedTimestamp = MidiClock.Now, - MessageTimestamp = e.Timestamp - }; + // helps prevent any race conditions with main loop and its output + if (!continueWaiting) return; - receivedMessage.NumWords = e.FillWords(out receivedMessage.Word0, out receivedMessage.Word1, out receivedMessage.Word2, out receivedMessage.Word3); + //Console.WriteLine("DEBUG: MessageReceived"); + index++; - m_receivedMessagesQueue.Enqueue(receivedMessage); + var receivedMessage = new ReceivedMidiMessage() + { + Index = index, + ReceivedTimestamp = MidiClock.Now, + MessageTimestamp = e.Timestamp + }; - if (settings.SingleMessage) - { - continueWaiting = false; - } - }; + if (e.Timestamp < lastMessageTimestamp) + { + outOfOrderMessageCount++; + } - while (continueWaiting && !_hasEndpointDisconnected) - { - Thread.Sleep(0); - } + lastMessageTimestamp = e.Timestamp; - }); - messageListener.Start(); + receivedMessage.NumWords = e.FillWords(out receivedMessage.Word0, out receivedMessage.Word1, out receivedMessage.Word2, out receivedMessage.Word3); + lock (m_receivedMessagesQueue) + { + m_receivedMessagesQueue.Enqueue(receivedMessage); + } - // open the connection - if (connection.Open()) - { - while (continueWaiting) + if (settings.SingleMessage) + { + continueWaiting = false; + } + }; + + m_terminateMessageListenerThread.WaitOne(); + }); + messageListener.Start(); + + + // Console display background thread ----------------------------------------------------- + var messageConsoleDisplay = new Thread(() => { - if (Console.KeyAvailable) + while (continueWaiting) { - var keyInfo = Console.ReadKey(true); + m_displayMessageThreadWakeup.WaitOne(5000); - if (keyInfo.Key == ConsoleKey.Escape) + if (m_displayMessageQueue.Count > 0) { - continueWaiting = false; + ReceivedMidiMessage message; - AnsiConsole.WriteLine(); - AnsiConsole.MarkupLine(Strings.MonitorEscapePressedMessage); + lock (m_displayMessageQueue) + { + message = m_displayMessageQueue.Dequeue(); + } + + if (startTimestamp == 0) + { + // gets timestamp of first message we receive and uses that so all others are an offset + startTimestamp = message.ReceivedTimestamp; + } + + if (lastReceivedTimestamp == 0) + { + // gets timestamp of first message we receive and uses that so all others are an offset from previous message + lastReceivedTimestamp = message.ReceivedTimestamp; + } + + if (message.Index == 1) + { + displayTable.OutputHeader(); + } + + // calculate offset from the last message received + var offsetMicroseconds = MidiClock.ConvertTimestampToMicroseconds( + message.ReceivedTimestamp - lastReceivedTimestamp); + + // set our last received so we can calculate offsets + lastReceivedTimestamp = message.ReceivedTimestamp; + + + displayTable.OutputRow(message, offsetMicroseconds); } - + + //Thread.Sleep(0); } + }); + messageConsoleDisplay.Start(); - if (_hasEndpointDisconnected) - { - continueWaiting = false; - AnsiConsole.MarkupLine(AnsiMarkupFormatter.FormatError(Strings.EndpointDisconnected)); - } - else if (continueWaiting && m_receivedMessagesQueue.Count > 0) + // File writing background thread ----------------------------------------------------- + if (captureWriter != null) + { + var messageFileWriter = new Thread(() => { - lock (m_receivedMessagesQueue) + while (continueWaiting) { - int iter = 0; + m_fileMessageThreadWakeup.WaitOne(5000); - while (m_receivedMessagesQueue.Count > 0 && iter < 50) + if (m_fileWriterMessagesQueue.Count > 0) { - iter++; // we need to take a break if we're being spammed, so this tracks iterations - - var message = m_receivedMessagesQueue.Dequeue(); + ReceivedMidiMessage message; - if (startTimestamp == 0) + lock (m_fileWriterMessagesQueue) { - // gets timestamp of first message we receive and uses that so all others are an offset - startTimestamp = message.ReceivedTimestamp; + message = m_fileWriterMessagesQueue.Dequeue(); } - if (lastReceivedTimestamp == 0) - { - // gets timestamp of first message we receive and uses that so all others are an offset from previous message - lastReceivedTimestamp = message.ReceivedTimestamp; - } - // TODO: Maybe re-display the header if it's been a while since last header, and it is off-screen (index delta > some number) + // write header if first message if (message.Index == 1) { - displayTable.OutputHeader(); + captureWriter.WriteLine("#"); + captureWriter.WriteLine($"# Windows MIDI Services Console Capture {DateTime.Now.ToLongDateString()}"); + captureWriter.WriteLine($"# Endpoint: {endpointId.ToString()}"); + captureWriter.WriteLine($"# Annotation: {settings.AnnotateCaptureFile.ToString()}"); + captureWriter.WriteLine($"# Delimiter: {settings.FieldDelimiter.ToString()}"); + captureWriter.WriteLine("#"); + } + WriteMessageToFile(settings, captureWriter, message); - if (captureWriter != null && capturingToFile) - { - captureWriter.WriteLine("#"); - captureWriter.WriteLine($"# Windows MIDI Services Console Capture {DateTime.Now.ToLongDateString()}"); - captureWriter.WriteLine($"# Endpoint: {endpointId.ToString()}"); - captureWriter.WriteLine($"# Annotation: {settings.AnnotateCaptureFile.ToString()}"); - captureWriter.WriteLine($"# Delimiter: {settings.FieldDelimiter.ToString()}"); - captureWriter.WriteLine("#"); - } + } - } + //Thread.Sleep(0); + } + }); + messageFileWriter.Start(); + } - // calculate offset from the last message received - var offsetMicroseconds = MidiClock.ConvertTimestampToMicroseconds(message.ReceivedTimestamp - lastReceivedTimestamp); - displayTable.OutputRow(message, offsetMicroseconds); + // Main UI loop ----------------------------------------------------- + while (continueWaiting) + { + if (Console.KeyAvailable) + { + var keyInfo = Console.ReadKey(true); - if (captureWriter != null) - { - WriteMessageToFile(settings, captureWriter, message); - } + if (keyInfo.Key == ConsoleKey.Escape) + { + continueWaiting = false; + // wake up the threads so they terminate + m_terminateMessageListenerThread.Set(); + m_fileMessageThreadWakeup.Set(); + m_displayMessageThreadWakeup.Set(); - // set our last received so we can calculate offsets - lastReceivedTimestamp = message.ReceivedTimestamp; + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine("🛑 " + Strings.MonitorEscapePressedMessage); + } + + } + if (_hasEndpointDisconnected) + { + continueWaiting = false; + m_terminateMessageListenerThread.Set(); + m_displayMessageThreadWakeup.Set(); + m_fileMessageThreadWakeup.Set(); + + AnsiConsole.MarkupLine("❎ " + AnsiMarkupFormatter.FormatError(Strings.EndpointDisconnected)); + } + else if (continueWaiting && m_receivedMessagesQueue.Count > 0) + { + // we load up the various queues here + + ReceivedMidiMessage message; + + // pull from the incoming messages queue + lock (m_receivedMessagesQueue) + { + message = m_receivedMessagesQueue.Dequeue(); + } + + + // add to the display queue + lock (m_displayMessageQueue) + { + m_displayMessageQueue.Enqueue(message); + } + m_displayMessageThreadWakeup.Set(); + + // add to the file writer queue if we're capturing to file + if (captureWriter != null) + { + lock (m_fileWriterMessagesQueue) + { + m_fileWriterMessagesQueue.Enqueue(message); } + m_fileMessageThreadWakeup.Set(); } } - - // we don't need to update the display more often. A tight loop really ties up the CPU. - // actual message receive timings are based on the worker thread - if (continueWaiting) Thread.Sleep(125); + + if (continueWaiting) Thread.Sleep(10); } } else { AnsiConsole.MarkupLine(AnsiMarkupFormatter.FormatError(Strings.ErrorUnableToOpenEndpoint)); return (int)MidiConsoleReturnCode.ErrorOpeningEndpointConnection; + } _continueWatchingDevice = false; @@ -352,9 +444,33 @@ public override int Execute(CommandContext context, Settings settings) session.DisconnectEndpointConnection(connection.ConnectionId); } + // Summary information + + if (outOfOrderMessageCount > 0) + { + string message = "❎ " + outOfOrderMessageCount.ToString(); + + if (outOfOrderMessageCount > 1) + { + // multiple messages out of order + message += " messages received out of sent timestamp order."; + } + else + { + // single message out of order + message += " message received out of sent timestamp order."; + } + + AnsiConsole.MarkupLine(AnsiMarkupFormatter.FormatError(message)); + } + else + { + AnsiConsole.MarkupLine("✅ No messages received out of expected timestamp order."); + } + if (captureWriter != null) { - AnsiConsole.MarkupLine("Messages written to " + AnsiMarkupFormatter.FormatFileName(fileName)); + AnsiConsole.MarkupLine("✅ Messages written to " + AnsiMarkupFormatter.FormatFileName(fileName)); captureWriter.Close(); } diff --git a/src/user-tools/midi-console/Midi/CustomTable/MidiMessageTable.cs b/src/user-tools/midi-console/Midi/CustomTable/MidiMessageTable.cs index 65a820cd..333f2916 100644 --- a/src/user-tools/midi-console/Midi/CustomTable/MidiMessageTable.cs +++ b/src/user-tools/midi-console/Midi/CustomTable/MidiMessageTable.cs @@ -2,13 +2,14 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using Windows.Devices.Midi2; namespace Microsoft.Devices.Midi2.ConsoleApp { - internal struct MidiMessageTableColumn + internal class MidiMessageTableColumn { public int ParameterIndex { get; set; } public string HeaderText { get; set; } @@ -98,16 +99,28 @@ private void BuildStringFormats() const int timestampOffsetValueColumnWidth = 9; + private bool m_verbose = false; + private int m_offsetColumnNumber = 2; + private int m_deltaColumnNumber = 5; + + private string m_offsetValueDefaultColor = "darkseagreen"; + private string m_offsetValueErrorColor = "red"; + + private string m_deltaValueDefaultColor = "lightskyblue3_1"; + private string m_deltaValueErrorColor = "red"; + public MidiMessageTable(bool verbose) { + m_verbose = verbose; + // todo: localize strings Columns.Add(new MidiMessageTableColumn(0, "Index", 8, true, "grey", "")); Columns.Add(new MidiMessageTableColumn(1, "Message Timestamp", 19, false, "darkseagreen2", "N0")); // timestamp - Columns.Add(new MidiMessageTableColumn(2, "From Last", timestampOffsetValueColumnWidth, false, "darkseagreen", "")); // offset + Columns.Add(new MidiMessageTableColumn(m_offsetColumnNumber, "From Last", timestampOffsetValueColumnWidth, false, m_offsetValueDefaultColor, "")); // offset Columns.Add(new MidiMessageTableColumn(3, "", -2, true, "grey", "")); // offset label if (verbose) Columns.Add(new MidiMessageTableColumn(4, "Received Timestamp", 19, false, "skyblue2", "N0")); // recv timestamp - if (verbose) Columns.Add(new MidiMessageTableColumn(5, "Receive \u0394", timestampOffsetValueColumnWidth, false, "lightskyblue3_1", "")); // delta + if (verbose) Columns.Add(new MidiMessageTableColumn(m_deltaColumnNumber, "Receive \u0394", timestampOffsetValueColumnWidth, false, "lightskyblue3_1", "")); // delta if (verbose) Columns.Add(new MidiMessageTableColumn(6, "", -2, true, "grey", "")); // delta label Columns.Add(new MidiMessageTableColumn(7, "Words", 8, false, "deepskyblue1", "")); Columns.Add(new MidiMessageTableColumn(8, "", 8, true, "deepskyblue2", "")); @@ -115,7 +128,7 @@ public MidiMessageTable(bool verbose) Columns.Add(new MidiMessageTableColumn(10, "", 8, true, "deepskyblue4", "")); if (verbose) Columns.Add(new MidiMessageTableColumn(11, "Gr", 2, false, "indianred", "")); if (verbose) Columns.Add(new MidiMessageTableColumn(12, "Ch", 2, true, "mediumorchid3", "")); - Columns.Add(new MidiMessageTableColumn(13, "Message Type", -35, false,"steelblue1_1", "")); + if (verbose) Columns.Add(new MidiMessageTableColumn(13, "Message Type", -35, false,"steelblue1_1", "")); BuildStringFormats(); } @@ -130,7 +143,9 @@ public void OutputRow(ReceivedMidiMessage message, double deltaMicrosecondsFromP { string data = string.Empty; - string detailedMessageType = MidiMessageUtility.GetMessageFriendlyNameFromFirstWord(message.Word0); + string detailedMessageType = string.Empty; + + if (m_verbose) detailedMessageType = MidiMessageUtility.GetMessageFriendlyNameFromFirstWord(message.Word0); string word0 = string.Empty; string word1 = string.Empty; @@ -165,36 +180,46 @@ public void OutputRow(ReceivedMidiMessage message, double deltaMicrosecondsFromP double deltaValue = 0; string deltaUnitLabel = string.Empty; - double deltaSecheduledTimestampMicroseconds = 0.0; - - if (message.ReceivedTimestamp >= message.MessageTimestamp) - { - deltaSecheduledTimestampMicroseconds = MidiClock.ConvertTimestampToMicroseconds(message.ReceivedTimestamp - message.MessageTimestamp); - } - else + if (m_verbose) { - // we received the message early, so the offset is negative - deltaSecheduledTimestampMicroseconds = -1 * MidiClock.ConvertTimestampToMicroseconds(message.MessageTimestamp - message.ReceivedTimestamp); - } + double deltaSecheduledTimestampMicroseconds = 0.0; + if (message.ReceivedTimestamp >= message.MessageTimestamp) + { + deltaSecheduledTimestampMicroseconds = MidiClock.ConvertTimestampToMicroseconds(message.ReceivedTimestamp - message.MessageTimestamp); + } + else + { + // we received the message early, so the offset is negative + deltaSecheduledTimestampMicroseconds = -1 * MidiClock.ConvertTimestampToMicroseconds(message.MessageTimestamp - message.ReceivedTimestamp); + } - AnsiConsoleOutput.ConvertToFriendlyTimeUnit(deltaSecheduledTimestampMicroseconds, out deltaValue, out deltaUnitLabel); + AnsiConsoleOutput.ConvertToFriendlyTimeUnit(deltaSecheduledTimestampMicroseconds, out deltaValue, out deltaUnitLabel); + } string groupText = string.Empty; - var messageType = MidiMessageUtility.GetMessageTypeFromMessageFirstWord(message.Word0); - - if (MidiMessageUtility.MessageTypeHasGroupField(messageType)) + if (m_verbose) { - groupText = MidiMessageUtility.GetGroupFromMessageFirstWord(message.Word0).NumberForDisplay.ToString().PadLeft(2); + var messageType = MidiMessageUtility.GetMessageTypeFromMessageFirstWord(message.Word0); + + if (MidiMessageUtility.MessageTypeHasGroupField(messageType)) + { + groupText = MidiMessageUtility.GetGroupFromMessageFirstWord(message.Word0).NumberForDisplay.ToString().PadLeft(2); + } } string channelText = string.Empty; - if (MidiMessageUtility.MessageTypeHasChannelField(messageType)) + if (m_verbose) { - channelText = MidiMessageUtility.GetChannelFromMessageFirstWord(message.Word0).NumberForDisplay.ToString().PadLeft(2); + var messageType = MidiMessageUtility.GetMessageTypeFromMessageFirstWord(message.Word0); + + if (MidiMessageUtility.MessageTypeHasChannelField(messageType)) + { + channelText = MidiMessageUtility.GetChannelFromMessageFirstWord(message.Word0).NumberForDisplay.ToString().PadLeft(2); + } } // some cleanup @@ -218,11 +243,23 @@ public void OutputRow(ReceivedMidiMessage message, double deltaMicrosecondsFromP if (deltaValueText.Length > timestampOffsetValueColumnWidth) { deltaValueText = "######"; + deltaUnitLabel = ""; + Columns[m_deltaColumnNumber].DataColor = m_deltaValueErrorColor; + } + else + { + Columns[m_deltaColumnNumber].DataColor = m_deltaValueDefaultColor; } if (offsetValueText.Length > timestampOffsetValueColumnWidth) { offsetValueText = "######"; + offsetUnitLabel = ""; + Columns[m_offsetColumnNumber].DataColor = m_offsetValueErrorColor; + } + else + { + Columns[m_offsetColumnNumber].DataColor = m_offsetValueDefaultColor; }