Inspired by Jeffrey Hicks Christmas prompts [#1,#2] for PowerShell, I thought I’d do my part to give some Christmas spirit as well.
UPDATE! I have added a PowerShell-v2-compatible version at the bottom of the post.
Combining Jeffreys prompts with my code should take you a long way towards getting in the right Christmas mood. Note, that although I’m not a total beginner when it comes to music, working with Console.Beep is kind of restricting, so take it for what it is.
While writing my little Christmas melody, I tried to keep with the PowerShell best practices as well. I started out hard-coding the note frequencies in an array, but scrapped it and created a function to automatically convert a note name to it’s corresponding frequency instead. While I were at it, I added the MIDI note number as well, in case anyone might need that. And to top it of, I added a method that let you play the note.
I’d like to challenge my fellow PowerShell scripters to create more cool PowerShell music! If you do make something, please leave me a comment below so I can have a listen 🙂
Merry Christmas!
| # define melody | |
| $melody = @( | |
| @('g4',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('c5',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('c5',[Duration]::EIGHTH), | |
| @('d5',[Duration]::EIGHTH), | |
| @('c5',[Duration]::EIGHTH), | |
| @('b4',[Duration]::EIGHTH), | |
| @('a4',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('a4',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('a4',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('d5',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('d5',[Duration]::EIGHTH), | |
| @('e5',[Duration]::EIGHTH), | |
| @('d5',[Duration]::EIGHTH), | |
| @('c5',[Duration]::EIGHTH), | |
| @('b4',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('g4',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH) | |
| @('g4',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('e5',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('e5',[Duration]::EIGHTH), | |
| @('f5',[Duration]::EIGHTH), | |
| @('e5',[Duration]::EIGHTH), | |
| @('d5',[Duration]::EIGHTH), | |
| @('c5',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('a4',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('g4',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('a4',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('d5',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('b4',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('c5',[Duration]::EIGHTH) | |
| ) | |
| # play melody | |
| foreach ($note in $melody) { | |
| if (-not($note[0] -eq 'PAUSE')) { | |
| (ConvertTo-NoteFrequency $note[0]).Play($note[1]) | |
| } | |
| else { | |
| Start-Sleep -Milliseconds $note[1] | |
| } | |
| } |
| Add-Type -TypeDefinition @" | |
| public enum Duration | |
| { | |
| WHOLE = 1600, | |
| HALF = WHOLE/2, | |
| QUARTER = HALF/2, | |
| EIGHTH = QUARTER/2, | |
| SIXTEENTH = EIGHTH/2, | |
| } | |
| "@ | |
| function ConvertTo-NoteFrequency { | |
| <# | |
| .SYNOPSIS | |
| Convert note names to MIDI note number and frequency. | |
| .DESCRIPTION | |
| This function takes a note name and will convert it to it's MIDI note number | |
| as well as the frequency of the note. It uses the 12-tone scale and | |
| supports both the 'regular', and the 'German' scale where 'H' is used instead of 'B'. | |
| .EXAMPLE | |
| ConvertTo-NoteFrequency 'A#4' | |
| .EXAMPLE | |
| ConvertTo-NoteFrequency 'b5' -UseAlternateScale | |
| .EXAMPLE | |
| (ConvertTo-NoteFrequency 'e5').Play(200) | |
| .EXAMPLE | |
| (ConvertTo-NoteFrequency 'e5').Play([Duration]::QUARTER) | |
| .NOTES | |
| Author: Øyvind Kallstad | |
| Date: 14.12.2014 | |
| Version: 1.0 | |
| #> | |
| [CmdletBinding()] | |
| param ( | |
| # The name of the note. | |
| [Parameter(Position = 0, Mandatory)] | |
| [ValidateNotNullorEmpty()] | |
| [string] $Note, | |
| # Default octave to append to note if none are present. | |
| [Parameter()] | |
| [ValidateRange(0,8)] | |
| [int] $DefaultOctave = 0, | |
| # Use this switch to use the alternate scale used in parts of northern europe where B = H. | |
| [Parameter()] | |
| [switch] $UseAlternateScale | |
| ) | |
| # split note input into the note part and the octave part | |
| $noteSplit = $Note -split '(\d)' | |
| $noteName = $noteSplit[0] | |
| [int]$octave = $noteSplit[1] | |
| Write-Verbose "Note Name: $noteName" | |
| # convert to lower case | |
| $noteName = $noteName.ToLower() | |
| # replace flats with sharps | |
| $noteName = $noteName -replace 'db', 'c#' | |
| $noteName = $noteName -replace 'eb', 'd#' | |
| $noteName = $noteName -replace 'gb', 'f#' | |
| # handle alternate scale | |
| if($UseAlternateScale) { | |
| $scale = @('a','a#','h','c','c#','d','d#','e','f','f#','g','g#') | |
| $noteName = $noteName -replace 'b', 'a#' | |
| $regexString = '^[acdefgh]#{0,1}$' | |
| } | |
| else { | |
| $scale = @('a','a#','b','c','c#','d','d#','e','f','f#','g','g#') | |
| $noteName = $noteName -replace 'bb', 'a#' | |
| $regexString = '^[abcdefg]#{0,1}$' | |
| } | |
| Write-Verbose "Note Name after conversion: $noteName" | |
| # validate note name | |
| if (-not($noteName -match $regexString)) { | |
| Write-Warning "'$($Note)' is not a valid note name!" | |
| } | |
| else { | |
| # if no octave information is given, add default octave | |
| if(-not($octave)) { | |
| $octave = $DefaultOctave | |
| } | |
| Write-Verbose "Ocatave: $octave" | |
| # if note is above 'c', subtract 1 to the octave - since 'c' marks the beginning of the next octave | |
| $inputOctave = $octave | |
| if(-not($scale[0..2] -contains $noteName)) { | |
| if($octave -gt 0) { | |
| $octave-- | |
| Write-Verbose "Octave after conversion: $($octave)" | |
| } | |
| } | |
| # find the number of half-steps up from 'A' | |
| $halfSteps = 0 | |
| foreach ($scaleNote in $scale) { | |
| if(-not($scaleNote -eq $noteName)) { | |
| $halfSteps++ | |
| } | |
| else { | |
| break | |
| } | |
| } | |
| Write-Verbose "Number of half-steps up from A: $($halfSteps)" | |
| # we initially set the frequency of A0 | |
| # then update it with the frequency of 'A' in the same octave as the Note | |
| $aFrequencyInNoteOctave = 27.5 | |
| $aFrequencyInNoteOctave *= (1 -shl $octave) | |
| Write-Verbose "The frequency of 'A' in octave $($octave) is $($aFrequencyInNoteOctave)" | |
| # calculate the note frequency | |
| $noteFrequency = [math]::Pow([math]::Pow(2,(1/12)),$halfSteps) * $aFrequencyInNoteOctave | |
| # calculate the MIDI note number of the note | |
| $midiNoteNumber = [math]::Round(12 * [math]::Log(($noteFrequency/440),2)) + 69 | |
| $output = [PSCustomObject] [Ordered] @{ | |
| Input = $Note | |
| Note = $noteName + $inputOctave | |
| Frequency = $noteFrequency | |
| MidiNumber = $midiNoteNumber | |
| } | |
| # add method for playing the note | |
| $output | Add-Member -Name 'Play' -MemberType ScriptMethod -Value {param([int]$Length)[System.Console]::Beep($this.Frequency,$Length)} -Force | |
| Write-Output $output | |
| } | |
| } |
| # PowerShell v2 compatible version | |
| try { | |
| Add-Type -TypeDefinition @" | |
| public enum Duration | |
| { | |
| WHOLE = 1600, | |
| HALF = WHOLE/2, | |
| QUARTER = HALF/2, | |
| EIGHTH = QUARTER/2, | |
| SIXTEENTH = EIGHTH/2, | |
| } | |
| "@ | |
| } | |
| catch{ } | |
| function ConvertTo-NoteFrequency { | |
| <# | |
| .SYNOPSIS | |
| Convert note names to MIDI note number and frequency. | |
| .DESCRIPTION | |
| This function takes a note name and will convert it to it's MIDI note number | |
| as well as the frequency of the note. It uses the 12-tone scale and | |
| supports both the 'regular', and the 'German' scale where 'H' is used instead of 'B'. | |
| .EXAMPLE | |
| ConvertTo-NoteFrequency 'A#4' | |
| .EXAMPLE | |
| ConvertTo-NoteFrequency 'b5' -UseAlternateScale | |
| .EXAMPLE | |
| (ConvertTo-NoteFrequency 'e5').Play(200) | |
| .EXAMPLE | |
| (ConvertTo-NoteFrequency 'e5').Play([Duration]::QUARTER) | |
| .NOTES | |
| Author: Øyvind Kallstad | |
| Date: 14.12.2014 | |
| Version: 1.0 | |
| #> | |
| [CmdletBinding()] | |
| param ( | |
| # The name of the note. | |
| [Parameter(Position = 0, Mandatory = $true)] | |
| [ValidateNotNullorEmpty()] | |
| [string] $Note, | |
| # Default octave to append to note if none are present. | |
| [Parameter()] | |
| [ValidateRange(0,8)] | |
| [int] $DefaultOctave = 0, | |
| # Use this switch to use the alternate scale used in parts of northern europe where B = H. | |
| [Parameter()] | |
| [switch] $UseAlternateScale | |
| ) | |
| # split note input into the note part and the octave part | |
| $noteSplit = $Note -split '(\d)' | |
| $noteName = $noteSplit[0] | |
| [int]$octave = $noteSplit[1] | |
| Write-Verbose "Note Name: $noteName" | |
| # convert to lower case | |
| $noteName = $noteName.ToLower() | |
| # replace flats with sharps | |
| $noteName = $noteName -replace 'db', 'c#' | |
| $noteName = $noteName -replace 'eb', 'd#' | |
| $noteName = $noteName -replace 'gb', 'f#' | |
| # handle alternate scale | |
| if($UseAlternateScale) { | |
| $scale = @('a','a#','h','c','c#','d','d#','e','f','f#','g','g#') | |
| $noteName = $noteName -replace 'b', 'a#' | |
| $regexString = '^[acdefgh]#{0,1}$' | |
| } | |
| else { | |
| $scale = @('a','a#','b','c','c#','d','d#','e','f','f#','g','g#') | |
| $noteName = $noteName -replace 'bb', 'a#' | |
| $regexString = '^[abcdefg]#{0,1}$' | |
| } | |
| Write-Verbose "Note Name after conversion: $noteName" | |
| # validate note name | |
| if (-not($noteName -match $regexString)) { | |
| Write-Warning "'$($Note)' is not a valid note name!" | |
| } | |
| else { | |
| # if no octave information is given, add default octave | |
| if(-not($octave)) { | |
| $octave = $DefaultOctave | |
| } | |
| Write-Verbose "Ocatave: $octave" | |
| # if note is above 'c', subtract 1 to the octave - since 'c' marks the beginning of the next octave | |
| $inputOctave = $octave | |
| if(-not($scale[0..2] -contains $noteName)) { | |
| if($octave -gt 0) { | |
| $octave-- | |
| Write-Verbose "Octave after conversion: $($octave)" | |
| } | |
| } | |
| # find the number of half-steps up from 'A' | |
| $halfSteps = 0 | |
| foreach ($scaleNote in $scale) { | |
| if(-not($scaleNote -eq $noteName)) { | |
| $halfSteps++ | |
| } | |
| else { | |
| break | |
| } | |
| } | |
| Write-Verbose "Number of half-steps up from A: $($halfSteps)" | |
| # we initially set the frequency of A0 | |
| # then update it with the frequency of 'A' in the same octave as the Note | |
| $aFrequencyInNoteOctave = 27.5 | |
| for ($i = 0; $i -lt $octave; $i++) { | |
| $aFrequencyInNoteOctave = $aFrequencyInNoteOctave * 2 | |
| } | |
| Write-Verbose "The frequency of 'A' in octave $($octave) is $($aFrequencyInNoteOctave)" | |
| # calculate the note frequency | |
| $noteFrequency = [math]::Pow([math]::Pow(2,(1/12)),$halfSteps) * $aFrequencyInNoteOctave | |
| # calculate the MIDI note number of the note | |
| $midiNoteNumber = [math]::Round(12 * [math]::Log(($noteFrequency/440),2)) + 69 | |
| $output = New-Object -TypeName 'PSObject' | |
| $output | Add-Member -MemberType NoteProperty -Name 'Input' -Value $Note | |
| $output | Add-Member -MemberType NoteProperty -Name 'Note' -Value ($notaName + $inputOctave) | |
| $output | Add-Member -MemberType NoteProperty -Name 'Frequency' -Value $noteFrequency | |
| $output | Add-Member -MemberType NoteProperty -Name 'MidiNumber' -Value $midiNoteNumber | |
| $output | Add-Member -Name 'Play' -MemberType ScriptMethod -Value {param([int]$Length)[System.Console]::Beep($this.Frequency,$Length)} | |
| Write-Output $output | |
| } | |
| } | |
| # define melody | |
| $melody = @( | |
| @('g4',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('c5',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('c5',[Duration]::EIGHTH), | |
| @('d5',[Duration]::EIGHTH), | |
| @('c5',[Duration]::EIGHTH), | |
| @('b4',[Duration]::EIGHTH), | |
| @('a4',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('a4',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('a4',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('d5',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('d5',[Duration]::EIGHTH), | |
| @('e5',[Duration]::EIGHTH), | |
| @('d5',[Duration]::EIGHTH), | |
| @('c5',[Duration]::EIGHTH), | |
| @('b4',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('g4',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH) | |
| @('g4',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('e5',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('e5',[Duration]::EIGHTH), | |
| @('f5',[Duration]::EIGHTH), | |
| @('e5',[Duration]::EIGHTH), | |
| @('d5',[Duration]::EIGHTH), | |
| @('c5',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('a4',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('g4',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('a4',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('d5',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('b4',[Duration]::EIGHTH), @('PAUSE',[Duration]::SIXTEENTH), | |
| @('c5',[Duration]::EIGHTH) | |
| ) | |
| # play melody | |
| foreach ($note in $melody) { | |
| if (-not($note[0] -eq 'PAUSE')) { | |
| (ConvertTo-NoteFrequency $note[0]).Play($note[1]) | |
| } | |
| else { | |
| Start-Sleep -Milliseconds $note[1] | |
| } | |
| } |
One comment