module Exercise06 where import Data.List import Effects import Types sinPeriod :: Signal sinPeriod = sin . (2*pi*) sqwPeriod :: Signal sqwPeriod x = if x < 0.5 then -1 else 1 sawPeriod :: Signal sawPeriod x = -1 + 2*x triPeriod :: Signal triPeriod x = if x <= 0.5 then 4*x - 1 else -4*x +3 silence :: Signal silence = const 0 -- NOTE: the formula is taken from https://pages.mtu.edu/~suits/NoteFreqCalcs.html f :: Semitone -> Hz f n = 440.0 * (2 ** (fromInteger n / 12.0)) osc :: Signal -> ADSR -> Oscillator osc signal adsr_config semitone duration = adsr adsr_config duration (\t -> signal (let x = t * f semitone in x - fromInteger (floor x))) adsr :: ADSR -> Seconds -> Signal -> Signal adsr (attack,decay,sustain,release) duration sig = \x -> sig x * if x <= attack then dxdt_attack*x else --ATTACK if x <= att_plus_sustain then dxdt_decay * (x - decay_zero_at) else --DECAY if x <= duration_minus_rel then sustain else --SUSTAIN dxdt_release*(x - duration) --RELEASE where -- use a lot of constants which can be evaluated on setup instead of once per sample dxdt_attack = 1 / attack -- 'dxdt' for differential att_plus_sustain = attack + decay dxdt_decay = (-1+sustain)/decay decay_zero_at = -1/dxdt_decay + attack duration_minus_rel = duration - release dxdt_release = -sustain/release mix :: [SampledSignal] -> SampledSignal mix as = let l = fromIntegral $ length as in map (/l) (foldr1 mix2 as) mix2 :: SampledSignal -> SampledSignal -> SampledSignal mix2 [] [] = [] mix2 as [] = as mix2 [] bs = bs mix2 (a:as) (b:bs) = (a+b) : mix2 as bs {-WETT-} -- you can add new oscillators here piano :: Oscillator piano = osc sawPeriod (0.01, 0.1, 0.7, 0.2) lead :: Oscillator lead = osc sqwPeriod (0.01, 0.2, 0.3, 0.1) bass :: Oscillator bass = osc sawPeriod (0.001, 0.2, 0.9, 0.1) none :: Oscillator none = osc silence (1,1,0.5,1) -- you can add more effects here -- My personal favourite: Makes everything better when at 0,1-0,3! -- Adds harmonics by using soft distortion saturation :: Double -> DSPEffect saturation d = map (\x -> (d*sqrt (abs x) * signum x) + (1-d)*x) -- For experts gainInDecibel :: Double -> DSPEffect gainInDecibel d = map (*x) where x = 2 ** (d / 6) normalize :: DSPEffect normalize sampSig = map (*x) sampSig where x = 1 / foldr max 0 (map abs sampSig) -- Ever heard of soft-knee-compression? If you haven't, here's a link: -- https://en.wikipedia.org/wiki/Dynamic_range_compression#Soft_and_hard_knees -- This is my very own creation: A no-knee / curved-curve compressor. -- Forget everything you know about compressors, most rules do not apply here. I suggest just playing with the parameters. -- -- Why, you ask? Well, musicians look for creativity and love new ideas. So here they are. -- (And definitely not because I wanted to implement a regular compressor but failed and declared this strange behaviour a feature. -- However, it does produce interesting, although possibly underwhelming results. And, to my defense, I now actually know what -- the issue originally was, but I now prefer this interesting behaviour.) -- -- Note: In order to unleash the full potential, make sure the input signal is properly normalized. -- -- parameters: ratio (linearly, i.e. 0.5 => 1:6 in dB); keep it very low for noticable results (0-0.01) -- threshold (linearly, e.g. 0.5 => -6 dB); should also be kept between 0 and 0.3 -- attack time (in s), -- release time (in s) strangeCompressor :: Double -> Double -> Double -> Double -> DSPEffect strangeCompressor ratio thresh att rel = (`compress` 0) where -- There is no standard as for what the time in seconds attack and release actually means. In this case, -- it specifies the time the compressor take to archive 63% of it's targeted compression, which seems to -- be the most common implementation. attack_magic = 0.37 ** (1/(att*sampleRate)) release_magic = 0.37 ** (1/(rel*sampleRate)) ratio_magic = 1 - ratio compress [] _ = [] compress (x:xs) last_reduction = (x * (1-this_reduction)) : compress xs this_reduction where targeted_reduction = max 0 ((abs x - thresh) * ratio_magic) this_reduction | last_reduction > targeted_reduction = targeted_reduction + (last_reduction - targeted_reduction) * release_magic | last_reduction == targeted_reduction = last_reduction | otherwise = targeted_reduction - (targeted_reduction - last_reduction) * attack_magic lowPass :: Double -> DSPEffect -- Filter out high frequencies by averaging each sample with their successors. -- Filtering is quite an important feature for any synthesizer! lowPass freq samples = lowPass' (samples ++ replicate samplesToAverageInt 0) (length samples) where samplesToAverage = sampleRate / freq / 2 samplesToAverageInt = round samplesToAverage lowPass' :: SampledSignal -> Int -> SampledSignal lowPass' xs@(x:xs') l | l <= 0 = [] | otherwise = (sum (take samplesToAverageInt xs) / samplesToAverage) : lowPass' xs' (l-1) -- mix the signals with some volume mixTracks :: [SampledSignal] -> [Double] -> SampledSignal mixTracks trks vols = mix $ zipWith (\trk vol -> map (* vol) trk) trks vols -- here we mix it all together and apply the effects notesToSignal :: (Oscillator -> [Note] -> SampledSignal) -> [[Note]] -> SampledSignal notesToSignal playNotes tracks = audio where -- specify the instruments of each track -- try the following instruments with the mario.mid file instrs = cycle [piano, bass] -- instrs = cycle [none, bass, lead, piano] audioTracks = zipWith playNotes instrs tracks audioNoEffects = mixTracks audioTracks [0.8, 0.7] audio = addEffects [strangeCompressor 0 0 0.01 0.2, normalize, lowPass 8000, saturation 0.2] audioNoEffects -- To add effects, replace the line above by the line at the end of this comment and change the list off effects to your own effects. -- Note that the effects are applied from right to left, i.e. the rightmost effect is applied first. -- audio = addEffects [clip 0.9, addGain 4] audioNoEffects -- If you want to apply an effect only to parts of the signal, you can use the function applyEffectToInterval, as shown below. -- Thereby the first argument specifies the interval to which the effect should be applied in seconds. -- audio = applyEffectToInterval (2, 6) audioNoEffects distortion {-TTEW-}