module Dynamics ( addDynamics , dyn , sinTimeDynamics , sinPitchDynamics , expPitchDynamics , DynamicsMap) where import Data.KMeans (kmeansGen) import Data.List (find) import Data.Maybe (fromJust) import Music type AbsStartTime = Rational type PitchIntValue = Int type ClusterMusicA = (FullPitch, (AbsStartTime, PitchIntValue)) type MusicCluster = Music ClusterMusicA type Cluster = [ClusterMusicA] -- | Gets the time of the note within its cluster as a Double in the range -- [0,1] (where 0 represents that the note is at the start of the cluster, -- and 1 that it's at the end of the cluster) and the pitch of the note -- within the cluster (where 0 represents the lowest note in the cluster and -- 1 the highest note in the cluster) and returns another Double in the range -- [0,1] that represents how loud the note should be played, where 0 is as -- soft as possible (but not silent!) and 1 is as loud as possible. type DynamicsMap = Double -- ^ Time location of note in cluster, in range [0,1]. -> Double -- ^ Pitch location in cluster, in range [0,1]. -> Double -- ^ Volume of the note, in range [0,1]. -- | Returns a dynamic between 25% and 75% volume, based on one full sine -- oscillation sinTimeDynamics :: DynamicsMap sinTimeDynamics x _ = 0.25 + (0.25 * sin (2 * pi * x)) -- | Returns a dynamic between 0% and 100% volume, based on one full sine -- oscillation. Note that this sounds ridiculous sinPitchDynamics :: DynamicsMap sinPitchDynamics x _ = 0.25 + (0.25 * sin (2 * pi * x)) -- | Returns a dynamic between 0% and 100% volume, based on the exponential -- quantile function and the relative pitch height of the note in the cluster. expPitchDynamics :: DynamicsMap expPitchDynamics _ y = max 0.45 (min 0.8 (-log (1 - y))) -- | Adds `Dynamic` to all notes in the given `Music` using `expPitchDynamics`. dyn :: (ToMusicCore a) => Music a -> MusicCore dyn m = addDynamics m expPitchDynamics -- | Adds `Dynamic` to all notes in the given `Music` using the given `DynamicsMap` addDynamics :: (ToMusicCore a) => Music a -> DynamicsMap -> MusicCore addDynamics m' dynMap = do let m = toMusicCore m' let mCluster = coreToCluster m let clusters = cluster mCluster -- Generate dynamics for notes per cluster, and then concatenate the clusters. let dynamics = concatMap (addDynamicsToCluster dynMap) clusters addDynamicsToMCore mCluster dynamics addDynamicsToMCore :: MusicCluster -> Cluster -> MusicCore addDynamicsToMCore m c = fmap add m where add (_,info) = -- Find the element in c with matching absolute start time and pitch int value, -- and get the FullPitch (that contains the dynamic) from that element and put -- it in the note. fst $ fromJust $ find ((info==) . snd) c bounds :: [ClusterMusicA] -> ((Double, Double),(Double, Double)) bounds m = ( (fromRational (minimum (map (fst . snd) m)), fromIntegral (minimum (map (snd . snd) m))) , (fromRational (maximum (map (fst . snd) m)), fromIntegral (maximum (map (snd . snd) m))) ) addDynamicsToCluster :: DynamicsMap -> Cluster -> Cluster addDynamicsToCluster _ [] = [] addDynamicsToCluster f c = do let ((minTime,minNote),(maxTime,maxNote)) = bounds c -- Returns a value in [0,1] that indicates how far the Note is in the cluster. let tProg t = ((fromRational t) - minTime) / (maxTime - minTime) -- Returns a value in [0,1] that indicates how high the Note is in the cluster. let pProg n = ((fromIntegral n) - minNote) / (maxNote - minNote) let pToDyn ((p',attrs),(t,p)) = do let dynDouble = f (tProg t) (pProg p) if dynDouble < 0 || dynDouble > 1 then error "Result from DynamicsMap is not in range [0,1]." else do let maxDynNum = fromIntegral (fromEnum (maxBound :: Dynamic)) :: Double let dynNum = round (maxDynNum * dynDouble) let d = Dynamic (toEnum dynNum :: Dynamic) ((p',d:attrs),(t,p)) map pToDyn c -- | Clusters a MusicCluster. The number of clusters is equal to half cluster :: MusicCluster -> [Cluster] cluster m = kmeansGen gen k (notes m) where gen :: ClusterMusicA -> [Double] gen (_,(x,y)) = [fromRational x, fromIntegral y] -- The number of clusters is equal to the duration of the music divided -- by 4 (we assume that 4 beats go into a measure.) k = max 1 (round ((fromRational (duration m) :: Double) / 4)) -- | Assings 2d coordinates to all music Notes (not Rests), where the x is the -- absolute start time of the Note and the y is the Pitch of the Note -- represented as a number (in other words, a very abstract representation of -- the notes on actual sheet music). Also removes PitchAttributes, because -- they aren't needed here. coreToCluster :: MusicCore -> MusicCluster coreToCluster = calcClusterInfo . fmap (\p -> (p,(0,0)) ) -- | Adds absolute times to Notes. calcClusterInfo :: MusicCluster -> MusicCluster calcClusterInfo (m1@(Rest l) :+: m2) = m1 :+: fmap (addTime l) (calcClusterInfo m2) calcClusterInfo (m1@(Note l _) :+: m2) = calcClusterInfo m1 :+: fmap (addTime l) (calcClusterInfo m2) calcClusterInfo (m1 :=: m2) = calcClusterInfo m1 :=: calcClusterInfo m2 calcClusterInfo (m1 :+: m2) = calcClusterInfo m1 :+: fmap (addTime (duration m1)) (calcClusterInfo m2) calcClusterInfo r@(Rest _) = r calcClusterInfo (Note l (p,(x,_))) = Note l (p,(x, fromEnum p)) -- | Calculates the duration of a piece of Music. duration :: Music a -> Duration duration (m1 :+: m2) = (+) (duration m1) (duration m2) duration (m1 :=: m2) = max (duration m1) (duration m2) duration (Note l _) = l duration (Rest l) = l -- | Adds an amount of time to the AbsStartTime field of a ClusterMusicA Note. addTime :: Duration -> ClusterMusicA -> ClusterMusicA addTime t (p,(x,y)) = (p,(x+t,y))