{-# LANGUAGE FlexibleContexts #-} module Main where import qualified Sound.MIDI.Message.Class.Query as Query import qualified Sound.MIDI.Message.Channel.Voice as VoiceMsg import qualified Sound.MIDI.File.Event as FileEvent import qualified Sound.MIDI.File.Load as Load import qualified Sound.MIDI.File as MidiFile import qualified Data.EventList.Absolute.TimeBody as AbsEventList import qualified Data.EventList.Relative.TimeBody as EventList import qualified Diagrams.Backend.Postscript.CmdLine as PS import qualified Diagrams.Backend.CmdLine as Cmd import Diagrams.Prelude import qualified Options.Applicative as OP import qualified System.IO as IO import Text.Printf (hPrintf, ) import Control.Monad (when, ) import qualified Data.Map as Map; import Data.Map (Map, ) import qualified Data.NonEmpty as NonEmpty import qualified Data.Monoid as Mn import Data.Foldable (foldMap, fold, ) import Data.Tuple.HT (mapSnd, ) type Diag = Diagram PS.B numLong :: Int numLong = 15 globalScale :: Double globalScale = 1/7 horsep, versep :: Double horsep = 40*globalScale versep = 80*globalScale hormargin, horextramargin :: Double hormargin = 65*globalScale horextramargin = 100*globalScale horlen, verlen :: Int -> Double horlen n = fromIntegral n * horsep verlen n = fromIntegral n * versep normalLW, thickLW :: Diag -> Diag normalLW = lwO (5*globalScale) thickLW = lwO (8*globalScale) labels :: Diag labels = hcat' (with & sep .~ horsep) $ map (\str -> text str # fontSizeL horsep) $ map (:[]) "CDEFGABCDEFGABC" long :: Int -> Diag long numIntervals = hcat' (with & sep .~ horsep) $ map (\(wid, col) -> vrule (verlen numIntervals) # wid # lc col) $ let l = (normalLW, black); h = (thickLW, darkgreen) in [l, l, h, l, h, l, h, l, h, l, h, l, l, l, l] across :: Int -> Diag across numIntervals = vcat' (with & sep .~ versep) $ take (succ numIntervals) $ cycle $ let len = horlen (numLong-1) in map (# normalLW) [hrule len, hrule len # lc grey] grid :: Int -> Diag grid numIntervals = alignTL (long numIntervals) <> alignTL (across numIntervals) <> translateX (-hormargin) (alignTL (vrule (verlen numIntervals) # normalLW # lc grey)) <> translateX (horlen (numLong-1) + hormargin) (alignTL (vrule (verlen numIntervals) # normalLW # lc grey)) <> translateY (0.5*versep) labels <> (translateX (-horextramargin) $ translateY (verlen 2) $ alignTL $ lc white $ rect (horlen (numLong-1) + 2*horextramargin) (verlen (numIntervals+4))) dots :: [(DotType, (Int, Double))] -> Diag dots poss = flip foldMap poss $ \(typ, (x,y)) -> translate (r2 (horlen x, - versep * y)) $ let warning txt = (fontSizeL horsep $ text txt) <> (lwO 0 $ fc yellow $ circle (horsep*0.4)) in case typ of Valid -> normalLW $ fc blue $ circle (horsep*0.25) Semitone -> warning "#" TooLow -> warning "!" TooHigh -> warning "!" noteMap :: Map Int (Bool, Int) noteMap = let o = True; x = False semitones = cycle [o,x,o,x,o,o,x,o,x,o,x,o] in Map.fromList $ take 25 $ zip [0..] $ zip semitones $ NonEmpty.tail $ NonEmpty.scanl (+) (-1) $ map fromEnum semitones data DotType = Valid | Semitone | TooLow | TooHigh deriving (Eq, Ord) layoutDots :: TimeStep -> VoiceMsg.Pitch -> MidiFile.T -> [(DotType, (Int, Double))] layoutDots (TimeStep timeStep) zeroKey (MidiFile.Cons typ division tracks) = map (\(t, (dottyp,p)) -> (dottyp, (p,t))) $ AbsEventList.toPairList $ AbsEventList.mapTime ((/timeStep) . realToFrac) $ AbsEventList.mapMaybe (\fev -> do FileEvent.MIDIEvent ev <- Just fev (_c, (_v, p, True)) <- Query.noteExplicitOff ev let pz = VoiceMsg.subtractPitch zeroKey p return $ case Map.lookup pz noteMap of Just (s, n) -> (if s then Valid else Semitone, n) Nothing -> let ( pmin, (_, nmin)) = Map.findMin noteMap (_pmax, (_, nmax)) = Map.findMax noteMap in if pz < pmin then (TooLow, nmin) else (TooHigh, nmax)) $ EventList.toAbsoluteEventList 0 $ MidiFile.mergeTracks typ $ map (MidiFile.secondsFromTicks division) tracks newtype ZeroKey = ZeroKey Int instance Cmd.Parseable ZeroKey where parser = OP.option (ZeroKey <$> OP.auto) (OP.long "zerokey" Mn.<> OP.metavar "INT" Mn.<> OP.value (ZeroKey 60) Mn.<> OP.help "MIDI key for the lowest note line") newtype TimeStep = TimeStep Double instance Cmd.Parseable TimeStep where parser = OP.option (TimeStep <$> OP.auto) (OP.long "timestep" Mn.<> OP.metavar "SECONDS" Mn.<> OP.value (TimeStep 0.1) Mn.<> OP.help "time step between lines") newtype Input = Input FilePath instance Cmd.Parseable Input where parser = OP.argument (Input <$> OP.str) (OP.metavar "INPUT") diag :: TimeStep -> ZeroKey -> Input -> IO Diag diag timeStep (ZeroKey zeroKey) (Input path) = do midi <- Load.fromFile path let cloud = layoutDots timeStep (VoiceMsg.toPitch zeroKey) midi sorted = Map.fromListWith (++) $ map (mapSnd (:[])) cloud warning typ msg = let n = length $ fold $ Map.lookup typ sorted in when (n > 0) $ hPrintf IO.stderr "Warning: %i %s\n" n msg warning Semitone "semitones" warning TooLow "notes are too low" warning TooHigh "notes are too high" return $ dots cloud <> (grid $ ceiling $ maximum $ map (snd . snd) cloud) main :: IO () main = PS.mainWith diag