A Haskell shell for computing musical scales, in 58 lines of code.

On my way home from work I had an idea for something I thought I’d find handy. Two hours later, I’ve written it — in Haskell of course. :-)

Context: I occasionally make what might kindly be called music, using computers and the like, and I don’t have any formal musical training. Consequently, I sometimes run into trouble regarding keys and scales. The idea I had was for something which would tell me, given a set of notes (ie the ones I’ve already used in a piece), what scales those notes are in — and consequently what other notes might also fit well with them. Long ago when I used Bars & Pipes on the Amiga, it had a tool for doing this. I could buy one for my Mac for about 8UKP (I may yet — it has a pretty GUI, which I might not be bothered to do), but it seemed I should be able to knock something up. The data types aren’t exactly complicated.

So I did it. I’ll put the code at the end of this post, or you can easily download it from here, in 133 lines of literate Haskell (58 lines of actual code). Here’s a screenshot showing typical use:

Haskell scale finder screenshot

Once again, I’ve been super impressed by how easy Haskell made this — and I’m particularly loving the Shellac library which gives me an interactive shell with history and graceful recovery from errors — all in 4 lines of code!

> react = mapM_ (uncurry printScale) . checkFit . parseNotes
>     where printScale :: String -> [Note] -> Sh () ()
>           printScale n ns = shellPutStrLn (n ++ ": " ++ ppNotes ns)
> main = runShell (mkShellDescription [] react) haskelineBackend ()

OK, I’m not using any of its juicier features like completion, state, automatic help, etc., but even so, it’s a great little library.

I could wrap it in a GUI, but to be honest, this’ll probably do the trick for me, assuming I haven’t made any big screw-ups, or got the scale definitions wrong. If anyone’s looking for a minimal Shellac example, maybe it’ll be useful.

(Update 2009-09-16 13:37: To get this up and running, if you’re not already a Haskeller, the following should suffice: 1. Download and install The Haskell Platform – nice ‘n’ easy. 2. Open a shell and issue cabal install Shellac-haskeline or possibly sudo cabal install --global Shellac-haskeline. 3. Download the code (from hpaste.org) and (still in a shell) issue ghci Scales, and when that’s loaded issue main. Or if you want to compile an executable, try ghc --make -main-is Scale.lhs Scale.lhs, just like in the screenshot. That should do it — I’d love to know if I’ve missed anything and/or if that works!)

Here’s the code (but, again, it’s prettier and easier to download from hpaste):

> module Scale where

> import Data.List

I'm going to use Shellac for interactivity...

> import System.Console.Shell
> import System.Console.Shell.Backend.Haskeline
> import System.Console.Shell.ShellMonad

... HughesPJ for pretty printing (in a small way, but it's just so pretty)...

> import Text.PrettyPrint.HughesPJ

... and Text.Regex for splitting a string on spaces.

> import Text.Regex

So the idea is to compute the possible scales a given set of notes
might be part of.

Scales can be represented simply as lists of intervals, naturally
denoting the tonic as the value 0.

> type Interval = Int
> data Scale = Scale {
>       scaleName :: String,
>       scaleIntervals :: [Interval]
>       } deriving Show

We know a few different scales, namely the ones whose Wikipedia
articles made a reasonable amount of sense to us... :-) This may be
stupid and/or wrong, and there are certainly nicer ways to represent
these scales, but it'll do for now.

> knownScales :: [Scale]
> knownScales = [Scale "Major" [0, 2, 4, 5, 7, 9, 11]
>               ,Scale "Minor"            [0, 2, 3, 5, 7, 8, 10]
>               ,Scale "Harmonic Minor"   [0, 2, 3, 5, 7, 8, 11]
>               ,Scale "Major Pentatonic"          [0, 2, 4, 7, 9]
>               ,Scale "Relative Minor Pentatonic" [0, 3, 5, 7, 10]
>               ,Scale "Yo" [0, 2, 5, 7, 9]
>               ]

Notes (A, B, C, etc.) can also be represented as integers.

> type Note = Int

Then we can use modular arithmetic (Z12) without any hassles.

