Repository: microsoft/terminal (language: cpp)

Issue:
Add support for the DECPS escape sequence
# Description of the new feature/enhancement

The `DECPS` (_Play Sound_) escape sequence was first introduced on the DEC VT520 terminals, and provides applications with a way to play a sequence of musical notes. The supported functionality is fairly rudimentary, but it's good enough for generating basic sound effects in games, making your build scripts play a little jingle when they complete successfully, or having your login MOTD wish you a happy birthday every year.

Here's a little video demonstrating the sort of effects you can achieve (this is using a proof-of-concept implementation I've been experimenting with in conhost). Make sure the video player is not muted, otherwise you're obviously not going to hear anything.

https://user-images.githubusercontent.com/4181424/103430785-8957c700-4bbf-11eb-966c-14e916fbd03a.mp4

# Proposed technical implementation details (optional)

My current implementation is using the Windows midi APIs to generate the notes, and I thought the square wave synth was probably the most appropriate instrument to match the kind of sounds that would be produced by terminals from that era.

I'm still working out some of the details, but I'd be keen to submit a PR at some point if this is something you might be willing to include.


Discussion / hints:

Fix patch:
diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt
index 2cc27be650e..877e0f34ed7 100644
--- a/.github/actions/spelling/expect/expect.txt
+++ b/.github/actions/spelling/expect/expect.txt
@@ -539,6 +539,7 @@ DECNRCM
 DECOM
 deconstructed
 DECPCTERM
+DECPS
 DECRC
 DECREQTPARM
 DECRLM
@@ -632,6 +633,7 @@ dllmain
 DLLVERSIONINFO
 DLOAD
 DLOOK
+Dls
 dmp
 DOCTYPE
 docx
@@ -1006,6 +1008,7 @@ HKLM
 hlocal
 hlsl
 HMENU
+HMIDIOUT
 hmod
 hmodule
 hmon
@@ -1393,6 +1396,7 @@ MAKELANGID
 MAKELONG
 MAKELPARAM
 MAKELRESULT
+MAKEWORD
 malloc
 manpage
 MAPBITMAP
diff --git a/OpenConsole.sln b/OpenConsole.sln
index df144adc2de..f13e0cb8b2b 100644
--- a/OpenConsole.sln
+++ b/OpenConsole.sln
@@ -410,6 +410,10 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "InteractivityOneCore", "src
 EndProject
 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RendererWddmCon", "src\renderer\wddmcon\lib\wddmcon.vcxproj", "{75C6F576-18E9-4566-978A-F0A301CAC090}"
 EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Audio", "Audio", "{40BD8415-DD93-4200-8D82-498DDDC08CC8}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MidiAudio", "src\audio\midi\lib\midi.vcxproj", "{3C67784E-1453-49C2-9660-483E2CC7F7AD}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		AuditMode|Any CPU = AuditMode|Any CPU
@@ -3449,6 +3453,46 @@ Global
 		{75C6F576-18E9-4566-978A-F0A301CAC090}.Release|x64.Build.0 = Release|x64
 		{75C6F576-18E9-4566-978A-F0A301CAC090}.Release|x86.ActiveCfg = Release|Win32
 		{75C6F576-18E9-4566-978A-F0A301CAC090}.Release|x86.Build.0 = Release|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.AuditMode|Any CPU.ActiveCfg = AuditMode|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.AuditMode|ARM.ActiveCfg = AuditMode|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.AuditMode|ARM64.ActiveCfg = AuditMode|ARM64
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.AuditMode|ARM64.Build.0 = AuditMode|ARM64
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.AuditMode|DotNet_x64Test.ActiveCfg = AuditMode|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.AuditMode|DotNet_x86Test.ActiveCfg = AuditMode|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.AuditMode|x64.ActiveCfg = AuditMode|x64
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.AuditMode|x64.Build.0 = AuditMode|x64
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.AuditMode|x86.ActiveCfg = AuditMode|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.AuditMode|x86.Build.0 = AuditMode|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Debug|Any CPU.ActiveCfg = Debug|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Debug|ARM.ActiveCfg = Debug|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Debug|ARM64.ActiveCfg = Debug|ARM64
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Debug|ARM64.Build.0 = Debug|ARM64
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Debug|DotNet_x64Test.ActiveCfg = Debug|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Debug|DotNet_x86Test.ActiveCfg = Debug|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Debug|x64.ActiveCfg = Debug|x64
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Debug|x64.Build.0 = Debug|x64
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Debug|x86.ActiveCfg = Debug|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Debug|x86.Build.0 = Debug|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Fuzzing|Any CPU.ActiveCfg = Fuzzing|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Fuzzing|ARM.ActiveCfg = Fuzzing|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Fuzzing|ARM64.ActiveCfg = Fuzzing|ARM64
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Fuzzing|ARM64.Build.0 = Fuzzing|ARM64
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Fuzzing|DotNet_x64Test.ActiveCfg = Fuzzing|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Fuzzing|DotNet_x86Test.ActiveCfg = Fuzzing|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Fuzzing|x64.ActiveCfg = Fuzzing|x64
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Fuzzing|x64.Build.0 = Fuzzing|x64
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Fuzzing|x86.ActiveCfg = Fuzzing|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Fuzzing|x86.Build.0 = Fuzzing|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Release|Any CPU.ActiveCfg = Release|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Release|ARM.ActiveCfg = Release|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Release|ARM64.ActiveCfg = Release|ARM64
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Release|ARM64.Build.0 = Release|ARM64
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Release|DotNet_x64Test.ActiveCfg = Release|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Release|DotNet_x86Test.ActiveCfg = Release|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Release|x64.ActiveCfg = Release|x64
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Release|x64.Build.0 = Release|x64
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Release|x86.ActiveCfg = Release|Win32
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD}.Release|x86.Build.0 = Release|Win32
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -3552,6 +3596,8 @@ Global
 		{8222900C-8B6C-452A-91AC-BE95DB04B95F} = {05500DEF-2294-41E3-AF9A-24E580B82836}
 		{06EC74CB-9A12-428C-B551-8537EC964726} = {E8F24881-5E37-4362-B191-A3BA0ED7F4EB}
 		{75C6F576-18E9-4566-978A-F0A301CAC090} = {05500DEF-2294-41E3-AF9A-24E580B82836}
