hmp3-ng-2.14.3: Lexer.hs
--
-- Copyright (c) 2005-2008 Don Stewart - http://www.cse.unsw.edu.au/~dons
-- Copyright (c) 2008, 2019, 2020 Galen Huntington
--
-- This program is free software; you can redistribute it and/or
-- modify it under the terms of the GNU General Public License as
-- published by the Free Software Foundation; either version 2 of
-- the License, or (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-- General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program; if not, write to the Free Software
-- Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
-- 02111-1307, USA.
--
-- Lexer for mpg321 messages
module Lexer ( parser ) where
import Base
import Syntax (Msg(..),Status(..),Frame(..),Info(..),Id3(..),File(..),Tag(..))
import FastIO (FiltHandle(..), checkF, getPacket, dropSpaceEnd)
import qualified Data.ByteString.Char8 as P
import qualified Data.ByteString.UTF8 as UTF8
import Control.Monad.Except
import Control.Monad.Trans (lift)
------------------------------------------------------------------------
pSafeHead :: ByteString -> Char
pSafeHead s = if P.null s then ' ' else P.head s
readPS :: ByteString -> Int
readPS = fst . fromJust . P.readInt
doP :: ByteString -> Msg
doP s = S case pSafeHead s of
'0' -> Stopped
'1' -> Paused
'2' -> Playing
'3' -> Stopped -- used by mpg321
_ -> Playing
-- _ -> error "Invalid Status"
-- Frame decoding status updates (once per frame).
doF :: ByteString -> Msg
doF s = R Frame {
currentFrame = readPS f0
, framesLeft = readPS f1
, currentTime = read . P.unpack $ f2
, timeLeft = max 0 . read . P.unpack $ f3
}
where
f0 : f1 : f2 : f3 : _ = P.split ' ' s
-- Outputs information about the mp3 file after loading.
doS :: ByteString -> Msg
doS s = let fs = P.split ' ' s
in I Info {
{-
version = fs !! 0
, layer = read . P.unpack $ fs !! 1
, sampleRate = read . P.unpack $ fs !! 2
, playMode = fs !! 3
, modeExtns = read . P.unpack $ fs !! 4
, bytesPerFrame = read . P.unpack $ fs !! 5
, channelCount = read . P.unpack $ fs !! 6
, copyrighted = toEnum (read $ P.unpack (fs !! 7))
, checksummed = toEnum (read $ P.unpack (fs !! 8))
, emphasis = read $ P.unpack $ fs !! 9
, bitrate = read $ P.unpack $ fs !! 10
, extension = read $ P.unpack $ fs !! 11
-}
userinfo = mconcat
["mpeg "
,fs !! 0
," "
,fs !! 10
,"kbit/s "
,(P.pack . show) (readPS (fs !! 2) `div` 1000 :: Int)
,"kHz"]
}
-- Track info if ID fields are in the file, otherwise file name.
-- 30 chars per field?
doI :: ByteString -> Msg
doI s = let f = dropSpaceEnd . P.dropWhile isSpace $ s
in case P.take 4 f of
cs | cs == "ID3:" -> F . File $
let ttl = toId id3 . splitUp . P.drop 4 $ f
-- mpg321 sometimes returns null titles
in if P.null (id3title ttl) then Left f else Right ttl
| otherwise -> F . File . Left $ f
where
-- a default
id3 :: Id3
id3 = Id3 "" "" "" ""
-- break the ID3 string up
splitUp :: ByteString -> [ByteString]
splitUp f
| f == P.empty = []
| otherwise
= let (a,xs) = P.splitAt 30 f -- we expect it to be
xs' = splitUp xs
in a : xs'
-- and some ugly code:
toId :: Id3 -> [ByteString] -> Id3
toId i ls =
let arg n = normalise $ ls !! n
j = case length ls of
0 -> i
1 -> i { id3title = arg 0 }
2 -> i { id3title = arg 0
, id3artist = arg 1 }
_ -> i { id3title = arg 0
, id3artist = arg 1
, id3album = arg 2 }
in j { id3str =
mconcat $ intersperse " : " $ filter (not . P.null)
[id3artist j, id3album j, id3title j] }
-- strip spaces, and decide if UTF-8 or ISO-8859-1
normalise :: ByteString -> ByteString
normalise raw =
let bs = P.dropWhile isSpace . dropSpaceEnd $ raw
in if UTF8.replacement_char `elem` UTF8.toString bs
then UTF8.fromString $ P.unpack bs
else bs
------------------------------------------------------------------------
parser :: FiltHandle -> IO (Either (Maybe String) Msg)
parser h = runExceptT do
x <- lift $ getPacket h
-- bad packets are generally just \n in ID3 (and not of interest anyway)
let skip = throwError Nothing
when (P.length x < 3) skip
let (pre, m) = P.splitAt 3 x
at : code : sp : _ = P.unpack pre
when (at /= '@' || sp /= ' ') skip
-- TODO: make doX functions total
case code of
'R' -> pure $ T Tag
'I' -> pure $ doI m
'S' -> pure $ doS m
'F' -> do
b <- lift $ checkF h
if b then pure $ doF m else skip
'P' -> pure $ doP m
'E' -> throwError $ Just $ "mpg321 error: " ++ P.unpack m
_ -> skip