{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE AllowAmbiguousTypes #-} module Fadno.Meter where import Fadno.Notation import Fadno.Note import Control.Lens hiding (Empty,pre) import Data.Foldable import Data.Ratio import GHC.Real import Data.Maybe import Data.List (sort,sortBy,nub) import Data.Function import Control.Arrow import qualified Data.Map.Strict as M import Safe type HasRatioNotes t n p = (Traversable t, HasNote n p Rational) -- -- REBAR -- -- | use 'rebar' with multiple input "bars". rebars :: (HasRatioNotes b n p, HasRatioNotes c m p,Monoid p,Eq p, Monoid (c m), Monoid (b n),HasTimeSignature (c m), Snoc (c m) (c m) m m,HasTie m,Show (c m)) => TimeSignature -> [b n] -> [c m] rebars ts = rebar ts . mconcat -- | Given a time signature and a "bar" (Traversable "b" of HasNotes "n"), -- make new "bars" (Traversable "c" of HasNotes "m"), -- partitioning notes, applying ties as needed, -- and decorating with the time signature. rebar :: (HasRatioNotes b n p, HasRatioNotes c m p,Monoid p,Eq p,Monoid (c m), HasTimeSignature (c m),Snoc (c m) (c m) m m,HasTie m,Show (c m)) => TimeSignature -> b n -> [c m] rebar ts = reverse . fixup . foldl' go [mempty] . fmap fromNote where tslen = tsToRatio ts go bss@[] _ = bss -- error case but might as well be total go bss@(b:bs) n | barlen == tslen = go (mempty:bss) n | newBarLen <= tslen = (b |> n):bs | otherwise = {- trace1' "otherwise" (remaining, ndur) $ -} go ((b |> pre):bs) post where barlen = sumDurs b ndur = view noteDur n newBarLen = barlen + ndur remaining = newBarLen - tslen post = tieMay TStop . set noteDur remaining $ n pre = tieMay TStart . set noteDur (ndur - remaining) $ n -- fixup: set head and tail timesigs fixup [] = [] fixup bs = over _head fixLast . joinLast . over _last (timeSignature ?~ ts) $ bs fixLast b | barlen == tslen = b | otherwise = case tsFromRatio' ts barlen of Just t -> b & timeSignature ?~ t Nothing -> b where barlen = sumDurs b -- joinLast: apply heuristic that an additional bar less than 1/2 the ts length -- should be merged with the prior bar. joinLast aas@(a:b:as) | sumDurs a + sumDurs b < tslen * (3%2) = (b `mappend` a):as -- leave notes tied. why not. | otherwise = aas joinLast a = a -- | 'rebar' using 'Bar' and 'Note\'' for output. rebar' :: (HasRatioNotes b n p,Monoid p,Eq p,Show p) => TimeSignature -> b n -> [Bar (Note' p Rational)] rebar' = rebar -- | 'rebars' using 'Bar' and 'Note\'' for output. rebars' :: (HasRatioNotes b n p,Monoid (b n),Monoid p,Eq p,Show p) => TimeSignature -> [b n] -> [Bar (Note' p Rational)] rebars' = rebars -- | Set tie if not a rest tieMay :: (Eq a, Monoid a, HasNote s a d, HasTie s) => Tie -> s -> s tieMay end v | isRest v = v | otherwise = over tie setTie v where setTie o@(Just old) | old == end = o | otherwise = Just TBoth setTie Nothing = Just end -- -- TIE RULES -- -- | Representable duration denominators. -- -- For standard base-2 durs, 2 and 4 are spurious as they reduce to 1, -- thus 1 plus the "dot values" 3,7. -- -- For non-standard (quintuples etc), we admit 2 and 4 as well, for e.g. 2%5, -- a "half-note" under a quarter-quintuple. Anything greater -- exceeding the understanding limit: 8%17 can certainly be represented as -- a half-note, but it makes little sense to the reader. validDenoms :: [Integer] validDenoms = [1,2,3,4,7] -- | Max representable duration. maxDur :: Rational maxDur = 2 -- | Test for representational duration per 'validDenoms' and 'maxDur'. validDur :: Rational -> Bool validDur r = r == maxDur || (r < maxDur && numerator r `elem` validDenoms) -- | Tie rules that work across any denominators, such that -- 5%8 -> [1%2,1%8], 9%16 -> [1%2,1%16], 11%16 -> [1%2,3%16], -- 13%16 -> [3%2,1%16], 9%4 -> [2,1%4]. splitDur :: Rational -> [Rational] splitDur r | r < 0 = error "splitDur: negative duration" | r == 0 = [] | validDur r = [r] -- NB: subtraction doesn't preserve denom (17:%20 - 3%5 -> 1%4, not 5%20) | otherwise = split:splitDur (r - split) where split = findSplit r -- | Find split by 1) finding largest power-of-2 fraction under value or -- 2) finding longest power-of-two denominator split, up to 8. findSplit :: Rational -> Rational findSplit r = case filter validDur candidates of [] -> splitOnValid (v:_) -> v where n = numerator r d = denominator r pow2s = [x | p <- [(1 :: Integer)..], x<-[2^p]] denomPow2s = reverse $ takeWhile (\v -> v < d && d `rem` v == 0) pow2s candidates = filter ( (n * cd `div` d) :% (d `div` cd)) denomPow2s splitOnValid = case filter (<= min r maxDur) $ map (:%d) $ reverse $ takeWhile ( r (v:_) -> v -- | Apply rules in 'splitDur' and tie affected notes. tieRules :: (HasRatioNotes b n p, HasTie n, Monoid p, Eq p, Show n, HasRatioNotes c m p, HasTie m, Monoid (c m), Snoc (c m) (c m) m m) => b n -> c m tieRules = foldl' apply mempty where apply r n = case splitDur (view noteDur n) of [] -> error $ "tieRules: empty result from splitDur for " ++ show n [_] -> r |> set tie (view tie n) (fromNote n) ds -> foldl (|>) r . fixLast . fixFirst . map mkTied $ ds where mkTied d = tieMay TBoth (set noteDur d (fromNote n)) forOrgTie t = case view tie n of Nothing -> t (Just a) | a == t -> t | otherwise -> TBoth fixFirst = over (_head.tie) (fmap (const (forOrgTie TStart))) fixLast = over (_last.tie) (fmap (const (forOrgTie TStop))) -- | Monomorphic-result 'tieRules tieRules' :: (HasRatioNotes b n p, HasTie n, Monoid p, Eq p, Show n) => b n -> Bar (Note' p Rational) tieRules' = tieRules -- -- SELECT TIMESIG -- -- | Weights and pulse values for pre-configured TSs. data TsConfig = TsConfig { _tSig :: TimeSignature , _tWeight :: Rational , _tPulse :: Rational } deriving (Eq,Show) makeLenses ''TsConfig selectTimeSig :: HasRatioNotes t n p => [t n] -> Maybe TimeSignature selectTimeSig phrases = fmap fst $ headMay $ selectTimeSigs phrases -- | Combine scores from phrases. selectTimeSigs :: HasRatioNotes t n p => [t n] -> [(TimeSignature,Rational)] selectTimeSigs = mergeScores . preferDivisableHeads . map selectTsConfigs where mergeScores = sortBy (flip compare `on` snd) . M.toList . foldl1 (M.unionWith (+)) . map (M.fromListWith max . map (_tSig &&& _tWeight)) -- | nutty heuristic that overweights a TS for a uniform duration divisor preferDivisableHeads :: [[TsConfig]] -> [[TsConfig]] preferDivisableHeads [] = [] preferDivisableHeads [a] = [a] preferDivisableHeads phraseTss = case sequence (map headMay phraseTss) of Nothing -> phraseTss Just heads | length (nub heads) == 1 -> phraseTss | otherwise -> case nub $ (zipWith commonDivHeur <*> tail) (map tsConfigToDur heads) of [] -> phraseTss [a] -> maybe phraseTss ((:phraseTss).return) $ tsConfigFromDur 100 a _ -> phraseTss -- | main heuristic is finding the common divisible duration, -- with requirement that it must be greater than 1/4 the difference between the durations. -- Hopefully avoids crappy tiny TSs like 2/8. commonDivHeur :: Rational -> Rational -> Rational commonDivHeur d1 d2 | d1 == d2 = d1 | c / (abs (d1 - d2)) > (1%4) = min d1 d2 | otherwise = c where c = d1 / fromIntegral (numerator (d1/d2)) tsConfigToDur :: TsConfig -> Rational tsConfigToDur = tsToRatio . _tSig -- | Attempt to construct a TS config from duration tsConfigFromDur :: Rational -> Rational -> Maybe TsConfig tsConfigFromDur weight = fmap (\t -> TsConfig t weight (minMedianDur (_tsUnit t))) . tsFromRatio -- | Pre-configured timesigs. tsConfigs :: [TsConfig] tsConfigs = [TsConfig (4/:Q4) 9 (1%4) ,TsConfig (3/:Q4) 8 (1%4) ,TsConfig (6/:Q8) 8 (3%8) ,TsConfig (12/:Q8) 7 (3%8) ,TsConfig (2/:Q4) 6 (1%4) ,TsConfig (5/:Q4) 5 (5%4) ,TsConfig (5/:Q8) 5 (5%8) ,TsConfig (7/:Q4) 5 (7%4) ,TsConfig (7/:Q8) 5 (7%8) ,TsConfig (9/:Q8) 5 (3%8) ] -- | Given a median note duration, minima for acceptable quanta. minMedianDur :: Quanta -> Rational minMedianDur q = fromMaybe (1%32) . lookup q $ [(Q8,1%32),(Q4,1%16),(Q2,1%4)] -- | Given a phrase, select configs selectTsConfigs :: HasRatioNotes t n p => t n -> [TsConfig] selectTsConfigs phrase | null phrase = [] | otherwise = sortBy (flip compare `on` _tWeight) $ mapMaybe (evalTsConfig phrase) -- append custom-length TS as lowest-weight choice (maybe tsConfigs (:tsConfigs) (tsConfigFromDur 4 (sumDurs phrase))) -- | Filter and score time signatures per heuristics. evalTsConfig :: HasRatioNotes t n p => t n -> TsConfig -> Maybe TsConfig evalTsConfig phrase c@(TsConfig ts@(TimeSignature _n q) _ pulse) | medianDur < minMedianDur q = Nothing -- density filter | phraseDur < tsDur = Nothing -- min duration filter | tsDur >= 2 = Nothing -- too long TS | phraseDur == tsDur = Just $ over tWeight (* (9%4)) c -- exact length match bonus, | otherwise = Just $ over tWeight computeWeight c where medianDur = sort (toListOf (traverse.noteDur) phrase) !! (phraseLength `div` 2) phraseLength = length phrase phraseDur = sumDurs phrase tsDur = tsToRatio ts -- scale by pulse coverage, subtract by divisibility by ts duration computeWeight w = (w * pulseCoverage pulse phrase) - (phraseDur `frem` tsDur) -- | 'rem' for 'RealFrac' frem :: RealFrac a => a -> a -> a frem a b = let (_ :: Int,f) = properFraction (a/b) in f isDivBy :: RealFrac a => a -> a -> Bool isDivBy a b = 0 == frem a b -- | Compute percentage of notes falling on pulse values. pulseCoverage :: HasRatioNotes t n p => Rational -> t n -> Rational pulseCoverage pulse phrase = fromIntegral pulseNoteCount % fromIntegral (length phrase) where pulseNoteCount = length . filter ((`isDivBy` pulse) . fst) . mapTime $ phrase