+		{40BD8415-DD93-4200-8D82-498DDDC08CC8} = {89CDCC5C-9F53-4054-97A4-639D99F169CD}
+		{3C67784E-1453-49C2-9660-483E2CC7F7AD} = {40BD8415-DD93-4200-8D82-498DDDC08CC8}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {3140B1B7-C8EE-43D1-A772-D82A7061A271}
diff --git a/src/audio/midi/MidiAudio.cpp b/src/audio/midi/MidiAudio.cpp
new file mode 100644
index 00000000000..66617a0b63f
--- /dev/null
+++ b/src/audio/midi/MidiAudio.cpp
@@ -0,0 +1,116 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "precomp.h"
+#include "MidiAudio.hpp"
+#include "../terminal/parser/stateMachine.hpp"
+
+namespace
+{
+    class MidiOut
+    {
+    public:
+        static constexpr auto NOTE_OFF = 0x80;
+        static constexpr auto NOTE_ON = 0x90;
+        static constexpr auto PROGRAM_CHANGE = 0xC0;
+
+        // We're using a square wave as an approximation of the sound that the
+        // original VT525 terminals might have produced. This is probably not
+        // quite right, but it works reasonably well.
+        static constexpr auto SQUARE_WAVE_SYNTH = 80;
+
+        MidiOut() noexcept
+        {
+            midiOutOpen(&handle, MIDI_MAPPER, NULL, NULL, CALLBACK_NULL);
+            OutputMessage(PROGRAM_CHANGE, SQUARE_WAVE_SYNTH);
+        }
+        ~MidiOut() noexcept
+        {
+            midiOutClose(handle);
+        }
+        void OutputMessage(const int b1, const int b2, const int b3 = 0, const int b4 = 0) noexcept
+        {
+            midiOutShortMsg(handle, MAKELONG(MAKEWORD(b1, b2), MAKEWORD(b3, b4)));
+        }
+
+        MidiOut(const MidiOut&) = delete;
+        MidiOut(MidiOut&&) = delete;
+        MidiOut& operator=(const MidiOut&) = delete;
+        MidiOut& operator=(MidiOut&&) = delete;
+
+    private:
+        HMIDIOUT handle = nullptr;
+    };
+}
+
+using namespace std::chrono_literals;
+
+MidiAudio::~MidiAudio() noexcept
+{
+    try
+    {
+#pragma warning(suppress : 26447)
+        // We acquire the lock here so the class isn't destroyed while in use.
+        // If this throws, we'll catch it, so the C26447 warning is bogus.
+        _inUseMutex.lock();
+    }
+    catch (...)
+    {
+        // If the lock fails, we'll just have to live with the consequences.
+    }
+}
+
+void MidiAudio::Initialize()
+{
+    _shutdownFuture = _shutdownPromise.get_future();
+}
+
+void MidiAudio::Shutdown()
+{
+    // Once the shutdown promise is set, any note that is playing will stop
+    // immediately, and the Unlock call will exit the thread ASAP.
+    _shutdownPromise.set_value();
+}
+
+void MidiAudio::Lock()
+{
+    _inUseMutex.lock();
+}
+
+void MidiAudio::Unlock()
+{
+    // We need to check the shutdown status before releasing the mutex,
+    // because after that the class could be destroyed.
+    const auto shutdownStatus = _shutdownFuture.wait_for(0s);
+    _inUseMutex.unlock();
+    // If the wait didn't timeout, that means the shutdown promise was set,
+    // so we need to exit the thread ASAP by throwing an exception.
+    if (shutdownStatus != std::future_status::timeout)
+    {
+        throw Microsoft::Console::VirtualTerminal::StateMachine::ShutdownException{};
+    }
+}
+
+void MidiAudio::PlayNote(const int noteNumber, const int velocity, const std::chrono::microseconds duration) noexcept
+try
+{
+    // The MidiOut is a local static because we can only have one instance,
+    // and we only want to construct it when it's actually needed.
+    static MidiOut midiOut;
+
+    if (velocity)
+    {
+        midiOut.OutputMessage(MidiOut::NOTE_ON, noteNumber, velocity);
+    }
+
+    // By waiting on the shutdown future with the duration of the note, we'll
+    // either be paused for the appropriate amount of time, or we'll break out
+    // of the wait early if we've been shutdown.
+    _shutdownFuture.wait_for(duration);
+
+    if (velocity)
+    {
+        midiOut.OutputMessage(MidiOut::NOTE_OFF, noteNumber, velocity);
+    }
+}
+CATCH_LOG()
diff --git a/src/audio/midi/MidiAudio.hpp b/src/audio/midi/MidiAudio.hpp
new file mode 100644
index 00000000000..435276940ab
--- /dev/null
+++ b/src/audio/midi/MidiAudio.hpp
@@ -0,0 +1,36 @@
+/*++
+Copyright (c) Microsoft Corporation
+Licensed under the MIT license.
+
+Module Name:
+- MidiAudio.hpp
+
+Abstract:
+  This modules provide basic MIDI support with blocking sound output.
+  */
+
+#pragma once
+
+#include <future>
+#include <mutex>
+
+class MidiAudio
+{
+public:
+    MidiAudio() = default;
+    MidiAudio(const MidiAudio&) = delete;
+    MidiAudio(MidiAudio&&) = delete;
+    MidiAudio& operator=(const MidiAudio&) = delete;
+    MidiAudio& operator=(MidiAudio&&) = delete;
+    ~MidiAudio() noexcept;
+    void Initialize();
+    void Shutdown();
+    void Lock();
+    void Unlock();
+    void PlayNote(const int noteNumber, const int velocity, const std::chrono::microseconds duration) noexcept;
+
+private:
+    std::promise<void> _shutdownPromise;
+    std::future<void> _shutdownFuture;
+    std::mutex _inUseMutex;
+};
diff --git a/src/audio/midi/lib/midi.vcxproj b/src/audio/midi/lib/midi.vcxproj
new file mode 100644
index 00000000000..e6e7318a45b
--- /dev/null
+++ b/src/audio/midi/lib/midi.vcxproj
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <ProjectGuid>{3c67784e-1453-49c2-9660-483e2cc7f7ad}</ProjectGuid>
+    <Keyword>Win32Proj</Keyword>
+    <RootNamespace>midi</RootNamespace>
+    <ProjectName>MidiAudio</ProjectName>
+    <TargetName>MidiAudio</TargetName>
+    <ConfigurationType>StaticLibrary</ConfigurationType>
+  </PropertyGroup>
+  <Import Project="$(SolutionDir)src\common.build.pre.props" />
+  <Import Project="$(SolutionDir)src\common.nugetversions.props" />
+  <ItemGroup>
+    <ClCompile Include="..\MidiAudio.cpp" />
+    <ClCompile Include="..\precomp.cpp">
+      <PrecompiledHeader>Create</PrecompiledHeader>
+    </ClCompile>
+  </ItemGroup>
+  <ItemGroup>
+    <ClInclude Include="..\MidiAudio.hpp" />
+    <ClInclude Include="..\precomp.h" />
+  </ItemGroup>
+  <!-- Careful reordering these. Some default props (contained in these files) are order sensitive. -->
+  <Import Project="$(SolutionDir)src\common.build.post.props" />
+  <Import Project="$(SolutionDir)src\common.nugetversions.targets" />
+</Project>
diff --git a/src/audio/midi/precomp.cpp b/src/audio/midi/precomp.cpp
new file mode 100644
index 00000000000..c51e9b31b2f
--- /dev/null
+++ b/src/audio/midi/precomp.cpp
@@ -0,0 +1,4 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "precomp.h"
diff --git a/src/audio/midi/precomp.h b/src/audio/midi/precomp.h
new file mode 100644
index 00000000000..7e6eb17ddb8
--- /dev/null
+++ b/src/audio/midi/precomp.h
@@ -0,0 +1,30 @@
+/*++
+Copyright (c) Microsoft Corporation
+Licensed under the MIT license.
+
+Module Name:
+- precomp.h
+
+Abstract:
+- Contains external headers to include in the precompile phase of console build process.
+- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building).
+--*/
+
+#pragma once
+
+// clang-format off
+
+// This includes support libraries from the CRT, STL, WIL, and GSL
+#include "LibraryIncludes.h"
+
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
+#define NOMCX
+#define NOHELP
+#define NOCOMM
+#endif
+
+// Windows Header Files:
+#include <windows.h>
+
+// clang-format on
diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp
index 5bbe2cc367f..a224ca22c4d 100644
--- a/src/cascadia/TerminalControl/ControlCore.cpp
+++ b/src/cascadia/TerminalControl/ControlCore.cpp
@@ -105,6 +105,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         auto pfnShowWindowChanged = std::bind(&ControlCore::_terminalShowWindowChanged, this, std::placeholders::_1);
         _terminal->SetShowWindowCallback(pfnShowWindowChanged);
 
