netease-fm-1.2.2: lib/FM/Player.hs
{-# LANGUAGE RecordWildCards, FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module FM.Player (
MonadPlayer, runPlayer
, PlayerState (..)
, isStopped
, Player (..)
, initPlayer
, play
, pause
, resume
, stop
, updateVolume
) where
import Control.Concurrent (ThreadId, forkFinally, killThread, myThreadId, throwTo)
import Control.Concurrent.Async (async, cancel, poll)
import Control.Concurrent.STM.Lock
import Control.Concurrent.STM.TMVar
import Control.Concurrent.STM.TVar
import Control.Exception (asyncExceptionFromException, AsyncException (..))
import Control.Monad.IO.Class
import Control.Monad.Reader
import Control.Monad.STM (atomically)
import Data.Maybe (isJust, fromJust)
import System.IO
import System.Process (ProcessHandle, runInteractiveProcess, waitForProcess, terminateProcess)
import qualified FM.Song as Song
newtype MonadPlayer a = MonadPlayer (ReaderT Player IO a)
deriving (Functor, Applicative, Monad, MonadIO, MonadReader Player)
runPlayer :: Player -> MonadPlayer a -> IO a
runPlayer player (MonadPlayer m) = runReaderT m player
data PlayerState = Playing Song.Song
| Paused Song.Song
| Stopped
isStopped :: PlayerState -> Bool
isStopped Stopped = True
isStopped _ = False
data PlayerContext = PlayerContext {
inHandle :: Handle
, outHandle :: Handle
, errHandle :: Handle
, processHandle :: ProcessHandle
, parentThreadId :: ThreadId
, childThreadId :: ThreadId
}
data Player = Player {
mpg123Context :: TMVar PlayerContext
, playerState :: TVar PlayerState
, playerVolume :: Int
, playerMuted :: Bool
, playerLock :: Lock
}
initPlayer :: (MonadIO m) => m Player
initPlayer = liftIO $ do
mpg123Context <- newEmptyTMVarIO
playerState <- newTVarIO Stopped
let playerVolume = 100
let playerMuted = False
playerLock <- newLockIO
return Player {..}
type FetchUrl = Song.Song -> IO (Maybe String)
type FetchLyrics = Song.Song -> IO Song.Lyrics
type Notify a = a -> IO ()
play :: (MonadIO m, MonadReader Player m) => Song.Song -> FetchUrl -> FetchLyrics -> Notify () -> Notify Bool -> Notify (Double, Double) -> Notify String -> m ()
play song@Song.Song {..} fetchUrl fetchLyrics onBegin onTerminate onProgress onLyrics = do
Player {..} <- ask
(inHandle, outHandle, errHandle, processHandle) <- liftIO $ runInteractiveProcess "mpg123" ["-R"] Nothing Nothing
let initHandle h = liftIO $ do
hSetBinaryMode h False
hSetBuffering h LineBuffering
initHandle inHandle
initHandle outHandle
lyricsAsync <- liftIO $ async $ fetchLyrics song
let
notifyLyrics :: Song.Lyrics -> Double -> Notify String -> IO Song.Lyrics
notifyLyrics (Song.Lyrics lyrics) time notify = Song.Lyrics <$>
case span (\l -> fst l <= time) lyrics of
([], restLyrics) -> return restLyrics
(curLyrics, restLyrics) -> do
notify $ snd (last curLyrics)
return restLyrics
loop :: Bool -> (Double, Double, Maybe Song.Lyrics) -> String -> IO ()
loop _ (len, cur, _) "@P 0" = onProgress (len, cur)
loop True (0, 0, Nothing) "@P 2" = do
onBegin ()
continue $ loop True (0, 0, Nothing)
loop True _ out@('@':'F':_) = do
let len = read (words out !! 4)
onProgress (len, 0)
continue $ loop False (len, 0, Nothing)
loop False (len, cur, lyrics) out@('@':'F':_) = do
let cur' = read (words out !! 3)
when (floor cur' /= floor cur) $ onProgress (len, cur')
lyrics' <- case lyrics of
Just lyrics -> Just <$> notifyLyrics lyrics cur' onLyrics
Nothing -> do
lyrics <- poll lyricsAsync
case lyrics of
Just (Right lyrics) -> Just <$> notifyLyrics lyrics cur' onLyrics
_ -> return Nothing
continue $ loop False (len, cur', lyrics')
loop b ctx ('@':_) = continue $ loop b ctx
loop _ _ _ = return () -- encounter mpg123 internal error.
continue :: (String -> IO ()) -> IO ()
continue cont = do
isEOF <- hIsEOF outHandle
unless isEOF $ cont =<< hGetLine outHandle
playerThread = do
atomically $ waitLock playerLock Acquired
url <- fetchUrl song
when (isJust url) $ do
hPutStrLn inHandle $ "V " ++ show (if playerMuted then 0 else playerVolume)
hPutStrLn inHandle $ "L " ++ fromJust url
loop True (0, 0, Nothing) =<< hGetLine outHandle
cleanUp e = do
PlayerContext {..} <- atomically $ do
writeTVar playerState Stopped
takeTMVar mpg123Context
cancel lyricsAsync
forM_ [inHandle, outHandle, errHandle] $ \h -> hClose h
terminateProcess processHandle
waitForProcess processHandle
case e of
Right _ -> onTerminate True
Left e -> case asyncExceptionFromException e of
Just ThreadKilled -> onTerminate False
_ -> throwTo parentThreadId e
atomically $ releaseLock playerLock
parentThreadId <- liftIO myThreadId
childThreadId <- liftIO $ forkFinally playerThread cleanUp
void $ liftIO $ atomically $ do
putTMVar mpg123Context PlayerContext {..}
writeTVar playerState (Playing song)
acquireLock playerLock
-- | pause, resume and stop are idempotent.
pause :: (MonadIO m, MonadReader Player m) => m ()
pause = do
Player {..} <- ask
inHandle <- liftIO $ atomically $ do
state <- readTVar playerState
case state of
Playing s -> do
PlayerContext {..} <- readTMVar mpg123Context
writeTVar playerState (Paused s)
return $ Just inHandle
_ -> return Nothing
when (isJust inHandle) $ liftIO $ hPutStrLn (fromJust inHandle) "P"
resume :: (MonadIO m, MonadReader Player m) => m ()
resume = do
Player {..} <- ask
inHandle <- liftIO $ atomically $ do
state <- readTVar playerState
case state of
Paused s -> do
PlayerContext {..} <- readTMVar mpg123Context
writeTVar playerState (Playing s)
return $ Just inHandle
_ -> return Nothing
when (isJust inHandle) $ liftIO $ hPutStrLn (fromJust inHandle) "P"
stop :: (MonadIO m, MonadReader Player m) => m ()
stop = do
Player {..} <- ask
childThreadId <- liftIO $ atomically $ do
state <- readTVar playerState
case state of
Stopped -> return Nothing
_ -> do
PlayerContext {..} <- readTMVar mpg123Context
return $ Just childThreadId
when (isJust childThreadId) $ liftIO $ killThread (fromJust childThreadId)
liftIO $ atomically $ waitLock playerLock Released
updateVolume :: (MonadIO m, MonadReader Player m) => m ()
updateVolume = do
Player {..} <- ask
h <- fmap inHandle <$> liftIO (atomically $ tryReadTMVar mpg123Context)
let v = if playerMuted then 0 else playerVolume
liftIO $ maybe (return ()) (\h -> hPutStrLn h $ "V " ++ show v) h