packages feed

alsa-midi-0.3: examples/PlayMIDI.hs

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