-
Notifications
You must be signed in to change notification settings - Fork 8.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
31 changed files
with
556 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
|
||
#include "precomp.h" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Oops, something went wrong.