+        auto pfnPlayMidiNote = std::bind(&ControlCore::_terminalPlayMidiNote, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
+        _terminal->SetPlayMidiNoteCallback(pfnPlayMidiNote);
+
         // MSFT 33353327: Initialize the renderer in the ctor instead of Initialize().
         // We need the renderer to be ready to accept new engines before the SwapChainPanel is ready to go.
         // If we wait, a screen reader may try to get the AutomationPeer (aka the UIA Engine), and we won't be able to attach
@@ -201,6 +204,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         {
             _renderer->TriggerTeardown();
         }
+
+        _shutdownMidiAudio();
     }
 
     bool ControlCore::Initialize(const double actualWidth,
@@ -1223,6 +1228,66 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         }
     }
 
+    // Method Description:
+    // - Plays a single MIDI note, blocking for the duration.
+    // Arguments:
+    // - noteNumber - The MIDI note number to be played (0 - 127).
+    // - velocity - The force with which the note should be played (0 - 127).
+    // - duration - How long the note should be sustained (in microseconds).
+    void ControlCore::_terminalPlayMidiNote(const int noteNumber, const int velocity, const std::chrono::microseconds duration)
+    {
+        // We create the audio instance on demand, and lock it for the duration
+        // of the note output so it can't be destroyed while in use.
+        auto& midiAudio = _getMidiAudio();
+        midiAudio.Lock();
+
+        // We then unlock the terminal, so the UI doesn't hang while we're busy.
+        auto& terminalLock = _terminal->GetReadWriteLock();
+        terminalLock.unlock();
+
+        // This call will block for the duration, unless shutdown early.
+        midiAudio.PlayNote(noteNumber, velocity, duration);
+
+        // Once complete, we reacquire the terminal lock and unlock the audio.
+        // If the terminal has shutdown in the meantime, the Unlock call
+        // will throw an exception, forcing the thread to exit ASAP.
+        terminalLock.lock();
+        midiAudio.Unlock();
+    }
+
+    // Method Description:
+    // - Returns the MIDI audio instance, created on demand.
+    // Arguments:
+    // - <none>
+    // Return Value:
+    // - a reference to the MidiAudio instance.
+    MidiAudio& ControlCore::_getMidiAudio()
+    {
+        if (!_midiAudio)
+        {
+            _midiAudio = std::make_unique<MidiAudio>();
+            _midiAudio->Initialize();
+        }
+        return *_midiAudio;
+    }
+
+    // Method Description:
+    // - Shuts down the MIDI audio system if previously instantiated.
+    // Arguments:
+    // - <none>
+    // Return Value:
+    // - <none>
+    void ControlCore::_shutdownMidiAudio()
+    {
+        if (_midiAudio)
+        {
+            // We lock the terminal here to make sure the shutdown promise is
+            // set before the audio is unlocked in the thread that is playing.
+            auto lock = _terminal->LockForWriting();
+            _midiAudio->Shutdown();
+        }
+    }
+
     bool ControlCore::HasSelection() const
     {
         return _terminal->IsSelectionActive();
@@ -1513,10 +1578,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation
     }
     void ControlCore::_connectionOutputHandler(const hstring& hstr)
     {
-        _terminal->Write(hstr);
+        try
+        {
+            _terminal->Write(hstr);
 
-        // Start the throttled update of where our hyperlinks are.
-        _updatePatternLocations->Run();
+            // Start the throttled update of where our hyperlinks are.
+            _updatePatternLocations->Run();
+        }
+        catch (...)
+        {
+            // We're expecting to receive an exception here if the terminal
+            // is closed while we're blocked playing a MIDI note.
+        }
     }
 
     // Method Description:
diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h
index 9593ee0c5a2..9cf4355a6ce 100644
--- a/src/cascadia/TerminalControl/ControlCore.h
+++ b/src/cascadia/TerminalControl/ControlCore.h
@@ -17,6 +17,7 @@
 
 #include "ControlCore.g.h"
 #include "ControlSettings.h"
+#include "../../audio/midi/MidiAudio.hpp"
 #include "../../renderer/base/Renderer.hpp"
 #include "../../cascadia/TerminalCore/Terminal.hpp"
 #include "../buffer/out/search.h"
@@ -273,8 +274,16 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         void _terminalCursorPositionChanged();
         void _terminalTaskbarProgressChanged();
         void _terminalShowWindowChanged(bool showOrHide);
+        void _terminalPlayMidiNote(const int noteNumber,
+                                   const int velocity,
+                                   const std::chrono::microseconds duration);
 #pragma endregion
 
+        std::unique_ptr<MidiAudio> _midiAudio;
+
+        MidiAudio& _getMidiAudio();
+        void _shutdownMidiAudio();
+
 #pragma region RendererCallbacks
         void _rendererWarning(const HRESULT hr);
         void _renderEngineSwapChainChanged();
