Sequencing
Now that we added automatic amplitude adjustment in the previous article, it will be much easier to combine waves however I see fit. I'm eager to
start producing melodies, so now I'm going to do some sequencing. By sequencing, I mean simply creating a sequence of notes. A note is a
combination of a duration and a tone. We've already figured out how to produce tones; to give a note a particular duration, I'm going to create
a clip
function in the new Sequencing.hs
module:
clip :: Double -> WaveFunction -> WaveFunction
clip duration waveFunc t = if t > duration then 0 else waveFunc t
The clip
function is simply a step function, where the original sound function is played for duration
seconds, after which the sound
ceases (a 0
value is produced). To support sequencing, we'll also need to do the opposite, and delay a WaveFunction
for a number of seconds:
delay :: Double -> WaveFunction -> WaveFunction
delay delayTime waveFunc t = if t < delayTime then 0 else waveFunc (t - delayTime)
The delay
function basically outputs 0
for delayTime
seconds, and shifts the WaveFunction
in time by that same amount.
Now let's put this all together in Main.hs
. I want the code for writing my melody to feel really natural, so I'm going with this type signature:
notes :: [(Int,Tone,Int)]
This notes
function will be a List
of (Int,Tone,Int)
. This is the first time we're using a Haskell Tuple
, which is simply an aggregate data
type with positional values. In this case, there will be an Int
representing a duration, Tone
representing the tone, of course, and an Int
for the octave. Here are my notes:
notes = [
(1,D,5), (1,E,5),
(2,F,5), (1,E,5), (1,D, 5), (2,Cs,5), (1,D,5), (1,E, 5),
(2,A,4), (1,B,4), (1,Cs,5), (2,D, 5), (1,C,5), (1,Bf,4),
(2,A,4), (1,G,4), (1,F, 4), (2,G, 4), (2,A,4),
(1,G,4), (1,F,4), (1,E, 4), (1,F, 4), (4,D,4) ]
The duration value is an integer for the sake of simplicity; I'll be treating each unit like an eighth note. Each eighth note will be a quarter of a second:
noteLenth :: Double
noteLenth = 0.25
So how do we go from a sequence of notes to a WaveFunction
? As we saw previously, combining multiple tones into a chord was simply adding the
function values together. Sequencing is the same, only we're delaying and clipping the notes before combining. We also need to keep track of how
how far into the sequence we are going by maintaining an accumulator for the total number of seconds:
audioSeq :: [(Int,Tone,Int)] -> (WaveFunction,Double)
audioSeq = foldl playNote (nullWave, 0)
The foldl
function here means fold left. The foldl
function starts with an initial value and combines it with the first
value in the list using a given function. This resulting value is then combined with the next value in the list with the same function. This repeats
until all values are "folded" into one result value. In our case, the initial value is (nullWave, 0)
; we start with silence (nullWave
), with our
time accumulator starting at zero. The list we'll be folding is the sequence of notes, and the function to combine them is playNote
which is defined as follows:
playNote :: (WaveFunction,Double) -> (Int,Tone,Int) -> (WaveFunction,Double)
playNote (currentSeq,pos) (dur,tone,oct) =
(addWaves currentSeq thisNote, pos + duration)
where
duration = fromIntegral dur * noteLength
thisNote = delay pos $ clip duration $ applyTone tone oct sineWave
We simply create a sineWave
tone at a given pitch and octave, clip it to its given duration, and delay it so it's at the end of the sequence.
This tone is added to the sequence produced so far, and the time accumulator is advanced by the duration of the note. Now let's produce the audio
samples from this audio sequence:
samples :: [Int16]
samples = sampleInt16 melody sampleRate dur
where (melody,dur) = audioSeq notes
And now we some something resembling music!
To see the code in its entirety, check out the article-5 branch of the wave-machine
repository.
I'm excited that I was able to create a simple melody. My goal in the next article will be to combine this melody with a base line.