hmp3-ng-2.19.1: Decoder.hs
-- Copyright (c) 2005-2008 Don Stewart - http://www.cse.unsw.edu.au/~dons
-- Copyright (c) 2008, 2019-2026 Galen Huntington
-- SPDX-License-Identifier: GPL-2.0-or-later
-- Wire protocol for mpg123
module Decoder (
mp3Tool, mpgParser, Cmd(..), cmdToBS,
Msg(..), Id3(..), Status(..), Frame(..),
) where
import Base
import Text (trim, readIntM, showInt, guessEncoding)
import Data.ByteString.Char8 qualified as P
mp3Tool :: IsString a => a
mp3Tool = "mpg123"
------------------------------------------------------------------------
-- Send commands to mpg123
data Cmd = Load !ByteString | Jump !(Fixed E2) | Pause | Quit
cmdToBS :: Cmd -> ByteString
cmdToBS (Load f) = "L " <> f
cmdToBS (Jump s) = "J " <> (P.pack . show) s <> "s"
cmdToBS Pause = "P" -- (un)pauses
cmdToBS Quit = "Q"
------------------------------------------------------------------------
-- Receive messages from mpg123
data Msg = I !Id3 | S !ByteString | F !Frame | P !Status
deriving stock (Eq, Show)
-- ID3 info
data Id3 = Id3
{ title :: !ByteString
, artist :: !ByteString
, album :: !ByteString
, str :: !ByteString
-- , year :: Maybe ByteString
-- , genre :: Maybe ByteString }
} deriving stock (Eq, Show)
-- Frame decoding status updates (once per frame).
-- Current-time and time-remaining are numbers with two decimal places.
data Frame = Frame { elapsed :: !(Fixed E2), left :: !(Fixed E2) }
deriving stock (Eq, Show)
-- Stop/pause status.
data Status = Stopped | Paused | Playing
deriving stock (Eq, Show)
doP :: ByteString -> Maybe Msg
doP s = do
(p, _) <- P.uncons s
case p of
'0' -> pure $ P Stopped
'1' -> pure $ P Paused
'2' -> pure $ P Playing
_ -> Nothing -- don't need P 3 at end of song
-- Frame decoding status updates (once per frame).
doF :: ByteString -> Maybe Msg
doF s = do
_ : _ : f2 : f3 : _ <- pure $ P.split ' ' s
elapsed <- readMaybe $ P.unpack f2
left <- max 0 <$> readMaybe (P.unpack f3)
pure $ F Frame { elapsed, left }
-- Info about mp3 file after loading.
-- Breakdown from mpg123 README.remote (as numbers):
-- 0 = mpeg type (string)
-- 1 = layer (int)
-- 2 = sampling frequency (int)
-- 3 = mode (string)
-- 4 = mode extension (int)
-- 5 = framesize (int)
-- 6 = stereo (int)
-- 7 = copyright (int)
-- 8 = error protection (int)
-- 9 = emphasis (int)
-- 10 = bitrate (int)
-- 11 = extension (int)
doS :: ByteString -> Maybe Msg
doS s = do
let fs = P.split ' ' s
guard $ length fs >= 11
hz <- readIntM $ fs !! 2
pure $ S $ mconcat [
"mpeg ", fs !! 0, " ", fs !! 10, "kb/s ", showInt $ hz `div` 1000, "kHz"]
-- Track info if ID fields are in the file, otherwise file name.
doI :: ByteString -> Maybe Msg
doI s = I <$> do
("ID3:", info) <- pure $ P.splitAt 4 s
let id3 = parseId3 info
guard $ not $ P.null $ id3.title -- title sometimes empty
pure id3
-- Format: title (30), author (30), album (30), year (4), comment (30), genre
-- We currently only use the first three.
parseId3 :: ByteString -> Id3
parseId3 = toId . cut where
cut f | P.null f = []
| True = let (a, xs) = P.splitAt 30 f
in guessEncoding (trim a) : cut xs
toId ls = Id3 (arg 0) (arg 1) (arg 2) $ mconcat $ intersperse " : "
$ filter (not . P.null) [arg 1, arg 2, arg 0]
where arg = fromMaybe "" . (ls !?)
-- Parse line; on failure, return Just only if error to report.
mpgParser :: ByteString -> Either (Maybe String) Msg
mpgParser line = do
-- bad packets are generally just \n in ID3 (and not of interest anyway)
let quiet = maybe (Left Nothing) pure
code <- quiet do
'@' : c : ' ' : _ <- pure $ P.unpack line
pure c
let m = P.drop 3 line
case code of
'I' -> quiet $ doI m
'S' -> quiet $ doS m
'F' -> quiet $ doF m
'P' -> quiet $ doP m
'E' -> Left $ Just $ P.unpack m
_ -> quiet Nothing