module Main where import qualified Sound.ALSA.Sequencer as SndSeq import qualified Sound.ALSA.SequencerFFI as SndSeqFFI import qualified Sound.MIDI.File as MIDIFile import qualified Sound.MIDI.File.Load as MIDILoad import qualified Data.EventList.Relative.TimeBody as EventList import qualified Numeric.NonNegative.Wrapper as NonNeg import Control.Concurrent (threadDelay) import Control.Exception (bracket) -- import Control.Monad.Error () -- Monad instance for Either import Control.Monad (when) import System.Console.GetOpt (getOpt, ArgOrder(..), OptDescr(..), ArgDescr(..), usageInfo) import System.Environment (getArgs) mkEvent :: SndSeqFFI.Queue -> SndSeqFFI.Address -> SndSeqFFI.Address -> MIDIFile.Event -> SndSeqFFI.Event mkEvent queue srcAddress dstAddress ev = let (typ, dat) = SndSeq.eventFromMIDIEvent queue ev in SndSeqFFI.Event { SndSeqFFI.typ = typ, SndSeqFFI.tag = 0, SndSeqFFI.queue = SndSeqFFI.queueDirect, SndSeqFFI.time = SndSeqFFI.TimeStampTick (SndSeqFFI.TickTime 0), SndSeqFFI.timeMode = SndSeqFFI.TimeModeRelative, SndSeqFFI.eventLength = SndSeqFFI.EventLengthFixed, SndSeqFFI.priority = SndSeqFFI.PriorityNormal, SndSeqFFI.source = srcAddress, SndSeqFFI.dest = dstAddress, SndSeqFFI.eventData = dat} play :: SndSeqFFI.Address -> FilePath -> IO () play dstAddress fileName = do MIDIFile.Cons typ _div tracks <- MIDILoad.fromFile fileName -- print midi let track = case typ of MIDIFile.Parallel -> foldl EventList.merge EventList.empty tracks MIDIFile.Serial -> EventList.concat tracks MIDIFile.Mixed -> EventList.concat tracks bracket (SndSeq.createClient SndSeqFFI.openOutput "midi player") SndSeq.deleteClient $ \ client -> SndSeq.withNamedQueue client "playmidi out queue" $ \ queue -> do putStrLn "playing" port <- SndSeq.createOutputPort client "player output" let srcAddress = SndSeq.portAddress client port EventList.mapM_ (threadDelay . (5000*) . fromInteger . NonNeg.toNumber) (\ev -> (SndSeq.sendPlainEvent client $ let seqEv = mkEvent queue srcAddress dstAddress ev in case ev of MIDIFile.MetaEvent (MIDIFile.SetTempo _) -> seqEv{SndSeqFFI.dest = SndSeqFFI.addressTimer} _ -> seqEv) >> SndSeq.drainOutput client) track parseAddress :: String -> Either String SndSeqFFI.Address parseAddress str0 = let addrs = do (client, ':':str1) <- reads str0 (port, []) <- reads str1 return $ SndSeq.numAddressEither client port in case addrs of [addr] -> addr _ -> Left "address must be a string like '128:0'" {- parseAddressNaive :: String -> SndSeqFFI.Address parseAddressNaive str = let (client, ':':port) = break (':'==) str in SndSeqFFI.Address (read client) (read port) -} data Flags = Flags { optHelp :: Bool, optPort :: Either String SndSeqFFI.Address } options :: [OptDescr (Flags -> Flags)] options = Option ['h'] ["help"] (NoArg (\ flags -> flags{optHelp = True})) "show options" : Option ['p'] ["port"] (ReqArg (\str flags -> flags{optPort = parseAddress str}) "PORT") "destination PORT" : [] main :: IO () main = do argv <- getArgs let (opts, files, errors) = getOpt RequireOrder options argv when (not (null errors)) (ioError . userError . concat $ errors) let flags = foldr ($) (Flags {optHelp = False, optPort = Left "no port specified"}) opts when (optHelp flags) (putStrLn (usageInfo "Usage: playmidi [OPTIONS] FILES ..." options)) port <- either (ioError . userError) return (optPort flags) mapM_ (play port) files