š¼ Functionally programming The King of Pop
A quick note
Well over a year has passed since I started this post ā the real world got in the way. Iāve spent āsomeā time revisiting the post itself but also the code, music and process around something it. As something a little āout thereā in both (semi-)musical and programming endeavours, I hope it was worth itā¦
Oh, but if youāre feeling a bit TL;DR, skip to the music.
Where were we?
In the previous post, we looked at the Haskell library Euterpea to create simple music using (functional) programming techniques and General MIDI.
In this part, weāll take some of these ideas further, and create something resembling a full piece of music, in fact a cover of a well known song. As itās concentrating more on the audio side than the coding side, I assume some basic music / audio engineering knowledge, but donāt let that put you off.
Choosing a song
The King of Pop
It seems appropriate in the (EDIT: still) current light of EPA-destruction and climate treaty withdrawals to choose the mid-90s ode to the salvation of the planet by the late King of Pop himself, Michael Jackson.
Yes, Iām talking about Jacksonās most successful UK single and 1995ās UK Christmas #1, Earth Song!
Bad
Of course, it is completely inappropriate to choose this song, as:
- Itās over six minutes long (on the album)
- Has an epic, non-conventional song structure
- That key change mid-songā¦
- Itās heavily produced ā layered synths, R&B-like sections, rock guitars, live drums, strings sections, sound effects, gospel choirs, a harp (!), brass sections.
- Lots of funk elements (which rely on years of player experience), i.e.Ā hidden complexity. This is a problem with code too.
- The default SoundFont Iām using to do this is prettyā¦ limited, even for General Midi.
But then, it wouldnāt be fun without a few challenges!
Making some music
What about us?
ā¹ļø Interactive is always more fun, so Iāll be providing a few GHCId (interactive REPL) commands along the way. To get set up, you can see the previous post, or hereās the short version:
- First make sure you have your MIDI synth running (try
timidity -iA -Os -A 300 -f
for example). - Then, in your Euterpea project directory (you can clone this sandbox if you want),
- run
stack repl
. - Work out what MIDI channel youāre on using the
devices
command, and call thischannel
.
Theyāll be marked up a bit like this:
Ī»> playDev channel $ c 4 wn
Chords
For simplicity I used 3 voices almost everywhere:
abMinor :: Dur -> Music Pitch
= aChordOf [cf 3, ef 3, af 3]
abMinor
= aChordOf [cf 3, gf 3, af 3]
abMin7 -- etc...
Note my aChordof
helper function (which saves a lot of typing / promotes re-use). It takes a list of notes (without duration) and creates some Music
.
aChordOf :: [Dur -> Music a] -> Dur -> Music a
= chord $ map ($dur) notes aChordOf notes dur
A quick aside on $
Wait ā whatās $dur
? Is this Bash or PHP suddenly? š
ā¹ļø Haskellās ubiquitous yet mysterious-to-newbies $
operator can be used for more than just avoiding brackets, as I found out a while ago myself, to some confusion. For example, you can apply a parameter to a partially applied function:
Ī»> inc x = x + 1 -- or just: inc x = (+1)
Ī»> ($ 123) inc
124
The parser doesnāt need a space for it, either (but you do need the parentheses):
Ī»> map ($3) [(+1), (*2), (div 6)]
[4,6,2]
So in Euterpea ($en) (c 4)
is just applying c 4
(the note, needing a duration) to en
(eighth note, A.K.A. quaver, the duration)!
What about us?
In your REPL:
Ī»> playDev channel $ rest chord [af 4 en, cf 4 en , ef 4 en]
š¼...
or something a bit more melodic:
Ī»> aChordOf notes dur = chord $ map ($dur) notes
Ī»> abMinor = aChordOf [af 4, cf 4, ef 4]
Ī»> introRun = aMinor qn :+: line [cf 6 en, bf 5 en, af 5 en, ef 5 en]
Ī»> playDev channel $ introRun
š¼...
Sounding familiar?
Drum Patterns
Luckily, the drum pattern here is relatively simple. The subtleties and performance are another matter, but weāre in the business of approximating here.
I find that the list format of line
is useful for drum patterns, and itās best if possible to keep these even measures, or you go mad. Eighth notes are good here, so letās adopt that.
Just like clean coding, I find well named music variables can help understand the code. Here Iāve used vaguely onomatopoeiac names for drums, and later extracted these to allow duration variations; yes, I think Iāve just coined higher-ordered percussion. WAT.
-- Helpers of type Dur -> Music (Pitch, Volume)
-- ...will depend a lot on your synth and SoundFont / patchset
= addVolume 115 . perc BassDrum1
boomOf = addVolume 70 . perc AcousticSnare
smakOf = addVolume 40 . perc ClosedHiHat
tssOf
-- Basic sounds
= boomOf en
boom = smakOf en
smak = tssOf en
tss
--Rest notes aliases
= rest en
da = rest sn d_
Once we have that vocabulary defined, itās easy to talk about simple drum patterns!
-- A basic bass, snare rock beat
= forever $ line [boom, da, smak, da]
rockBeat
= forever $ line [addVolume 10 $ perc Tambourine sn]
shaker
-- Re-use existing aliases then double the speed
= tempo 2 $ line [boom, boom, smak, boom :=: tss]
fill
-- An 8-bar beat with a fast fill at the very end
= cut 8 $ (cut 7.75 rockBeat :+: fill) beat
Note how we did the fill by re-using existing eighth-note definitions and using the tempo
modifier to double the speed of the whole phrase. DRY in action! (the fill itself needs some work though, sorry, purists).
The cut
function is very useful ā note how it can take non-integer durations too ā so reusing drum patterns is easy, as you can cut off the end (adding a fill instead) rather than duplicating a large part of the content. More DRY, essentially.
What about us?
Try it in your REPL:
Ī»> playDevS channel $ rest qn :+: beat
Note that:
- we add a small rest (
rest qn
) to help with the hiccups with some MIDI synths when starting playback playDevS
is the strict variant ofplayDev
, which does timing better (see the interesting arguments around this behaviour from the creators).
Hi-hats
-- Basic disco-style hihats
= line [tss, tss :=: sh]
discoHats
-- Has some variation on the last beat in the section
= forever $ line [times 7 discoHats, tss, tss :=: tssss] hats2
Layering sounds in MIDI
Is this a good idea?
Not only is this definitely not semantic either (a bit like View code in your Model in MVC), but this is not even vaguely a good idea anyway: Your results will definitely differ. Rendering the output is the only guarantee, and even thatās not so much fun. 1
So that said weāll continue regardless šā¦
Thicken drums
I found the SoundFont I was using (Timidity plus FreePat, I think) was nice in places but horrible in others, and especially lacked some thickness in the drums. So creating that huge 90s drum sound was challenging.
- Thicker bass drums by layering a low tom (!), more like an 808 kick TBH, on to the bassdrum at a lower volume when we wanted the full weight of the drums (chorus, outro, etc)
- Layering the electric snare on top of the acoustic one made a nice punchy sound.
- The clap sound for the (second) bridge / pre-chorus I eventually got closer to (oh ā the SoundFontās actual clap was totally broken for me) by using a little rimshot (too thin / short by itself), a high wood block at low volume to give the reverby strong percussive sound, with a bit of Cabasa (a latin shaker-like instrument) for the missing noise in the upper part of the spectrum. Phew.
- Also for the second bridge, the drummer uses a gentle but very tight closed hihat. The sound was far too open (splashy / sizzly) even when closed. For the very tight sound, I ended up overlaying a mute triangle (sharp decay, high tone) with a very quiet closed hihat. Not ideal, but better.
Phatten the bass
Subharmonics (octaves below the actual note) are nice when used well. This is a very blunt way of doing this. Remember we havenāt got any signal processing capabilities here ā weāre committed to 100% General MIDI! š¤
Spread the pads
Now I will confess a complete lack of knowledge around arranging pads and strings, so most of this feels very wrong, and pretty basic at best. Perhaps someone out there can advise better practices.
The idea was to take the simple three-part harmonies and replicate across instruments, layering and mixing to get nearer to the sound. Several parts were transposed an octave up (transpose 12
) to occupy more of the spectrum. Some parts had tension notes (e.g.Ā sus9
), which generally were only in the upper registers, sounded a bit too muddy otherwise. All in all, I had to keep turning the velocities (volumes) down, itās easy to drown the mix in midrange, when the actual song is very full.
Building up the song structure
Repetition and sections
In the previous post I explained how coding could be used to avoid repetition in the notation of the music (at the expense of some complexity, of course). With the amount of variation that crept in, the usefulness of this actually decreased quite a lot, but never mindā¦
Like with any refactoring, a good start is simply extracting well named constants / functions. Then we can use built-in combinators (see the useful Euterpea quick reference for more):
times n
(orforever
) to repeat sectionscut t
to chop from the start (especially ifforever
is involved)transpose n
(especially:transpose 12
)tempo 2
(doubling the speed)
Sections
As mentioned, I found it useful to keep named constants for parts of sections. For example:
= line [abMinor hn, dbMajor hn]
basicChords = line [abMinor hn, dbMajor hn, eb7 wn]
climaxChords = times 2 (times 2 basicChords :+: climaxChords) verseChords
Phrasing
Itās worth mentioning the phrase
function. This allows us to add all sorts of performance-type changes including dynamics like Crescendos, staccato (I used a phrase [Art $ Staccato 0.9]
to dampen bass strings ā though this is a real fudge as any even beginner bass players know not to let notes ring, plus the common slide or glissando used here isnāt possible AFAICT).
As I incessantly tweaked further, this seems like the only way to start to get dynamics (though canāt be used for fades unfortunately).
Complexity
Drum fills
Percussion is tricky, and fills are definitely hard to learn or notate. As always it helps to be able to hear the parts individually (this drumming video was useful).
Eventually it comes down to the amount of time you can devote to obsessing over the exact fills ā whether itās getting them exactly right, or even bothering with the variations at all. Session musicians are often very, very good at what they do (without grabbing the spotlight!), and I have even more respect for them now.
A useful tip was to run the whole thing at double speed using tempo 2
, meaning that the eighth-note shortcuts defined earlier are re-usable for sixteenth notes.
Basslines
As an former / occasional dabbler in bass guitar and various bass-driven genres, Iāve long been aware the subtleties of āsimpleā basslines. As usual though, this one caught me out completely: the main four notes (Aā, B, Dāā¦ and Eā) are clearā¦ but the excellent performance throughout the track has complexity in the form of timing nuances (funk!), dynamics, ornaments and many variations / fills that make this sequencing very hard. Iām not going to lie: a huge amount of trial and error, and referencing bass tabs was necessary, and even then the result is massively simplified.
The code song
Iāve skipped the many definitions of functions used , but by now you should be able to imagine what they look like.
earthSong :: Music (Pitch, Volume)
= bpm 69.16 $ line
earthSong
[ rest (sn)
-- ā¬ (intro) ā¬
4 (introBacking :=: introPiano)
, cut
-- ā¬ "What about sunrise?" ā¬
, voicesFor verseChords
-- ā¬ "Did you ever stop to notice.." ā¬
:=: (phrase [Dyn $ Loudness 01] . phrase [Dyn $ Crescendo 0.2]) (stringsFor bridgeChords)
, thinBassOf bridgeBassLine :=: voicesFor bridgeChords
-- TODO: harp arpeggios
-- ā¬ oohh ooh oooooohayahh ā¬
:=: voicesFor chorusChords
, stringsFor chorusChords :=: (cut 7.5 shaker :+: ss)
:=: (rest 7.75 :+: chillGuitarLick)
-- ā¬ "What have we done to the world? Look what we've done..." ā¬
:=: voicesFor verseChords
, stringsFor verseChords
-- ā¬ "Did you ever stop to notice..." ā¬
:=: stringsFor bridgeChords
, thinBassOf bridgeBassLine :=: voicesFor bridgeChords
:=: (rest 3.75 :+: fastFill)
-- ā¬ oohh ooh oooooohayahh ā¬
7 (chorusMusic :=: phatBeat) :+: (boom :=: cshh :=: eb7Fade)
, cut
-- ā¬ "I used to dream, used to glance beyond the stars..." ā¬
:=: thinBeat
, midBassOf funkBridgeBassLine :=: stringsFor bridgeChords
:=: voicesFor bridgeChords
-- ā¬ oohh ooh oooooohayahh ā¬
:=: chorusMusic
, phatBeat
-- ā¬ ....heyeyyeeyeaa! ā¬
7 phatBeat :+: longFill) :=: chorusMusicUp
, (cut -- TODO: pre-beat guitar bends
-- ā¬ "What about yesterday? (what about us?)" ā¬
4 (phatBeat :=: chorusMusicUp
, times :=: powerGuitarUp
:=: brass)
7 $ chorusMusicUp :=: powerGuitarUp
, cut :=: phatBeat
:=: brass
:=: cshh :=: endChord
, boom ]
Rendering audio
A multi-step processā¦ ended up getting a bit DevOpsy after all hundreds of the run-throughs (aargh).
- Compile program (GHC, Stack)
- Run executable to produce MIDI file
- Run Timidity (now in Docker) to produce OGG
- Rebuild Hakyll site
- Deploy here to wrap in HTML 52
And nowā¦
Using Web Audio, the complete, generated audio, together with Jacksonās acapella audio (hacked out from the music video by the Internets).
Try some basic remixing ā you can alter the volume of each audio.3 Unfortunately the syncing / timing breaks in some browsers a bit, which is pretty annoying. Recommendation: donāt fast-forward / rewind (the first time, at least).
I learnt this the hard way when I recently changed my default SoundFont (to a better one, even) 18 months after starting, and the audio was unrecognisable. Docker to the rescue thoughā¦ā©ļø
Lesson learnt: browser media caching isā¦ annoying.ā©ļø
I could even have rendered multiple parts as audio files aka stems so you could remix live, but wellā¦ā©ļø