Homework Assignment 5
=====================
CPSC 432/532
Solution prepared by Daniel Winograd-Cort with students' input
> {-# LANGUAGE Arrows #-}
> module HW5 where
> import Euterpea
> import Control.Arrow ((>>>), arr)
> import Euterpea.Music.Signal.SigFuns
Exercise 1
----------
Using the Euterpea function osc, create a simple sinusoidal wave,
but using different table sizes, and different frequencies, and see
if you can hear the differences (report on what you hear). Use
outFile to write your results to a file, and be sure to use a decent
set of speakers or headphones.
Solution:
One should notice that smaller table sizes and lower frequencies
produce grainier sounds with more artifacts.
Exercise 2
----------
Chapter 18 defines a vibrato function, which varies a signal’s
frequency at a given rate and depth. Define an analogous function
tremolo that varies the volume at a given rate and depth. However,
in a sense, tremolo is a kind of envelope (infinite in duration),
so define it as a signal source, with which you can then shape
whatever signal you wish. Consider the “depth” to be the fractional
change to the volume — i.e. a value of 0 would result in no tremolo,
a value of 0.1 would vary the amplitude from 0.9 to 1.1, and so on.
Test your result.
Solution:
> tremolo :: Clock c => Double -> Double -> SigFun c () Double
> tremolo rate depth = proc _ -> do
> t <- osc (tableSinesN 4096 [1]) 0 -< rate
> outA -< 1 + (t * depth)
Exercise 3
----------
Define an ADSR (“attack/decay/sustain/release”) envelope generator
(i.e. a signal source) called envADSR, with type:
> type DPair = (Double, Double) -- pair of duration and amplitude
> envADSR :: DPair -> DPair -> DPair -> Double -> AudSF () Double
The three DPair arguments are the duration and amplitude of the
attack, decay, and release “phases,” respectively, of the envelope.
The sustain phase should hold the value of the decay phase. The
fourth argument is the duration of the entire envelope, and thus the
duration of the sustain phase should be that value minus the sum of
the durations of the other three phases. (Hint: use Euterpea’s
envLinSeg function.) Test your result.
Solution:
> envADSR (adur, aamp) (ddur, damp) (rdur, ramp) tdur =
> envLineSeg [0, aamp, damp, damp, ramp]
> [adur, ddur, tdur - sum [adur, ddur, rdur], rdur]
Exercise 4
----------
Generate a signal that causes clipping, and listen to the result.
Then use simpleClip to “clean it up” somewhat — can you hear the
difference?
Solution:
If done properly, the difference should be easy to hear.
Exercise 5
----------
Write a more ambitious clipping function. In particular, one that
uses some kind of non-linear reduction in the signal amplitude as it
approaches plus or minus one (rather than abruptly “sticking” at
plus or minus one, as in simpleClip).
Solutions:
Recognizing that arctan x is approximately linear when x is small,
we can create an arctan clipping function. The value of arctan x
never exceeds pi/2, so we can normalize to that:
> atanClip :: Clock c => SigFun c Double Double
> atanClip = arr f where
> f x = atan (x * pi/2) * 2 / pi
Another interesting technique is to use a hyperbolic function:
> hyperbolicClip :: Clock c => SigFun c Double Double
> hyperbolicClip = arr f where
> f x = if x > 0 then 1 - (1/(1 + x)) else (1/(1 - x)) - 1
Other techniques are also acceptable.
Exercise 6
----------
Define two instruments, each of type Instr (AudSF () Double). These
can be as simple as you like, but each must take at least two
“Params.” Define an InstrMap that uses these, and then use renderSF
to “drive” your instruments from a Music1 value. Test your result.
Solution:
Here are two instruments that use at least two "Params":
> instr1 :: Instr (AudSF () Double)
> instr1 dur ap vol [rate, depth] =
> let d = fromRational dur
> v = fromIntegral vol / 100
> table = tableSinesN 4096 [1, 0.5, 0.25, 0.125]
> in proc _ -> do
> trem <- tremolo rate depth -< ()
> env <- envASR (d/10) d (d/5) -< ()
> aud <- osc table 0 -< apToHz ap
> outA -< aud * env * (trem - depth) * v
> instr2 :: Instr (AudSF () Double)
> instr2 dur ap vol [n, pNum, pStrength] =
> instr2Rec (apToHz ap) (round n)
> where
> table = tableSinesN 4096 [1]
> v = fromIntegral vol / 100 / n
> instr2Rec f 0 = constA f >>> osc table 0 >>> arr (*v)
> instr2Rec f n =
> proc _ -> do
> sig <- osc table 0 -< f
> recSig <- instr2Rec (f * pNum) (n - 1) -< ()
> outA -< (recSig * pStrength + sig) / (1 + pStrength)
The first instrument takes rate and depth params and uses them to
create a tremolo. The second one takes arguments n, pNum, and
pStrength and generates an overtone series with n overtones, each
spaced by a factor of pNum, with amplitudes multiplied by pStrength.
Now we can test these out - we'll play a scale with our instruments.
> scaleWithParams :: [Double] -> Music1
> scaleWithParams ps = line $ map addParams
> [c 5 qn, d 5 qn, e 5 qn, f 5 qn,
> g 5 qn, a 5 qn, b 5 qn, c 6 hn]
> where addParams (Prim (Note d p)) =
> Prim (Note d (p, [Params ps]))
> scale1 = instrument (Custom "Instr1") $
> scaleWithParams [5, 0.2]
> scale2 = instrument (Custom "Instr2") $
> scaleWithParams [10, 1.5, 0.8]
> out1 = uncurry (outFile "instr1.wav") $
> renderSF scale1 [(Custom "Instr1", instr1)]
> out2 = uncurry (outFile "instr2.wav") $
> renderSF scale2 [(Custom "Instr2", instr2)]