diff --git a/src/cascadia/TerminalControl/TerminalControlLib.vcxproj b/src/cascadia/TerminalControl/TerminalControlLib.vcxproj
index 250cb06cbe9..92ab903986c 100644
--- a/src/cascadia/TerminalControl/TerminalControlLib.vcxproj
+++ b/src/cascadia/TerminalControl/TerminalControlLib.vcxproj
@@ -147,9 +147,13 @@
     <PRIResource Include="Resources\en-US\Resources.resw" />
     <OCResourceDirectory Include="Resources" />
   </ItemGroup>
+  <ItemGroup>
+    <SDKReference Include="Microsoft.Midi.GmDls, Version=10.0.22000.0" />
+  </ItemGroup>
   <!-- ========================= Project References ======================== -->
   <ItemGroup>
     <ProjectReference Include="..\..\types\lib\types.vcxproj" />
+    <ProjectReference Include="..\..\audio\midi\lib\midi.vcxproj" />
     <ProjectReference Include="..\..\buffer\out\lib\bufferout.vcxproj" />
     <ProjectReference Include="$(OpenConsoleDir)src\renderer\base\lib\base.vcxproj" />
     <ProjectReference Include="..\..\renderer\atlas\atlas.vcxproj" />
diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp
index b8e32d53cee..fe0fbeab27c 100644
--- a/src/cascadia/TerminalCore/Terminal.cpp
+++ b/src/cascadia/TerminalCore/Terminal.cpp
@@ -963,6 +963,15 @@ WORD Terminal::_TakeVirtualKeyFromLastKeyEvent(const WORD scanCode) noexcept
 #endif
 }
 
+// Method Description:
+// - Get a reference to the the terminal's read/write lock.
+// Return Value:
+// - a ticket_lock which can be used to manually lock or unlock the terminal.
+til::ticket_lock& Terminal::GetReadWriteLock() noexcept
+{
+    return _readWriteLock;
+}
+
 Viewport Terminal::_GetMutableViewport() const noexcept
 {
     // GH#3493: if we're in the alt buffer, then it's possible that the mutable
@@ -1302,6 +1311,15 @@ void Terminal::SetShowWindowCallback(std::function<void(bool)> pfn) noexcept
     _pfnShowWindowChanged.swap(pfn);
 }
 
+// Method Description:
+// - Allows setting a callback for playing MIDI notes.
+// Arguments:
+// - pfn: a function callback that takes a note number, a velocity level, and a duration
+void Terminal::SetPlayMidiNoteCallback(std::function<void(const int, const int, const std::chrono::microseconds)> pfn) noexcept
+{
+    _pfnPlayMidiNote.swap(pfn);
+}
+
 // Method Description:
 // - Sets the cursor to be currently on. On/Off is tracked independently of
 //   cursor visibility (hidden/visible). On/off is controlled by the cursor
diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp
index 635993d4238..a042ec360f3 100644
--- a/src/cascadia/TerminalCore/Terminal.hpp
+++ b/src/cascadia/TerminalCore/Terminal.hpp
@@ -92,6 +92,7 @@ class Microsoft::Terminal::Core::Terminal final :
 
     [[nodiscard]] std::unique_lock<til::ticket_lock> LockForReading();
     [[nodiscard]] std::unique_lock<til::ticket_lock> LockForWriting();
+    til::ticket_lock& GetReadWriteLock() noexcept;
 
     short GetBufferHeight() const noexcept;
 
@@ -124,6 +125,7 @@ class Microsoft::Terminal::Core::Terminal final :
     void CopyToClipboard(std::wstring_view content) override;
     void SetTaskbarProgress(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::TaskbarState state, const size_t progress) override;
     void SetWorkingDirectory(std::wstring_view uri) override;
+    void PlayMidiNote(const int noteNumber, const int velocity, const std::chrono::microseconds duration) override;
     void ShowWindow(bool showOrHide) override;
     void UseAlternateScreenBuffer() override;
     void UseMainScreenBuffer() override;
@@ -201,6 +203,7 @@ class Microsoft::Terminal::Core::Terminal final :
     void SetCursorPositionChangedCallback(std::function<void()> pfn) noexcept;
     void TaskbarProgressChangedCallback(std::function<void()> pfn) noexcept;
     void SetShowWindowCallback(std::function<void(bool)> pfn) noexcept;
+    void SetPlayMidiNoteCallback(std::function<void(const int, const int, const std::chrono::microseconds)> pfn) noexcept;
 
     void SetCursorOn(const bool isOn);
     bool IsCursorBlinkingAllowed() const noexcept;
@@ -270,6 +273,7 @@ class Microsoft::Terminal::Core::Terminal final :
     std::function<void()> _pfnCursorPositionChanged;
     std::function<void()> _pfnTaskbarProgressChanged;
     std::function<void(bool)> _pfnShowWindowChanged;
+    std::function<void(const int, const int, const std::chrono::microseconds)> _pfnPlayMidiNote;
 
     RenderSettings _renderSettings;
     std::unique_ptr<::Microsoft::Console::VirtualTerminal::StateMachine> _stateMachine;
diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp
index f888cfa67f9..6899c7ba4a4 100644
--- a/src/cascadia/TerminalCore/TerminalApi.cpp
+++ b/src/cascadia/TerminalCore/TerminalApi.cpp
@@ -188,6 +188,11 @@ void Terminal::SetWorkingDirectory(std::wstring_view uri)
     _workingDirectory = uri;
 }
 
+void Terminal::PlayMidiNote(const int noteNumber, const int velocity, const std::chrono::microseconds duration)
+{
+    _pfnPlayMidiNote(noteNumber, velocity, duration);
+}
+
 void Terminal::UseAlternateScreenBuffer()
 {
     // the new alt buffer is exactly the size of the viewport.
diff --git a/src/host/consoleInformation.cpp b/src/host/consoleInformation.cpp
index ba812189c87..d5399b3e764 100644
--- a/src/host/consoleInformation.cpp
+++ b/src/host/consoleInformation.cpp
@@ -372,6 +372,40 @@ Microsoft::Console::CursorBlinker& CONSOLE_INFORMATION::GetCursorBlinker() noexc
     return _blinker;
 }
 
+// Method Description:
+// - Returns the MIDI audio instance, created on demand.
+// Arguments:
+// - <none>
+// Return Value:
+// - a reference to the MidiAudio instance.
+MidiAudio& CONSOLE_INFORMATION::GetMidiAudio()
+{
+    if (!_midiAudio)
+    {
+        _midiAudio = std::make_unique<MidiAudio>();
+        _midiAudio->Initialize();
+    }
+    return *_midiAudio;
+}
+
+// Method Description:
+// - Shuts down the MIDI audio system if previously instantiated.
+// Arguments:
+// - <none>
+// Return Value:
+// - <none>
+void CONSOLE_INFORMATION::ShutdownMidiAudio()
+{
+    if (_midiAudio)
+    {
+        // We lock the console here to make sure the shutdown promise is
+        // set before the audio is unlocked in the thread that is playing.
+        LockConsole();
+        _midiAudio->Shutdown();
+        UnlockConsole();
+    }
+}
+
 // Method Description:
 // - Generates a CHAR_INFO for this output cell, using the TextAttribute
 //      GetLegacyAttributes method to generate the legacy style attributes.
