packages feed

midi-utils-0.1.0.0: src/Sound/MIDI/Utils/Run.hs

module Sound.MIDI.Utils.Run
  ( playMidi
  , playMidiFile
  , listAvailableUsers
  , User(userPort, userClientName, userPortName)
  , Port, mkPort
  ) where


import Control.Monad ((>=>))
import Data.Function (on)
import System.Process
import qualified System.IO as SIO
import qualified Data.ByteString as BS

import qualified Text.Parsec as Ps
import qualified Text.Parsec.Token as Ps

import qualified Sound.MIDI.File.Save as Save
import qualified Sound.MIDI.File.Load as Load
import Sound.MIDI.File (T)


newtype Port = Port { getPort :: (Int, Int) }
  deriving (Eq)


mkPort :: (Int, Int) -> Port
mkPort = Port


instance Show Port where
  show (Port (c, p)) = concat [show c, ":", show p]


data User = User { userPort :: Port
                 , userClientName :: String
                 , userPortName :: String
                 } deriving (Show, Eq)


data PlayMidi = PlayMidi { midiPort :: Port, midiFile :: FilePath }


toProcess :: PlayMidi -> CreateProcess
toProcess (PlayMidi { midiPort = p, midiFile = f }) = shell $ concat ["aplaymidi", " -p ", show p, " ", f]


-- | Play MIDI data to the specified ALSA sequencer port.
playMidi :: Port -> T -> IO ()
playMidi p m = do
  (b1,b2) <- createPipe
  SIO.hSetBinaryMode b2 True
  BS.hPutStr b2 midiBS
  createProcess ((midiProc "-") { std_in=UseHandle b1 })
  pure ()
  where midiBS = BS.pack $ Save.toByteList m
        midiProc f = toProcess (PlayMidi p f)


playMidiFile :: Port -> FilePath -> IO ()
playMidiFile p = Load.fromFile >=> playMidi p


availableUsers :: IO (Either String [User])
availableUsers = (checkParse <$> runParse <$> usersRaw)
  where usersRaw = readProcess "aplaymidi" ["-l"] ""
        parsePort = ((curry Port `on` fromInteger) <$> decimal <*> (Ps.char ':' *> decimal))
        parseCname = Ps.manyTill Ps.anyChar (Ps.try (Ps.space *> Ps.space))
        parsePname = Ps.manyTill Ps.anyChar (Ps.try Ps.newline)
        parseUser = User
                    <$> Ps.between Ps.spaces Ps.spaces parsePort
                    <*> (parseCname <* Ps.spaces) <*> parsePname
        parseUsers = Ps.manyTill Ps.anyChar Ps.newline *> Ps.many parseUser
        runParse = Ps.parse parseUsers ""
        checkParse (Left _) = Left "Failed to parse users"
        checkParse (Right r) = Right r
        decimal = read <$> Ps.many1 Ps.digit


listAvailableUsers :: IO [User]
listAvailableUsers = either (const []) id <$> availableUsers