Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
6 commits merged into from
Jun 1, 2022

Conversation

j4james
Copy link
Collaborator

@j4james j4james commented May 31, 2022

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

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.

@ghost ghost added Area-VT Virtual Terminal sequence support Issue-Feature Complex enough to require an in depth planning process and actual budgeted, scheduled work. Product-Terminal The new Windows Terminal. labels May 31, 2022
@j4james j4james marked this pull request as ready for review May 31, 2022 11:03
// so we need to exit the thread ASAP by throwing an exception.
if (shutdownStatus != std::future_status::timeout)
{
throw Microsoft::Console::VirtualTerminal::StateMachine::ShutdownException{};
Copy link
Member

@lhecker lhecker May 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using an exception to abort processing the next notes, would it make sense to use a boolean member instead and just return early in all further calls to PlayNote?

That way we wouldn't need to make any accommodations for this new exception type and the control flow would be contained entirely within this class.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with using a boolean is we then have to propagate that state up through the entire call stack, including all the following (and then some):

  • ControlCore::_terminalPlayMidiNote
  • Terminal::PlayMidiNote
  • AdaptDispatch::PlaySounds
  • OutputStateMachineEngine::ActionCsiDispatch
  • StateMachine::_ActionCsiDispatch
  • StateMachine::ProcessCharacter
  • StateMachine::ProcessString
  • ControlCore::_connectionOutputHandler

But half of those calls already use a boolean return value to signal conpty passthrough, so we'd need to come up with some other mechanism for differentiating between those states. In comparison the exception solution seemed a whole lot easier.

Copy link
Member

@lhecker lhecker May 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I thought it'd be fine to just do this:

void MidiAudio::PlayNote(const int noteNumber, const int velocity, const std::chrono::microseconds duration)
{
    if (_shutdown)
    {
        return;
    }

    // ...
}

It would still process the VT sequence, but effectively stop doing anything with it.
I thought that this would be equivalent to using an exception. After all, we don't have a ShutdownException for other sequences either. 🤔

Copy link
Member

@lhecker lhecker May 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see... The problem is this bit right?

but also signals the thread that it needs to exit as soon as possible.

I'll have to reread the PR again, because I haven't entirely understood yet why this is necessary. The "unlocked interval" is ControlCore::_terminalPlayMidiNote right? But that one only plays a single note, so the early return as suggested above should completely very quickly as well. I suppose I misunderstood something.

The reason I'm vary of using an exception here is because a lot of our code isn't exception safe (including ControlCore::_terminalPlayMidiNote and MidiAudio itself) and so it scares me a bit personally.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A note can last up to about 8 seconds I think. But even if it's only say half a second, you could have a whole buffer of notes still pending that could last for minutes. If we just handled this the way we do other sequences, the UI would hang for that entire duration, because we usually only release the terminal lock once the buffer is empty.

So that's why I've got the terminal temporarily unlocked while the sounds is in progress. But if the app/tab is closed during that period, we can't just carry on processing the rest of the buffer, because the user wouldn't want the remaining 10 minutes of audio to be output after they've closed.

Also at that point, half of the framework would have been destroyed anyway, so any attempt to continue processing output would likely end up in something crashing.

Copy link
Member

@lhecker lhecker May 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I was able to express my suggestion well, so just to be sure:
What I'm suggesting would be (as far as I can see) entirely equivalent to your current approach. After Shutdown has been called, all future calls to PlayNote return early and the remaining buffer finishes playing immediately. So it shouldn't block the UI more than it does now.

In my opinion the benefits from an exception-less approach in this particular case are an improved robustness against breaking this feature accidentally in the future. For instance if someone were to add a CATCH_LOG along this call chain in a few years, or maybe a COM call in-between, would anyone remember to test this particular VT sequence's behavior during shutdown?
The boolean flag has the benefit that the "behavior" is contained entirely within this class within a single file. This makes it "robust" in my view.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I'm suggesting would be (as far as I can see) entirely equivalent to your current approach. After Shutdown has been called, all future calls to PlayNote return early and the remaining buffer finishes playing immediately.

But that's not the way it currently works. When Shutdown is called, the exception it triggers in the Unlock is like a direct jump to ControlCore::_connectionOutputHandler. Once that returns there is zero further processing - there are no future calls to PlayNote or anything else. If there were, that would almost certainly kill the app, because everything would already have been deleted.

Shutdown is called from the ControlCore destructor, which also goes on to destroy the MidiAudio class. The only thing keeping it alive at that point is the lock in the destructor, and it'll be able to acquire that as soon as the the Unlock method throws. From then on there will be no more MidiAudio, no more ControlCore, no more Terminal, and no more TermControl.

That's why I say we need to exit the thread ASAP. I mean like immediately that instant. There's no time for anything else. I know this feels a bit precarious, but I can't see a cleaner way to handle it. What you're proposing would just crash (at least as I understand it).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense for TermControl. Who in conhost is going to handle this exception? Or are we going to bubble it up and out as a failure? I know we have a lot of failure states for exceptions coming out of lower tiers in conhost already...

I see that MIDI is shut down during ConsoleCloseProcessState; what happens when there are multiple applications connected to one console session and one of them exits (perhaps while a note is playing?) What if it isn't the one playing the note? Are we "deathly serial" here and will only process the exit after we shut down MIDI?

Will another process in the same session stand a chance of Initialize-ing MIDI again afterwards?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In conhost it's caught in the WriteConsoleWImplHelper and returns as an E_ABORT (the ShutdownException derives from wil::ResultException).

I'm not sure I understand what you mean by multiple processes in the same session. The MIDI handle itself is a shared resource that stays open for as long as the terminal process is running. The MidiAudio class that gets shutdown when a tab is closed is just a thing for managing the note blocking. That can be recreated as often as you like - it's just a field in theControlCore class.

I've tested opening and closing multiple tabs while they're all playing sounds at the same time, so I know that works. I don't know if you mean something else though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I'm thinking about one conhost session (not in Terminal) with multiple attached processes. I misremembered the purpose of CloseConsoleProcessState, and I thought that it would fire per attached process exit. You can safely ignore that question now that I know CCPS happens per console exit.

ACK on it being caught in WriteConsoleWImplHelper. 😄

src/audio/midi/MidiAudio.cpp Show resolved Hide resolved
src/cascadia/TerminalControl/ControlCore.cpp Show resolved Hide resolved
@jerch
Copy link

jerch commented May 31, 2022

@j4james Oh grats for getting it all wrapped up in a working format. 👍

Offtopic: Regarding #13091 (comment) plz hang on, hope to get down to the testing on the weekend.

Edit: In case you wondered - I want to address DECPS again later on with a more general sound addon also capable for PCM transports. But for this I first have to get the base64-sixel thingy straight (still needs arm simd and general lib cleanup).

@j4james
Copy link
Collaborator Author

j4james commented May 31, 2022

Offtopic: Regarding #13091 (comment) plz hang on, hope to get down to the testing on the weekend.

That will be great, thank you! I've actually also got a game I wanted to share with you at some point, which I'm hoping will be playable on your VT525, and which makes use of the DECPS sequence for sound effects. You can see a demo video of it here: #12631 (comment)

It works fairly well on both the VT100 and VT240 (tested with MAME), but you obviously need a VT525 for the full color support and sound effects. I still need to tweak the sounds to play one note at a time, though, since I was originally assuming multi-note support in DECPS.

@jerch
Copy link

jerch commented May 31, 2022

... You can see a demo video of it here: ... It works fairly well on both the VT100 ...

Wait wot? How do you draw those pacman symbols on a VT100? 🤯

@j4james
Copy link
Collaborator Author

j4james commented May 31, 2022

Wait wot? How do you draw those pacman symbols on a VT100? 🤯

Yeah, the VT100 can't do the graphics - it falls back to a basic ASCII-art rendition with some of the line-drawing symbols from the special graphics character set. I just meant that it was playable in terms of frame rate and responsiveness, so I figured a real hardware terminal should be able to handle it just as well.

Copy link
Member

@carlos-zamora carlos-zamora left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! This all seems pretty straightforward to me. The main question I have is if there's any way to keep the noexcepts, but it seems like the exception is the way to go.

Comment on lines +3476 to +3485
{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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing we can remove the Fuzzing config here

Copy link
Member

@DHowett DHowett May 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a dependency of host. It must be built for fuzzing because host must be built for fuzzing.

Copy link
Collaborator Author

@j4james j4james May 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. I wasn't sure what exactly was needed here - I just accepted what Visual Studio generated automatically. I'll remove the fuzzing if that's not necessary.

Edit: Only saw @DHowett's message after I replied. Fuzzing gets to live.

@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lhecker or @zadjii-msft:
Would there be any value to wrapping all of these in a method try-catch to preserve the noexcept-ness?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. If an exception is getting this far, it's likely because we want it to fall through to the next level, otherwise it would typically have been handled in the _SafeExecute method.

Comment on lines +3456 to +3465
{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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work with AuditMode too? Just making sure before it gets committed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, if the build below is succeeding with the AuditMode|***.Build.0 lines (which mean that audit build is enabled), this is working in audit mode.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can definitely compile it with the AuditMode configuration selected if that's what you mean. And I know it's actually doing something because it picked up a bunch of issues I needed to fix. I don't know if we actually need all of those options though - again I just accepted whatever Visual Studio created for me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, just making sure we're actually listening to whatever AuditMode catches, because VS adds it in and we don't check it sometimes. Sounds like everything is good here then :)

@carlos-zamora
Copy link
Member

@msftbot merge this in 15 minutes

@ghost ghost added the AutoMerge Marked for automatic merge by the bot when requirements are met label Jun 1, 2022
@ghost
Copy link

ghost commented Jun 1, 2022

Hello @carlos-zamora!

Because you've given me some instructions on how to help merge this pull request, I'll be modifying my merge approach. Here's how I understand your requirements for merging this pull request:

  • I won't merge this pull request until after the UTC date Wed, 01 Jun 2022 17:52:55 GMT, which is in 15 minutes

If this doesn't seem right to you, you can tell me to cancel these instructions and use the auto-merge policy that has been configured for this repository. Try telling me "forget everything I just told you".

@DHowett DHowett removed the AutoMerge Marked for automatic merge by the bot when requirements are met label Jun 1, 2022
@ghost ghost merged commit 9dca6c2 into microsoft:main Jun 1, 2022
@DHowett
Copy link
Member

DHowett commented Jun 1, 2022

Thanks for doing this! Since it adds a new lib and all, I'm gonna give it a post-merge review. Sorry for not being ahead of the ball @j4james!

Quick notes before I dive in: I'm worried about adding a new library dependency to conhost -- we're measured in the Windows Perf. Gates (WPG) on image loads, commit, disk I/O on startup, etc. and I am worried that loading dead parts of winmm might cause us some trouble there.

Should we consider delay binding the MIDI bits until they're first used? Maybe it won't help...

Note to self: I'll have to add a sources and dirs and learn what we need to do to add a new library to the conhost build. We haven't done that in literal years! 😄

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if anything else happens while the note was playing? Are we resizing, reflowing, allowing the user to lock the console with a pending selection or open the property sheet? If so, how do each of those things impact what happens next?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good question. In conhost it should be fine I think. When we release the lock, it's similar to what would happen if we'd just reached the end of the output buffer at that point. And then when we reacquire the lock, that's similar to receiving a new chunk of content that needs to be processed.

It gets trickier with a conpty connection though. Conpty doesn't know that it's blocked on the other side, so when you resize, I think it still sends through frame updates to redraw the page. But because those are blocked, they build up and produce a bit of a mess when they finally get through.

You can test with a sequence like this, which play 16 seconds worth of notes in a single call:

printf "\e[4;32;1;2;3;4;5;6;7;8;9;10;11;12;13;14;15;16,~"

I suspect that part of the problem is bash wanting to redraw the prompt when we resize, which we don't always handle very well at the best of times. But it just looks a little messed up - it's not like it's going to kill anything (I hope).

Also worth mentioning that the passthrough mode doesn't have this problem as far as I can see.

// so we need to exit the thread ASAP by throwing an exception.
if (shutdownStatus != std::future_status::timeout)
{
throw Microsoft::Console::VirtualTerminal::StateMachine::ShutdownException{};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense for TermControl. Who in conhost is going to handle this exception? Or are we going to bubble it up and out as a failure? I know we have a lot of failure states for exceptions coming out of lower tiers in conhost already...

I see that MIDI is shut down during ConsoleCloseProcessState; what happens when there are multiple applications connected to one console session and one of them exits (perhaps while a note is playing?) What if it isn't the one playing the note? Are we "deathly serial" here and will only process the exit after we shut down MIDI?

Will another process in the same session stand a chance of Initialize-ing MIDI again afterwards?

@DHowett
Copy link
Member

DHowett commented Jun 1, 2022

This is awesome, as usual. Thank you 😄

@j4james
Copy link
Collaborator Author

j4james commented Jun 1, 2022

Should we consider delay binding the MIDI bits until they're first used?

That seems reasonable. I know there's already a bit of a delay the first time you play something, so I don't think it would matter too much if this made it slightly worse. Is this just a linker flag we need to throw in somewhere?

#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();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgive me for jumping in like this, but isn't using an object while it is being destructed undefined behavior anyway? So this needs to be prevented in some other way?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't have thought so. Obviously the destructor itself is going to be using the object while it is being destructed, so I don't see why it should be any different for other methods. It's just not something that can be relied on in general, because a destructor may be busy cleaning up resources that the other methods depend on. But that's not the case here.

I may be wrong though. I'm not an expert on the standard. Do you have a reference to somewhere that documents this as undefined behavior? If necessary we could always call the Lock method manually before the destructor is reached - that's just a bit messier.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://docs.microsoft.com/en-us/cpp/standard-library/mutex-class-stl?view=msvc-170#dtormutex_destructor and https://en.cppreference.com/w/cpp/thread/mutex/%7Emutex say that the behavior is undefined if an std::mutex is destroyed while owned. It looks like that will happen here, as MidiAudio::~MidiAudio locks _inUseMutex and leaves it locked and the members are then destroyed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sorry, I misunderstood what @PetterS was saying. If the issue is just that the mutex shouldn't be destroyed while locked, we can just unlock it here as well. We're only acquiring the lock to make sure the playing thread isn't still holding it - we don't need to retain the lock.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, thanks. Perhaps it is OK as long as the mutex is also unlocked here.

@ghost
Copy link

ghost commented Jul 6, 2022

🎉Windows Terminal Preview v1.15.186 has been released which incorporates this pull request.:tada:

Handy links:

ghost pushed a commit that referenced this pull request Jul 14, 2022
## Summary of the Pull Request

The original `DECPS` implementation made use of the Windows MIDI APIs to
generate the sound, but that required a 3MB package dependency for the
GS wavetable DLS. This PR reimplements the `MidiAudio` class using
`DirectSound`, so we can avoid that dependency.

## References

The original `DECPS` implementation was added in PR #13208, but was
hidden behind a velocity flag in #13258.

## PR Checklist
* [x] Closes #13252
* [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: #13252

## Detailed Description of the Pull Request / Additional comments

The way it works is by creating a sound buffer with a single triangle
wave that is played in a loop. We generate different notes simply by
adjusting the frequency at which that buffer is played.

When we need a note to end, we just set the volume to its minimum value
rather than stopping the buffer. If we don't do that, the repeated
starting and stopping tends to produce a lot of static in the output. We
also use two buffers, which we alternate between notes, as another way
to reduce that static.

One other thing worth mentioning is the handling of the buffer position.
At the end of each note we save the current position, and then use an
offset from that position when starting the following note. This helps
produce a clearer separation between tones when repeating sequences of
the same note.

In an ideal world, we should really have something like an attack-decay-
sustain-release envelope for each note, but the above hack seems to work
reasonably well, and keeps the implementation simple.

## Validation Steps Performed

I've manually tested both conhost and Terminal with the sample tunes
listed in issue #8687, as well as a couple of games that I have which
make use of `DECPS` sound effects.
DHowett pushed a commit that referenced this pull request Jul 19, 2022
## Summary of the Pull Request

The original `DECPS` implementation made use of the Windows MIDI APIs to
generate the sound, but that required a 3MB package dependency for the
GS wavetable DLS. This PR reimplements the `MidiAudio` class using
`DirectSound`, so we can avoid that dependency.

## References

The original `DECPS` implementation was added in PR #13208, but was
hidden behind a velocity flag in #13258.

## PR Checklist
* [x] Closes #13252
* [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: #13252

## Detailed Description of the Pull Request / Additional comments

The way it works is by creating a sound buffer with a single triangle
wave that is played in a loop. We generate different notes simply by
adjusting the frequency at which that buffer is played.

When we need a note to end, we just set the volume to its minimum value
rather than stopping the buffer. If we don't do that, the repeated
starting and stopping tends to produce a lot of static in the output. We
also use two buffers, which we alternate between notes, as another way
to reduce that static.

One other thing worth mentioning is the handling of the buffer position.
At the end of each note we save the current position, and then use an
offset from that position when starting the following note. This helps
produce a clearer separation between tones when repeating sequences of
the same note.

In an ideal world, we should really have something like an attack-decay-
sustain-release envelope for each note, but the above hack seems to work
reasonably well, and keeps the implementation simple.

## Validation Steps Performed

I've manually tested both conhost and Terminal with the sample tunes
listed in issue #8687, as well as a couple of games that I have which
make use of `DECPS` sound effects.

(cherry picked from commit bc79867)
Service-Card-Id: 84270205
Service-Version: 1.15
@j4james j4james deleted the feature-decps branch August 31, 2022 00:38
zadjii-msft pushed a commit that referenced this pull request Apr 25, 2023
## Summary of the Pull Request

There are certain escape sequences that use the `VTParameters::subspan`
method to access a subsection of the provided parameter list. When the
parameter list is empty, that `subspan` call can end up using an offset
that is out of range, which causes the terminal to crash. This PR stops
that from happening by clamping the offset so it's in range.

## References and Relevant Issues

This bug effected the `DECCARA` and `DECRARA` operations introduced in
PR #14285, and the `DECPS` operation introduced in PR #13208.

## Validation Steps Performed

I've manually confirmed that the sequences mentioned above are no longer
crashing when executed with an empty parameter list, and I've added a
little unit test that checks `VTParameters::subspan` method is returning
the expected results when passed an offset that is out of range.

## PR Checklist
- [x] Closes #15234
- [x] Tests added/passed
- [ ] Documentation updated
- [ ] Schema updated (if necessary)
DHowett pushed a commit that referenced this pull request Apr 25, 2023
## Summary of the Pull Request

There are certain escape sequences that use the `VTParameters::subspan`
method to access a subsection of the provided parameter list. When the
parameter list is empty, that `subspan` call can end up using an offset
that is out of range, which causes the terminal to crash. This PR stops
that from happening by clamping the offset so it's in range.

## References and Relevant Issues

This bug effected the `DECCARA` and `DECRARA` operations introduced in
PR #14285, and the `DECPS` operation introduced in PR #13208.

## Validation Steps Performed

I've manually confirmed that the sequences mentioned above are no longer
crashing when executed with an empty parameter list, and I've added a
little unit test that checks `VTParameters::subspan` method is returning
the expected results when passed an offset that is out of range.

## PR Checklist
- [x] Closes #15234
- [x] Tests added/passed
- [ ] Documentation updated
- [ ] Schema updated (if necessary)

(cherry picked from commit e413a41)
Service-Card-Id: 89001985
Service-Version: 1.17
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-VT Virtual Terminal sequence support Issue-Feature Complex enough to require an in depth planning process and actual budgeted, scheduled work. Product-Terminal The new Windows Terminal.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for the DECPS escape sequence
7 participants