hscdio-0.1.0.0: src/Foreign/Libcdio/CdText.hs
{-# LANGUAGE ForeignFunctionInterface #-}
{-|
Description: Functions and types providing metadata about the disc contents.
Copyright: (c) 2018-2021 Sam May
License: GPL-3.0-or-later
Maintainer: ag@eitilt.life
Stability: stable
Portability: non-portable (requires libcdio)
Metadata is stored in a binary format both library-internally and on the disc.
Most audio archivists will recognize it as "those information fields in a CUE
file" (though there are [other
formats](https://www.gnu.org/software/libcdio/cd-text-format.html#Sony-Text-File-Format-_0028Input-Sheet-Version-0_002e7T_0029)
as well), and casual listeners will recognize it as the scrolling text that
you're always happy to see, on the rare times your music player shows it.
Little-used and even-less-known, however, is that a single disc can
theoretically contain metadata in up to eight different languages; because of
the complexity that introduces, no unifying datatype is provided in favour of a
function-based interface.
= @cdtext.h@
== Defines
* @MIN_CDTEXT_FIELD@ (removed; identical to @'minBound' :: 'Field'@)
* @MAX_CDTEXT_FIELD@ (removed; identical to @'maxBound' :: 'Field'@)
== Types
* @cdtext_field_t@ -> 'Field'
- @CDTEXT_FIELD_INVALID@ (removed; handled via 'Nothing')
* @cdtext_genre_t@ -> 'Genre'
- @CDTEXT_GENRE_UNUSED@ (removed; handled via 'Nothing')
* @cdtext_lang_t@ -> 'Language'
- @CDTEXT_LANGUAGE_UNKNOWN@ -> 'UnknownLanguage'
- /Before libcdio 2.1.0: (often effectively removed; handled via 'Nothing')/
- @CDTEXT_LANGUAGE_INVALID@ (removed; handled via 'Nothing')
- @CDTEXT_LANGUAGE_BLOCK_UNUSED@ (removed; handled via 'Nothing')
* @cdtext_t@ (removed; merged into 'Cdio' objects)
== Symbols
* @cdtext_data_init@ -> 'cdTextDataInit'
* @cdtext_destroy@ (removed; handled via the garbage collector)
* @cdtext_field2str@ -> 'fieldString'
* @cdtext_genre2str@ -> 'genreString'
* @cdtext_get@ (removed; type conversion makes it identical to @cdtext_get_const@)
* @cdtext_get_const@ -> 'cdTextGet'
* @cdtext_get_first_track@ -> 'firstTrack'
* @cdtext_get_genre@ -> 'genre'
* @cdtext_get_language@ -> 'language'
* @cdtext_get_last_track@ -> 'lastTrack'
* @cdtext_init@ (removed; internal function without much external occasion)
* @cdtext_lang2str@ -> 'languageString'
* @cdtext_list_languages@ -> 'listLanguages'
* @cdtext_list_languages_v2@ -> 'listAllLanguages'
* @cdtext_select_language@ -> 'selectLanguage'
* @cdtext_set@ (removed; primarily intended for internal use, and is more limited than would be expected)
* @cdtext_set_language_index@ -> 'selectLanguageIndex'
* @cdtext_str2lang@ -> 'parseLanguage'
= "Sound.Libcdio.Read.CdText"
While similar functionality is provided, the
@"Sound.Libcdio.Read".'Sound.Libcdio.Read.CdText'@ is written as a separate
monadic interface rather than in the 'Cdio'-bound style used here.
* 'cdTextDataInit' -> 'Sound.Libcdio.Read.CdText.parseCdText'
* 'cdTextGet' -> 'Sound.Libcdio.Read.CdText.discId', @'snd' 'Sound.Libcdio.Read.CdText.genre'@, and 'Sound.Libcdio.Read.CdText.info'
* 'genre' -> @'fst' 'Sound.Libcdio.Read.CdText.genre'@
* 'listAllLanguages' -> 'Sound.Libcdio.Read.CdText.languages'
* 'listLanguages' (removed; obsolete and imprecise)
* 'selectLanguage' -> 'Sound.Libcdio.Read.CdText.withLanguage'
* 'selectLanguageIndex' -> 'Sound.Libcdio.Read.CdText.withIndex'
-}
module Foreign.Libcdio.CdText
( -- * Types
Cdio
, Field ( .. )
, Genre ( .. )
, Language ( .. )
-- * Description
, fieldString
, languageString
, parseLanguage
, genreString
-- * Management
, cdTextDataInit
, listLanguages
, listAllLanguages
, selectLanguage
, selectLanguageIndex
-- * Access
, cdTextGet
, genre
, language
, firstTrack
, lastTrack
) where
import qualified Data.List as L
import qualified Data.Maybe as Y
import qualified Foreign.C.String as C
import qualified Foreign.C.Types as C
import qualified Foreign.Ptr as C
import qualified Foreign.Marshal.Array as M
import qualified Foreign.Marshal.Utils as M
import qualified System.IO.Unsafe as IO.Unsafe
import Foreign.Libcdio.Device
import Foreign.Libcdio.Marshal
import Foreign.Libcdio.Track
import Foreign.Libcdio.Types.Enums
import Foreign.Libcdio.Types.Internal
import Foreign.Libcdio.Version
import Sound.Libcdio.Common
-- | Return a canonical English name of the given genre, as opposed to the
-- machine representation returned by the 'Show' instance.
genreString :: Genre -> String
genreString = IO.Unsafe.unsafePerformIO .
C.peekCString . genreString' . fromIntegral . fromEnum
foreign import ccall safe "cdio/compat/cdtext.h cdtext_genre2str"
genreString' :: CGenre -> C.CString
-- | Return a canonical English name of the given language, as opposed to the
-- machine representation returned by the 'Show' instance.
languageString :: Language -> String
languageString = IO.Unsafe.unsafePerformIO .
C.peekCString . languageString' . fromIntegral . fromEnum
foreign import ccall safe "cdio/compat/cdtext.h cdtext_lang2str"
languageString' :: CLanguage -> C.CString
-- | Transform a string returned by 'languageString' back into its
-- 'Language' representation.
--
-- /Before libcdio 2.1.0: Always returns 'Nothing'/
parseLanguage :: String -> Maybe Language
parseLanguage s
| libcdioVersionNum >= makeVersion [2,1] = IO.Unsafe.unsafePerformIO $ do
i <- C.withCString s parseLanguage'
return . fmap (toEnum . fromIntegral) $ maybeError [languageInvalid, languageUnused] i
| otherwise = Nothing
foreign import ccall safe "cdio/compat/cdtext.h cdtext_str2lang_safe"
parseLanguage' :: C.CString -> IO CLanguage
foreign import ccall safe "cdio/compat/cdtext.h language_unknown"
languageUnknown :: CLanguage
foreign import ccall safe "cdio/compat/cdtext.h language_invalid"
languageInvalid :: CLanguage
foreign import ccall safe "cdio/compat/cdtext.h language_unused"
languageUnused :: CLanguage
-- | Describe the type of data in a human-readable manner, as opposed to the
-- machine representation returned by the 'Show' instance.
fieldString :: Field -> String
fieldString = IO.Unsafe.unsafePerformIO .
C.peekCString . fieldString' . fromIntegral . fromEnum
foreign import ccall safe "cdio/compat/cdtext.h cdtext_field2str"
fieldString' :: CField -> C.CString
-- | Attempt to retrieve the value of a given field from the disc (if
-- 'Nothing') or track (if 'Just') metadata, in the currently-active language.
-- Note that 'GenreName' and 'DiscId' will /always/ refer to the disc-level
-- data.
cdTextGet :: Cdio -> Field -> Maybe Track -> IO (Maybe String)
cdTextGet _ _ (Just DiscPregap) = return Nothing
cdTextGet _ _ (Just DiscLeadout) = return Nothing
cdTextGet c GenreName _ = cdTextGet_ c GenreName 0
cdTextGet c DiscId _ = cdTextGet_ c DiscId 0
cdTextGet c f Nothing = cdTextGet_ c f 0
cdTextGet c f (Just t) = cdTextGet_ c f t
cdTextGet_ :: Cdio -> Field -> Track -> IO (Maybe String)
cdTextGet_ c f t = withCdText' Nothing c $ \x ->
cdTextGet' x (fromIntegral $ fromEnum f) (fromIntegral $ fromEnum t) >>= M.maybePeek C.peekCString
foreign import ccall safe "cdio/compat/cdtext.h cdtext_get_const"
cdTextGet' :: C.Ptr CdText -> CField -> CTrack -> IO C.CString
-- | The machine-readable genre describing the music on this disc. For any
-- associated human-readable name or subgenre, use @'cdTextGet' c 'GenreName'
-- 'Nothing'@.
genre :: Cdio -> IO (Maybe Genre)
genre c = do
g <- withCdText c genre'
return $ if g == Just 0 then Nothing else toEnum . fromIntegral <$> g
foreign import ccall safe "cdio/compat/cdtext.h cdtext_get_genre"
genre' :: C.Ptr CdText -> IO CGenre
-- | Indicate which language results will (currently) be returned in. See
-- 'selectLanguage' to change this.
language :: Cdio -> IO (Maybe Language)
language c = fmap (toEnum . fromIntegral) <$> withCdText c language'
foreign import ccall safe "cdio/compat/cdtext.h cdtext_get_language"
language' :: C.Ptr CdText -> IO CLanguage
-- | The earliest track with any associated metadata in the current language.
-- Note that this may differ from 'minTrack'.
firstTrack :: Cdio -> IO (Maybe Track)
firstTrack c = fmap (toEnum . fromIntegral) . maybeError [0] <$> withCdText' 0 c firstTrack'
foreign import ccall safe "cdio/compat/cdtext.h cdtext_get_first_track"
firstTrack' :: C.Ptr CdText -> IO CTrack
-- | The final track with any associated metadata in the current language.
-- Note that this may differ from 'maxTrack'.
lastTrack :: Cdio -> IO (Maybe Track)
lastTrack c = fmap (toEnum . fromIntegral) . maybeError [0] <$> withCdText' 0 c lastTrack'
foreign import ccall safe "cdio/compat/cdtext.h cdtext_get_last_track"
lastTrack' :: C.Ptr CdText -> IO CTrack
-- | Try to set the data associated with the given language as active for
-- future calls to 'cdTextGet' and similar. If passed 'UnknownLanguage' or the
-- CDTEXT does not provide the one requested, selects the first (default) data
-- set instead, and returns 'False'.
selectLanguage :: Cdio -> Language -> IO Bool
selectLanguage c l = fmap M.toBool . withCdText' 0 c $ \x ->
selectLanguage' x . fromIntegral $ fromEnum l
foreign import ccall safe "cdio/compat/cdtext.h cdtext_select_language"
selectLanguage' :: C.Ptr CdText -> CLanguage -> IO CBool
-- | Retrieve the languages included in the disc metadata. Note that this does
-- not save the index position or any duplicate language blocks (if that is
-- desired, see 'listAllLanguages' instead).
listLanguages :: Cdio -> IO [Language]
listLanguages c = withCdText' [] c $ \x -> do
l' <- listLanguages' x
ls <- peekLanguages l' [languageUnknown, languageInvalid, languageUnused]
return . L.nub $ Y.catMaybes ls
foreign import ccall safe "cdio/compat/cdtext.h cdtext_list_languages"
listLanguages' :: C.Ptr CdText -> IO (C.Ptr CLanguage)
-- | Retrieve the languages included in the disc metadata, in the order they
-- occur, and respecting any empty language blocks if there's a valid language
-- after them (empty blocks at the end are cleaned away).
--
-- /Before libcdio 2.1.0: Acts as 'listLanguages'/
listAllLanguages :: Cdio -> IO [Maybe Language]
listAllLanguages c = withCdText' [] c $ \x -> do
l' <- listAllLanguages' x
ls <- peekLanguages l' [languageInvalid, languageUnused]
return $ case L.dropWhileEnd Y.isNothing ls of
[] -> [Just UnknownLanguage]
ls' -> ls'
foreign import ccall safe "cdio/compat/cdtext.h cdtext_list_languages_v2_safe"
listAllLanguages' :: C.Ptr CdText -> IO (C.Ptr CLanguage)
peekLanguages :: C.Ptr CLanguage -> [CLanguage] -> IO [Maybe Language]
peekLanguages p es
| p == C.nullPtr = return []
| otherwise = map (fmap (toEnum . fromIntegral) . maybeError es) <$> M.peekArray 8 p
-- | Select the language at the given index of 'listAllLanguages' for future
-- data retrieval. If the index is out of bounds or corresponds to a 'Nothing'
-- in 'listAllLanguages', the first (default) data set is selected instead and
-- 'False is returned.
selectLanguageIndex :: Cdio -> Word -> IO Bool
selectLanguageIndex c i = M.toBool <$> withCdText' 0 c (flip selectLanguageIndex' $ fromIntegral i)
foreign import ccall safe "cdio/compat/cdtext.h cdtext_set_language_index_safe"
selectLanguageIndex' :: C.Ptr CdText -> C.CInt -> IO CBool
{- Primarily intended as an internal function
-- | Set the given field at the given track to the given value.
cdTextSet :: Cdio -> Field -> Maybe String -> Maybe Track -> IO ()
cdTextSet _ _ _ (Just DiscPregap) = mempty
cdTextSet _ _ _ (Just DiscLeadout) = mempty
cdTextSet c f v Nothing = cdTextSet_ c f v 0
cdTextSet c f v (Just t) = cdTextSet_ c f v t
cdTextSet_ :: Cdio -> Field -> Maybe String -> Track -> IO ()
cdTextSet_ c f v t = withCdText_ c $ \x -> M.maybeWith C.withCString v $ \s ->
cdTextSet' x (toCEnum f) s (withTrack t) C.nullPtr
foreign import ccall safe "cdio/compat/cdtext.h cdtext_set"
cdTextSet' :: C.Ptr CdText -> CField -> C.CString -> CTrack -> C.CString -> IO ()
-}