Add support for the DECPS (Play Sound) escape sequence (#13208)

## Summary of the Pull Request

The `DECPS` (Play Sound) escape sequence provides applications with a
way to play a basic sequence of musical notes. This emulates
functionality that was originally supported on the DEC VT520 and VT525
hardware terminals.

## PR Checklist
* [x] Closes #8687
* [x] CLA signed.
* [ ] Tests added/passed
* [ ] Documentation updated.
* [ ] Schema updated.
* [x] I've discussed this with core contributors already. Issue number
where discussion took place: #8687

## Detailed Description of the Pull Request / Additional comments

When a `DECPS` control is executed, any further output is blocked until
all the notes have finished playing. So to prevent the UI from hanging
during this period, we have to temporarily release the console/terminal
lock, and then reacquire it before returning.

The problem we then have is how to deal with the terminal being closed
during that unlocked interval. The way I've dealt with that is with a
promise that is set to indicate a shutdown. This immediately aborts any
sound that is in progress, but also signals the thread that it needs to
exit as soon as possible.

The thread exit is achieved by throwing a custom exception which is
recognised by the state machine and rethrown instead of being logged.
This gets it all the way up to the root of the write operation, so it
won't attempt to process anything further output that might still be
buffered.

## Validation Steps Performed

Thanks to the testing done by @jerch on a real VT525 terminal, we have a
good idea of how this sequence is supposed to work, and I'm fairly
confident that our implementation is reasonably compatible.

The only significant difference I'm aware of is that we support multiple
notes in a sequence. That was a feature that was documented in the
VT520/VT525 manual, but didn't appear to be supported on the actual
device.
This commit is contained in:
James Holderness
2022-06-01 18:53:56 +01:00
committed by GitHub
parent 7dbe741e1a
commit 9dca6c27ee
31 changed files with 556 additions and 39 deletions

View File

@@ -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

View File

@@ -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}

View File

@@ -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()

View File

@@ -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;
};

View File

@@ -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>

View File

@@ -0,0 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"

30
src/audio/midi/precomp.h Normal file
View File

@@ -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

View File

@@ -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:

View File

@@ -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();

View File

@@ -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" />

View File

@@ -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

View File

@@ -92,6 +92,7 @@ public:
[[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 @@ public:
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 @@ public:
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 @@ private:
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;

View File

@@ -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.

View File

@@ -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.

View File

@@ -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>

View File

@@ -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,

View File

@@ -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:

View File

@@ -67,6 +67,7 @@ public:
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;

View File

@@ -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 @@ public:
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 @@ private:
Microsoft::Console::VirtualTerminal::VtIo _vtIo;
Microsoft::Console::CursorBlinker _blinker;
std::unique_ptr<MidiAudio> _midiAudio;
};
#define ConsoleLocked() (ServiceLocator::LocateGlobals()->getConsoleInformation()->ConsoleLock.OwningThread == NtCurrentTeb()->ClientId.UniqueThread)

View File

@@ -145,6 +145,8 @@ public:
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)

View File

@@ -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;

View File

@@ -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;
});
}

View File

@@ -141,6 +141,8 @@ namespace Microsoft::Console::VirtualTerminal
StringHandler RequestSetting() override; // DECRQSS
bool PlaySounds(const VTParameters parameters) override; // DECPS
private:
enum class ScrollDirection
{

View File

@@ -138,6 +138,8 @@ public:
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

View File

@@ -221,6 +221,11 @@ public:
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...");

View File

@@ -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;

View File

@@ -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

View File

@@ -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)

View File

@@ -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
{

View File

@@ -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"));
}

View File

@@ -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
};