{-# LANGUAGE ForeignFunctionInterface #-} {-| Description: Values related to on-disc data layout: track boundaries, sector sizes, address formats, etc. Copyright: (c) 2018-2021 Sam May License: GPL-3.0-or-later Maintainer: ag@eitilt.life Stability: stable Portability: non-portable (requires libcdio) The bulk of this module deals with the sizes of various parts of disc sectors, and so a brief description of each is likely helpful; for more information, see the libcdio user guide, or any of the other various documentation that people have written. Depending on the accuracy required, multiple methods of encoding data onto discs were developed enabling different degrees of error detection and recovery. For ease of implementation, these check regions are spaced evenly throughout the data, effectively dividing it into blocks of 'sectorSize' bytes. The inner structure of each block is thus: = Sector layout == Audio As audio CDs are intended to be listened to by human ears, often in noisy environments, byte-perfect fidelity was deemed unnecessary; data fills the entire sector without leaving room for checksums or synchronization points. This structure is more technically described as "CD-DA" (Digital Audio). @ |- 'sectorSize' -| @ == Data (Mode 1) The original structure described in the /Yellow Book/ standard. Mode 1, Form 1 is notably the layout used for CD-ROMs. It would be most correct to describe these as "Mode 1" and "Mode 2" with no reference to form, but the libcdio documentation has adopted the practice below as a parallel to the XA names. === Mode 1, Form 1 @ |------------------------------------ 'sectorSize' -------------------------------------| 'syncSize' + 'headerSize' + __'dataSize'__ + 'errorDetectionSize' + 'padSize' + 'errorCorrectionSize' |------------------------------- 'syncedSize' -------------------------------| |-------------------- 'tailSize' --------------------| @ === Mode 1, Form 2 @ |------------ 'sectorSize' -----------| 'syncSize' + 'headerSize' + __'dataSizeRaw'__ |------ 'syncedSize' ------| @ == XA (Mode 2) Developed as a later extension to the Mode 1, Form 2 (technically just "Mode 2") standard above to allow different data to be interleaved. === Mode 2, Form 1 @ |--------------------------------------- 'sectorSize' ----------------------------------------| 'syncSize' + 'headerSize' + 'subheaderSize' + __'dataSize'__ + 'errorDetectionSize' + 'errorCorrectionSize' |---------- 'syncHeaderSizeXa' ---------| |-------------- 'tailSizeXa' --------------| |------ 'headerSizeXa' ------| |---------------------------------- 'syncedSize' ----------------------------------| |--------------------------- 'dataSizeRaw' ---------------------------| @ === Mode 2, Form 2 @ |--------------------------- 'sectorSize' --------------------------| 'syncSize' + 'headerSize' + 'subheaderSize' + __'dataSizeRawXa'__ + 'padSizeXa' |---------- 'syncHeaderSizeXa' ---------| |------ 'headerSizeXa' ------| |--------------------- 'syncedSize' ---------------------| |-------------- 'dataSizeRaw' --------------| |----- 'taggedDataSizeRaw' -----| @ = @sector.h@ == Defines * @CDIO_CD_CHUNK_SIZE@ -> 'chunkSize' * @CDIO_CD_ECC_SIZE@ -> 'errorCorrectionSize' * @CDIO_CD_EDC_SIZE@ -> 'errorDetectionSize' * @CDIO_CD_FRAMESIZE@ -> 'dataSize' * @CDIO_CD_FRAMESIZE_RAW@ -> 'sectorSize' * @CDIO_CD_FRAMESIZE_RAW0@ -> 'dataSizeRaw' * @CDIO_CD_FRAMESIZE_RAW1@ -> 'syncedSize' * @CDIO_CD_FRAMESIZE_RAWER@ -> 'sectorSizeMax' * @CDIO_CD_FRAMESIZE_SUB@ -> 'framesizeSub' * @CDIO_CD_FRAMES_PER_MIN@ -> 'framesPerMin' * @CDIO_CD_FRAMES_PER_SEC@ -> 'framesPerSec' * @CDIO_CD_HEADER_SIZE@ -> 'headerSize' * @CDIO_CD_M1F1_ZERO_SIZE@ -> 'padSize' * @CDIO_CD_MAX_LSN@ (removed; identical to @'maxBound' :: 'Lsn'@) * @CDIO_CD_MAX_SESSIONS@ -> 'maxSessions' * @CDIO_CD_MINS@ -> 'cdMins' * @CDIO_CD_MIN_LSN@ (removed; identical to @'minBound' :: 'Lsn'@) * @CDIO_CD_MIN_SESSION_NO@ -> 'minSessionNo' * @CDIO_CD_NUM_OF_CHUNKS@ -> 'chunksPerSector' * @CDIO_CD_SECS_PER_MIN@ -> 'secsPerMin' * @CDIO_CD_SUBHEADER_SIZE@ -> 'subheaderSize' * @CDIO_CD_SYNC_SIZE@ -> 'syncSize' * @CDIO_CD_XA_HEADER@ -> 'headerSizeXa' * @CDIO_CD_XA_SYNC_HEADER@ -> 'syncHeaderSizeXa' * @CDIO_CD_XA_TAIL@ -> 'tailSizeXa' * @CDIO_POSTGAP_SECTORS@ -> 'postgapSectors' * @CDIO_PREGAP_SECTORS@ -> 'pregapSectors' * @M2F2_SECTOR_SIZE@ -> 'dataSizeRawXa' * @M2RAW_SECTOR_SIZE@ (removed; identical to 'dataSizeRaw') * @M2SUB_SECTOR_SIZE@ -> 'taggedDataSizeRaw' * @msf_t_SIZEOF@ (removed; structural constant not required for Haskell) == Types * @cdio_cd_minutes_sectors@ (removed; most values were simple calculations on @CDIO_CD_FRAMES_PER_MIN@, which can be done manually) - @CDIO_CD_MAX_SECTORS@ -> 'maxSectors' * @cdio_subchannel@ -> 'SubchannelData' - @CDIO_SUBCHANNEL_SUBQ_DATA@ -> 'QChannelData' * @flag_t@ -> 'Flags' and 'Flag' - @SCMS@ -> 'SerialCopyManagement' == Symbols * @CDIO_SECTOR_SYNC_HEADER@ -> 'sectorSyncHeader' * @cdio_lba_to_lsn@ -> 'lbaToLsn' * @cdio_lba_to_msf@ -> 'lbaToMsf' * @cdio_lba_to_msf_str@ -> 'lbaToMsfStr' * @cdio_lsn_to_lba@ -> 'lsnToLba' * @cdio_lsn_to_msf@ -> 'lsnToMsf' * @cdio_msf_to_lba@ -> 'msfToLba' * @cdio_msf_to_lsn@ -> 'msfToLsn' * @cdio_msf_to_str@ -> 'msfToStr' = "Sound.Libcdio.Read.Data" * 'Flag' (currently removed; unused in the translated portion of the interface) * 'Flags' (currently removed; unused in the translated portion of the interface) * 'Lba' (removed; only one address format is needed, and 'Lsn' is more widespread) * 'Msf' (removed; only one address format is needed, and timestamps can be confusing for non-audio data) * 'SubchannelData' (currently removed; unused in the translated portion of the interface) * 'cdMins' -> 'Sound.Libcdio.Read.Data.maxCdMinutes' * 'chunkSize' (removed; unnecessary low-level detail) * 'chunksPerSector' (removed; unnecessary low-level detail) * 'framesizeSub' (removed; unnecessary low-level detail) * 'lbaToLsn' (removed; only 'Lsn' is used in @Sound@) * 'lbaToMsf' (removed; only 'Lsn' is used in @Sound@) * 'lbaToMsfStr' -> 'Sound.Libcdio.Read.Data.audioTimestamp' * 'lsnToLba' (removed; only 'Lsn' is used in @Sound@) * 'lsnToMsf' (removed; only 'Lsn' is used in @Sound@) * 'maxSectors' -> 'Sound.Libcdio.Read.Data.maxSectors' * 'maxSessions' (removed; explicit session handling not yet implemented by libcdio) * 'minSessionNo' (removed; explicit session handling not yet implemented by libcdio) * 'msfToLba' (removed; only 'Lsn' is used in @Sound@) * 'msfToLsn' (removed; only 'Lsn' is used in @Sound@) * 'msfToStr' (removed; only 'Lsn' is used in @Sound@) * 'postgapSectors' -> 'Sound.Libcdio.Read.Data.defaultPostgapSectors' * 'pregapSectors' -> 'Sound.Libcdio.Read.Data.defaultPregapSectors' * 'secsPerMin' (removed; real-world constant) * 'sectorSyncHeader' (removed; unnecessary low-level detail) * 'framesPerMin' (removed; simple arithmatic of 'framesPerSec' All size constants (e.g. 'sectorSize') likewise removed as unnecessary. -} module Foreign.Libcdio.Sector ( -- * Types SubchannelData ( .. ) , Flags , Flag ( .. ) , Lba , Lsn , Msf ( .. ) -- * Conversions , lbaToLsn , lsnToLba , lbaToMsf , msfToLba , lsnToMsf , msfToLsn , lbaToMsfStr , msfToStr -- * Basic counts , chunkSize , minSessionNo, maxSessions , pregapSectors, postgapSectors , maxSectors , framesizeSub -- ** Units of time , chunksPerSector , framesPerSec , secsPerMin , framesPerMin , cdMins -- * Sector blocks , sectorSize , syncedSize , sectorSizeMax -- ** Headers , sectorSyncHeader , syncSize , headerSize , subheaderSize , headerSizeXa , syncHeaderSizeXa -- ** Data , dataSize , dataSizeRaw , dataSizeRawXa , taggedDataSizeRaw -- ** Error correction , errorDetectionSize , errorCorrectionSize , padSize , padSizeXa , tailSize , tailSizeXa ) where import qualified Data.Array.BitArray as A import qualified Data.ByteString as BS import qualified Foreign.C.String as C import qualified Foreign.C.Types as C import qualified Foreign.Ptr as C import qualified Foreign.Marshal.Alloc as M import qualified Foreign.Marshal.Utils as M import qualified Foreign.Storable as S import qualified System.IO.Unsafe as IO.Unsafe import Foreign.Libcdio.Marshal import Foreign.Libcdio.Types.Enums import Foreign.Libcdio.Types.Internal import Foreign.Libcdio.Types.Offsets import Foreign.Libcdio.Util -- | The collection of metadata describing a particular track. type Flags = A.BitArray Flag -- | The number of sectors spanned by a track pre-gap by default. pregapSectors :: Word pregapSectors = fromIntegral pregapSectors' foreign import ccall safe "cdio/compat/sector.h pregap_sectors" pregapSectors' :: C.CUInt -- | The number of sectors spanned by a track post-gap by default. postgapSectors :: Word postgapSectors = fromIntegral postgapSectors' foreign import ccall safe "cdio/compat/sector.h postgap_sectors" postgapSectors' :: C.CUInt -- | The typical maximum length of a disc, though it's not a strict limit. cdMins :: Word cdMins = fromIntegral cdMins' foreign import ccall safe "cdio/compat/sector.h cd_mins" cdMins' :: C.CUInt -- | The number of seconds in a minute. secsPerMin :: Word secsPerMin = fromIntegral secsPerMin' foreign import ccall safe "cdio/compat/sector.h secs_per_min" secsPerMin' :: C.CUInt -- | The number of disc sectors comprising a second of audio data. framesPerSec :: Word framesPerSec = fromIntegral framesPerSec' foreign import ccall safe "cdio/compat/sector.h frames_per_sec" framesPerSec' :: C.CUInt -- | The number of bytes in a disc sector's sync header. syncSize :: Word syncSize = fromIntegral syncSize' foreign import ccall safe "cdio/compat/sector.h sync_size" syncSize' :: C.CUInt -- | The size of the smallest meaningful segment of data, in bytes. chunkSize :: Word chunkSize = fromIntegral chunkSize' foreign import ccall safe "cdio/compat/sector.h chunk_size" chunkSize' :: C.CUInt -- | The number of data chunks comprising a disc sector. chunksPerSector :: Word chunksPerSector = fromIntegral chunksPerSector' foreign import ccall safe "cdio/compat/sector.h chunks_per_frame" chunksPerSector' :: C.CUInt -- | The size of a segment of data in the subchannel, in bytes. framesizeSub :: Word framesizeSub = fromIntegral framesizeSub' foreign import ccall safe "cdio/compat/sector.h framesize_sub" framesizeSub' :: C.CUInt -- | The size of the address of a data sector, in bytes. headerSize :: Word headerSize = fromIntegral headerSize' foreign import ccall safe "cdio/compat/sector.h header_size" headerSize' :: C.CUInt -- | The size of the subheader of an XA sector, in bytes. subheaderSize :: Word subheaderSize = fromIntegral subheaderSize' foreign import ccall safe "cdio/compat/sector.h subheader_size" subheaderSize' :: C.CUInt -- | The size of the EDC error correction segment, in bytes. errorDetectionSize :: Word errorDetectionSize = fromIntegral errorDetectionSize' foreign import ccall safe "cdio/compat/sector.h edc_size" errorDetectionSize' :: C.CUInt -- | The amount of padding between the EDC and ECC segments in a Mode 1 sector, -- in bytes. padSize :: Word padSize = fromIntegral padSize' foreign import ccall safe "cdio/compat/sector.h mode1_pad_size" padSize' :: C.CUInt -- | The size of the ECC error correction segment, in bytes. errorCorrectionSize :: Word errorCorrectionSize = fromIntegral errorCorrectionSize' foreign import ccall safe "cdio/compat/sector.h ecc_size" errorCorrectionSize' :: C.CUInt -- | The amount of data which may be contained in a disc sector /with/ error -- correction, in bytes. dataSize :: Word dataSize = fromIntegral dataSize' foreign import ccall safe "cdio/compat/sector.h framesize" dataSize' :: C.CUInt -- | The size of an entire disc sector, in bytes. sectorSize :: Word sectorSize = fromIntegral sectorSize' foreign import ccall safe "cdio/compat/sector.h framesize_raw" sectorSize' :: C.CUInt -- | The maximum number of bytes that may be returned from a single call. sectorSizeMax :: Word sectorSizeMax = fromIntegral sectorSizeMax' foreign import ccall safe "cdio/compat/sector.h framesize_rawer" sectorSizeMax' :: C.CUInt -- | The size of a disc sector in bytes, ignoring the sync header. syncedSize :: Word syncedSize = fromIntegral syncedSize' foreign import ccall safe "cdio/compat/sector.h framesize_raw1" syncedSize' :: C.CUInt -- | The amount of data which may be contained in a disc sector /without/ error -- correction, in bytes. dataSizeRaw :: Word dataSizeRaw = fromIntegral dataSizeRaw' foreign import ccall safe "cdio/compat/sector.h framesize_raw0" dataSizeRaw' :: C.CUInt -- | The total size of the meaningful XA sector headers, in bytes. headerSizeXa :: Word headerSizeXa = fromIntegral headerSizeXa' foreign import ccall safe "cdio/compat/sector.h header_size_xa" headerSizeXa' :: C.CUInt -- | The total size of all data sector error correction segments, in bytes. tailSize :: Word tailSize = errorDetectionSize + padSize + errorCorrectionSize -- | The total size of all XA sector error correction segments, in bytes. tailSizeXa :: Word tailSizeXa = fromIntegral tailSizeXa' foreign import ccall safe "cdio/compat/sector.h tail_size_xa" tailSizeXa' :: C.CUInt -- | The total size of all headers in an XA sector, including the sync marker. syncHeaderSizeXa :: Word syncHeaderSizeXa = fromIntegral syncHeaderSizeXa' foreign import ccall safe "cdio/compat/sector.h sync_size_xa" syncHeaderSizeXa' :: C.CUInt -- | The amount of padding at the end of an XA sector without error correction, -- in bytes. padSizeXa :: Word padSizeXa = dataSizeRaw - taggedDataSizeRaw -- | The byte sequence used to mark the start of a disc sector, to allow -- correcting drift while reading. sectorSyncHeader :: BS.ByteString sectorSyncHeader = IO.Unsafe.unsafePerformIO $ BS.packCStringLen (sectorSyncHeader', fromIntegral syncSize) foreign import ccall safe "cdio/compat/sector.h sector_sync_header" sectorSyncHeader' :: C.Ptr C.CChar -- | The amount of data contained in an XA sector without error correction, in -- bytes. dataSizeRawXa :: Word dataSizeRawXa = fromIntegral dataSizeRawXa' foreign import ccall safe "cdio/compat/sector.h data_size_xa" dataSizeRawXa' :: C.CUInt -- | The size of tagged data (counting the subheader) contained in an XA sector -- without error correction, in bytes. taggedDataSizeRaw :: Word taggedDataSizeRaw = fromIntegral taggedDataSizeRaw' foreign import ccall safe "cdio/compat/sector.h tagged_size_xa" taggedDataSizeRaw' :: C.CUInt -- | How many sessions are allowed on a disc. maxSessions :: Word maxSessions = fromIntegral maxSessions' foreign import ccall safe "cdio/compat/sector.h max_session" maxSessions' :: C.CUInt -- | The smallest session number on a disc. minSessionNo :: Word minSessionNo = fromIntegral minSessionNo' foreign import ccall safe "cdio/compat/sector.h min_session" minSessionNo' :: C.CUInt -- | A shortcut for calculating @'framesPerSec' * 'secsPerMin'@ framesPerMin :: Word framesPerMin = fromIntegral framesPerMin' foreign import ccall safe "cdio/compat/sector.h frames_per_min" framesPerMin' :: C.CUInt -- | The maximum number of sectors allowed to be stored on a disc. maxSectors :: Word maxSectors = fromIntegral maxSectors' foreign import ccall safe "cdio/compat/sector.h max_sectors" maxSectors' :: C.CUInt -- | Minute\/second\/frame structure for addresses. Generally only makes sense -- for audio discs. data Msf = Msf { minute :: Word , second :: Word -- ^ 'secsPerMin' , frame :: Word -- ^ 'framesPerSec' } deriving ( Show, Read ) instance Eq Msf where l == r = compare l r == EQ instance Ord Msf where compare l r = case minute l' `compare` minute r' of EQ -> case second l' `compare` second r' of EQ -> frame l' `compare` frame r' x -> x x -> x where l' = normalize l r' = normalize r normalize msf@(Msf m s f) | f >= framesPerSec = normalize $ Msf m (s + div f framesPerSec) (mod f framesPerSec) | s >= secsPerMin = Msf (m + div s secsPerMin) (mod s secsPerMin) f | otherwise = msf instance Bounded Msf where minBound = Msf 0 0 0 maxBound = Msf maxBound (secsPerMin - 1) (framesPerSec - 1) instance S.Storable Msf where sizeOf _ = msfSizeOf alignment _ = msfAlign peek c = do m <- S.peekByteOff c msfM s <- S.peekByteOff c msfS f <- S.peekByteOff c msfF return $ Msf (fromBcd8 m) (fromBcd8 s) (fromBcd8 f) poke c hs = do S.pokeByteOff c msfM . toBcd8 $ minute hs S.pokeByteOff c msfS . toBcd8 $ second hs S.pokeByteOff c msfF . toBcd8 $ frame hs instance Enum Msf where fromEnum msf = fromInteger $ toInteger (frame msf) + toInteger (second msf) * fromIntegral framesPerSec + toInteger (minute msf) * fromIntegral framesPerMin toEnum i = Msf { minute = div i' framesPerMin , second = flip mod secsPerMin $ div i' framesPerSec , frame = mod i' framesPerSec } where i' = fromIntegral i pred (Msf 0 0 0) = error "Enum.pred(Msf): tried to take `pred' of minBound" pred (Msf m s f) | f >= framesPerSec = pred $ Msf m (s + div f framesPerSec) (mod f framesPerSec) | s >= secsPerMin = pred $ Msf (m + div s secsPerMin) (mod s secsPerMin) f | f == 0 && s == 0 = Msf (m - 1) (secsPerMin - 1) (framesPerSec - 1) | f == 0 = Msf m (s - 1) (framesPerSec - 1) | otherwise = Msf m s (f - 1) succ msf@(Msf m s f) | msf >= maxBound = error "Enum.succ(Msf): tried to take `succ' of maxBound" | otherwise = let (fd, fm) = overflow f 1 framesPerSec (sd, sm) = overflow s fd secsPerMin in Msf (m + sd) sm fm where -- Relies on unsigned type, and @y + b < maxBound@ overflow x y b | x > maxBound - y = (div (x - b + y) b + 1, mod (x - b + y) b) | otherwise = (div (x + y) b, mod (x + y) b) {- Use @sizeOf Msf@ msfSizeof :: Word msfSizeof = fromIntegral msfSizeof' foreign import ccall safe "cdio/compat/sector.h sizeof_msf_const" msfSizeof' :: C.CSize -} {- Never used after definition. -- | Address in one of the two primary address formats. data CdromAddr = MsfAddr Msf | LbaAddr Lba deriving ( Eq, Show, Read, Ord ) -- | Note that, while the 'peek' implementation attempts to detect which form -- the address takes, it may get it wrong and read a 'Msf' address as an 'Lba' -- or vice versa. If the type is otherwise known, it may be better to read -- the pointer as that type directly, and wrap into the union manually. instance S.Storable CdromAddr where sizeOf _ = adrSizeOf alignment _ = adrAlign peek c = do msf <- S.peek $ C.castPtr c lba <- S.peek $ C.castPtr c -- Barring uncleared bits, an LBA has an extra byte. if mod 0x100 lba /= 0 || second msf >= secsPerMin || frame msf >= framesPerSec then return $ LbaAddr lba else return $ MsfAddr msf poke c (MsfAddr hs) = S.poke (C.castPtr c) hs poke c (LbaAddr hs) = S.poke (C.castPtr c) hs -} -- | Print a logical address as the corresponding timestamp, assuming audio -- data. lbaToMsfStr :: Lba -> String lbaToMsfStr l = IO.Unsafe.unsafePerformIO $ lbaToMsfStr' (fromIntegral l) >>= peekFString foreign import ccall safe "cdio/compat/sector.h cdio_lba_to_msf_str" lbaToMsfStr' :: CLba -> IO C.CString -- | Print a disc timestamp in the standard "MM:SS:FF" format. msfToStr :: Msf -> String msfToStr msf = IO.Unsafe.unsafePerformIO $ M.with msf msfToStr' >>= C.peekCString foreign import ccall safe "cdio/compat/sector.h cdio_msf_to_str" msfToStr' :: C.Ptr Msf -> IO C.CString -- | Convert an LBA address into the corresponding LSN. lbaToLsn :: Lba -> Lsn lbaToLsn = fromIntegral . lbaToLsn' . fromIntegral -- C erroneously returns @lba_t@. foreign import ccall safe "cdio/compat/sector.h cdio_lba_to_lsn" lbaToLsn' :: CLba -> CLba -- | Convert an LBA address into the corresponding timestamp, assuming audio -- data. lbaToMsf :: Lba -> Msf lbaToMsf l = IO.Unsafe.unsafePerformIO . M.alloca $ \m' -> lbaToMsf' (fromIntegral l) m' >> S.peek m' foreign import ccall safe "cdio/compat/sector.h cdio_lba_to_msf" lbaToMsf' :: CLba -> C.Ptr Msf -> IO () -- | Convert an LSN address into the corresponding LBA. lsnToLba :: Lsn -> Lba lsnToLba = fromIntegral . lsnToLba' . fromIntegral foreign import ccall safe "cdio/compat/sector.h cdio_lsn_to_lba" lsnToLba' :: CLsn -> CLba -- | Convert an LSN address into the corresponding timestamp, assuming audio -- data. lsnToMsf :: Lsn -> Msf lsnToMsf l = IO.Unsafe.unsafePerformIO . M.alloca $ \m' -> lsnToMsf' (fromIntegral l) m' >> S.peek m' foreign import ccall safe "cdio/compat/sector.h cdio_lsn_to_msf" lsnToMsf' :: CLsn -> C.Ptr Msf -> IO () -- | Convert a timestamp into the corresponding LBA address, assuming audio -- data. msfToLba :: Msf -> Lba msfToLba = fromIntegral . IO.Unsafe.unsafePerformIO . flip M.with msfToLba' foreign import ccall safe "cdio/compat/sector.h cdio_msf_to_lba" msfToLba' :: C.Ptr Msf -> IO CLsn -- | Convert a timestamp into the corresponding LSN address, assuming audio -- data. msfToLsn :: Msf -> Lsn msfToLsn = fromIntegral . IO.Unsafe.unsafePerformIO . flip M.with msfToLsn' foreign import ccall safe "cdio/compat/sector.h cdio_msf_to_lsn" msfToLsn' :: C.Ptr Msf -> IO CLsn {- Not exported -- | Convert a split timestamp into the corresponding LBA address, assuming -- audio data. msf3ToLba :: Word -> Word -> Word -> Lba msf3ToLba m s f = fromIntegral $ msf3ToLba' (fromIntegral m) (fromIntegral s) (fromIntegral f) foreign import ccall safe "cdio/compat/sector.h cdio_msf3_to_lba" msf3ToLba' :: C.CUInt -> C.CUInt -> C.CUInt -> CLba -- | Read a timestamp in the form MM:SS:FF into the corresponding LBA address, -- assuming audio data. mmssffToLba :: String -> Maybe Lba mmssffToLba s = invalidLba . IO.Unsafe.unsafePerformIO $ C.withCString s mmssffToLba foreign import ccall safe "cdio/compat/sector.h cdio_mmssff_to_lba" mmssffToLba' :: C.CString -> IO CLba -}