diff --git a/src/host/host-common.vcxitems b/src/host/host-common.vcxitems
index fdd645c5940..af33f9b31ae 100644
--- a/src/host/host-common.vcxitems
+++ b/src/host/host-common.vcxitems
@@ -118,6 +118,11 @@
     <ClInclude Include="..\_output.h" />
     <ClInclude Include="..\_stream.h" />
   </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\..\audio\midi\lib\midi.vcxproj">
+      <Project>{3c67784e-1453-49c2-9660-483e2cc7f7ad}</Project>
+    </ProjectReference>
+  </ItemGroup>
   <ItemDefinitionGroup>
     <ClCompile>
       <AdditionalIncludeDirectories>$(IntDir)..\OpenConsoleProxy;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
diff --git a/src/host/output.cpp b/src/host/output.cpp
index 64716ca3c02..e2138963d5c 100644
--- a/src/host/output.cpp
+++ b/src/host/output.cpp
@@ -499,7 +499,7 @@ void SetActiveScreenBuffer(SCREEN_INFORMATION& screenInfo)
 // TODO: MSFT 9450717 This should join the ProcessList class when CtrlEvents become moved into the server. https://osgvsowi/9450717
 void CloseConsoleProcessState()
 {
-    const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
+    auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
     // If there are no connected processes, sending control events is pointless as there's no one do send them to. In
     // this case we'll just exit conhost.
 
@@ -512,6 +512,8 @@ void CloseConsoleProcessState()
 
     HandleCtrlEvent(CTRL_CLOSE_EVENT);
 
+    gci.ShutdownMidiAudio();
+
     // Jiggle the handle: (see MSFT:19419231)
     // When we call this function, we'll only actually close the console once
     //      we're totally unlocked. If our caller has the console locked, great,
diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp
index 82f2d871aa3..e355248d52d 100644
--- a/src/host/outputStream.cpp
+++ b/src/host/outputStream.cpp
@@ -7,6 +7,7 @@
 
 #include "_stream.h"
 #include "getset.h"
+#include "handle.h"
 #include "directio.h"
 #include "output.h"
 
@@ -355,6 +356,34 @@ void ConhostInternalGetSet::SetWorkingDirectory(const std::wstring_view /*uri*/)
 {
 }
 
+// Routine Description:
+// - Plays a single MIDI note, blocking for the duration.
+// Arguments:
+// - noteNumber - The MIDI note number to be played (0 - 127).
+// - velocity - The force with which the note should be played (0 - 127).
+// - duration - How long the note should be sustained (in milliseconds).
+// Return value:
+// - true if successful. false otherwise.
+void ConhostInternalGetSet::PlayMidiNote(const int noteNumber, const int velocity, const std::chrono::microseconds duration)
+{
+    // We create the audio instance on demand, and lock it for the duration
+    // of the note output so it can't be destroyed while in use.
+    auto& midiAudio = ServiceLocator::LocateGlobals().getConsoleInformation().GetMidiAudio();
+    midiAudio.Lock();
+
+    // We then unlock the console, so the UI doesn't hang while we're busy.
+    UnlockConsole();
+
+    // This call will block for the duration, unless shutdown early.
+    midiAudio.PlayNote(noteNumber, velocity, duration);
+
+    // Once complete, we reacquire the console lock and unlock the audio.
+    // If the console has shutdown in the meantime, the Unlock call
+    // will throw an exception, forcing the thread to exit ASAP.
+    LockConsole();
+    midiAudio.Unlock();
+}
+
 // Routine Description:
 // - Resizes the window to the specified dimensions, in characters.
 // Arguments:
diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp
index 79316f78ea5..0bb642caeac 100644
--- a/src/host/outputStream.hpp
+++ b/src/host/outputStream.hpp
@@ -67,6 +67,7 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal::
     void CopyToClipboard(const std::wstring_view content) override;
     void SetTaskbarProgress(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::TaskbarState state, const size_t progress) override;
     void SetWorkingDirectory(const std::wstring_view uri) override;
+    void PlayMidiNote(const int noteNumber, const int velocity, const std::chrono::microseconds duration) override;
 
     bool IsConsolePty() const override;
     bool IsVtInputEnabled() const override;
diff --git a/src/host/server.h b/src/host/server.h
index 1a7ca9ba821..1489caf19d8 100644
--- a/src/host/server.h
+++ b/src/host/server.h
@@ -28,6 +28,7 @@ Revision History:
 #include "../server/WaitQueue.h"
 
 #include "../host/RenderData.hpp"
+#include "../audio/midi/MidiAudio.hpp"
 
 // clang-format off
 // Flags flags
@@ -138,6 +139,9 @@ class CONSOLE_INFORMATION :
     friend class CommonState;
     Microsoft::Console::CursorBlinker& GetCursorBlinker() noexcept;
 
+    MidiAudio& GetMidiAudio();
+    void ShutdownMidiAudio();
+
     CHAR_INFO AsCharInfo(const OutputCellView& cell) const noexcept;
 
     RenderData renderData;
@@ -153,6 +157,7 @@ class CONSOLE_INFORMATION :
 
     Microsoft::Console::VirtualTerminal::VtIo _vtIo;
     Microsoft::Console::CursorBlinker _blinker;
+    std::unique_ptr<MidiAudio> _midiAudio;
 };
 
 #define ConsoleLocked() (ServiceLocator::LocateGlobals()->getConsoleInformation()->ConsoleLock.OwningThread == NtCurrentTeb()->ClientId.UniqueThread)
diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp
index f2dcb1c2a8d..3a8905737eb 100644
--- a/src/terminal/adapter/ITermDispatch.hpp
+++ b/src/terminal/adapter/ITermDispatch.hpp
@@ -145,6 +145,8 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
                                        const DispatchTypes::DrcsCharsetSize charsetSize) = 0; // DECDLD
 
     virtual StringHandler RequestSetting() = 0; // DECRQSS
+
+    virtual bool PlaySounds(const VTParameters parameters) = 0; // DECPS
 };
 inline Microsoft::Console::VirtualTerminal::ITermDispatch::~ITermDispatch() {}
 #pragma warning(pop)
