{- | Basic types and functions for dealing with Jammit song packages. -} {-# LANGUAGE CPP #-} module Sound.Jammit.Base ( Instrument(..) , Part(..) , AudioPart(..) , SheetPart(..) , titleToPart , titleToAudioPart , partToInstrument , audioPartToInstrument , Info(..) , Track(..) , loadInfo , loadTracks , findJammitDir , songSubdirs ) where #if !MIN_VERSION_base(4,8,0) import Control.Applicative ((<$>), (<*>)) #endif import Control.Applicative (liftA2) import Control.Arrow ((***)) import Control.Exception (evaluate) import Control.Monad (filterM, guard) import Data.Char (toLower, toUpper) import Data.Maybe (catMaybes) import System.Environment (lookupEnv) import Text.Read (readMaybe) import qualified Data.Map as Map import qualified Data.PropertyList as PL import qualified System.Directory as Dir import System.FilePath (()) import qualified System.Info as Info -- | The Enum instance corresponds to the number used in the "instrument" -- property, and the names (used by Show/Read) are capitalized versions of those -- used in the "skillLevel" property. data Instrument = Guitar | Bass | Drums | Keyboard | Vocal deriving (Eq, Ord, Show, Read, Enum, Bounded) data Part = PartGuitar1 -- ^ Used for both Guitar and Guitar 1 | PartGuitar2 | PartBass | PartDrums1 -- ^ Used for both Drums and Drums 1 | PartDrums2 -- ^ Rarely used. Seen in \"Space Truckin'\" | PartKeys1 -- ^ Used for both Keys and Keys 1 | PartKeys2 | PartPiano -- ^ Rarely used. Seen in \"The Answer Lies Within\" and \"Wait for Sleep\" | PartSynth -- ^ Rarely used. Seen in \"Wait for Sleep\" | PartOrgan -- ^ Rarely used. Seen in \"Smoke on the Water\" | PartVocal | PartBVocals deriving (Eq, Ord, Show, Read, Enum, Bounded) data AudioPart = Only Part -- ^ An audio file for a single notated part. | Without Instrument -- ^ The backing track for an instrument package. deriving (Eq, Ord, Show, Read) data SheetPart = Notation Part -- ^ For any instrument, the notation sheet music. | Tab Part -- ^ For guitar and bass, the tablature sheet music. deriving (Eq, Ord, Show, Read) titleToPart :: String -> Maybe Part titleToPart s = case s of "Guitar" -> Just PartGuitar1 "Guitar 1" -> Just PartGuitar1 "Guitar 2" -> Just PartGuitar2 "Bass" -> Just PartBass "Drums" -> Just PartDrums1 "Drums 1" -> Just PartDrums1 "Drums 2" -> Just PartDrums2 "Keys" -> Just PartKeys1 "Keys 1" -> Just PartKeys1 "Keys 2" -> Just PartKeys2 "Piano" -> Just PartPiano "Synth" -> Just PartSynth "Organ" -> Just PartOrgan "Vocal" -> Just PartVocal "B Vocals" -> Just PartBVocals _ -> Nothing titleToAudioPart :: String -> Instrument -> Maybe AudioPart titleToAudioPart "Band" i = Just $ Without i titleToAudioPart s _ = Only <$> titleToPart s partToInstrument :: Part -> Instrument partToInstrument p = case p of PartGuitar1 -> Guitar PartGuitar2 -> Guitar PartBass -> Bass PartDrums1 -> Drums PartDrums2 -> Drums PartKeys1 -> Keyboard PartKeys2 -> Keyboard PartPiano -> Keyboard PartSynth -> Keyboard PartOrgan -> Keyboard PartVocal -> Vocal PartBVocals -> Vocal audioPartToInstrument :: AudioPart -> Instrument audioPartToInstrument (Only p) = partToInstrument p audioPartToInstrument (Without i) = i data Info = Info { album :: String , artist :: String , bpm :: String , copyright :: String , countInBeats :: Integer , courtesyOf :: String , demo :: Bool , explicit :: Bool , genre :: String , instrument :: Instrument , publishedBy :: String , skillLevel :: Either Integer [(Instrument, Integer)] , sku :: String , slow :: Double , title :: String , version :: Integer , writtenBy :: String } deriving (Eq, Ord, Show, Read) instance PL.PropertyListItem Info where fromPropertyList pl = PL.fromPlDict pl >>= \dict -> Info <$> (Map.lookup "album" dict >>= PL.fromPlString) <*> (Map.lookup "artist" dict >>= PL.fromPlString) <*> (Map.lookup "bpm" dict >>= PL.fromPlString) <*> (Map.lookup "copyright" dict >>= PL.fromPlString) <*> (Map.lookup "countInBeats" dict >>= PL.fromPlInt ) <*> (Map.lookup "courtesyOf" dict >>= PL.fromPlString) <*> (Map.lookup "demo" dict >>= fromPlEnum ) <*> (Map.lookup "explicit" dict >>= fromPlEnum ) <*> (Map.lookup "genre" dict >>= PL.fromPlString) <*> (Map.lookup "instrument" dict >>= fromPlEnum ) <*> (Map.lookup "publishedBy" dict >>= PL.fromPlString) <*> (Map.lookup "skillLevel" dict >>= fromPlSkills) <*> (Map.lookup "sku" dict >>= PL.fromPlString) <*> (Map.lookup "slow" dict >>= PL.fromPlReal ) <*> (Map.lookup "title" dict >>= PL.fromPlString) <*> (Map.lookup "version" dict >>= PL.fromPlInt ) <*> (Map.lookup "writtenBy" dict >>= PL.fromPlString) toPropertyList info = PL.plDict $ Map.fromList [ ("album" , PL.plString $ album info) , ("artist" , PL.plString $ artist info) , ("bpm" , PL.plString $ bpm info) , ("copyright" , PL.plString $ copyright info) , ("countInBeats", PL.plInt $ countInBeats info) , ("courtesyOf" , PL.plString $ courtesyOf info) , ("demo" , plEnum $ demo info) , ("explicit" , plEnum $ explicit info) , ("genre" , PL.plString $ genre info) , ("instrument" , plEnum $ instrument info) , ("publishedBy" , PL.plString $ publishedBy info) , ("skillLevel" , plSkills $ skillLevel info) , ("sku" , PL.plString $ sku info) , ("slow" , PL.plReal $ slow info) , ("title" , PL.plString $ title info) , ("version" , PL.plInt $ version info) , ("writtenBy" , PL.plString $ writtenBy info) ] fromPlEnum :: (Enum a) => PL.PropertyList -> Maybe a fromPlEnum pl = toEnum . fromIntegral <$> PL.fromPlInt pl plEnum :: (Enum a) => a -> PL.PropertyList plEnum = PL.plInt . fromIntegral . fromEnum fromPlSkills :: PL.PropertyList -> Maybe (Either Integer [(Instrument, Integer)]) fromPlSkills pl = case (PL.fromPlInt pl, PL.fromPlDict pl) of (Nothing, Nothing) -> Nothing (Just i , _ ) -> Just $ Left i (_ , Just d ) -> let getSkill :: (String, PL.PropertyList) -> Maybe (Instrument, Integer) getSkill (x, y) = liftA2 (,) (readMaybe $ capitalize x) (PL.fromPlInt y) capitalize "" = "" capitalize (c:cs) = toUpper c : map toLower cs in fmap Right $ mapM getSkill $ Map.toList d plSkills :: Either Integer [(Instrument, Integer)] -> PL.PropertyList plSkills (Left i ) = PL.plInt i plSkills (Right sl) = PL.plDict $ Map.fromList $ map (map toLower . show *** PL.plInt) sl loadInfo :: FilePath -> IO (Maybe Info) loadInfo dir = PL.fromPropertyList <$> readXmlPropertyListFromFile' (dir "info.plist") data Track = Track { trackClass :: String , identifier :: String , scoreSystemHeight :: Maybe Integer , scoreSystemInterval :: Maybe Integer , trackTitle :: Maybe String } deriving (Eq, Ord, Show, Read) instance PL.PropertyListItem Track where toPropertyList t = PL.plDict $ Map.fromList $ catMaybes [ Just ("class" , PL.plString $ trackClass t) , Just ("identifier" , PL.plString $ identifier t) , (\i -> ("scoreSystemHeight" , PL.plInt i)) <$> scoreSystemHeight t , (\i -> ("scoreSystemInterval", PL.plInt i)) <$> scoreSystemInterval t , (\s -> ("title" , PL.plString s)) <$> trackTitle t ] fromPropertyList pl = PL.fromPlDict pl >>= \d -> Track <$> (Map.lookup "class" d >>= PL.fromPlString) <*> (Map.lookup "identifier" d >>= PL.fromPlString) <*> Just (Map.lookup "scoreSystemHeight" d >>= PL.fromPlInt ) <*> Just (Map.lookup "scoreSystemInterval" d >>= PL.fromPlInt ) <*> Just (Map.lookup "title" d >>= PL.fromPlString) loadTracks :: FilePath -> IO (Maybe [Track]) loadTracks dir = PL.listFromPropertyList <$> readXmlPropertyListFromFile' (dir "tracks.plist") -- | Reads strictly so as not to exhaust our allowed open files. readXmlPropertyListFromFile' :: FilePath -> IO PL.PropertyList readXmlPropertyListFromFile' f = do str <- readFile f _ <- evaluate $ length str either fail return $ PL.readXmlPropertyList str -- | Tries to find the top-level Jammit library directory on Windows or -- Mac OS X. findJammitDir :: IO (Maybe FilePath) findJammitDir = case Info.os of "mingw32" -> do var <- lookupEnv "LocalAppData" case var of Just local -> jammitIn local Nothing -> return Nothing "darwin" -> do home <- Dir.getHomeDirectory jammitIn $ home "Library" "Application Support" _ -> return Nothing where jammitIn dir = do let jmt = dir "Jammit" b <- Dir.doesDirectoryExist jmt return $ guard b >> Just jmt -- | Gets the contents of a directory without the @.@ and @..@ special paths, -- and adds the directory to the front of all the names to make absolute paths. lsAbsolute :: FilePath -> IO [FilePath] lsAbsolute d = map (d ) . filter (`notElem` [".", ".."]) <$> Dir.getDirectoryContents d -- | Searches a directory and all subdirectories for folders containing a Jammit -- info file. songSubdirs :: FilePath -> IO [FilePath] songSubdirs dir = do isSong <- Dir.doesFileExist $ dir "info.plist" let here = [dir | isSong] subdirs <- lsAbsolute dir >>= filterM Dir.doesDirectoryExist (here ++) . concat <$> mapM songSubdirs subdirs