{- | It is recommended to view this Haddock documentation using the @-q local@ option so that, for example, the types @Data.EventList.Relative.TimeBody.T@, @Sound.MIDI.File.T@, and @Sound.MIDI.File.Event.T@ don't all get displayed simply as @T@. Otherwise, hover over the types to see what is referred to. -} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} module Sound.MIDI.Util ( -- * Types Beats(..), Seconds(..), BPS(..) -- * Reading\/writing MIDI files , decodeFile, encodeFileBeats, minResolution -- * Tempos , readTempo, showTempo , makeTempo, applyTempo, unapplyTempo, applyTempoTrack, unapplyTempoTrack , TempoMap, makeTempoMap, applyTempoMap, unapplyTempoMap -- * Measures and time signatures , readSignature, showSignature , MeasureMap, MeasureBeats, MeasureMode(..), measures, makeMeasureMap , applyMeasureMap, unapplyMeasureMap -- * Track names , trackName, setTrackName, readTrackName, showTrackName -- * Misc. track operations , trackSplitZero, trackGlueZero, trackTakeZero, trackDropZero , trackJoin, trackSplit, trackTake, trackDrop , extractFirst ) where import qualified Data.Map as Map import Data.Maybe (listToMaybe, mapMaybe, isNothing) import Data.Monoid (Monoid) import Data.Ratio (numerator, denominator) import qualified Numeric.NonNegative.Wrapper as NN import qualified Numeric.NonNegative.Class as NNC import qualified Sound.MIDI.File as F import qualified Sound.MIDI.File.Event as E import qualified Sound.MIDI.File.Event.Meta as Meta import qualified Data.EventList.Absolute.TimeBody as ATB import qualified Data.EventList.Relative.TimeBody as RTB -- | A hack to look up mappings in a 'Map.Map' using either one of -- two key types, which increase together. data DoubleKey a b = DoubleKey !a !b | LookupA !a | LookupB !b deriving (Show, Read) instance (Ord a, Ord b) => Eq (DoubleKey a b) where dk1 == dk2 = compare dk1 dk2 == EQ instance (Ord a, Ord b) => Ord (DoubleKey a b) where compare (DoubleKey a1 _ ) (DoubleKey a2 _ ) = compare a1 a2 -- A is arbitrary compare (DoubleKey a1 _ ) (LookupA a2 ) = compare a1 a2 compare (DoubleKey _ b1) (LookupB b2) = compare b1 b2 compare (LookupA a1 ) (DoubleKey a2 _ ) = compare a1 a2 compare (LookupA a1 ) (LookupA a2 ) = compare a1 a2 compare (LookupA _ ) (LookupB _ ) = error "compare: internal error! tried to compare LookupA and LookupB" compare (LookupB b1) (DoubleKey _ b2) = compare b1 b2 compare (LookupB _ ) (LookupA _ ) = error "compare: internal error! tried to compare LookupB and LookupA" compare (LookupB b1) (LookupB b2) = compare b1 b2 -- | Musical time, measured in beats a.k.a. quarter notes. newtype Beats = Beats { fromBeats :: NN.Rational } deriving (Eq, Ord, Show, Monoid, NNC.C, Num, Real, Fractional, RealFrac) -- | Real time, measured in seconds. newtype Seconds = Seconds { fromSeconds :: NN.Rational } deriving (Eq, Ord, Show, Monoid, NNC.C, Num, Real, Fractional, RealFrac) -- | A ratio between musical time and real time, measured in beats per second. newtype BPS = BPS { fromBPS :: NN.Rational } deriving (Eq, Ord, Show, Monoid, NNC.C, Num, Real, Fractional, RealFrac) -- | Creates a tempo as a ratio of a music duration to a real time duration. makeTempo :: Beats -> Seconds -> BPS makeTempo (Beats b) (Seconds s) = BPS $ b / s -- | Uses a tempo to convert from musical time to real time. applyTempo :: BPS -> Beats -> Seconds applyTempo (BPS bps) (Beats b) = Seconds $ b / bps -- | Uses a tempo to convert from real time to musical time. unapplyTempo :: BPS -> Seconds -> Beats unapplyTempo (BPS bps) (Seconds s) = Beats $ bps * s -- | Assigns units to the tracks in a MIDI file. Supports both the common -- ticks-based files as well as real-time SMPTE-encoded files. decodeFile :: F.T -> Either [RTB.T Beats E.T] [RTB.T Seconds E.T] decodeFile (F.Cons _typ dvn trks) = case dvn of F.Ticks res -> let readTime tks = Beats $ fromIntegral tks / fromIntegral res in Left $ map (RTB.mapTime readTime) trks F.SMPTE fps tksPerFrame -> let realFps = case fps of 29 -> 29.97 _ -> fromIntegral fps readTime tks = Seconds $ fromIntegral tks / (realFps * fromIntegral tksPerFrame) in Right $ map (RTB.mapTime readTime) trks -- | Encodes the tracks' beat positions in ticks, using the given resolution. -- Positions will be rounded if necessary; see 'minResolution'. encodeFileBeats :: F.Type -> Integer -> [RTB.T Beats E.T] -> F.T encodeFileBeats typ res = F.Cons typ (F.Ticks $ fromIntegral res) . map (RTB.discretize . RTB.mapTime (* fromIntegral res)) -- | To correctly encode all the given tracks without rounding, -- the resolution must be a multiple of the returned number. minResolution :: [RTB.T Beats E.T] -> Integer minResolution = foldr lcm 1 . map (denominator . NN.toNumber . fromBeats) . concatMap RTB.getTimes -- | Extracts the tempo from a tempo change event. readTempo :: E.T -> Maybe BPS readTempo (E.MetaEvent (Meta.SetTempo uspqn)) = let spqn = fromIntegral uspqn / 1000000 qnps = recip spqn in Just $ BPS qnps readTempo _ = Nothing -- | Creates a MIDI event to set the tempo to the given value. -- Rounds the tempo to the nearest whole \"microseconds per beat\" if necessary. showTempo :: BPS -> E.T showTempo (BPS qnps) = let spqn = recip qnps uspqn = spqn * 1000000 in E.MetaEvent $ Meta.SetTempo $ round uspqn -- | Given a MIDI event, if it is a time signature event, returns the length -- of one measure set by the time signature. readSignature :: E.T -> Maybe Beats readSignature (E.MetaEvent (Meta.TimeSig n d _ _)) = Just $ let writtenFraction = fromIntegral n / (2 ^ d) sigLength = 4 * writtenFraction in Beats sigLength readSignature _ = Nothing -- | If the given number is @2 ^ n@ where @n@ is a non-negative integer, -- returns @n@. logBase2 :: Integer -> Maybe Integer logBase2 x = go 0 1 where go !p !y = case compare x y of EQ -> Just p GT -> go (p + 1) (y * 2) LT -> Nothing -- | Given a measure length, tries to encode it as a MIDI time signature. showSignature :: Beats -> Maybe E.T showSignature (Beats sigLength) = let writtenFraction = NN.toNumber $ sigLength / 4 num = fromIntegral $ numerator writtenFraction in do denomPow <- logBase2 $ denominator writtenFraction Just $ E.MetaEvent $ case denomPow of 0 -> Meta.TimeSig (num * 4) 2 24 8 1 -> Meta.TimeSig (num * 2) 2 24 8 _ -> Meta.TimeSig num (fromIntegral denomPow) 24 8 -- For prettiness we make the denominator at least /4, -- so for example 4/4 is not encoded as 1/1 even though it is simpler. -- | Should never happen. Signifies an internal error in the creation of a -- 'Map.Map': either there was no event at position 0, or the 'Map.Map' contains -- a 'LookupA' or 'LookupB'. translationError :: (Show t) => String -> t -> a translationError f t = error $ "Sound.MIDI.Util." ++ f ++ ": internal error! couldn't translate position " ++ show t -- | Converts between positions in musical time and real time. newtype TempoMap = TempoMap (Map.Map (DoubleKey Beats Seconds) BPS) makeTempoMap :: RTB.T Beats E.T -> TempoMap makeTempoMap = TempoMap . Map.fromAscList . go 0 0 2 . RTB.mapMaybe readTempo where go :: Beats -> Seconds -> BPS -> RTB.T Beats BPS -> [(DoubleKey Beats Seconds, BPS)] go b s bps rtb = (DoubleKey b s, bps) : case RTB.viewL rtb of Nothing -> [] Just ((db, bps'), rtb') -> go (b + db) (s + applyTempo bps db) bps' rtb' applyTempoMap :: TempoMap -> Beats -> Seconds applyTempoMap (TempoMap tm) bts = case Map.lookupLE (LookupA bts) tm of Just (DoubleKey b s, bps) -> s + applyTempo bps (bts - b) _ -> translationError "applyTempoMap" bts unapplyTempoMap :: TempoMap -> Seconds -> Beats unapplyTempoMap (TempoMap tm) secs = case Map.lookupLE (LookupB secs) tm of Just (DoubleKey b s, bps) -> b + unapplyTempo bps (secs - s) _ -> translationError "unapplyTempoMap" secs applyTempoTrack :: TempoMap -> RTB.T Beats a -> RTB.T Seconds a applyTempoTrack tm = RTB.fromAbsoluteEventList . ATB.mapTime (applyTempoMap tm) . RTB.toAbsoluteEventList 0 unapplyTempoTrack :: TempoMap -> RTB.T Seconds a -> RTB.T Beats a unapplyTempoTrack tm = RTB.fromAbsoluteEventList . ATB.mapTime (unapplyTempoMap tm) . RTB.toAbsoluteEventList 0 -- | Converts between a simple beat position, -- and a measure offset plus a beat position. newtype MeasureMap = MeasureMap (Map.Map (DoubleKey Beats Int) Beats) -- | A number of measures (starting from 0), and an offset within that measure -- (also starting from 0). type MeasureBeats = (Int, Beats) -- | What to do when 'makeMeasureMap' finds a misaligned time signature? data MeasureMode = Error -- ^ Throw an exception. | Ignore -- ^ Ignore it. | Truncate -- ^ Truncate the previous measure. deriving (Eq, Ord, Show, Read, Enum, Bounded) -- | The duration of a number of measures in a given time signature. measures :: Int -> Beats -> Beats measures m b = fromIntegral m * b -- | Computes the measure map, given the tempo track from the MIDI. makeMeasureMap :: MeasureMode -> RTB.T Beats E.T -> MeasureMap makeMeasureMap mm = MeasureMap . Map.fromAscList . go 0 0 4 . RTB.mapMaybe readSignature where go :: Beats -> Int -> Beats -> RTB.T Beats Beats -> [(DoubleKey Beats Int, Beats)] go b m tsig rtb = (DoubleKey b m, tsig) : case RTB.viewL rtb of Nothing -> [] Just ((db, tsig'), rtb') -> case properFraction $ db / tsig of (dm, 0 ) -> go (b + db) (m + dm) tsig' rtb' (dm, leftoverMsrs) -> case mm of Error -> error $ unwords [ "makeMeasureMap: misaligned time signature found after" , show m , "measures and" , show $ fromBeats db , "beats" ] Ignore -> go b m tsig $ RTB.delay db rtb' Truncate -> let leftoverBeats = leftoverMsrs * tsig truncated = (DoubleKey (b + measures dm tsig) (m + dm), leftoverBeats) in truncated : go (b + db) (m + dm + 1) tsig' rtb' -- | Uses the measure map to compute which measure a beat position is in. applyMeasureMap :: MeasureMap -> Beats -> MeasureBeats applyMeasureMap (MeasureMap mm) bts = case Map.lookupLE (LookupA bts) mm of Just (DoubleKey b msr, tsig) -> let msrs = floor $ (bts - b) / tsig leftover = (bts - b) - fromIntegral msrs * tsig in (msr + msrs, leftover) _ -> translationError "applyMeasureMap" bts -- | Uses the measure map to convert a measures+beats position to just beats. unapplyMeasureMap :: MeasureMap -> MeasureBeats -> Beats unapplyMeasureMap (MeasureMap mm) (msr, bts) = case Map.lookupLE (LookupB msr) mm of Just (DoubleKey b m, tsig) -> b + fromIntegral (msr - m) * tsig + bts _ -> translationError "unapplyMeasureMap" (msr, bts) -- | Combines 'trackTakeZero' and 'trackDropZero'. trackSplitZero :: (NNC.C t) => RTB.T t a -> ([a], RTB.T t a) trackSplitZero rtb = case RTB.viewL rtb of Just ((dt, x), rtb') | dt == NNC.zero -> case trackSplitZero rtb' of (xs, rtb'') -> (x : xs, rtb'') _ -> ([], rtb) -- | Prepends the given events to the event list at position zero. trackGlueZero :: (NNC.C t) => [a] -> RTB.T t a -> RTB.T t a trackGlueZero xs rtb = foldr (RTB.cons NNC.zero) rtb xs -- | Returns the list of events at position zero of the event list. trackTakeZero :: (NNC.C t) => RTB.T t a -> [a] trackTakeZero = fst . trackSplitZero -- | Drops all events at position zero of the event list. trackDropZero :: (NNC.C t) => RTB.T t a -> (RTB.T t a) trackDropZero = snd . trackSplitZero -- | Looks for a track name event at position zero. trackName :: (NNC.C t) => RTB.T t E.T -> Maybe String trackName = listToMaybe . mapMaybe readTrackName . trackTakeZero -- | Removes any existing track name events at position zero and adds a new one. setTrackName :: (NNC.C t) => String -> RTB.T t E.T -> RTB.T t E.T setTrackName s rtb = case trackSplitZero rtb of (zero, rest) -> let zero' = showTrackName s : filter (isNothing . readTrackName) zero in trackGlueZero zero' rest readTrackName :: E.T -> Maybe String readTrackName (E.MetaEvent (Meta.TrackName s)) = Just s readTrackName _ = Nothing showTrackName :: String -> E.T showTrackName = E.MetaEvent . Meta.TrackName -- | Equivalent to 'Control.Monad.join', except 'RTB.T' doesn't have a 'Monad' -- instance, presumably because 'RTB.merge' has an 'Ord' constraint. trackJoin :: (NNC.C t, Ord a) => RTB.T t (RTB.T t a) -> RTB.T t a trackJoin rtb = case RTB.viewL rtb of Nothing -> RTB.empty Just ((dt, x), rtb') -> RTB.delay dt $ RTB.merge x $ trackJoin rtb' -- | Combines 'trackTake' and 'trackDrop'. trackSplit :: (NNC.C t) => t -> RTB.T t a -> (RTB.T t a, RTB.T t a) trackSplit t rtb = case RTB.viewL rtb of Nothing -> (RTB.empty, RTB.empty) Just ((dt, x), rtb') -> case NNC.split t dt of (_, (True , d)) {- t <= dt -} -> (RTB.empty, RTB.cons d x rtb') (_, (False, d)) {- t > dt -} -> case trackSplit d rtb' of (taken, dropped) -> (RTB.cons dt x taken, dropped) -- | Drops all events at or after the given time from the event list. trackTake :: (NNC.C t) => t -> RTB.T t a -> RTB.T t a trackTake t rtb = fst $ trackSplit t rtb -- | Drops the given amount of time from the start of the event list. -- Events that are exactly at the time that is dropped will be kept in the list. trackDrop :: (NNC.C t) => t -> RTB.T t a -> RTB.T t a trackDrop t rtb = snd $ trackSplit t rtb -- | Finds and extracts the first event for which the function returns 'Just'. extractFirst :: (NNC.C t) => (a -> Maybe b) -> RTB.T t a -> Maybe ((t, b), RTB.T t a) extractFirst f rtb = do ((dt, x), rtb') <- RTB.viewL rtb case f x of Just y -> return ((dt, y), rtb') Nothing -> do ((dt_, y_), rtb_) <- extractFirst f rtb' return ((NNC.add dt dt_, y_), RTB.cons dt x rtb_)