PowerShell Cookbook

Search

Categories

 

On this page

PowerShell RPN Calculator
PowerScripting Podcast 85 – Eventing, Transactions, Security
PowerShell Cookbook Now Available on iPhone
PowerShell equivalent of NET HELPMSG
More advanced HTTP scripting: Facebook Photo Album Downloader
Auto-Updating With No Reboot
Why So Secure?
Get-Help –Online
Things more likely to kill you than Swine Flu
PowerShell Audio Sequencer

Archive

Blogroll

Disclaimer
I work for Microsoft.

The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

RSS 2.0 | Atom 1.0 | CDF

Send mail to the author(s) E-mail

Total Posts: 257
This Year: 8
This Month: 2
This Week: 0
Comments: 785

Sign In

 Monday, October 12, 2009
Monday, October 12, 2009 8:08:26 PM (Pacific Daylight Time, UTC-07:00) ( )

Adam Barr blogged bits and pieces of a PowerShell RPN calculator a few years ago: first the basics, and then some tweaks to clean it up. An RPN calculator, if you haven’t played with one before, flips the way you enter data. Rather than type “2 + 2”, you type “2 2 +”. RPN-style calculation supposedly has lots of great benefits. While I understand it and can do it, I wouldn’t say I “get it.”

Anyways.

One of the things he runs into with the last version is the PowerShell’s (version one) inability to dynamically invoke static methods:

… you should be able to write:

$add = [Decimal]::Add
$add.Invoke(2,3)

but it doesn't work. That's not exactly the same as getting the value of a static property, but maybe there's a related issue, or maybe I just can't figure out how to do it right.

Well, in version two it does work, and that makes the RPN calculator a whole lot cleaner.

Another interesting aspect about the latest implementation is that it has a lot of very similar code segments. It has tables for operations on the Double class that take one argument, operations on the Double class that take two arguments, operations on the Math class that take one argument, and operations on the Math class that take two arguments. Afterward, there are four nearly identical blocks of code that perform the operation and store the result.

Note: This of course isn’t a slag on the implementation, this is just continuing the tinkering on an interesting concept.

We can simplify this in two ways:

  1. Don’t create hard-coded lists of operators. Instead, we’ll look at all methods in the Math and Double class to see if one matches. This approach gives us 30-something operators for free, and perhaps more in future versions of the .NET Framework.
  2. Don’t create hard-coded lists of arity: the number of arguments consumed by an operator. Instead, we’ll look at the method overloads that match the operator name, and see how many arguments they consume.

Now, there are some subtleties to both points:

  1. The method names in the Decimal and Math classes get kind of long. You don’t want to have to write “2 3 Multiply” in an RPN calculator. To work around that, we’ll define a hashtable of shortcuts that simply map operators to their names.
  2. Some operators have multiple overloads. For example, the one-argument Round method rounds a number to zero decimal points. The two-argument Round method rounds it to the specified number of decimal points.

By leveraging PowerShell’s built-in support for introspection and dynamic method invocation, we now have a script that is both much shorter, and much more powerful.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
##############################################################################
##
## rpn.ps1
##
##############################################################################

<#

.SYNOPSIS
Evaluates a statement as an RPN calculator. Supports all operations from
System.Math and System.Decimal.

.EXAMPLE
rpn 2 2 +

.EXAMPLE
rpn 2 3 + 1.3 / 2 Round2 Negate

#>



$s = new-object System.Collections.Stack
$n = 0d

$shortcuts = @{
    "+" = "Add"; "-" = "Subtract"; "/" = "Divide"; "*" = "Multiply";
    "%" = "Remainder"; "^" = "Pow"; "||" = "Abs"
}

:ARGLOOP foreach ($a in $args) {
    if($shortcuts[$a]) { $a = $shortcuts[$a] }

    ## First, see if it's a number. If so, push it.
    try { $s.Push( [Decimal] $a ); continue } catch {}

    ## It's an operation. Extract the operation name
    ## (such as Floor, Round, etc.) It may also represent a
    ## specific operation (such as Round2 - Round to specified precision).
    $argCountList = $a -replace "(\D+)(\d*)",'$2'
    $op = $a.Substring(0, $a.Length - $argCountList.Length)

    ## We support any static operations from the Decimal or Math classes
    foreach($type in [Decimal],[Math])
    {
        if($definition = $type::$op)
        {
            ## If they haven't specifically given the number of arguments,
            ## see how many this method supports. We go through each overload
            ## definition, and see how many commas it has.
            if(-not $argCountList)
            {
                $argCountList = $definition.OverloadDefinitions |
                    Foreach-Object { ($_ -split ", ").Count } |
                    Sort-Object -Unique
            }

            ## Now, for each overload, see if we can call it.
            foreach($argCount in $argCountList)
            {
                try
                {
                    $methodArguments = $s.ToArray()[($argCount-1)..0]
                    $result = $type::$op.Invoke($methodArguments)

                    ## If we were able to call the method, pop all of its
                    ## arguments off of the stack.
                    $null = 1..$argCount | % { $s.Pop() }

                    ## Then push the result
                    $s.Push($result)
                    continue ARGLOOP
                }
                catch
                {
                    ## If we catch an error, try with the next number of
                    ## arguments
                }
            }
        }
    }
}

$s
Comments [0] | | # 
 Monday, September 28, 2009
Monday, September 28, 2009 4:11:07 PM (Pacific Daylight Time, UTC-07:00) ( )

Last week, I dropped by to be interviewed on the PowerScripting Podcast. We chatted about providers, eventing, transactions, security, and a whole slew of V2 improvements that you probably didn’t know about!

Give it a listen: http://powerscripting.wordpress.com/2009/09/27/episode-85-lee-holmes-talks-about-v2/

Comments [0] | | # 
 Thursday, September 24, 2009
Friday, September 25, 2009 4:08:06 AM (Pacific Daylight Time, UTC-07:00) ( )

Call me as shocked as anybody, but the iPhone app store now has a new entry:

image

Although I can’t find any official information about it on the rest of the internet, O’Reilly has teamed up with Lexcycle (authors of the Stanza iPhone book reading application) to create iPhone applications for many of the top O’Reilly cookbooks.

Just when you thought the “PowerShell Pocket Reference” was a good value per pound, this one comes in at a net weight of zero! Definitely a pocket-sized reference worth carrying around.

Note: As you browse through the book, many of the code samples are cut off by the default zoom. To see the code in its entirety, use the familiar iPhone pinch + zoom gestures to zoom in and out.

Comments [0] | | # 
 Monday, September 14, 2009
Tuesday, September 15, 2009 12:33:05 AM (Pacific Daylight Time, UTC-07:00) ( )

Sometimes, people ask, “What’s the equivalent of NET HELPMSG” in PowerShell?

NET HELPMSG is probably the easiest to remember (and works in PowerShell of course,) but PowerShell improves the experience a bunch by way of the .NET Framework.

Suppose you get error 0x80070652 from an installer:

C:\Users\leeholm>net helpmsg 0x80070652
The syntax of this command is:

NET HELPMSG
message#

Oops, it doesn’t support hex. You need to take the last 4 digits:

C:\Users\leeholm>net helpmsg 0652
0652 is not a valid Windows network message number.

More help is available by typing NET HELPMSG 3871.

Oops, forgot to convert it to decimal.

C:\Users\leeholm>set /a c = 0x652
1618
C:\Users\leeholm>net helpmsg 1618

Another installation is already in progress. Complete that installation before proceeding with this install.

Perfect.

No matter your preference, PowerShell makes this easier. The System.ComponentModel.Win32Exception class supports constructors for all of the main scenarios, so PowerShell’s type casting support gives another way (albeit with a slightly ungainly syntax:)

[D:\documents\tools]
PS:163 > [ComponentModel.Win32Exception] 0x80070652
Another installation is already in progress. Complete that installation before proceeding with this install

[D:\documents\tools]
PS:164 > [ComponentModel.Win32Exception] 0x80070652
Another installation is already in progress. Complete that installation before proceeding with this install

[D:\documents\tools]
PS:165 > [ComponentModel.Win32Exception] –2147023278
Another installation is already in progress. Complete that installation before proceeding with this install

[D:\documents\tools]
PS:166 > [ComponentModel.Win32Exception] 0x0652
Another installation is already in progress. Complete that installation before proceeding with this install

[D:\documents\tools]
PS:167 > [ComponentModel.Win32Exception] 1618
Another installation is already in progress. Complete that installation before proceeding with this install

So don’t go replacing all of your NET HELPMSG muscle memory, but when you find yourself struggling with the requirements, give the [ComponentModel.Win32Exception] class a try.

Comments [0] | | # 
 Friday, September 04, 2009
Friday, September 04, 2009 7:20:03 AM (Pacific Daylight Time, UTC-07:00) ( )

I wanted to download a photo album from Facebook, but of course there’s no simple API or option to do that. Searching the internet finds a couple of options (a Firefox plugin, a Facebook app,) but I couldn’t seem to find anything standalone.

This gives another great opportunity to talk about advanced HTTP scripting in PowerShell. We’ve talked about it in the past (here: http://www.leeholmes.com/blog/AdvancedHTTPASPNetScriptingWithPowerShell.aspx,) so this script introduces some new techniques.

There are a couple of challenges for scripting Facebook, especially using the techniques in the earlier post.

The first is that the login sequence is secure :) The login page gets served over SSL, meaning that the basic Send-TcpRequest commands we did previously won’t work. SSL requires encryption and a complex handshake. Luckily the System.Net.WebClient class provides that for use.

The second issue is that the cookies are dynamic, and given out in two stages. You need to first connect to the main Facebook homepage, at which point you get a couple of session cookies. Then, you connect to the login page, at which point you get some login cookies. Once you have THOSE, you can script the rest of Facebook.

Here is Get-FacebookCookie.ps1:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
## Get-FacebookCookie.ps1
## Logs into Facebook, returning the cookie required for further operations

param($Credential)

$Credential = Get-Credential $Credential

## Get initial cookies
$wc = New-Object System.Net.WebClient
$wc.Headers.Add("User-Agent", "User-Agent: Mozilla/4.0 (compatible; MSIE 7.0;)")

$result = $wc.DownloadString("http://www.facebook.com/")
$cookie = $wc.ResponseHeaders["Set-Cookie"]
$cookie = ($cookie.Split(',') -match '^\S+=\S+;' -replace ';.*','') -join '; '

## Login
$bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($credential.Password)
$password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
[System.Runtime.InteropServices.Marshal]::ZeroFreeBstr($bstr)

$wc = New-Object System.Net.WebClient
$wc.Headers.Add("User-Agent", "User-Agent: Mozilla/4.0 (compatible; MSIE 7.0;)")
$wc.Headers.Add("Cookie", $cookie)
$postValues = New-Object System.Collections.Specialized.NameValueCollection
$postValues.Add("email", $credential.Username)
$postValues.Add("pass", $password)

## Get the resulting cookie, and convert it into the form to be returned in the query string
$result = $wc.UploadValues("https://login.facebook.com/login.php?login_attempt=1", $postValues)
$cookie = $wc.ResponseHeaders["Set-Cookie"]
$cookie = ($cookie.Split(',') -match '^\S+=\S+;' -replace ';.*','') -join '; '
$cookie

The most complex part of that script comes from the Set-Cookie headers returned by the server. Information in that header limits the domain of the cookie, expiration dates, and more. Browsers use this information to determine cookie policies, but we want to just blindly feed it back to Facebook. The little match, replace, and join combination converts the “Set-Cookie” syntax into one suitable to return to the server.

Once we’ve logged into Facebook, we download the album page, cycling through successive pages until we stop finding photos to download. Since you can derive the URL to the large size images from the thumbnails, we don’t need to do an extra request for the photo information page. If you wanted to extract photo comments as well, then you would have to.

Here is Get-FacebookAlbum.ps1:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
## Get-FacebookAlbum.ps1
## Downloads the images attached to a Facebook photo album
param($Album = $(Read-Host "Inital album URL (for example, http://www.facebook.com/album.php?aid=12345&id=12345&ref=nf)"),
    $Prefix,
    $Cookie
    )
   
$albumId = $album -replace '.*id=([\d+]+).*','$1'

if(-not $Cookie)
{
    $cookie = Get-FacebookCookie
}

$pageNumber = 1
do
{
    ## Go through each page in the album. Extract the thumbnail images (which have a pattern
    ## of /s______.jpg or ______s.jpg)
    $foundPhotos = $false
    "Getting album $album, page $pageNumber"

    $wc = New-Object System.Net.WebClient
    $wc.Headers.Add("User-Agent", "User-Agent: Mozilla/4.0 (compatible; MSIE 7.0;)")
    $wc.Headers.Add("Cookie", $cookie)
    $result = $wc.DownloadString($album + "&page=$pageNumber")

    ## Regex for images
    $regex = "<\s*img\s*[^>]*?src\s*=\s*[`"']*([^`"'>]+)[^>]*?>"
   
    $photos = ($result | Select-String $regex -AllMatches).Matches | % { $_.Groups[1].Value }
    $photos = $photos -match "_" + $albumId + "_"
    $photos = $photos -replace '/s','/n'
    $photos = $photos -replace '_s.jpg','_n.jpg'
   
    "Found photos:"
    $photos
   
    $wc = New-Object System.Net.WebClient
   
    ## For each of the photos we found, download the large size
    foreach($photo in $photos | ? { $_.Trim() })
    {
        $foundPhotos = $true
        $uri = [Uri] $photo
        $dest = $uri.LocalPath.Replace("\", "_")
        $dest = $uri.LocalPath.Replace("/", "_")
        $dest = Join-Path $pwd "$Prefix$dest"
        if(-not (Test-Path $dest))
        {
            "Downloading $dest"
            $wc.DownloadFile($photo, $dest)
        }
    }
   
    $pageNumber++
} while($foundPhotos);

"(None)"

 

So in under 100 lines of code, we’ve got ourselves an image downloader.

Comments [3] | | # 
 Friday, July 10, 2009
Saturday, July 11, 2009 6:18:00 AM (Pacific Daylight Time, UTC-07:00) ( )

Despite being a fan of security, I’ve always run Windows Update in “Download and Notify” mode for all of my machines. Having Windows Update reboot my machine without warning at 3:00 AM is just too destructive.

This has two negative effects:

- Security patches that don’t require a reboot are needlessly postponed.
- Programs that auto-update frequently via Microsoft Update turn into a permanent task-bar nag that you have updates to install.

After doing some research, it turns out that it’s possible to have the best of both worlds with just a little configuration. Set Microsoft Update to install updates automatically, and then set a Group Policy flag that prevents auto-reboot. Problem solved!

http://blogs.technet.com/mu/archive/2008/10/02/windows-update-and-automatic-reboots.aspx

Comments [3] | | # 
 Monday, July 06, 2009
Monday, July 06, 2009 6:28:12 PM (Pacific Daylight Time, UTC-07:00) ( )

After working with PowerShell for awhile, then starting to write scripts, you quickly learn about PowerShell’s Execution Policy and other security features. You might even throw up your hands, and ask, “Why is this configuration so HARD? Why can’t it be as easy as VBS and Perl?” You can double-click on a VBS or CMD script and have it run automatically. Those scripts could invoke PowerShell anyways, so why put all this effort into security?

Double-click-to-run illustrates a weakness in vbscript’s design, not PowerShell’s. You could also double-click an EXE and have it do whatever you want. We’re concerned about being the entry point for an attack, not the means by which it is carried out.

The problem is that the world is extremely sensitive to scripting security. More sensitive than they are about EXE security. This is mostly driven by an over-reaction to a rude awakening: in the times of Melissa / ILoveYou, many people didn’t even know that these things called “scripts” were functionally equivalent to executables. That burned the Outlook developers, Exchange developers, network administrators, system administrators, and poor CEOs that thought a love letter must most naturally be about them.

During those outbreaks, millions of people watched their Outlook inbox fill with “love letters” from intelligent co-workers. Since the virus wasn’t a worm, each one of those was the result of somebody double-clicking on a script out of curiosity.

Fast forward a few years, and many of you already live in a world of retribution and over-reaction where “security experts” give guidance on how to remove WSH and scripting support from Windows: http://www.bing.com/search?q=uninstall+wsh. On the PowerShell team, we constantly hear from people impacted by these Mordac-like policies applied to PowerShell. Execution Policies often get abused as user restrictions, as misguided and ineffective as that is. See our Security Guiding Principles for more information about why Execution Policies are not user restrictions!

Being beyond reproach when it comes to security is crucial to PowerShell’s success. It’s the reason you can sing loudly from the rooftops for things such as this: http://it.slashdot.org/it/05/08/05/0411254.shtml.

Now, the lack of shell association indeed made it difficult to use PowerShell scripts from other automation programs in V1. While usability and security tend to conflict, we’re always looking for ways to get both. For example, in V2, we added the right-click option “Run with PowerShell” to address the interactive case. We added a “-File” parameter to PowerShell.exe to address the "dumb quoting and path translation" issues that used to make it difficult to launch PowerShell scripts from outside of PowerShell. We added an “–ExecutionPolicy” parameter (and other execution policy scopes) to let you use PowerShell.exe to automate tasks with no permanent system impact.

By writing a PowerShell script, you’re already among the very small population of computer users that understand scripting, security, and the impact of malware. It’s easy to lose track of the fact that not everybody has the same experience, background, and knowledge. PowerShell’s security features are designed to level the playing field for everybody else.

Comments [0] | | # 
 Tuesday, June 09, 2009
Tuesday, June 09, 2009 6:42:33 PM (Pacific Daylight Time, UTC-07:00) ( )

In CTP2 of PowerShell v2, we added a new parameter to Get-Help: Online. This parameter lets help authors declare an “online version” of their help topic, which PowerShell then launches with your default browser. For the PowerShell cmdlet help topics, we redirect to the excellent (and more frequently updated) online cmdlet help topics: http://technet.microsoft.com/en-us/library/dd347701.aspx.

PowerShell supports online help content for both MAML-based help topics (http://msdn.microsoft.com/en-us/library/bb525433(VS.85).aspx, http://blogs.msdn.com/powershell/archive/2006/09/14/Draft-Creating-Cmdlet-Help.aspx, http://blogs.msdn.com/powershell/archive/2008/12/24/powershell-v2-external-maml-help.aspx) and inline help comments (http://technet.microsoft.com/en-us/library/dd819489.aspx.)

To add support for the –Online parameter to your own cmdlet help, add a maml:navigationLink element with a maml:uri node to the maml:relatedLinks section in your MAML help. The help system uses the first navigation link that contains a URI as the target for the link.

<maml:relatedLinks>
              <maml:navigationLink>
                     <maml:linkText>Online version:</maml:linkText>
                     <maml:uri>http://go.microsoft.com/fwlink/?LinkID=113279</maml:uri>
              </maml:navigationLink>
(…)
</maml:relatedLinks>

When writing comment-based help, use the .LINK tag for the same effect:

001
002
003
004
005
006
007
008
## .SYNOPSIS
## Gets an object from the system
## .LINK
## http://www.microsoft.com/technet/scriptcenter/hubs/msh.mspx
function Get-MyObject
{
    "Hello!"
}

For more information about this feature, see Get-Help Get-Help –Parameter Online.

Comments [0] | | # 
 Wednesday, April 29, 2009
Wednesday, April 29, 2009 5:35:41 PM (Pacific Daylight Time, UTC-07:00) ( )

There are a few. Based on worldwide numbers (160 confirmed deaths, 3000 “suspected” cases)

  • Falling out of bed (900 confirmed deaths)
  • Falling down the stairs (1,690 confirmed deaths)
  • Big storm (874 confirmed deaths)
  • Drinking binge (346 confirmed deaths)

Of course, the statistic left out by the news

  • Normal flu (36,000 confirmed deaths per year)

http://www.nsc.org/research/odds.aspx

Comments [2] | | # 
 Monday, April 20, 2009
Monday, April 20, 2009 9:53:43 PM (Pacific Daylight Time, UTC-07:00) ( )

I got forwarded an addictive interactive sequencer yesterday (http://lab.andre-michelle.com/tonematrix) and was immediately hooked. I asked an internal mailing list if there was any kind of hardware that lets you do this kind of thing on the couch, and got the response -- “you mean MIDI?” That’s close, but it is closer to a very simplified sequencer.

I play classical guitar... even being a fan of electronic music, I had never seen a sequencer used, or tried to make anything in one. I’m sure some researcher out there would love to have me for a “out of touch with reality” anthropology study.

Then I wondered, “Why should GUI folks have all the fun?”

88 lines later, a PowerShell Sequencer / Tracker was born: http://www.leeholmes.com/projects/PsTracker/PsTracker.zip. Even as a jaded scripter, I’m constantly amazed how compact PowerShell is. Given an example input:

# Replace any dash with something else to make a sound in that spot.
# Format: <NOTE><OCTAVE> <PATTERN>
# If you restrict yourself to a pentatonic scale (i.e. CDEGAC), anything sounds good.
# Instruments: # ([Toub.Sound.Midi.GeneralMidiInstruments] | gm -static -mem Property | % { $_.Name } ) -join " "

# .Instrument OverdrivenGuitar

C5 ---------OO-OO--
A5 -------OO-OO---O
G4 --------------O-
E4 ----------------
D4 X---X---X---X---
C4 ----------------
A4 ----------------
G3 ----X-----------
E3 ----------------
D3 ----------------
C3 ----------------
A3 -------OO-OO----
G2 ----------------
E2 ----------------
D2 ----------------
C2 ----------------

# .Instrument SquareLead
C6 -X-X-XX-X--XXX

# .Instrument Ocarina
C7 ---------------X-X-XX-X--XXX

# .Instrument Kalimba
C8 -----------------X---X---X---X--

This is all it takes to process it:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
#requires -Version 2
param($path, $bpm)

$scriptPath = & { Split-Path $myInvocation.ScriptName }

$trackEntries = @{}

function Update-Track
{
    $trackEntries.Clear()
    $instrument = $null

    foreach($line in Get-Content $path)
    {
        if($line -match ".*Instrument (.+)([\s]*)$")
        {
            $instrument = $matches[1]
            if(-not $trackEntries[$instrument]) { $trackEntries[$instrument] = @{} }
        }
        elseif($line -notmatch "#|(^[\s]*$)")
        {
            $note,$measures = -split $line
            for($measure = 0; $measure -lt $measures.Length; $measure++)
            {
                if($measures[$measure] -ne "-")
                {
                    $trackEntries[$instrument][$measure] = @($trackEntries[$instrument][$measure] + $note)
                }
                $trackEntries[$instrument]["Length"] = [Math]::Max($trackEntries[$instrument]["Length"], $measure)
            }
        }
    }
}


$fsw = New-Object System.IO.FileSystemWatcher (Split-Path (Resolve-Path $path).ProviderPath),$path
Register-ObjectEvent $fsw Changed -SourceIdentifier TrackUpdated

Update-Track

Add-Type -Path (Join-Path $scriptPath "Toub.Sound.Midi.dll")
[Toub.Sound.Midi.MidiPlayer]::OpenMidi()

try
{
    $sleep = 250
    if($bpm) { $sleep = 1000 * 120 / (8 * $bpm) }

    $currentMeasures = @{}
    while($true)
    {
        $activeNotes = @()
   
        foreach($instrument in $trackEntries.Keys)
        {
            if(-not $currentMeasures[$instrument]) { $currentMeasures[$instrument] = 0 }
            $mappedInstrument = [Toub.Sound.Midi.GeneralMidiInstruments]::$instrument

            [Toub.Sound.Midi.MidiPlayer]::Play(
                (New-Object Toub.Sound.Midi.ProgramChange 0,0,$mappedInstrument) )
   
            foreach($note in $trackEntries[$instrument][$currentMeasures[$instrument]])
            {
                [Toub.Sound.Midi.MidiPlayer]::Play( (New-Object Toub.Sound.Midi.NoteOn 0,0,$note,127) )
                $activeNotes += New-Object Toub.Sound.Midi.NoteOff 0,0,$note,127
            }

            $currentMeasures[$instrument] =
                ($currentMeasures[$instrument] + 1) % (1 + $trackEntries[$instrument]["Length"])
               
        }

        Start-Sleep -m $sleep
        $activeNotes | % { [Toub.Sound.Midi.MidiPlayer]::Play($_) }

        if(Get-Event *TrackUpdated*)
        {
            Remove-Event TrackUpdated
            Update-Track
        }
    }
}
finally
{
    [Toub.Sound.Midi.MidiPlayer]::CloseMidi()
    Unregister-Event TrackUpdated
    Remove-Event *TrackUpdated*
}

 

For example:

.\Start-Tracker track.txt 60

If your system has a MIDI instrument for “Cowbells,” make sure to add more of them! This script builds on Stephen Toub's MIDI library, which I can't seem to find a reference to any longer.

As an aside, that research junket eventually led me to playing with a more feature-rich (free) sequencer called Linux Multimedia Studio. Keeping with the basis of starting with a pentatonic scale, this took only about an hour or two: http://www.leeholmes.com/projects/PsTracker/strive.mp3.

 

Comments [0] | | #