diff --git a/src/terminal/adapter/ITerminalApi.hpp b/src/terminal/adapter/ITerminalApi.hpp
index c2f0d7f71e9..c805ec6e128 100644
--- a/src/terminal/adapter/ITerminalApi.hpp
+++ b/src/terminal/adapter/ITerminalApi.hpp
@@ -70,6 +70,7 @@ namespace Microsoft::Console::VirtualTerminal
         virtual void CopyToClipboard(const std::wstring_view content) = 0;
         virtual void SetTaskbarProgress(const DispatchTypes::TaskbarState state, const size_t progress) = 0;
         virtual void SetWorkingDirectory(const std::wstring_view uri) = 0;
+        virtual void PlayMidiNote(const int noteNumber, const int velocity, const std::chrono::microseconds duration) = 0;
 
         virtual bool ResizeWindow(const size_t width, const size_t height) = 0;
         virtual bool IsConsolePty() const = 0;
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index bce951e5938..4d63b29338a 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -2686,3 +2686,39 @@ void AdaptDispatch::_ReportDECSTBMSetting()
     response.append(L"r\033\\"sv);
     _api.ReturnResponse({ response.data(), response.size() });
 }
+
+// Routine Description:
+// - DECPS - Plays a sequence of musical notes.
+// Arguments:
+// - params - The volume, duration, and note values to play.
+// Return value:
+// - True if handled successfully. False otherwise.
+bool AdaptDispatch::PlaySounds(const VTParameters parameters)
+{
+    // If we're a conpty, we return false so the command will be passed on
+    // to the connected terminal. But we need to flush the current frame
+    // first, otherwise the visual output will lag behind the sound.
+    if (_api.IsConsolePty())
+    {
+        _renderer.TriggerFlush(false);
+        return false;
+    }
+
+    // First parameter is the volume, in the range 0 to 7. We multiply by
+    // 127 / 7 to obtain an equivalent MIDI velocity in the range 0 to 127.
+    const auto velocity = std::min(parameters.at(0).value_or(0), 7) * 127 / 7;
+    // Second parameter is the duration, in the range 0 to 255. Units are
+    // 1/32 of a second, so we multiply by 1000000us/32 to obtain microseconds.
+    using namespace std::chrono_literals;
+    const auto duration = std::min(parameters.at(1).value_or(0), 255) * 1000000us / 32;
+    // The subsequent parameters are notes, in the range 0 to 25.
+    return parameters.subspan(2).for_each([=](const auto param) {
+        // Values 1 to 25 represent the notes C5 to C7, so we add 71 to
+        // obtain the equivalent MIDI note numbers (72 = C5).
+        const auto noteNumber = std::min(param.value_or(0), 25) + 71;
+        // But value 0 is meant to be silent, so if the note number is 71,
+        // we set the velocity to 0 (i.e. no volume).
+        _api.PlayMidiNote(noteNumber, noteNumber == 71 ? 0 : velocity, duration);
+        return true;
+    });
+}
diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp
index e132444182c..626434e69de 100644
--- a/src/terminal/adapter/adaptDispatch.hpp
+++ b/src/terminal/adapter/adaptDispatch.hpp
@@ -141,6 +141,8 @@ namespace Microsoft::Console::VirtualTerminal
 
         StringHandler RequestSetting() override; // DECRQSS
 
+        bool PlaySounds(const VTParameters parameters) override; // DECPS
+
     private:
         enum class ScrollDirection
         {
diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp
index ad724641160..a0c62883d38 100644
--- a/src/terminal/adapter/termDispatch.hpp
+++ b/src/terminal/adapter/termDispatch.hpp
@@ -138,6 +138,8 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons
                                const DispatchTypes::DrcsCharsetSize /*charsetSize*/) override { return nullptr; }
 
     StringHandler RequestSetting() override { return nullptr; }; // DECRQSS
+
+    bool PlaySounds(const VTParameters /*parameters*/) override { return false; }; // DECPS
 };
 
 #pragma warning(default : 26440) // Restore "can be declared noexcept" warning
diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp
index ae1a926d54e..9519de3a1a7 100644
--- a/src/terminal/adapter/ut_adapter/adapterTest.cpp
+++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp
@@ -221,6 +221,11 @@ class TestGetSet final : public ITerminalApi
         Log::Comment(L"SetWorkingDirectory MOCK called...");
     }
 
+    void PlayMidiNote(const int /*noteNumber*/, const int /*velocity*/, const std::chrono::microseconds /*duration*/) override
+    {
+        Log::Comment(L"PlayMidiNote MOCK called...");
+    }
+
     bool IsConsolePty() const override
     {
         Log::Comment(L"IsConsolePty MOCK called...");
diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp
index 244e7c64979..bf0d38f779f 100644
--- a/src/terminal/parser/OutputStateMachineEngine.cpp
+++ b/src/terminal/parser/OutputStateMachineEngine.cpp
@@ -620,6 +620,10 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete
         success = _dispatch->AssignColor(parameters.at(0), parameters.at(1).value_or(0), parameters.at(2).value_or(0));
         TermTelemetry::Instance().Log(TermTelemetry::Codes::DECAC);
         break;
+    case CsiActionCodes::DECPS_PlaySound:
+        success = _dispatch->PlaySounds(parameters);
+        TermTelemetry::Instance().Log(TermTelemetry::Codes::DECPS);
+        break;
     default:
         // If no functions to call, overall dispatch was a failure.
         success = false;
diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp
index 20083b474d8..548770f0989 100644
--- a/src/terminal/parser/OutputStateMachineEngine.hpp
+++ b/src/terminal/parser/OutputStateMachineEngine.hpp
@@ -142,7 +142,8 @@ namespace Microsoft::Console::VirtualTerminal
             XT_PushSgr = VTID("#{"),
             XT_PopSgr = VTID("#}"),
             DECSCPP_SetColumnsPerPage = VTID("$|"),
-            DECAC_AssignColor = VTID(",|")
+            DECAC_AssignColor = VTID(",|"),
+            DECPS_PlaySound = VTID(",~")
         };
 
         enum DcsActionCodes : uint64_t
diff --git a/src/terminal/parser/stateMachine.cpp b/src/terminal/parser/stateMachine.cpp
index 3a24b6f7b40..4fe3b58cbf3 100644
--- a/src/terminal/parser/stateMachine.cpp
+++ b/src/terminal/parser/stateMachine.cpp
@@ -385,7 +385,7 @@ static constexpr bool _isActionableFromGround(const wchar_t wch) noexcept
 // - wch - Character to dispatch.
 // Return Value:
 // - <none>
