packages feed

hosc-0.21: Sound/Osc/Text.hs

-- | A simple and unambigous text encoding for Osc.
module Sound.Osc.Text where

import Control.Monad {- base -}
import Data.Char {- base -}
import Numeric {- base -}
import Text.Printf {- base -}

import qualified Safe {- safe -}

import qualified Text.ParserCombinators.Parsec as P {- parsec -}

import Sound.Osc.Datum {- hosc -}
import Sound.Osc.Packet {- hosc3 -}
import qualified Sound.Osc.Time as Time {- hosc3 -}

-- | Precision value for floating point numbers.
type FpPrecision = Maybe Int

{- | Variant of 'showFFloat' that deletes trailing zeros.

>>> map (showFloatWithPrecision (Just 4)) [1, 2.0, pi]
["1.0","2.0","3.1416"]
-}
showFloatWithPrecision :: RealFloat n => FpPrecision -> n -> String
showFloatWithPrecision p n =
  let s = showFFloat p n ""
      s' = dropWhile (== '0') (reverse s)
  in case s' of
      '.' : _ -> reverse ('0' : s')
      _ -> reverse s'

{- | Hex encoded byte sequence.

>>> showBytes [0, 15, 16, 144, 255]
"000f1090ff"
-}
showBytes :: [Int] -> String
showBytes = concatMap (printf "%02x")

{- | Escape whites space (space, tab, newline) and the escape character (backslash).

>>> map escapeString ["str", "str ", "st r", "s\tr", "s\\tr", "\nstr"]
["str","str\\ ","st\\ r","s\\\tr","s\\\\tr","\\\nstr"]
-}
escapeString :: String -> String
escapeString txt =
  case txt of
    [] -> []
    c : txt' -> if c `elem` "\\\t\n " then '\\' : c : escapeString txt' else c : escapeString txt'

{- | Printer for Datum.

>>> let aDatumSeq = [Int32 1,Float 1.2,string "str",midi (0,0x90,0x40,0x60),blob [12,16], TimeStamp 100.0]
>>> map (showDatum (Just 5)) aDatumSeq
["1","1.2","str","00904060","0c10","429496729600"]
-}
showDatum :: FpPrecision -> Datum -> String
showDatum p d =
  case d of
    Int32 n -> show n
    Int64 n -> show n
    Float n -> showFloatWithPrecision p n
    Double n -> showFloatWithPrecision p n
    AsciiString s -> escapeString (ascii_to_string s)
    Blob s -> showBytes (blob_unpack_int s)
    TimeStamp t -> show (Time.ntpr_to_ntpi t)
    Midi m -> showBytes (midi_unpack_int m)

{- | Printer for Message.

>>> let aMessage = Message "/addr" [Int32 1, Int64 2, Float 3, Double 4, string "five", blob [6, 7], midi (8, 9, 10, 11)]
>>> showMessage (Just 4) aMessage
"/addr ,ihfdsbm 1 2 3.0 4.0 five 0607 08090a0b"

>>> let aMessageSeq = [Message "/c_set" [Int32 1, Float 2.3], Message "/s_new" [string "sine", Int32 (-1), Int32 1, Int32 1]]
>>> map (showMessage (Just 4)) aMessageSeq
["/c_set ,if 1 2.3","/s_new ,siii sine -1 1 1"]
-}
showMessage :: FpPrecision -> Message -> String
showMessage precision aMessage =
  unwords
    [ messageAddress aMessage
    , messageSignature aMessage
    , unwords (map (showDatum precision) (messageDatum aMessage))
    ]

{- | Printer for Bundle

>>> let aBundle = Bundle 1 [Message "/c_set" [Int32 1, Float 2.3, Int64 4, Double 5.6], Message "/memset" [string "addr", blob [7, 8]]]
>>> showBundle (Just 4) aBundle
"#bundle 4294967296 2 /c_set ,ifhd 1 2.3 4 5.6 /memset ,sb addr 0708"
-}
showBundle :: FpPrecision -> BundleOf Message -> String
showBundle precision aBundle =
  let messages = bundleMessages aBundle
  in unwords
      [ "#bundle"
      , show (Time.ntpr_to_ntpi (bundleTime aBundle))
      , show (length messages)
      , unwords (map (showMessage precision) messages)
      ]

-- | Printer for Packet.
showPacket :: FpPrecision -> PacketOf Message -> String
showPacket precision = at_packet (showMessage precision) (showBundle precision)

-- * Parser

-- | A character parser with no user state.
type P a = P.GenParser Char () a

-- | Run p then q, returning result of p.
(>>~) :: Monad m => m t -> m u -> m t
p >>~ q = p >>= \x -> q >> return x

-- | /p/ as lexeme, i.e. consuming any trailing white space.
lexemeP :: P t -> P t
lexemeP p = p >>~ P.many P.space

-- | Any non-space character.  Allow escaped space.
stringCharP :: P Char
stringCharP = (P.char '\\' >> P.space) P.<|> P.satisfy (\c -> not (isSpace c))

-- | Parser for string.
stringP :: P String
stringP = lexemeP (P.many1 stringCharP)

-- | Parser for Osc address.
oscAddressP :: P String
oscAddressP = do
  forwardSlash <- P.char '/'
  address <- stringP
  return (forwardSlash : address)

-- | Parser for Osc signature.
oscSignatureP :: P String
oscSignatureP =
  lexemeP
    ( do
        comma <- P.char ','
        types <- P.many1 (P.oneOf "ifsbhtdm") -- 1.0 = ifsb 2.0 = htdm
        return (comma : types)
    )

-- | Parser for decimal digit.
digitP :: P Char
digitP = P.oneOf "0123456789"

allowNegativeP :: Num n => P n -> P n
allowNegativeP p = do
  let optionMaybe x = P.option Nothing (liftM Just x) -- hugs...
  maybeNegative <- optionMaybe (P.char '-')
  number <- p
  return (maybe number (const (negate number)) maybeNegative)

-- | Parser for non-negative integer.
nonNegativeIntegerP :: (Integral n, Read n) => P n
nonNegativeIntegerP = lexemeP (fmap read (P.many1 digitP))

-- | Parser for integer.
integerP :: (Integral n, Read n) => P n
integerP = allowNegativeP nonNegativeIntegerP

-- | Parser for non-negative float.
nonNegativeFloatP :: (Fractional n, Read n) => P n
nonNegativeFloatP =
  lexemeP
    ( do
        integerPart <- P.many1 digitP
        _ <- P.char '.'
        fractionalPart <- P.many1 digitP
        return (read (concat [integerPart, ".", fractionalPart]))
    )

-- | Parser for non-negative float.
floatP :: (Fractional n, Read n) => P n
floatP = allowNegativeP nonNegativeFloatP

-- | Parser for hexadecimal digit.
hexdigitP :: P Char
hexdigitP = P.oneOf "0123456789abcdef"

-- | Byte parser.
byteP :: (Integral n, Read n) => P n
byteP = do
  c1 <- hexdigitP
  c2 <- hexdigitP
  case readHex [c1, c2] of
    [(r, "")] -> return r
    _ -> error "byteP?"

-- | Byte sequence parser.
byteSeqP :: (Integral n, Read n) => P [n]
byteSeqP = lexemeP (P.many1 byteP)

-- | Datum parser.
datumP :: Char -> P Datum
datumP typeChar = do
  case typeChar of
    'i' -> fmap Int32 integerP
    'f' -> fmap Float floatP
    's' -> fmap string stringP
    'b' -> fmap blob byteSeqP
    'h' -> fmap Int64 integerP
    'd' -> fmap Double floatP
    'm' -> fmap (Midi . midi_pack) (replicateM 4 byteP)
    't' -> fmap (TimeStamp . Time.ntpi_to_ntpr) integerP
    _ -> error "datumP: type?"

-- | Message parser.
messageP :: P Message
messageP = do
  address <- oscAddressP
  typeSignature <- oscSignatureP
  datum <- mapM datumP (Safe.tailNote "messageP" typeSignature)
  return (Message address datum)

-- | Bundle tag parser.
bundleTagP :: P String
bundleTagP = lexemeP (P.string "#bundle")

-- | Bundle parser.
bundleP :: P (BundleOf Message)
bundleP = do
  _ <- bundleTagP
  timestamp <- fmap Time.ntpi_to_ntpr integerP
  messageCount <- integerP
  messages <- replicateM messageCount messageP
  return (Bundle timestamp messages)

-- | Packet parser.
packetP :: P (PacketOf Message)
packetP = (fmap Packet_Bundle bundleP) P.<|> (fmap Packet_Message messageP)

-- | Run parser.
runP :: P t -> String -> t
runP p txt =
  case P.parse p "" txt of
    Left err -> error (show err)
    Right r -> r

{- | Run datum parser.

>>> parseDatum 'i' "-1" == Int32 (-1)
True

>>> parseDatum 'f' "-2.3" == Float (-2.3)
True
-}
parseDatum :: Char -> String -> Datum
parseDatum typ = runP (datumP typ)

{- | Run message parser.

>>> let aMessageSeq = [Message "/c_set" [Int32 1, Float 2.3, Int64 4, Double 5.6], Message "/memset" [string "addr", blob [7, 8]]]
>>> map (parseMessage . showMessage (Just 4)) aMessageSeq  == aMessageSeq
True
-}
parseMessage :: String -> Message
parseMessage = runP messageP

{- | Run bundle parser.

>>> let aBundle = Bundle 1 [Message "/c_set" [Int32 1, Float 2.3, Int64 4, Double 5.6], Message "/memset" [string "addr", blob [7, 8]]]
>>> parseBundle (showBundle (Just 4) aBundle) == aBundle
True
-}
parseBundle :: String -> BundleOf Message
parseBundle = runP bundleP

{- | Run packet parser.

>>> let aPacket = Packet_Bundle (Bundle 1 [Message "/c_set" [Int32 1, Float 2.3, Int64 4, Double 5.6], Message "/memset" [string "addr", blob [7, 8]]])
>>> parsePacket (showPacket (Just 4) aPacket) == aPacket
True
-}
parsePacket :: String -> PacketOf Message
parsePacket = runP packetP