I loathe BitLocker.  In fact, I often let a ‘ch’ phoneme slip in between the first and second vowel.  The reason is that it seems to be on some random hair-trigger.  Patching will trigger Recovery Mode.  Sometimes even having a USB HD or memory stick attached will trigger it.  I have a Surface Pro, in part to access my IT’s BitLocker Recovery Key escrow service.

Look at tablet.  Remember six digits.  Turn to laptop.  Type six digits.  Repeat.

Sometimes, helpful coworkers offer to read them out to me.  That’s great, but what about when this bites me while they’re not around?  That’s what just happened, and instead of doing it the hard way and being done in 5 minutes, I did it the harder way, and had an enjoyable hour defusing my frustration.  Oh, and of course I over-engineered it so it has the options for case-sensitivity and for punctuation characters.

Capital ach. Ee. Ell. Ell. Oh. Space. Double-you. Oh, Are. Ell. Dee. Bang.

function Out-VoiceByCharacter
{
    <#
    .Synopsis
    Play with System.Speech.Synthesis.SpeechSynthesizer

    .Description
    Wrap .NET's text-to-speech engine to say the input character-by-character.

    This is especially handy for entering the Bitlocker Recovery Key.

    .parameter InputObject
    Text to be spoken.

    .parameter Rate
    Speed of speech, from -10 (draaaaaawl) to +10 (spdy).  Default 0.

    .parameter Volume
    Volume.  Default 100.

    .parameterSpeaker
    Spoken voice.  Default is 'David', adult male.  String is matched as a
    substring against all available speakers' names, and the first match is
    selected.

    .parameter ListSpeaker
    List choices for -Speaker parameter.

    .parameter CaseSensitive
    Say 'Capital' in front of capital letters.  Normally, 'a' and 'A' are same.

    .parameter Punctation
    Speak punctuation characters, e.g. 'bang' for '!'.  Embedded dictionary can
    be replaced by $global:specials.
    
    #>

    param (
        [Parameter(ValueFromPipeline=$true)]
        [String[]]$InputObject = $null,

        [ValidateRange(-10,10)]
        [int]$Rate = 0,

        [ValidateRange(0,100)]
        [int]$Volume = 100,

        [string]$Speaker = 'David',

        [switch]$ListSpeaker,

        [switch]$CaseSensitive,

        [switch]$Punctuation
    );

    begin 
    {
        Add-Type -AssemblyName System.speech;
        $voice = New-Object System.Speech.Synthesis.SpeechSynthesizer;
        $voice.Rate = $Rate;
        $voice.Volume = $Volume;

        if (!$ListSpeaker -and (
            $mySpeaker = $voice.GetInstalledVoices().VoiceInfo |
            ? { $_.Name -match $Speaker } |
            Select-Object -First 1 -ExpandProperty Name
        ))
        {
            $voice.SelectVoice($mySpeaker);
        }
        else
        {
            Write-Warning "-Speaker '$speaker' not found.  Valid voices are:";
            $ListSpeaker = $true;

        } # if (!$ListSpeaker -and ...

        if ($ListSpeaker) 
        {
            $voice.GetInstalledVoices().VoiceInfo;

            # return doesn't work here.  
            $InputObject = $null;

        } # if ($ListSpeaker)

        if (!$specials)
        {
            $specials = @{
                '~' = 'tilde';             " " = 'space';
                '``' = 'backquote';        "`n" = 'newline';
                '!' = 'bang';              "`r" = 'carriage return';
                '@' = 'at';                '[' = 'open bracket';
                '#' = 'hash';              ']' = 'close bracket';  
                '$' = 'dollar sign';       '{' = 'open brace';         
                '%' = 'percent';           '}' = 'close brace';     
                '^' = 'caret';             '|' = 'pipe';   
                '&' = 'ampersand';         "\" = 'backwhack';       
                '*' = 'splat';             "'" = 'single quote';   
                '(' = 'open paren';        '"' = 'double qutoe';        
                ')' = 'close paren';       ':' = 'colon';         
                '-' = 'dash';              ';' = 'semicolon';  
                '_' = 'underscore';        '<' = 'less than';        
                '+' = 'plus';              '>' = 'greater than';  
                '=' = 'equals';            '.' = 'dot';    
                "`t" = 'tab';              '?' = 'question mark';  
                '/' = 'slash';
            
            }; # $specials = 
        
        } # if (!$specials

    } # begin

    process
    { 
        $InputObject | 
        % { 
            [char[]]$_ | 
            % { 
                $char = [string]$_;

                if ($CaseSensitive -and ($char -cmatch '[A-Z]')) 
                { $voice.Speak('Capital'); }

                if ($Punctuation -and ($tryPunctuation = $specials.$char)) 
                { 
                    $voice.Speak([string]$tryPunctuation); 
                
                } # if ($Punctuation -and ...
                else 
                { 
                    $voice.Speak([string]$char); 
                
                } # if ($Punctuation -and ... else
        
            } # [char[]]$_ | % {

        } # $InputObject | % {

    } # process

} # function Out-VoiceByCharacter