module ZMidi.Score.Quantise (
QMidiScore (..)
, ShortestNote (..)
, GridUnit (..)
, QBins (..)
, QDev (..)
, QDevPerc (..)
, quantise
, quantiseSafe
, quantiseQDevSafe
, avgQDev
, avgQDevQMS
, canBeQuantisedAt
, removeOverlap
, getMinGridSize
, qToQBins
, toQBins
, getNumForQBins
) where
import ZMidi.Score.Datatypes
import ZMidi.Score.Utilities
import Control.Arrow ( second )
import Text.Printf ( printf, PrintfArg )
import Data.Binary ( Binary )
import GHC.Generics ( Generic )
import Control.DeepSeq ( NFData )
import Data.Ratio ( Ratio, numerator, denominator )
acceptableQuantisationDeviation :: QDevPerc
acceptableQuantisationDeviation = 0.02
shortestNote :: ShortestNote
shortestNote = FourtyEighth
data QMidiScore = QMidiScore { qMidiScore :: MidiScore
, qShortestNote :: ShortestNote
, qGridUnit :: GridUnit
, totDeviation :: QDev
} deriving (Show, Eq, Generic)
instance Binary QMidiScore
data ShortestNote = Eighth | Sixteenth | ThirtySecond
| FourtyEighth | SixtyFourth
deriving (Eq, Show, Generic)
instance Binary ShortestNote
newtype GridUnit = GridUnit { gridUnit :: Int }
deriving ( Eq, Show, Num, Ord, Enum, Real, Integral, Binary )
newtype QBins = QBins { qbins :: Int }
deriving ( Eq, Show, Num, Ord, Enum, Real, Integral, Binary, NFData )
newtype QDev = QDev { qDev :: Int }
deriving ( Eq, Show, Num, Ord, Enum, Real, Integral, Binary )
newtype QDevPerc = QDevPerc { qDevPerc :: Double }
deriving ( Eq, Show, Num, Ord, Enum, Real, Floating
, Fractional, RealFloat, RealFrac, PrintfArg, Binary )
quantise :: MidiScore -> QMidiScore
quantise = either error id . quantiseSafe shortestNote
quantiseQDevSafe :: MidiScore -> Either String QMidiScore
quantiseQDevSafe ms = quantiseSafe shortestNote ms >>= qDevCheck
qDevCheck :: QMidiScore -> Either String QMidiScore
qDevCheck qm | d < acceptableQuantisationDeviation = Right qm
| otherwise = Left ("the average quantisation deviation is above "
++ printf "allowed deviation: %.3f < %.3f"
d acceptableQuantisationDeviation )
where d = avgQDevQMS qm
avgQDevQMS :: QMidiScore -> QDevPerc
avgQDevQMS qm = avgQDev (qGridUnit qm) (totDeviation qm)
(nrOfNotes . qMidiScore $ qm)
avgQDev :: GridUnit -> QDev -> Int -> QDevPerc
avgQDev gu qd nrn =
QDevPerc ((fromIntegral qd / fromIntegral nrn) / fromIntegral gu)
quantiseSafe :: ShortestNote -> MidiScore -> Either String QMidiScore
quantiseSafe sn (MidiScore k ts (TPB dv) mf tp _md vs) =
case dv `divMod` (qbins . toQBins $ sn) of
(gu, 0) -> let
(vs',d) = unzip . map (quantiseVoice . GridUnit $ gu) $ vs
md' = gcIOId . buildTickMap $ vs'
ts' = map (snapTimed . GridUnit $ gu) ts
ms' = MidiScore k ts' (TPB dv) mf tp md' vs'
in Right (QMidiScore ms' sn (GridUnit gu) (sum d))
_ -> Left ("MidiFile cannot be quantised: " ++ show dv ++
" cannot be divided by " ++ show (toQBins sn))
snapTimed :: GridUnit -> Timed a -> Timed a
snapTimed gu t = t {onset = fst . snap gu $ onset t}
quantiseVoice :: GridUnit -> Voice -> (Voice, QDev)
quantiseVoice gu = second sum . unzip . map snapEvent where
snapEvent :: Timed ScoreEvent -> (Timed ScoreEvent, QDev)
snapEvent (Timed ons dat) = let (t', d) = snap gu ons
in case dat of
(NoteEvent c p v l) -> (Timed t' (NoteEvent c p v (fst $ snap gu l)), d)
_ -> (Timed t' dat , d)
snap :: GridUnit -> Time -> (Time, QDev)
snap gu t | m == 0 = (t, 0)
| m > 0 = if (g m) >= m
then let t' = d * g
in (t', fromIntegral (t t'))
else let t' = (1+d) * g
in (t', fromIntegral (t' t ))
| otherwise = error "Negative time stamp found"
where (d,m) = t `divMod` g
g = fromIntegral gu
canBeQuantisedAt :: ShortestNote -> MidiScore -> Bool
canBeQuantisedAt sn ms = ((tpb . ticksPerBeat $ ms)
`mod` (qbins . toQBins $ sn)) == 0
removeOverlap :: Voice -> Voice
removeOverlap = foldr step [] where
step :: Timed ScoreEvent -> [Timed ScoreEvent] -> [Timed ScoreEvent]
step t [] = [t]
step t n = updateDur : n where
updateDur :: Timed ScoreEvent
updateDur = case getEvent t of
(NoteEvent c p v d) -> let nxt = onset . head $ n
d' = if onset t + d > nxt
then nxt onset t else d
in t {getEvent = NoteEvent c p v d'}
_ -> t
getMinGridSize :: ShortestNote -> MidiScore -> TPB
getMinGridSize q ms = case (tpb . ticksPerBeat $ ms) `divMod`
(qbins . toQBins $ q) of
(d,0) -> TPB d
_ -> error "getMinGridSize: invalid quantisation"
qToQBins :: QMidiScore -> QBins
qToQBins = toQBins . qShortestNote
toQBins :: ShortestNote -> QBins
toQBins Eighth = QBins 2
toQBins Sixteenth = QBins 4
toQBins ThirtySecond = QBins 8
toQBins FourtyEighth = QBins 12
toQBins SixtyFourth = QBins 16
getNumForQBins :: QBins -> Ratio Int -> Int
getNumForQBins (QBins q) r = numerator r * (q `div` denominator r)