-void StateMachine::_ActionExecute(const wchar_t wch) noexcept
+void StateMachine::_ActionExecute(const wchar_t wch)
 {
     _trace.TraceOnExecute(wch);
     _trace.DispatchSequenceTrace(_SafeExecute([=]() {
@@ -401,7 +401,7 @@ void StateMachine::_ActionExecute(const wchar_t wch) noexcept
 // - wch - Character to dispatch.
 // Return Value:
 // - <none>
-void StateMachine::_ActionExecuteFromEscape(const wchar_t wch) noexcept
+void StateMachine::_ActionExecuteFromEscape(const wchar_t wch)
 {
     _trace.TraceOnExecuteFromEscape(wch);
     _trace.DispatchSequenceTrace(_SafeExecute([=]() {
@@ -415,7 +415,7 @@ void StateMachine::_ActionExecuteFromEscape(const wchar_t wch) noexcept
 // - wch - Character to dispatch.
 // Return Value:
 // - <none>
-void StateMachine::_ActionPrint(const wchar_t wch) noexcept
+void StateMachine::_ActionPrint(const wchar_t wch)
 {
     _trace.TraceOnAction(L"Print");
     _trace.DispatchSequenceTrace(_SafeExecute([=]() {
@@ -444,7 +444,7 @@ void StateMachine::_ActionPrintString(const std::wstring_view string)
 // - wch - Character to dispatch.
 // Return Value:
 // - <none>
-void StateMachine::_ActionEscDispatch(const wchar_t wch) noexcept
+void StateMachine::_ActionEscDispatch(const wchar_t wch)
 {
     _trace.TraceOnAction(L"EscDispatch");
     _trace.DispatchSequenceTrace(_SafeExecuteWithLog(wch, [=]() {
@@ -459,7 +459,7 @@ void StateMachine::_ActionEscDispatch(const wchar_t wch) noexcept
 // - wch - Character to dispatch.
 // Return Value:
 // - <none>
-void StateMachine::_ActionVt52EscDispatch(const wchar_t wch) noexcept
+void StateMachine::_ActionVt52EscDispatch(const wchar_t wch)
 {
     _trace.TraceOnAction(L"Vt52EscDispatch");
     _trace.DispatchSequenceTrace(_SafeExecuteWithLog(wch, [=]() {
@@ -474,7 +474,7 @@ void StateMachine::_ActionVt52EscDispatch(const wchar_t wch) noexcept
 // - wch - Character to dispatch.
 // Return Value:
 // - <none>
-void StateMachine::_ActionCsiDispatch(const wchar_t wch) noexcept
+void StateMachine::_ActionCsiDispatch(const wchar_t wch)
 {
     _trace.TraceOnAction(L"CsiDispatch");
     _trace.DispatchSequenceTrace(_SafeExecuteWithLog(wch, [=]() {
@@ -632,7 +632,7 @@ void StateMachine::_ActionOscPut(const wchar_t wch)
 // - wch - Character to dispatch.
 // Return Value:
 // - <none>
-void StateMachine::_ActionOscDispatch(const wchar_t wch) noexcept
+void StateMachine::_ActionOscDispatch(const wchar_t wch)
 {
     _trace.TraceOnAction(L"OscDispatch");
     _trace.DispatchSequenceTrace(_SafeExecuteWithLog(wch, [=]() {
@@ -647,7 +647,7 @@ void StateMachine::_ActionOscDispatch(const wchar_t wch) noexcept
 // - wch - Character to dispatch.
 // Return Value:
 // - <none>
-void StateMachine::_ActionSs3Dispatch(const wchar_t wch) noexcept
+void StateMachine::_ActionSs3Dispatch(const wchar_t wch)
 {
     _trace.TraceOnAction(L"Ss3Dispatch");
     _trace.DispatchSequenceTrace(_SafeExecuteWithLog(wch, [=]() {
@@ -662,7 +662,7 @@ void StateMachine::_ActionSs3Dispatch(const wchar_t wch) noexcept
 // - wch - Character to dispatch.
 // Return Value:
 // - <none>
-void StateMachine::_ActionDcsDispatch(const wchar_t wch) noexcept
+void StateMachine::_ActionDcsDispatch(const wchar_t wch)
 {
     _trace.TraceOnAction(L"DcsDispatch");
 
@@ -978,7 +978,7 @@ void StateMachine::_EnterSosPmApcString() noexcept
 // - wch - Character that triggered the event
 // Return Value:
 // - <none>
-void StateMachine::_EventGround(const wchar_t wch) noexcept
+void StateMachine::_EventGround(const wchar_t wch)
 {
     _trace.TraceOnEvent(L"Ground");
     if (_isC0Code(wch) || _isDelete(wch))
@@ -1093,7 +1093,7 @@ void StateMachine::_EventEscape(const wchar_t wch)
 // - wch - Character that triggered the event
 // Return Value:
 // - <none>
-void StateMachine::_EventEscapeIntermediate(const wchar_t wch) noexcept
+void StateMachine::_EventEscapeIntermediate(const wchar_t wch)
 {
     _trace.TraceOnEvent(L"EscapeIntermediate");
     if (_isC0Code(wch))
@@ -1187,7 +1187,7 @@ void StateMachine::_EventCsiEntry(const wchar_t wch)
 // - wch - Character that triggered the event
 // Return Value:
 // - <none>
-void StateMachine::_EventCsiIntermediate(const wchar_t wch) noexcept
+void StateMachine::_EventCsiIntermediate(const wchar_t wch)
 {
     _trace.TraceOnEvent(L"CsiIntermediate");
     if (_isC0Code(wch))
@@ -1225,7 +1225,7 @@ void StateMachine::_EventCsiIntermediate(const wchar_t wch) noexcept
 // - wch - Character that triggered the event
 // Return Value:
 // - <none>
-void StateMachine::_EventCsiIgnore(const wchar_t wch) noexcept
+void StateMachine::_EventCsiIgnore(const wchar_t wch)
 {
     _trace.TraceOnEvent(L"CsiIgnore");
     if (_isC0Code(wch))
@@ -1574,7 +1574,7 @@ void StateMachine::_EventDcsIgnore() noexcept
 // - wch - Character that triggered the event
 // Return Value:
 // - <none>
-void StateMachine::_EventDcsIntermediate(const wchar_t wch) noexcept
+void StateMachine::_EventDcsIntermediate(const wchar_t wch)
 {
     _trace.TraceOnEvent(L"DcsIntermediate");
     if (_isC0Code(wch))
@@ -1786,7 +1786,7 @@ void StateMachine::ProcessCharacter(const wchar_t wch)
 // - <none>
 // Return Value:
 // - true if the engine successfully handled the string.
-bool StateMachine::FlushToTerminal() noexcept
+bool StateMachine::FlushToTerminal()
 {
     auto success{ true };
 
@@ -2012,11 +2012,15 @@ void StateMachine::_AccumulateTo(const wchar_t wch, VTInt& value) noexcept
 }
 
 template<typename TLambda>
-bool StateMachine::_SafeExecute(TLambda&& lambda) noexcept
+bool StateMachine::_SafeExecute(TLambda&& lambda)
 try
 {
     return lambda();
 }
+catch (const ShutdownException&)
+{
+    throw;
+}
 catch (...)
 {
     LOG_HR(wil::ResultFromCaughtException());
@@ -2024,7 +2028,7 @@ catch (...)
 }
 
 template<typename TLambda>
-bool StateMachine::_SafeExecuteWithLog(const wchar_t wch, TLambda&& lambda) noexcept
+bool StateMachine::_SafeExecuteWithLog(const wchar_t wch, TLambda&& lambda)
 {
     const bool success = _SafeExecute(std::forward<TLambda>(lambda));
     if (!success)
diff --git a/src/terminal/parser/stateMachine.hpp b/src/terminal/parser/stateMachine.hpp
index e4b2bfccae5..193734e27e2 100644
--- a/src/terminal/parser/stateMachine.hpp
+++ b/src/terminal/parser/stateMachine.hpp
@@ -61,26 +61,33 @@ namespace Microsoft::Console::VirtualTerminal
 
         void ResetState() noexcept;
 
-        bool FlushToTerminal() noexcept;
+        bool FlushToTerminal();
 
         const IStateMachineEngine& Engine() const noexcept;
         IStateMachineEngine& Engine() noexcept;
 
+        class ShutdownException : public wil::ResultException
+        {
+        public:
+            ShutdownException() noexcept :
+                ResultException(E_ABORT) {}
+        };
+
     private:
-        void _ActionExecute(const wchar_t wch) noexcept;
-        void _ActionExecuteFromEscape(const wchar_t wch) noexcept;
-        void _ActionPrint(const wchar_t wch) noexcept;
+        void _ActionExecute(const wchar_t wch);
+        void _ActionExecuteFromEscape(const wchar_t wch);
+        void _ActionPrint(const wchar_t wch);
         void _ActionPrintString(const std::wstring_view string);
-        void _ActionEscDispatch(const wchar_t wch) noexcept;
-        void _ActionVt52EscDispatch(const wchar_t wch) noexcept;
+        void _ActionEscDispatch(const wchar_t wch);
+        void _ActionVt52EscDispatch(const wchar_t wch);
         void _ActionCollect(const wchar_t wch) noexcept;
         void _ActionParam(const wchar_t wch);
-        void _ActionCsiDispatch(const wchar_t wch) noexcept;
+        void _ActionCsiDispatch(const wchar_t wch);
         void _ActionOscParam(const wchar_t wch) noexcept;
         void _ActionOscPut(const wchar_t wch);
-        void _ActionOscDispatch(const wchar_t wch) noexcept;
-        void _ActionSs3Dispatch(const wchar_t wch) noexcept;
-        void _ActionDcsDispatch(const wchar_t wch) noexcept;
+        void _ActionOscDispatch(const wchar_t wch);
+        void _ActionSs3Dispatch(const wchar_t wch);
+        void _ActionDcsDispatch(const wchar_t wch);
 
         void _ActionClear();
         void _ActionIgnore() noexcept;
@@ -106,12 +113,12 @@ namespace Microsoft::Console::VirtualTerminal
         void _EnterDcsPassThrough() noexcept;
         void _EnterSosPmApcString() noexcept;
 
-        void _EventGround(const wchar_t wch) noexcept;
+        void _EventGround(const wchar_t wch);
         void _EventEscape(const wchar_t wch);
-        void _EventEscapeIntermediate(const wchar_t wch) noexcept;
+        void _EventEscapeIntermediate(const wchar_t wch);
         void _EventCsiEntry(const wchar_t wch);
-        void _EventCsiIntermediate(const wchar_t wch) noexcept;
-        void _EventCsiIgnore(const wchar_t wch) noexcept;
+        void _EventCsiIntermediate(const wchar_t wch);
+        void _EventCsiIgnore(const wchar_t wch);
         void _EventCsiParam(const wchar_t wch);
         void _EventOscParam(const wchar_t wch) noexcept;
         void _EventOscString(const wchar_t wch);
@@ -121,7 +128,7 @@ namespace Microsoft::Console::VirtualTerminal
         void _EventVt52Param(const wchar_t wch);
         void _EventDcsEntry(const wchar_t wch);
         void _EventDcsIgnore() noexcept;
-        void _EventDcsIntermediate(const wchar_t wch) noexcept;
+        void _EventDcsIntermediate(const wchar_t wch);
         void _EventDcsParam(const wchar_t wch);
         void _EventDcsPassThrough(const wchar_t wch);
         void _EventSosPmApcString(const wchar_t wch) noexcept;
@@ -129,9 +136,9 @@ namespace Microsoft::Console::VirtualTerminal
         void _AccumulateTo(const wchar_t wch, VTInt& value) noexcept;
 
         template<typename TLambda>
-        bool _SafeExecute(TLambda&& lambda) noexcept;
+        bool _SafeExecute(TLambda&& lambda);
         template<typename TLambda>
-        bool _SafeExecuteWithLog(const wchar_t wch, TLambda&& lambda) noexcept;
+        bool _SafeExecuteWithLog(const wchar_t wch, TLambda&& lambda);
 
         enum class VTStates
         {
diff --git a/src/terminal/parser/telemetry.cpp b/src/terminal/parser/telemetry.cpp
index a40711ef506..c394faaf64b 100644
--- a/src/terminal/parser/telemetry.cpp
+++ b/src/terminal/parser/telemetry.cpp
@@ -281,6 +281,7 @@ void TermTelemetry::WriteFinalTraceLog() const
                                       TraceLoggingUInt32(_uiTimesUsed[XTPUSHSGR], "XTPUSHSGR"),
                                       TraceLoggingUInt32(_uiTimesUsed[XTPOPSGR], "XTPOPSGR"),
                                       TraceLoggingUInt32(_uiTimesUsed[DECAC], "DECAC"),
+                                      TraceLoggingUInt32(_uiTimesUsed[DECPS], "DECPS"),
                                       TraceLoggingUInt32Array(_uiTimesFailed, ARRAYSIZE(_uiTimesFailed), "Failed"),
                                       TraceLoggingUInt32(_uiTimesFailedOutsideRange, "FailedOutsideRange"));
         }
diff --git a/src/terminal/parser/telemetry.hpp b/src/terminal/parser/telemetry.hpp
index 196d13016bd..5e258d62d7b 100644
--- a/src/terminal/parser/telemetry.hpp
+++ b/src/terminal/parser/telemetry.hpp
@@ -108,6 +108,7 @@ namespace Microsoft::Console::VirtualTerminal
             XTPUSHSGR,
             XTPOPSGR,
             DECAC,
+            DECPS,
             // Only use this last enum as a count of the number of codes.
             NUMBER_OF_CODES
         };


Test patch: