PowerShell Audio Sequencer

Mon, Apr 20, 2009 3-minute read

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:

#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.