{- | This program receives MIDI commands and controls MPlayer accordingly. The program listens to MIDI channel 0. When the program receives AllNotesOff or AllNotesOn it pauses MPlayer. If the program receives a MIDI-CC 0 with value 0, then it seeks a certain position in the movie and triggers playback. You can set the seek time by sending the following controller changes: * CC 1: 100th seconds * CC 2: seconds * CC 3: minutes * CC 4: hours -} module Main where import qualified Sound.MIDI.Message.Channel as ChannelMsg import qualified Sound.MIDI.Message.Channel.Voice as VoiceMsg import qualified Sound.MIDI.Message.Channel.Mode as ModeMsg import qualified Sound.MIDI.ALSA.Check as Check import qualified Sound.ALSA.Sequencer.Client as Client import qualified Sound.ALSA.Sequencer.Port as Port import qualified Sound.ALSA.Sequencer.Event as Event import qualified Sound.ALSA.Sequencer as SndSeq import qualified Sound.ALSA.Exception as AlsaExc import qualified Control.Monad.Trans.State as MS import Control.Monad.IO.Class (MonadIO, liftIO) import Control.Monad (when, forever, ) import Data.Foldable (forM_, ) import qualified System.Directory as Dir import qualified System.Posix.Files as File import qualified System.IO as IO import qualified System.Environment as Env import qualified Control.Exception as Exc import qualified Text.Printf as Printf import Option.Utility (exitFailureMsg) defltPipeName :: FilePath defltPipeName = "/tmp/mppipe" seqName :: String seqName = "MPlayer control" channel :: ChannelMsg.Channel channel = ChannelMsg.toChannel 0 commands :: MonadIO io => IO.Handle -> [String] -> io () commands pipe = liftIO . mapM_ (\cmd -> putStrLn cmd >> IO.hPutStrLn pipe cmd) data Time = Time {hours, minutes, seconds, fracSecs :: Int} deriving (Show) main :: IO () main = do args <- Env.getArgs case args of _:_:_ -> exitFailureMsg "too many arguments" [pipeName] -> process pipeName [] -> Exc.bracket_ (File.createNamedPipe defltPipeName 0o644 >> putStrLn ("Created pipe: " ++ defltPipeName)) (Dir.removeFile defltPipeName) (process defltPipeName) process :: FilePath -> IO () process pipeName = (do pipe <- IO.openFile pipeName IO.WriteMode IO.hSetBuffering pipe IO.LineBuffering putStrLn $ "Start MPlayer like this:" putStrLn $ "mplayer -input file=" ++ pipeName putStrLn "" SndSeq.with SndSeq.defaultName SndSeq.Block $ \h -> do Client.setName (h :: SndSeq.T SndSeq.InputMode) seqName putStrLn $ "Created sequencer: " ++ seqName putStrLn $ "Start the live sequencer like this:" putStrLn $ "live-sequencer --new-out-port control --connect-to " ++ show seqName putStrLn "" putStrLn $ "or connect with the live sequencer like this:" putStrLn $ "aconnect Rewrite:1 " ++ show seqName putStrLn "" Port.withSimple h "control" (Port.caps [Port.capWrite, Port.capSubsWrite]) Port.typeMidiGeneric $ \ _p1 -> do flip MS.evalStateT (Time 0 0 0 0) $ forever $ do ev <- liftIO $ Event.input h -- print ev forM_ (Check.mode channel ev) $ \mode -> case mode of {- 'pause' toggles the playing mode To make sure, that 'pause' stops, we have to run the movie with 'seek' first. -} ModeMsg.AllNotesOff -> commands pipe ["seek 0", "pause"] ModeMsg.AllSoundOff -> commands pipe ["seek 0", "pause"] _ -> return () forM_ (Check.anyController channel ev) $ \(ctrl, val) -> do let setTime update cc = when (ctrl == VoiceMsg.toController cc) $ MS.modify update setTime (\t -> t {fracSecs = val}) 1 setTime (\t -> t {seconds = val}) 2 setTime (\t -> t {minutes = val}) 3 setTime (\t -> t {hours = val}) 4 when (ctrl == VoiceMsg.toController 0 && val == 0) $ do (Time hr m s f) <- MS.get commands pipe [Printf.printf "seek %d.%02d 2" (hr*3600 + m*60 + s) f] ) `AlsaExc.catch` \e -> putStrLn $ "alsa_exception: " ++ AlsaExc.show e