Keyboard Tricks
No. Not your PC keyboard. The other sort. In my case an AKAI MPK mini plus that I use with one of my other hats on. The musician (and author) Bill.
You see, I wrote a book. It’s about a band, touring, having disasters, falling in love, etc. And it’s going to be published shortly. But if you write a book about a band, they need songs. And they need to be original songs, else you’ll be paying royalties to the songwriters if you use their lyrics.
So I wrote my own songs, put them in the book. And I want my readers to hear them, so I have to record them. Which is fine, because I play guitar and bass, and drums. Still, some of the chord shapes were pretty awkward, and for one song I was having trouble moving multiple fingers with the required accuracy in the short time available, as it used some chords I had not been playing since age 15.
I wondered if PowerShell could help me. And I realised it could. A long time ago, when K&R roamed the planet (that’s Kernighan and Ritchie, whose book ‘The C Programming Language’ graced the desk of many a programmer), I wrote a program to print out guitar chord diagrams. It was hard work, because in C, you had to do everything. PowerShell does a lot more of the heavy lifting, so I decided to do it again, from scratch. In PowerShell.
But I had a new requirement - it should let me specify any guitar tuning, not just the standard E-A-D-G-B-E , so that I could pick a tuning that minimised the finger movements required between successive chords. It ought to be possible, because for chord changes to sound right, it’s often the case that some of the notes stay the same.
So here’s version 1…
#
# Script to generate chord shapes from guitar tuning and chord description
#
[CmdletBinding()]
param()
cls
#
# Notes are mapped to a numeric pitch, representing the number of semitones above our chosen root - in this case C
$Notes = @(
[pscustomobject]@{Name = 'C'; Pitch = 0;}
[pscustomobject]@{Name = 'D'; Pitch = 2;}
[pscustomobject]@{Name = 'E'; Pitch = 4;}
[pscustomobject]@{Name = 'F'; Pitch = 5;}
[pscustomobject]@{Name = 'G'; Pitch = 7;}
[pscustomobject]@{Name = 'A'; Pitch = 9;}
[pscustomobject]@{Name = 'B'; Pitch = 11;}
)
#
# it is conventional to describe chords using named intervals, which represent musical intervals from the major scale, modified
$Intervals = @(
[pscustomobject]@{Name = '1'; Offset = 0;}
[pscustomobject]@{Name = 'b3'; Offset = 3;}
[pscustomobject]@{Name = '3'; Offset = 4;}
[pscustomobject]@{Name = 'b5'; Offset = 6;}
[pscustomobject]@{Name = '5'; Offset = 7;}
[pscustomobject]@{Name = '#5'; Offset = 8;}
[pscustomobject]@{Name = 'bb7'; Offset = 9;}
[pscustomobject]@{Name = 'b7'; Offset = 10;}
)
# chords are built using sets of intervals
$Chords = @(
[pscustomobject]@{Name = 'major'; PrintName = ''; NoteList = @('1','3','5');}
[pscustomobject]@{Name = 'dominant 7'; PrintName = '7'; NoteList = @('1','3','5','b7');}
[pscustomobject]@{Name = 'diminished 7'; PrintName = '°'; NoteList = @('1','b3','b5','bb7');}
[pscustomobject]@{Name = 'minor'; PrintName = 'm'; NoteList = @('1','b3','5');}
[pscustomobject]@{Name = 'augmented'; PrintName = '+'; NoteList = @('1','3','#5');}
)
# these are some of the most common tunings for a six-string guitar
$Tunings = @(
[pscustomobject]@{TuningName = 'standard tuning' ; StringTuning = @('E','A','D','G','B','E') ;}
[pscustomobject]@{TuningName = 'DADGAD / D suspended' ; StringTuning = @('D','A','D','G','A','D') ;}
[pscustomobject]@{TuningName = 'Double DAD' ; StringTuning = @('D','A','D','D','A','D') ;}
[pscustomobject]@{TuningName = 'Drop D' ; StringTuning = @('D','A','D','G','B','E') ;}
[pscustomobject]@{TuningName = 'E Minor' ; StringTuning = @('E','B','E','G','B','E') ;}
[pscustomobject]@{TuningName = 'ICFTB 1' ; StringTuning = @('E','C','E','G','B','E') ;}
[pscustomobject]@{TuningName = 'Open G' ; StringTuning = @('D','G','D','G','B','D') ;}
[pscustomobject]@{TuningName = 'Open D' ; StringTuning = @('D','A','D','F#','A','D') ;}
)
$Script:Instrument = 'Guitar'
I’ve taken the decision to map notes from a name - ‘C’ - to an integer - 0 - and the rest of the notes are defined by the number of semitones they are away from C. That lets me do arithmetic in modulo 12 (western music being written using a 12-tone scale). This idea forms the basis of the intervals table, which is used to express the intervals (in semitones) between the notes of various chords, relative to the root note of the chord.
The various intervals can then be combined in groups of 3 or more to create chords. Not all combinations sound nice to the ear, but the chords table lists some common ones used in songwriting.
Now let’s add some functions for those structures:
function Get-NotePitch {
[CmdletBinding()]
param (
[string]$NoteName
)
#
# get the basic note
$NoteRow = $Notes | Where-Object {$_.Name -eq $NoteName[0]}
$Pitch = $NoteRow.Pitch
#
# adjust for trailing sharps or flats
for ($ix = 1; $ix -lt $NoteName.Length; $ix++) {
if ($NoteName[$ix] -eq 'b') {
$Pitch--
}
elseif ($NoteName[$ix] -eq '#') {
$Pitch++
}
else {
}
}
$Pitch
}
function Display-GuitarChord {
[CmdletBinding()]
param (
[string]$Root,
[string]$Type,
[int]$MaxFrets = 5
)
#
# get the root note of the chord
$Pitch = Get-NotePitch -NoteName $Root
#
# get the notes associated with the type of chord (e.g. major, augmented)
$ChordNotes = @()
$ChordRow = $Chords | Where-Object {$_.Name -eq $Type}
$ChordRow.NoteList | ForEach-Object {
# for each interval specified in the chord, get the number of semitones associated
$Interval = $_
$IntervalRow = $Intervals | Where-Object {$_.Name -eq $Interval}
# convert to a pitch
$Val = $Pitch + $IntervalRow.Offset
# bring it back into the range 0-11
$ChordNotes += ($Val % 12)
}
Write-Host -ForegroundColor Cyan "Notes in chord $Root$($ChordRow.PrintName) ($Type) : $($ChordNotes -join ',')"
# set up the open string notes of the guitar
$StringPitchArray = @()
$Script:GuitarTuning.StringTuning | ForEach-Object {
$StringNote = $_
$StringPitch = Get-NotePitch -NoteName $StringNote
$StringPitchArray += $StringPitch
}
# at each fret, determine the pitch of the string, and whether that pitch is found in the chord
for ($fret = 0; $fret -lt $MaxFrets; $fret++) {
# how to represent the fingering differs for the nut position (open) and where a string must be fretted
if ($fret -eq 0) {
$Divider = ' '
$DefaultChar = 'x'
}
else {
$Divider = '|'
$DefaultChar = ' '
}
$RowText = ' ' + $Divider
# work out what notes could be played from the chord for each string at the current fret
$StringPitchArray | ForEach-Object {
$StringPitch = ($_ + $fret) % 12
$Char = $DefaultChar
for ($index = 0; $index -lt $ChordNotes.Count; $index++) {
$Pitch = $ChordNotes[$index]
if ($StringPitch -eq $Pitch) {
$Char = $index + 1 # rather than simply showing a dot, show which note in the chord is being played (root = 1)
}
}
$RowText = $RowText + $Char + $Divider
}
$RowText | Out-Host
' -------------' | Out-Host
}
Write-Host ""
Write-Host ""
}
That lets us set a tuning at the start of the script and print out fingerings for the various chords. This section runs through all the different tunings I’ve added, looking for one that minimised the awkward chord changes:
$Tunings | ForEach-Object {
$Script:GuitarTuning = $_
Write-Host ('=' * 50)
Write-Host "Tuning set to $($Script:GuitarTuning.TuningName), strings tuned $($Script:GuitarTuning.StringTuning -join '-') "
Display-GuitarChord -Root 'C' -Type 'major'
Display-GuitarChord -Root 'A' -Type 'minor'
Display-GuitarChord -Root 'F#' -Type 'minor'
}
(In case you’re puzzled by the tuning ICFTB 1, it’s the tuning I decided on for my troublesome song.)
But then I realised I could use this infrastructure for the keyboard, which might add an extra dimension to the songs.
Now I can just about play single notes with a bit of time to work out where the dot is on the stave, work out if any sharps or flats apply, and then transfer that to a keyboard. It’s a slow process. But maybe if I can print out the keyboard showing the keys, I’d be able to print out fingering diagrams, and once again, spot easy chord changes.
In the end, it needed a bit of lateral thought, but regular expressions came to my rescue (don’t they always?). The piano keyboard is fixed, but complex to draw programatically, so I used ASCII art, and a regex to substitute notes that are used, and another regex to clear out the notes that aren’t used.
This is the piano section, which adds to the end of the above.
$Script:Instrument = 'Piano'
$PianoLayout = @"
| | | | | | | | | | | | | | | | | | | | |
| P | Q | | | R | S | T | | | P | Q | | | R | S | T | | | P | Q |
| | | | | | | | | | | | | | | | | | | | |
---------- | ------------- | --------- | ------------- | ---------
| | | | | | | | | | | | | | | | |
| C | D | E | F | G | A | B | C | D | E | F | G | A | B | C | D | E
| | | | | | | | | | | | | | | | |
"@
[string]$Script:PianoStartNote = 'C' # planned enhancement to print an offset subset of the keyboard
[int]$Script:PianoNotesToDisplay = 13 # planned enhancement to restrict the width of the keyboard
[int]$Script:PianoTranspose = 0 # non-zero, if we want to transpose by a number of semitones
function Display-PianoChord {
[CmdletBinding()]
param (
[string]$Root,
[string]$Type
)
#
# get the root note of the chord
$Pitch = Get-NotePitch -NoteName $Root
#
# get the notes associated with the type of chord (e.g. major, augmented)
$ChordNotes = @()
$ChordRow = $Chords | Where-Object {$_.Name -eq $Type}
$ChordRow.NoteList | ForEach-Object {
# for each interval specified in the chord, get the number of semitones associated
$Interval = $_
$IntervalRow = $Intervals | Where-Object {$_.Name -eq $Interval}
# convert to a pitch
$Val = $Pitch + $IntervalRow.Offset + $Script:PianoTranspose
# bring it back into the range 0-11
$ChordNotes += ($Val % 12)
}
Write-Host -ForegroundColor Cyan "Notes in chord $Root$($ChordRow.PrintName) ($Type) : $($ChordNotes -join ',')"
$NoteMap = 'CPDQEFRGSATB' # character equivalents of notes 0-11
$PianoDisplay = $PianoLayout
for ($index = 0; $index -lt $ChordNotes.Count; $index++) {
# for each note in the chord
$Note = $ChordNotes[$index]
$Substitution = $NoteMap[$Note] # substitute all occurrences of the letter that maps to the note ...
$PianoDisplay = $PianoDisplay -replace $Substitution,($index + 1) # with the index of the note in the chord
}
#
# now clear out anything that hasn't already been substituted for a number
$Regex = "[$NoteMap]"
$PianoDisplay = $PianoDisplay -replace $Regex,' '
$PianoDisplay | Out-Host
Write-Host ""
Write-Host ""
}
function Display-Chord {
[CmdletBinding()]
param (
[string]$Root,
[string]$Type,
[int]$MaxFrets = 5
)
if ($Script:Instrument -eq 'Guitar') {
Display-GuitarChord -Root $Root -Type $Type -MaxFrets $MaxFrets
}
elseif ($Script:Instrument -eq 'Piano') {
Display-PianoChord -Root $Root -Type $Type
}
}
$Script:Instrument = 'Piano'
Display-Chord -Root 'C' -Type 'major'
Display-Chord -Root 'A' -Type 'minor'
Display-Chord -Root 'F#' -Type 'minor'
cls
Write-Host "Chords for: I Come From The Blues"
if ($Script:Instrument -eq 'Guitar') {
Write-Host "Tuning: $($Script:GuitarTuning.StringTuning -join ' ')`n"
}
else {
Write-Host ""
}
Display-Chord -Root 'A' -Type 'minor'
Display-Chord -Root 'C' -Type 'augmented'
Display-Chord -Root 'D' -Type 'minor'
Display-Chord -Root 'Eb' -Type 'diminished 7'
Display-Chord -Root 'E' -Type 'dominant 7' -MaxFrets 6
Display-Chord -Root 'F' -Type 'minor' -MaxFrets 7
Write-Host "Done"
And for those who are curious, the book is called ‘Teardown’ (under the name William Campbell Powell) and it’ll be published by NineStar Press in December 2024. Get your library to buy it, then borrow it for free.