Scorange
OK, today, a bit of a departure. I just completed my first programming project using Haskell.
I call it Scorange. Why?
- It’s a portmanteau of ’score’ and ‘range’.
- It finally gives us something that rhymes with orange.
I’ve already demonstrated its use on my Csound blog. Briefly, here’s what it does: it allows me to create Csound score files of arpeggios, and automates a number of repetitive tasks. It also allows me to control the sound by generating an arbitrary number of parameters. (I’ve more discussion on this in the original article.)
Anyway, for regular readers of my WordPress blog — who may or may not be into Haskell or Csound — here’s what it sounds like. If you generated the score file using the program below, and processed the score file using Csound, you’d hear something like this:
Scorpio [mp3, 13.8 seconds, 218 K]
OK, now that you’ve heard it, most of you can leave. Haskell folks who’ve stumbled on to this page — the code is posted below (beyond the ‘more’ tag).
I definitely bought in to modular programming with this one. Thing is, it worked. As advertised, as soon as I’d gotten the system to compile, the compiler had taken care of a lot of debugging — it ran without errors the first time I tried it. Part of this was because I checked each module using ghc -Wall -c --make and made sure each file compiled without errors. When the time came to try my first example, it was almost impossible for it to fail (except for the usual typos, of course).
You need all six of the module files to get this to work. And of course, at the end, I’ve included an example — it’s the one that was used to produce “scorpio01.csd” on my Csound blog.
Event.hs
{-
Event.hs
Bruce H. McCosar
Compile with:
ghc -Wall -c --make Event.hs
-}
module Event where
data Event = Event
{
seconds :: Float, -- real time in seconds
barline :: Float, -- composition time in 'bars'
counter :: Float -- counter within each bar
}
type TimeLine = [Event]
stackEvents :: [Float] -> [Float] -> [Float] -> TimeLine
stackEvents (s:ss) (b:bs) (c:cs) =
Event {seconds=s,barline=b,counter=c} : stackEvents ss bs cs
stackEvents [] _ _ = []
stackEvents _ [] _ = []
stackEvents _ _ [] = []
Cellular.hs
{-
Cellular.hs
Bruce H. McCosar
Compile with:
ghc -Wall -c --make Cellular.hs
-}
module Cellular where
import Event
data CellField = CellFunction (Event -> Float)
| CellConstant Float
| CellPattern [Float]
data Cell = Cell
{
title :: String, -- labels column in Csound score file
fmstr :: String, -- format string for printf
field :: CellField -- cell content generator
}
Formatter.hs
{-
Formatter.hs
Bruce H. McCosar
Compile with:
ghc -Wall -c --make Formatter.hs
-}
module Formatter where
import Text.Printf
import List
import Event
import Cellular
putHeader :: [Cell] -> IO ()
putHeader (f:fs) = do
printf (title f ++ " ")
putHeader fs
putHeader [] = do putStr "\n"
generateFields :: TimeLine -> [Cell] -> [[Float]]
generateFields es (c:cs) =
let
results = case (field c) of
CellFunction f -> map f es
CellConstant k -> take (length es) (repeat k)
CellPattern tp -> take (length es) (cycle tp)
in
results : generateFields es cs
generateFields _ [] = []
generateFormats :: [Cell] -> [String]
generateFormats (f:fs) =
fmstr f : generateFormats fs
generateFormats [] = []
putField :: [String] -> [Float] -> IO ()
putField (fmt:fmts) (ent:ents) = do
printf fmt ent
putStr " "
putField fmts ents
putField _ [] = do putStr "\n"
putField [] _ = do putStr "\n"
putLines :: [String] -> [[Float]] -> IO ()
putLines fmt (dat:dats) = do
putStr "i "
putField fmt dat
putLines fmt dats
putLines _ [] = do putStr "\n"
putScore :: TimeLine -> [Cell] -> IO ()
putScore es fs =
let
fields = generateFields es fs
fmtstrings = generateFormats fs
in do
putStr "; "
putHeader fs
putLines fmtstrings (transpose fields)
Rhythm.hs
{-
Rhythm.hs
Bruce H. McCosar
Compile with:
ghc -Wall -c --make Rhythm.hs
-}
module Rhythm where
data Rhythm = Rhythm
{
beats :: Int, -- number of beats per bar
subdv :: Int, -- number of notes per beat
tempo :: Float, -- beats per minute
nbars :: Int, -- total repetitions (in bars)
width :: Float -- note width
}
notelist :: Rhythm -> [Float]
notelist rhythm =
let
maxnotes = (nbars rhythm) * (beats rhythm) * (subdv rhythm)
in
[ fromIntegral (e - 1) | e <- [1..maxnotes] ] seconds_per_note :: Rhythm -> Float
seconds_per_note rhythm =
let
seconds_per_beat = 60 / (tempo rhythm)
in
seconds_per_beat / (fromIntegral (subdv rhythm))
listSeconds :: Rhythm -> [Float]
listSeconds rhythm =
let
n = notelist rhythm
s = seconds_per_note rhythm
in
[s * i | i <- n] listBarline :: Rhythm -> [Float]
listBarline rhythm =
let
n = notelist rhythm
b = (fromIntegral (beats rhythm)) * (fromIntegral (subdv rhythm))
in
[i / b | i <- n] listCounter :: Rhythm -> [Float]
listCounter rhythm =
let
b = (beats rhythm) * (subdv rhythm)
c = [ fromIntegral (e - 1) | e <- [1..b] ]
k = fromIntegral (subdv rhythm)
in
cycle [i / k | i <- c]
Temporal.hs
{-
Temporal.hs
Bruce H. McCosar
Compile with:
ghc -Wall -c --make Temporal.hs
-}
module Temporal where
import Event
import Rhythm
timeline :: Rhythm -> TimeLine
timeline rhythm =
let
ss = listSeconds rhythm
bs = listBarline rhythm
cs = listCounter rhythm
in
stackEvents ss bs cs
Scorange.hs
{-
Scorange.hs
Bruce H. McCosar
Compile with:
ghc -Wall -c --make Scorange.hs
-}
module Scorange where
import Event
import Cellular
import Formatter
import Rhythm
import Temporal
scorange :: Rhythm -> [Cell] -> Int -> IO ()
scorange rhythm ucells inst =
let
events = timeline rhythm
p1 = Cell
{
title = "inst",
fmstr = "%4.0f",
field = CellConstant (fromIntegral inst)
}
p2 = Cell
{
title = " time ",
fmstr = "%10.5f",
field = CellFunction seconds
}
p3 = Cell
{
title = " width ",
fmstr = "%8.5f",
field = CellConstant (width rhythm * seconds_per_note rhythm)
}
p4 = Cell
{
title = " dur ",
fmstr = "%8.5f",
field = CellConstant (seconds_per_note rhythm)
}
cells = p1 : p2 : p3: p4 : ucells
in
putScore events cells
Example — genScorpio01.hs
On Linux, this finished program can be used to generate a score file using a shell redirect, for example ./genScorpio01 > scorpio01.sco
{-
genScorpio01
Compile with:
ghc -Wall --make genScorpio01.hs -o genScorpio
-}
import Event
import Rhythm
import Cellular
import Scorange
r1 :: Rhythm
r1 = Rhythm
{
beats = 4,
subdv = 4,
tempo = 141,
nbars = 8,
width = 1.22
}
notes1 :: Cell
notes1 = Cell
{
title = " pch ",
fmstr = "%5.2f",
field = CellPattern [6.07,6.07,7.02,7.07]
}
emphs1 :: Cell
emphs1 = Cell
{
title = " emphs",
fmstr = "%6.2f",
field = CellPattern [-2,-9,-5,-12]
}
panner1 :: Event -> Float
panner1 e =
let
t = 2*pi*(barline e)
in
(1 - sin t) / 2
pan1 :: Cell
pan1 = Cell
{
title = " pan ",
fmstr = "%5.2f",
field = CellFunction panner1
}
brighten1 :: Event -> Float
brighten1 e =
let
t = counter e
in
5 - t
brite1 :: Cell
brite1 = Cell
{
title = "brite",
fmstr = "%5.2f",
field = CellFunction brighten1
}
r2 :: Rhythm
r2 = Rhythm
{
beats = 4,
subdv = 4,
tempo = 141,
nbars = 8,
width = 1.11
}
notes2 :: Cell
notes2 = Cell
{
title = " pch ",
fmstr = "%5.2f",
field = CellPattern [8.07,8.10,9.02]
}
emphs2 :: Cell
emphs2 = Cell
{
title = " emphs",
fmstr = "%6.2f",
field = CellPattern [-3,-12,-6,-15]
}
panner2 :: Event -> Float
panner2 e =
let
t = 3*pi*(barline e)
in
(1 + sin t) / 2
pan2 :: Cell
pan2 = Cell
{
title = " pan ",
fmstr = "%5.2f",
field = CellFunction panner2
}
brighten2 :: Event -> Float
brighten2 e =
let
t = counter e
in
t + 5
brite2 :: Cell
brite2 = Cell
{
title = "brite",
fmstr = "%5.2f",
field = CellFunction brighten2
}
r3 :: Rhythm
r3 = Rhythm
{
beats = 4,
subdv = 1,
tempo = 141,
nbars = 8,
width = 1.54
}
notes3 :: Cell
notes3 = Cell
{
title = " pch ",
fmstr = "%5.2f",
field = CellPattern [5.07,6.02]
}
emphs3 :: Cell
emphs3 = Cell
{
title = " emphs",
fmstr = "%6.2f",
field = CellPattern [-5,-8,-6,-10]
}
panner3 :: Event -> Float
panner3 e =
let
t = 2*pi*(barline e)/2
in
0.4 + 0.1*(1 + sin t)
pan3 :: Cell
pan3 = Cell
{
title = " pan ",
fmstr = "%5.2f",
field = CellFunction panner3
}
brighten3 :: Event -> Float
brighten3 e =
let
t = 2*pi*(barline e)/4
in
2 + sin t
brite3 :: Cell
brite3 = Cell
{
title = "brite",
fmstr = "%5.2f",
field = CellFunction brighten3
}
main :: IO ()
main = do
scorange r1 [notes1,emphs1,pan1,brite1] 1001
scorange r2 [notes2,emphs2,pan2,brite2] 1002
scorange r3 [notes3,emphs3,pan3,brite3] 1003
Have you considered release the code on http://hackage.haskell.org as a Haskell library or application? Also, it’s probably worth always compiling with optimisatoins on: ghc -O2 –make
I’ve only been using Haskell since April 1st. I’ve written a lot of ‘toy’ applications to get to know the language. Scorange, however, I see as tying in to a bigger project down the road.
A few years ago I wrote a Python module for working with Pitch Class sets. I’ve also written some code (never released) for generating new chord progressions using simulated annealing. The arpeggio thing is a nice trick, but I’d like a simple, parsed language for specifying compositional elements and generating scorefiles automatically.
That said, in 11 days, I’ve become fairly comfortable with the core concepts of Haskell. However, I’m still relatively unfamiliar with darcs (I use subversion), haddock, Cabal, and the other serious programming tools. I think I’d probably like to get really good with the language before I start moving my projects out into the public sphere. Already, for instance, I’ve thought of a modification to the above modules … Default, for instance, for a nice set of prefabricated Cell functions (eg
sinewave max min periodfor panning.)In short, the project as it stands is probably too esoteric to be of general use. Still, it’s worthwhile posting the code … I certainly benefited from seeing examples while I was learning the language.
I’ll be darned. I just realized why your name is familiar:
Real World Haskell
An excellent introduction — I bought a hard copy of the book to read off line. The O’Reilly books have never let me down.