diff --git a/src/audio/midi/MidiAudio.cpp b/src/audio/midi/MidiAudio.cpp index 6440e97f66..dbb6c55e7e 100644 --- a/src/audio/midi/MidiAudio.cpp +++ b/src/audio/midi/MidiAudio.cpp @@ -5,10 +5,6 @@ #include "MidiAudio.hpp" #include "../terminal/parser/stateMachine.hpp" -#include - -#pragma comment(lib, "dxguid.lib") - using Microsoft::WRL::ComPtr; using namespace std::chrono_literals; @@ -17,12 +13,13 @@ using namespace std::chrono_literals; constexpr auto WAVE_SIZE = 16u; constexpr auto WAVE_DATA = std::array{ 128, 159, 191, 223, 255, 223, 191, 159, 128, 96, 64, 32, 0, 32, 64, 96 }; -MidiAudio::MidiAudio(HWND windowHandle) +void MidiAudio::_initialize(HWND windowHandle) noexcept { + _hwnd = windowHandle; _directSoundModule.reset(LoadLibraryExW(L"dsound.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32)); if (_directSoundModule) { - if (auto createFunction = GetProcAddressByFunctionDeclaration(_directSoundModule.get(), DirectSoundCreate8)) + if (const auto createFunction = GetProcAddressByFunctionDeclaration(_directSoundModule.get(), DirectSoundCreate8)) { if (SUCCEEDED(createFunction(nullptr, &_directSound, nullptr))) { @@ -35,55 +32,29 @@ MidiAudio::MidiAudio(HWND windowHandle) } } -MidiAudio::~MidiAudio() noexcept +void MidiAudio::BeginSkip() 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. - const auto lock = std::unique_lock{ _inUseMutex }; - } - catch (...) - { - // If the lock fails, we'll just have to live with the consequences. - } + _skip.SetEvent(); } -void MidiAudio::Initialize() +void MidiAudio::EndSkip() noexcept { - _shutdownFuture = _shutdownPromise.get_future(); + _skip.ResetEvent(); } -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 +void MidiAudio::PlayNote(HWND windowHandle, const int noteNumber, const int velocity, const std::chrono::milliseconds duration) noexcept try { + if (_skip.is_signaled()) + { + return; + } + + if (_hwnd != windowHandle) + { + _initialize(windowHandle); + } + const auto& buffer = _buffers.at(_activeBufferIndex); if (velocity && buffer) { @@ -106,10 +77,10 @@ try buffer->SetCurrentPosition((_lastBufferPosition + 12) % WAVE_SIZE); } - // 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); + // By waiting on the skip event with a maximum duration of the note, we'll + // either be paused for the appropriate amount of time, or we'll break out early + // because BeginSkip() was called. This happens for Ctrl+C or during shutdown. + _skip.wait(::base::saturated_cast(duration.count())); if (velocity && buffer) { diff --git a/src/audio/midi/MidiAudio.hpp b/src/audio/midi/MidiAudio.hpp index ad44bdc16e..5f6ac6af38 100644 --- a/src/audio/midi/MidiAudio.hpp +++ b/src/audio/midi/MidiAudio.hpp @@ -12,8 +12,6 @@ Abstract: #pragma once #include -#include -#include struct IDirectSound8; struct IDirectSoundBuffer; @@ -21,27 +19,20 @@ struct IDirectSoundBuffer; class MidiAudio { public: - MidiAudio(HWND windowHandle); - 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; + void BeginSkip() noexcept; + void EndSkip() noexcept; + void PlayNote(HWND windowHandle, const int noteNumber, const int velocity, const std::chrono::milliseconds duration) noexcept; private: + void _initialize(HWND windowHandle) noexcept; void _createBuffers() noexcept; + wil::slim_event_manual_reset _skip; + + HWND _hwnd = nullptr; wil::unique_hmodule _directSoundModule; - Microsoft::WRL::ComPtr _directSound; - std::array, 2> _buffers; + wil::com_ptr _directSound; + std::array, 2> _buffers; size_t _activeBufferIndex = 0; DWORD _lastBufferPosition = 0; - std::promise _shutdownPromise; - std::future _shutdownFuture; - std::mutex _inUseMutex; }; diff --git a/src/audio/midi/precomp.h b/src/audio/midi/precomp.h index fc7420d352..b300445837 100644 --- a/src/audio/midi/precomp.h +++ b/src/audio/midi/precomp.h @@ -25,7 +25,9 @@ Abstract: #endif // Windows Header Files: -#include +#include + #include +#include // clang-format on diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index dba86643b6..d8b51468cc 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -4,6 +4,10 @@ #include "pch.h" #include "ControlCore.h" +// MidiAudio +#include +#include + #include #include #include @@ -241,8 +245,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _renderer->TriggerTeardown(); } - - _shutdownMidiAudio(); } bool ControlCore::Initialize(const double actualWidth, @@ -401,9 +403,33 @@ namespace winrt::Microsoft::Terminal::Control::implementation const WORD scanCode, const ::Microsoft::Terminal::Core::ControlKeyStates modifiers) { + if (ch == L'\x3') // Ctrl+C or Ctrl+Break + { + _handleControlC(); + } + return _terminal->SendCharEvent(ch, scanCode, modifiers); } + void ControlCore::_handleControlC() + { + if (!_midiAudioSkipTimer) + { + _midiAudioSkipTimer = _dispatcher.CreateTimer(); + _midiAudioSkipTimer.Interval(std::chrono::seconds(1)); + _midiAudioSkipTimer.IsRepeating(false); + _midiAudioSkipTimer.Tick([weakSelf = get_weak()](auto&&, auto&&) { + if (const auto self = weakSelf.get()) + { + self->_midiAudio.EndSkip(); + } + }); + } + + _midiAudio.BeginSkip(); + _midiAudioSkipTimer.Start(); + } + bool ControlCore::_shouldTryUpdateSelection(const WORD vkey) { // GH#6423 - don't update selection if the key that was pressed was a @@ -1386,57 +1412,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - 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. + // 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); + _midiAudio.PlayNote(reinterpret_cast(_owningHwnd), noteNumber, velocity, std::chrono::duration_cast(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: - // - - // Return Value: - // - a reference to the MidiAudio instance. - MidiAudio& ControlCore::_getMidiAudio() - { - if (!_midiAudio) - { - const auto windowHandle = reinterpret_cast(_owningHwnd); - _midiAudio = std::make_unique(windowHandle); - _midiAudio->Initialize(); - } - return *_midiAudio; - } - - // Method Description: - // - Shuts down the MIDI audio system if previously instantiated. - // Arguments: - // - - // Return Value: - // - - 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 @@ -1521,6 +1504,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _closing = true; + // Ensure Close() doesn't hang, waiting for MidiAudio to finish playing an hour long song. + _midiAudio.BeginSkip(); + // Stop accepting new output and state changes before we disconnect everything. _connection.TerminalOutput(_connectionOutputEventToken); _connectionStateChangedRevoker.revoke(); diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index d2649b53a6..d7fcf432cf 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -288,6 +288,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _updateSelectionUI(); bool _shouldTryUpdateSelection(const WORD vkey); + void _handleControlC(); void _sendInputToConnection(std::wstring_view wstr); #pragma region TerminalCoreCallbacks @@ -305,10 +306,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation const std::chrono::microseconds duration); #pragma endregion - std::unique_ptr _midiAudio; - - MidiAudio& _getMidiAudio(); - void _shutdownMidiAudio(); + MidiAudio _midiAudio; + winrt::Windows::System::DispatcherQueueTimer _midiAudioSkipTimer{ nullptr }; #pragma region RendererCallbacks void _rendererWarning(const HRESULT hr); diff --git a/src/host/consoleInformation.cpp b/src/host/consoleInformation.cpp index 21c7428d5a..807be888b3 100644 --- a/src/host/consoleInformation.cpp +++ b/src/host/consoleInformation.cpp @@ -4,6 +4,10 @@ #include "precomp.h" #include +// MidiAudio +#include +#include + #include "misc.h" #include "output.h" #include "srvinit.h" @@ -373,38 +377,14 @@ Microsoft::Console::CursorBlinker& CONSOLE_INFORMATION::GetCursorBlinker() noexc } // Method Description: -// - Returns the MIDI audio instance, created on demand. +// - Returns the MIDI audio instance. // Arguments: // - // Return Value: // - a reference to the MidiAudio instance. MidiAudio& CONSOLE_INFORMATION::GetMidiAudio() { - if (!_midiAudio) - { - const auto windowHandle = ServiceLocator::LocateConsoleWindow()->GetWindowHandle(); - _midiAudio = std::make_unique(windowHandle); - _midiAudio->Initialize(); - } - return *_midiAudio; -} - -// Method Description: -// - Shuts down the MIDI audio system if previously instantiated. -// Arguments: -// - -// Return Value: -// - -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(); - } + return _midiAudio; } // Method Description: diff --git a/src/host/globals.cpp b/src/host/globals.cpp index d9450f0ce9..f36fc3686a 100644 --- a/src/host/globals.cpp +++ b/src/host/globals.cpp @@ -5,6 +5,10 @@ #include "globals.h" +// MidiAudio +#include +#include + #pragma hdrstop Globals::Globals() diff --git a/src/host/input.cpp b/src/host/input.cpp index 7a46081d20..2141f9d9fd 100644 --- a/src/host/input.cpp +++ b/src/host/input.cpp @@ -242,6 +242,24 @@ void HandleCtrlEvent(const DWORD EventType) } } +static void CALLBACK midiSkipTimerCallback(HWND, UINT, UINT_PTR idEvent, DWORD) noexcept +{ + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& midiAudio = gci.GetMidiAudio(); + + KillTimer(nullptr, idEvent); + midiAudio.EndSkip(); +} + +static void beginMidiSkip() noexcept +{ + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& midiAudio = gci.GetMidiAudio(); + + midiAudio.BeginSkip(); + SetTimer(nullptr, 0, 1000, midiSkipTimerCallback); +} + void ProcessCtrlEvents() { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); @@ -251,6 +269,8 @@ void ProcessCtrlEvents() return; } + beginMidiSkip(); + // Make our own copy of the console process handle list. const auto LimitingProcessId = gci.LimitingProcessId; gci.LimitingProcessId = 0; diff --git a/src/host/output.cpp b/src/host/output.cpp index 0a9fceb03c..140688fe42 100644 --- a/src/host/output.cpp +++ b/src/host/output.cpp @@ -512,8 +512,6 @@ 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 f136311a55..99af19715f 100644 --- a/src/host/outputStream.cpp +++ b/src/host/outputStream.cpp @@ -373,22 +373,15 @@ void ConhostInternalGetSet::SetWorkingDirectory(const std::wstring_view /*uri*/) // - 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. + // 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); + const auto windowHandle = ServiceLocator::LocateConsoleWindow()->GetWindowHandle(); + auto& midiAudio = ServiceLocator::LocateGlobals().getConsoleInformation().GetMidiAudio(); + midiAudio.PlayNote(windowHandle, noteNumber, velocity, std::chrono::duration_cast(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: diff --git a/src/host/server.h b/src/host/server.h index 1489caf19d..9d283833e2 100644 --- a/src/host/server.h +++ b/src/host/server.h @@ -140,7 +140,6 @@ public: Microsoft::Console::CursorBlinker& GetCursorBlinker() noexcept; MidiAudio& GetMidiAudio(); - void ShutdownMidiAudio(); CHAR_INFO AsCharInfo(const OutputCellView& cell) const noexcept; @@ -157,7 +156,7 @@ private: Microsoft::Console::VirtualTerminal::VtIo _vtIo; Microsoft::Console::CursorBlinker _blinker; - std::unique_ptr _midiAudio; + MidiAudio _midiAudio; }; #define ConsoleLocked() (ServiceLocator::LocateGlobals()->getConsoleInformation()->ConsoleLock.OwningThread == NtCurrentTeb()->ClientId.UniqueThread) diff --git a/src/interactivity/base/ServiceLocator.cpp b/src/interactivity/base/ServiceLocator.cpp index 6218dbfc01..47b7138f45 100644 --- a/src/interactivity/base/ServiceLocator.cpp +++ b/src/interactivity/base/ServiceLocator.cpp @@ -3,6 +3,10 @@ #include "precomp.h" +// MidiAudio +#include +#include + #include "../inc/ServiceLocator.hpp" #include "InteractivityFactory.hpp"