-- | This module defines the notion of volume.
module Temporal.Music.Volume(
        -- * Types
        Amp, Diap(..), Volume(..), Level, Accent,
        -- * VolumeLike
        VolumeLike(..), mapVolume,
        -- * Rendering
        amp, volumeAsDouble, diapAt) 
where

import Data.Default

-- | Amplitude.
type Amp = Double

-- | Diapason defines minimum and maximum bound for 'Volume' level.
-- Field 'diapLim' specifies volume limit. Value 'diapLim' is 
-- rendered to highest amplitude and @-diapLim@ is rendered to 
-- the lowest amplitude. All values that go beyond the limit are clipped.
data Diap = Diap
    { diapRange :: (Amp, Amp)
    , diapLim   :: Int
    } deriving (Show, Eq)


instance Default Diap where
    def = Diap (1e-5, 1) 5

-- | 'Accent' defines values between 'volumeLevel' values on logarithmic 
-- scale. 1 'Accent' == 1 'volumeLevel' 's step.
type Accent = Double 

-- | Volume levels.
type Level  = Int

-- | 'Volume' denotes 'Amp' value. It's not a 'Double' 
-- for ease of performing some musical transformations, such as
-- making notes louder or using accents. 'Volume' can be converted
-- to 'Amp' with function 'amp'.
data Volume = Volume {
        volumeDiap      :: Diap,
        volumeAccent    :: Accent,
        volumeLevel     :: Level
    } deriving (Show, Eq)


instance Default Volume where
    def = Volume def def def

-- | 'Volume' can be used alongside with many
-- other parameters (they can define timbre or pitch). 
-- Class 'VolumeLike' provides getters and setters for
-- data types that contain value of type 'Volume'. 
-- In "Temporal.Music.Score" module you can find many
-- functions that are defined in terms of this class. Once you
-- have chosen some note representation you can make an instance 
-- for it and use all volume-modifiers.
class VolumeLike a where
    setVolume :: Volume -> a -> a
    getVolume :: a -> Volume


instance VolumeLike Volume where
    setVolume = const id
    getVolume = id


-- | 'Volume' modifier.
mapVolume :: VolumeLike a =>  (Volume -> Volume) -> (a -> a)
mapVolume f x = setVolume (f (getVolume x)) x

--------------------------------------------
-- rendering
--

absVolume :: Volume -> Amp
absVolume v = diapAt (volumeDiap v) (volumeAsDouble v)

-- | Calculates amplitude for a 'Volume' -like value.
amp :: (VolumeLike a) => a -> Amp
amp = absVolume . getVolume

-- | Calculates value of type 'Volume' as coordinate 
-- within specidfied diapason. 1 corresponds to maximum bound 
-- and 0 corresponds to minimum bound.
volumeAsDouble :: Volume -> Double
volumeAsDouble v = 0.5 + d / 2
    where l = volumeLevel v
          a = volumeAccent v  
          c = (diapLim $ volumeDiap v)
          d = sat (-1) 1 $ (fromIntegral l + a) / fromIntegral c

sat :: Ord a => a -> a -> a -> a
sat a b x 
    | x < a = a
    | x > b = b
    | otherwise = x


-- | Mapps decibels to amplitudes within specified amplitude 
-- diapason, 0 turns to lower diapason value and 1 turns 
-- to higher diapason value.
diapAt :: Diap -> Double -> Double
diapAt = diapAt' . diapRange
    where diapAt' (low, high) x = (low * ) $ (high / low) ** x