> noteMod :: Note -> Note
> noteMod n = n `mod` 12

Given a base note and a scale, we can compute the notes in that scale.

> scaleNotes :: Note -> Scale -> [Note]
> scaleNotes base (Scale _ scale) =
>     nub . sort . map noteMod $ zipWith (+) (repeat base) scale

We will also want to refer to notes by their names.

> type NoteName = String

And so we need to know the order those names come in.  We'll
arbitrarily set C to 0.

> chromatic :: [NoteName]
> chromatic = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A",
>              "A#", "B"]

We need to be able to convert between note names and numbers easily.
We just die if anything goes wrong (i.e. unknown note name).

> namedNote :: NoteName -> Note
> namedNote name = case elemIndex name chromatic of
>                  Just a -> a
>                  Nothing -> error $ "unknown note " ++ name

> noteName :: Note -> NoteName
> noteName note = chromatic !! noteMod note

Given a set of base notes and a set of scales, we can compute all
possible combinations, for each one returning its name (e.g. "C
Major") and the notes found in it.

> mkScales :: [Note] -> [Scale] -> [(String, [Note])]
> mkScales notes scales =
>     do note <- notes
>        scale <- scales
>        return (noteName note ++ " " ++ scaleName scale,
>                scaleNotes note scale)

Then we can compute all the scales we know about.

> allScales :: [(String, [Note])]
> allScales = mkScales [0..11] knownScales

For I/O we want to be able to parse strings of space-separated note
names into lists of note numbers.

> parseNotes :: String -> [Note]
> parseNotes = map namedNote . splitRegex (mkRegex " +")

... and we want to be able to pretty print the same.

> ppNotes :: [Note] -> String
> ppNotes = show . hsep . map (text . noteName) . nub . sort . map noteMod

Then, to compute the scales a given set of notes belong to, we can
just stupidly brute force iterate over every note in the octave, and
every scale we know, and just decide if the set of notes we've got
fits or not.

> checkFit :: [Note] -> [(String, [Note])]
> checkFit notes = filter ((notes `isSubList`) . snd) allScales
>     where isSubList :: (Eq a) => [a] -> [a] -> Bool
>           isSubList x y = null $ x \\ y

OK, let's do some interactivity, courtesy of Shellac.

We interpret whatever the user enteres as a space-separated list of
note names, and in response we print the names (and notes) of every
scale those notes are in.

> react :: String -> Sh () ()
> react = mapM_ (uncurry printScale) . checkFit . parseNotes
>     where printScale :: String -> [Note] -> Sh () ()
>           printScale n ns = shellPutStrLn (n ++ ": " ++ ppNotes ns)

Our main function just runs that function in an interactive shell.

> main :: IO ()
> main = runShell (mkShellDescription [] react) haskelineBackend ()

6 Responses to “A Haskell shell for computing musical scales, in 58 lines of code.”

  1. Elmo
    September 16th, 2009 | 11:46 am

    OM to the G! This is very cool, for someone in the same boat of lacking music theory knowledge, this will be very handy. Some support for ‘flats’ as well as sharps would be nice and maybe make it more useful to people. :)

  2. September 16th, 2009 | 12:36 pm

    But flats and sharps are isomorphic! ;-) I’ll see what I can do.

    I realised I should have included info on how to get this running if you’re not already a Haskeller; so I’ll add that now…

  3. Thies
    September 17th, 2009 | 12:21 pm

    Cool Stuff! and really useful! it would be cool if the scalenotes of fitting scales would start at the basenote of the scale, so that for example G Major would be
    G A B C D E F#
    instead of
    C D E F# G A B
    That would make musical modes like phrygian and locrian differentiable in the first place. Perhaps i’ll give it a try to implement that myself.

  4. Mike
    September 17th, 2009 | 1:17 pm

    > But flats and sharps are isomorphic! ;-)

    Not really… a given key has flats _or_ sharps, not a mix of both.
    A scale will have the seven note letters, without repeating any (other than the first and last), each modified with only a sharp or flat (or bb or x). The Cb scale is spelled differently than the B natural, even though they’d hit the same MIDI note numbers; as are the other pairs (Db C#) (Gb F#) in major, and (Bb A#) (Eb D#) Ab G#) in minor.

    # major
    cf = “Cb Db Eb Fb Gb Ab Bb”
    gf = “Gb Ab Bb Cb Db Eb F ”
    df = “Db Eb F Gb Ab Bb C ”
    af = “Ab Bb C Db Eb F G ”
    ef = “Eb F G Ab Bb C D ”
    bf = “Bb C D Eb F G A ”
    f = “F G A Bb C D E ”
    c = “C D E F G A B ”
    g = “G A B C D E F#”
    d = “D E F# G A B C#”
    a = “A B C# D E F# G#”
    e = “E F# G# A B C# D#”
    b = “B C# D# E F# G# A#”
    fs = “F# G# A# B C# D# E#”
    cs = “C# D# E# F# G# A# B#”

    # natural minor
    afn = “Ab Bb Cb Db Eb Fb Gb”
    efn = “Eb F Gb Ab Bb Cb Db”
    bfn = “Bb C Db Eb F Gb Ab”
    fn = “F G Ab Bb C Db Eb”
    cn = “C D Eb F G Ab Bb”
    gn = “G A Bb C D Eb F ”
    dn = “D E F G A Bb C ”
    an = “A B C D E F G ”
    en = “E F# G A B C D ”
    bn = “B C# D E F# G A ”
    fsn = “F# G# A B C# D E ”
    csn = “C# D# E F# G# A B ”
    gsn = “G# A# B C# D# E F#”
    dsn = “D# E# F# G# A# B C#”
    asn = “A# B# C# D# E# F# G#”

    # harmonic minor
    afh = “Ab Bb Cb Db Eb Fb G ”
    efh = “Eb F Gb Ab Bb Cb D ”
    bfh = “Bb C Db Eb F Gb A ”
    fh = “F G Ab Bb C Db E ”
    ch = “C D Eb F G Ab B ”
    gh = “G A Bb C D Eb F#”
    dh = “D E F G A Bb C#”
    ah = “A B C D E F G#”
    eh = “E F# G A B C D#”
    bh = “B C# D E F# G A#”
    fsh = “F# G# A B C# D E#”
    csh = “C# D# E F# G# A B#”
    gsh = “G# A# B C# D# E Fx”
    dsh = “D# E# F# G# A# B Cx”
    ash = “A# B# C# D# E# F# Gx”

    # melodic minor, ascending
    afm = “Ab Bb Cb Db Eb F G ”
    efm = “Eb F Gb Ab Bb C D ”
    bfm = “Bb C Db Eb F G A ”
    fm = “F G Ab Bb C D E ”
    cm = “C D Eb F G A B ”
    gm = “G A Bb C D E F#”
    dm = “D E F G A B C#”
    am = “A B C D E F# G#”
    em = “E F# G A B C# D#”
    bm = “B C# D E F# G# A#”
    fsm = “F# G# A B C# D# E#”
    csm = “C# D# E F# G# A# B#”
    gsm = “G# A# B C# D# E# Fx”
    dsm = “D# E# F# G# A# B# Cx”
    asm = “A# B# C# D# E# Fx Gx”

    (sorry, that’s from a Ruby script that generates printed scales for the Mup program)

    Also, scales are generated by a pattern of whole and half steps.
    Major: W W h W W W h
    Minor: W h W W h W W
    Harmonic Minor: W h W W h WH h
    Asc. Melodic Minor: W h W W W W h
    Desc. Mel. Minor: (top down) W W h W W h W == natural minor

    Hope this helps…

  5. September 17th, 2009 | 1:19 pm

    Good point. My main interest is avoiding discord, so I primarily wanted to know what notes to avoid, but I guess that would also be worth doing. Shouldn’t be too hard – I’ll have a go soon if I don’t hear back from you. :-)

    Glad you like it!

  6. September 17th, 2009 | 1:23 pm

    Oops, that last comment directed at Thies, not Mike. But also: thanks Mike! :-)