-- | This module defines notion of pitch. 
module Temporal.Music.Pitch (
    Hz, Interval, c1, a1, transpose,
    -- * Pitch
    Pitch(..), fromStep, Bend, Octave, Step,
    -- * Scale
    Scale(..), fromIntervals, 
    scaleStep, scaleLength, 
    -- * PitchLike
    PitchLike(..), mapPitch,
    -- * Render  
    absPitch, pitchAsDouble, scaleAt
)
where

import Data.Default
import qualified Data.Vector as V

-- | Cycles per second
type Hz = Double

-- | Multiplier of Hz
type Interval = Double

-- | Middle C (261.626 Hz) 
c1 :: Hz
c1 = 261.626

-- | Middle A (440 Hz) 
a1 :: Hz
a1 = 440


-- Pitch

-- | 'Pitch' denotes 'Hz' value. But it's not a double for ease of
-- performing some musical transformations, transposition, bend, 
-- inversion, changing scales. 'Pitch' can be converted to 'Hz' with 
-- function 'hz'. Pitch contains 'Scale', and point on the tone plane. 
-- The point is a triple @(bend, octave, step)@. 'Bend'
-- denotes divergens from vertices of scale grid. 'Octave' and 'Step' 
-- are integers. 
data Pitch = Pitch {
        pitchScale   :: Scale,
        pitchBend    :: Bend,
        pitchOctave  :: Octave,
        pitchStep    :: Step
    } deriving (Show, Eq)

-- | 'Bend' represents tone's diversion from scale grid. 
type Bend   = Double
type Octave = Int
type Step   = Int      

-- | Calculates position on tone plane for value of type 'Pitch'.
pitchAsDouble :: Pitch -> Double
pitchAsDouble p = pitchBend p + fromIntegral (pitchAsInt p)

-- | Calculates integer position on tone plane for value of type 'Pitch'
-- without bend bias.
pitchAsInt :: Pitch -> Int
pitchAsInt p = pitchOctave p * (scaleLength $ pitchScale p) + pitchStep p     

instance Default Pitch where
    def = Pitch def def def def

-- Scale

-- | 'Scale' defines 2D grid (octave, step) in space of 'Hz' units. 
-- 'Bend' is a level of diversion from grid vertices.
-- 1-level bend is equal to 1 step. For tones with fractional bends frequency
-- is calculated with linear interpolation by nearest values in scale.
-- Example:
--
-- > s = Scale f0 d intervals
--
-- Here scale @s@ defines 2D grid that mapps center point @(0, 0)@ to
-- frequency @f0@ 'Hz'. Value 'd' is octave interval. Octave interval
-- is divided on steps. Steps are 'Interval' s from base frequency @f0@ 
-- to desired frequency. Let's define equal temperament scale:
--
-- > eqt = Scale c1 2 $ Vector.fromList $ (map ((2 **) . (/12)) [0..11])
data Scale = Scale {
        scaleBase   :: Hz,
        scaleOctave :: Interval,
        scaleSteps  :: V.Vector Interval
    } deriving (Show, Eq)


instance Default Scale where
    def = eqt c1
        where eqt = fromIntervals 2 (map ((2 **) . (/12)) [0 .. 11]) 

-- | 'Scale' constructor.
fromIntervals :: Interval -> [Interval] -> (Hz -> Scale)
fromIntervals octave steps = \f0 -> Scale f0 octave $ V.fromList steps

-- | Scale value on doubles          
scaleAt :: Scale -> Double -> Hz
scaleAt s x = scaleAtInt s d * bendCoeff s n r 
    where (d, r) = properFraction x          
          n      = mod d $ scaleLength s


-- | 'Pitch' can be used alongside with many
-- other parameters (they can define timbre or loudness). 
-- Class 'PitchLike' provides getters and setters for
-- data types that contain value of type 'Pitch'. 
-- 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 pitch-modifiers.
class PitchLike a where
    setPitch :: Pitch -> a -> a
    getPitch :: a -> Pitch

-- | Pitch modifier.
mapPitch :: PitchLike a => (Pitch -> Pitch) -> a -> a
mapPitch f x = setPitch (f $ getPitch x) x

instance PitchLike Pitch where
    setPitch = const id
    getPitch = id


-- | Constructs 'Pitch' from some step value. 'Bend' and
-- 'Octave' are set to zeroes. 'Scale' is set to default scale
-- which is defined in 'HasScale' class.
fromStep :: Int -> Pitch
fromStep a = def{ pitchStep = a }


-- | Gives scale multiplier
scaleStep :: Scale -> Int -> Interval
scaleStep s x = (scaleOctave s ^^ o) * scaleSteps s V.! n    
    where (o, n) = divMod x $ scaleLength s


-- | Gives number of steps in one octave.
scaleLength :: Scale -> Int
scaleLength = V.length . scaleSteps


-- | Transpose cycles per second by some interval.
transpose :: Interval -> Hz -> Hz
transpose k a = k * a

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

renderPitch :: Pitch -> Hz
renderPitch p = pitchScale p `scaleAt` pitchAsDouble p

-- | Calculates cycles per second for a pitch.
absPitch :: PitchLike a => a -> Hz
absPitch = renderPitch . getPitch


-- | scale value on integers          
scaleAtInt :: Scale -> Int -> Hz
scaleAtInt s x = scaleBase s * scaleStep s x

bendCoeff :: Scale -> Int -> Double -> Hz          
bendCoeff s n x
    | abs x < 1e-6 = 1
    | x > 0        = flip loginterpCoeff x       $ getTones s n $ n + 1
    | otherwise    = flip loginterpCoeff (abs x) $ getTones s n $ n - 1
    where getTones s n1 n2 = (getTone s n1, getTone s n2)  
          getTone  s x
            | x >= 0 && x < n = scaleSteps s V.! x
            | x == n          = o
            | x == -1         = scaleSteps s V.! (n-1) / o
            | otherwise       = error $ "scaleStep: out of bounds"
            where n = scaleLength s
                  o = scaleOctave s


loginterpCoeff :: (Double, Double) -> Double -> Double
loginterpCoeff (l, r) x = (r / l) ** x