{- | Low-level binary decoding for the Guitar Pro 5 (@.gp5@) file format.

This module only knows about GP5's own binary layout — it decodes into
the @GP5*@ types below, which mirror the file format closely (raw
duration codes, 1-based string numbers, dynamic values 1-16, etc). It
has no knowledge of "Termtab.Types"; "Termtab.Parser.GP5" is the layer
that translates @GP5*@ values into the domain 'Termtab.Types.Song'.

The @get*@ actions are meant to be run in sequence by the caller, in the
exact order fields appear in the file — there is no self-describing
structure to parse against, so getting the order or a skip length wrong
silently desynchronizes every subsequent read for the rest of the file.
-}
module Termtab.Parser.GP5.Internal (
    GP5ScoreInfo (..),
    GP5MeasureHeader (..),
    GP5Track (..),
    GP5Note (..),
    GP5Beat (..),
    GP5Bar (..),
    getVersion,
    getScoreInfo,
    skipLyrics,
    skipPageSetup,
    skipRSEMaster,
    getTempoName,
    getChannels,
    getMeasureHeader,
    getTrack,
    getBeat,
    getBar,
    getInt8',
    getInt32le',
    getU8,
    getByteSizeString,
    getIntByteSizeString,
    getIntSizeString,
) where

import Control.Monad (replicateM, replicateM_, when)
import Data.Binary.Get
import Data.Bits (testBit)
import Data.ByteString qualified as BS
import Data.Text qualified as T
import Data.Text.Encoding qualified as TE

{- | Header metadata read by 'getScoreInfo', decoded straight from the file
with no normalization.
-}
data GP5ScoreInfo = GP5ScoreInfo
    { GP5ScoreInfo -> Text
gsiTitle :: T.Text
    , GP5ScoreInfo -> Text
gsiSubtitle :: T.Text
    , GP5ScoreInfo -> Text
gsiArtist :: T.Text
    , GP5ScoreInfo -> Text
gsiAlbum :: T.Text
    , GP5ScoreInfo -> Text
gsiLyricist :: T.Text
    , GP5ScoreInfo -> Text
gsiComposer :: T.Text
    }
    deriving (Int -> GP5ScoreInfo -> ShowS
[GP5ScoreInfo] -> ShowS
GP5ScoreInfo -> String
(Int -> GP5ScoreInfo -> ShowS)
-> (GP5ScoreInfo -> String)
-> ([GP5ScoreInfo] -> ShowS)
-> Show GP5ScoreInfo
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> GP5ScoreInfo -> ShowS
showsPrec :: Int -> GP5ScoreInfo -> ShowS
$cshow :: GP5ScoreInfo -> String
show :: GP5ScoreInfo -> String
$cshowList :: [GP5ScoreInfo] -> ShowS
showList :: [GP5ScoreInfo] -> ShowS
Show, GP5ScoreInfo -> GP5ScoreInfo -> Bool
(GP5ScoreInfo -> GP5ScoreInfo -> Bool)
-> (GP5ScoreInfo -> GP5ScoreInfo -> Bool) -> Eq GP5ScoreInfo
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: GP5ScoreInfo -> GP5ScoreInfo -> Bool
== :: GP5ScoreInfo -> GP5ScoreInfo -> Bool
$c/= :: GP5ScoreInfo -> GP5ScoreInfo -> Bool
/= :: GP5ScoreInfo -> GP5ScoreInfo -> Bool
Eq)

{- | One measure header as read by 'getMeasureHeader'. GP5 only stores a
time\/key signature in the file when it /changes/ from the previous
measure, so every field here is already resolved to its effective value
(carried forward from the previous header where the file omitted it) —
callers never need to chase "last explicit value" themselves.
-}
data GP5MeasureHeader = GP5MeasureHeader
    { GP5MeasureHeader -> Int
gmhTimeNum :: Int
    , GP5MeasureHeader -> Int
gmhTimeDen :: Int
    , GP5MeasureHeader -> Int
gmhKeyRoot :: Int
    , GP5MeasureHeader -> Int
gmhKeyMode :: Int
    }
    deriving (Int -> GP5MeasureHeader -> ShowS
[GP5MeasureHeader] -> ShowS
GP5MeasureHeader -> String
(Int -> GP5MeasureHeader -> ShowS)
-> (GP5MeasureHeader -> String)
-> ([GP5MeasureHeader] -> ShowS)
-> Show GP5MeasureHeader
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> GP5MeasureHeader -> ShowS
showsPrec :: Int -> GP5MeasureHeader -> ShowS
$cshow :: GP5MeasureHeader -> String
show :: GP5MeasureHeader -> String
$cshowList :: [GP5MeasureHeader] -> ShowS
showList :: [GP5MeasureHeader] -> ShowS
Show, GP5MeasureHeader -> GP5MeasureHeader -> Bool
(GP5MeasureHeader -> GP5MeasureHeader -> Bool)
-> (GP5MeasureHeader -> GP5MeasureHeader -> Bool)
-> Eq GP5MeasureHeader
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: GP5MeasureHeader -> GP5MeasureHeader -> Bool
== :: GP5MeasureHeader -> GP5MeasureHeader -> Bool
$c/= :: GP5MeasureHeader -> GP5MeasureHeader -> Bool
/= :: GP5MeasureHeader -> GP5MeasureHeader -> Bool
Eq)

{- | One track's header as read by 'getTrack'. 'gtStrings' holds the open
pitch of each string (low to high count determined by string count, not
fixed at 6); 'gtProgram' is already resolved from the channel table
passed into 'getTrack' rather than being a raw channel index.
-}
data GP5Track = GP5Track
    { GP5Track -> Text
gtName :: T.Text
    , GP5Track -> [Int]
gtStrings :: [Int]
    , GP5Track -> Int
gtChannel :: Int
    , GP5Track -> Int
gtProgram :: Int
    , GP5Track -> Bool
gtIsDrums :: Bool
    }
    deriving (Int -> GP5Track -> ShowS
[GP5Track] -> ShowS
GP5Track -> String
(Int -> GP5Track -> ShowS)
-> (GP5Track -> String) -> ([GP5Track] -> ShowS) -> Show GP5Track
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> GP5Track -> ShowS
showsPrec :: Int -> GP5Track -> ShowS
$cshow :: GP5Track -> String
show :: GP5Track -> String
$cshowList :: [GP5Track] -> ShowS
showList :: [GP5Track] -> ShowS
Show, GP5Track -> GP5Track -> Bool
(GP5Track -> GP5Track -> Bool)
-> (GP5Track -> GP5Track -> Bool) -> Eq GP5Track
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: GP5Track -> GP5Track -> Bool
== :: GP5Track -> GP5Track -> Bool
$c/= :: GP5Track -> GP5Track -> Bool
/= :: GP5Track -> GP5Track -> Bool
Eq)

{- | One fretted note as read by 'getNote'. 'gnString' is the file's
1-based string number (so @gnString - 1@ indexes into 'gtStrings');
'gnDynamic' is GP5's raw 1-16 dynamic level, not a MIDI velocity —
"Termtab.Parser.GP5" is responsible for scaling it.
-}
data GP5Note = GP5Note
    { GP5Note -> Int
gnString :: Int
    , GP5Note -> Int
gnFret :: Int
    , GP5Note -> Int
gnDynamic :: Int
    }
    deriving (Int -> GP5Note -> ShowS
[GP5Note] -> ShowS
GP5Note -> String
(Int -> GP5Note -> ShowS)
-> (GP5Note -> String) -> ([GP5Note] -> ShowS) -> Show GP5Note
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> GP5Note -> ShowS
showsPrec :: Int -> GP5Note -> ShowS
$cshow :: GP5Note -> String
show :: GP5Note -> String
$cshowList :: [GP5Note] -> ShowS
showList :: [GP5Note] -> ShowS
Show, GP5Note -> GP5Note -> Bool
(GP5Note -> GP5Note -> Bool)
-> (GP5Note -> GP5Note -> Bool) -> Eq GP5Note
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: GP5Note -> GP5Note -> Bool
== :: GP5Note -> GP5Note -> Bool
$c/= :: GP5Note -> GP5Note -> Bool
/= :: GP5Note -> GP5Note -> Bool
Eq)

{- | One beat as read by 'getBeat'. 'gbDuration' is GP5's raw signed
duration code (see 'Termtab.Parser.GP5.decodeDuration' for the mapping),
not a 'Termtab.Types.Duration'.
-}
data GP5Beat = GP5Beat
    { GP5Beat -> Int
gbDuration :: Int
    , GP5Beat -> Bool
gbIsRest :: Bool
    , GP5Beat -> [GP5Note]
gbNotes :: [GP5Note]
    }
    deriving (Int -> GP5Beat -> ShowS
[GP5Beat] -> ShowS
GP5Beat -> String
(Int -> GP5Beat -> ShowS)
-> (GP5Beat -> String) -> ([GP5Beat] -> ShowS) -> Show GP5Beat
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> GP5Beat -> ShowS
showsPrec :: Int -> GP5Beat -> ShowS
$cshow :: GP5Beat -> String
show :: GP5Beat -> String
$cshowList :: [GP5Beat] -> ShowS
showList :: [GP5Beat] -> ShowS
Show, GP5Beat -> GP5Beat -> Bool
(GP5Beat -> GP5Beat -> Bool)
-> (GP5Beat -> GP5Beat -> Bool) -> Eq GP5Beat
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: GP5Beat -> GP5Beat -> Bool
== :: GP5Beat -> GP5Beat -> Bool
$c/= :: GP5Beat -> GP5Beat -> Bool
/= :: GP5Beat -> GP5Beat -> Bool
Eq)

{- | One measure of one track as read by 'getBar' — i.e. the beats for a
single (track, measure) cell of the file's bar grid.
-}
data GP5Bar = GP5Bar
    { GP5Bar -> [GP5Beat]
gbarBeats :: [GP5Beat]
    }
    deriving (Int -> GP5Bar -> ShowS
[GP5Bar] -> ShowS
GP5Bar -> String
(Int -> GP5Bar -> ShowS)
-> (GP5Bar -> String) -> ([GP5Bar] -> ShowS) -> Show GP5Bar
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> GP5Bar -> ShowS
showsPrec :: Int -> GP5Bar -> ShowS
$cshow :: GP5Bar -> String
show :: GP5Bar -> String
$cshowList :: [GP5Bar] -> ShowS
showList :: [GP5Bar] -> ShowS
Show, GP5Bar -> GP5Bar -> Bool
(GP5Bar -> GP5Bar -> Bool)
-> (GP5Bar -> GP5Bar -> Bool) -> Eq GP5Bar
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: GP5Bar -> GP5Bar -> Bool
== :: GP5Bar -> GP5Bar -> Bool
$c/= :: GP5Bar -> GP5Bar -> Bool
/= :: GP5Bar -> GP5Bar -> Bool
Eq)

-- | Read one byte as an unsigned 'Int' (0-255).
getU8 :: Get Int
getU8 :: Get Int
getU8 = Word8 -> Int
forall a b. (Integral a, Num b) => a -> b
fromIntegral (Word8 -> Int) -> Get Word8 -> Get Int
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Get Word8
getWord8

{- | Read one byte as a signed 'Int' (-128 to 127); plain 'Data.Binary.Get.getInt8'
with the sign already applied for callers working in 'Int'.
-}
getInt8' :: Get Int
getInt8' :: Get Int
getInt8' = do
    b <- Get Word8
getWord8
    let n = Word8 -> Int
forall a b. (Integral a, Num b) => a -> b
fromIntegral Word8
b :: Int
    return (if n >= 128 then n - 256 else n)

-- | Read a little-endian 32-bit signed integer as 'Int'.
getInt32le' :: Get Int
getInt32le' :: Get Int
getInt32le' = Int32 -> Int
forall a b. (Integral a, Num b) => a -> b
fromIntegral (Int32 -> Int) -> Get Int32 -> Get Int
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Get Int32
getInt32le

{- | Read a GP5 \"byte-size string\": a 1-byte length prefix followed by a
fixed-size, zero-padded field of @fieldLen@ bytes (only the first
/length/ bytes are meaningful). Used for fields with a fixed on-disk
width, e.g. track names.
-}
getByteSizeString :: Int -> Get T.Text
getByteSizeString :: Int -> Get Text
getByteSizeString Int
fieldLen = do
    strLen <- Get Int
getU8
    raw <- getByteString fieldLen
    return (TE.decodeUtf8 (BS.take strLen raw))

{- | Read a GP5 \"int-prefixed byte-size string\": a redundant 4-byte total
length (always @1 + str_len@, and discarded here), then a 1-byte
@str_len@, then @str_len@ bytes of UTF-8 text. This is GP5's most common
string encoding (score metadata, track names' long form, etc).
-}
getIntByteSizeString :: Get T.Text
getIntByteSizeString :: Get Text
getIntByteSizeString = do
    _total <- Get Int
getInt32le'
    strLen <- getU8
    raw <- getByteString strLen
    return (TE.decodeUtf8 raw)

{- | Read a plain length-prefixed string: a 4-byte length followed by that
many bytes of UTF-8 text (no redundant outer length, unlike
'getIntByteSizeString').
-}
getIntSizeString :: Get T.Text
getIntSizeString :: Get Text
getIntSizeString = do
    len <- Get Int
getInt32le'
    raw <- getByteString len
    return (TE.decodeUtf8 raw)

{- | Read the file's leading 30-byte version string and parse out
@(major, minor)@, e.g. @(5, 10)@ for @\"FICHIER GUITAR PRO v5.10\"@. Fails
(via @fail@) if the trailing @vX.Y@ suffix can't be found — this is the
first read 'Termtab.Parser.GP5.parseGP5File' performs, so a failure here
means the file isn't recognizable as any version of Guitar Pro at all.
-}
getVersion :: Get (Int, Int)
getVersion :: Get (Int, Int)
getVersion = do
    raw <- Int -> Get Text
getByteSizeString Int
30
    let s = Text -> String
T.unpack Text
raw
    case parseVersionSuffix s of
        Just (Int, Int)
v -> (Int, Int) -> Get (Int, Int)
forall a. a -> Get a
forall (m :: * -> *) a. Monad m => a -> m a
return (Int, Int)
v
        Maybe (Int, Int)
Nothing -> String -> Get (Int, Int)
forall a. String -> Get a
forall (m :: * -> *) a. MonadFail m => String -> m a
fail (String
"Unrecognised GP version string: " String -> ShowS
forall a. Semigroup a => a -> a -> a
<> String
s)

{- | Extract @(major, minor)@ from the last whitespace-separated token of a
version string (stripping a leading @v@), used by 'getVersion'.
-}
parseVersionSuffix :: String -> Maybe (Int, Int)
parseVersionSuffix :: String -> Maybe (Int, Int)
parseVersionSuffix String
s =
    case [String] -> [String]
forall a. [a] -> [a]
reverse (String -> [String]
words String
s) of
        (String
verStr : [String]
_) ->
            let stripped :: String
stripped = case String
verStr of Char
'v' : String
rest -> String
rest; String
_ -> String
verStr
             in case (Char -> Bool) -> String -> (String, String)
forall a. (a -> Bool) -> [a] -> ([a], [a])
break (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'.') String
stripped of
                    (String
majS, Char
'.' : String
minS) -> case (ReadS Int
forall a. Read a => ReadS a
reads String
majS, ReadS Int
forall a. Read a => ReadS a
reads String
minS) of
                        ([(Int
maj, String
"")], [(Int
mn, String
"")]) -> (Int, Int) -> Maybe (Int, Int)
forall a. a -> Maybe a
Just (Int
maj, Int
mn)
                        ([(Int, String)], [(Int, String)])
_ -> Maybe (Int, Int)
forall a. Maybe a
Nothing
                    (String, String)
_ -> case ReadS Int
forall a. Read a => ReadS a
reads String
stripped of
                        [(Int
maj, String
"")] -> (Int, Int) -> Maybe (Int, Int)
forall a. a -> Maybe a
Just (Int
maj, Int
0)
                        [(Int, String)]
_ -> Maybe (Int, Int)
forall a. Maybe a
Nothing
        [] -> Maybe (Int, Int)
forall a. Maybe a
Nothing

{- | Read the score info block: title through composer, then two more
copyright-ish strings, tab author, and instructions (all discarded),
then a variable number of \"notice\" strings whose count is given
in-line. Must run immediately after 'getVersion'.
-}
getScoreInfo :: Get GP5ScoreInfo
getScoreInfo :: Get GP5ScoreInfo
getScoreInfo = do
    title <- Get Text
getIntByteSizeString
    subtitle <- getIntByteSizeString
    artist <- getIntByteSizeString
    album <- getIntByteSizeString
    lyricist <- getIntByteSizeString
    composer <- getIntByteSizeString
    _copy1 <- getIntByteSizeString
    _copy2 <- getIntByteSizeString
    _tabAuthor <- getIntByteSizeString
    _instr <- getIntByteSizeString
    noticeCount <- getInt32le'
    replicateM_ noticeCount getIntByteSizeString
    return
        GP5ScoreInfo
            { gsiTitle = title
            , gsiSubtitle = subtitle
            , gsiArtist = artist
            , gsiAlbum = album
            , gsiLyricist = lyricist
            , gsiComposer = composer
            }

{- | Skip the lyrics block: a track number the lyrics apply to, followed by
5 lyric-line entries (each a starting-bar number plus a text blob), none
of which the domain model represents.
-}
skipLyrics :: Get ()
skipLyrics :: Get ()
skipLyrics = do
    _ <- Get Int
getInt32le'
    replicateM_ 5 $ do
        _ <- getInt32le'
        _ <- getIntSizeString
        return ()

{- | Skip the page setup block: 36 bytes of layout numbers followed by 11
header\/footer template strings. Irrelevant to playback or tab rendering.
-}
skipPageSetup :: Get ()
skipPageSetup :: Get ()
skipPageSetup = do
    Int -> Get ()
skip Int
36
    Int -> Get Text -> Get ()
forall (m :: * -> *) a. Applicative m => Int -> m a -> m ()
replicateM_ Int
11 Get Text
getIntByteSizeString

{- | Skip the RSE (Realistic Sound Engine) master effect block, whose
presence and size depends on the minor version: absent in GP5.0, 19
bytes in GP5.10.
-}
skipRSEMaster :: Int -> Get ()
skipRSEMaster :: Int -> Get ()
skipRSEMaster Int
minor = Bool -> Get () -> Get ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
when (Int
minor Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
>= Int
10) (Int -> Get ()
skip Int
19)

{- | Read the tempo's display name (e.g. \"Moderato\"); the numeric BPM is
read separately by the caller right after this.
-}
getTempoName :: Get T.Text
getTempoName :: Get Text
getTempoName = Get Text
getIntByteSizeString

{- | Read the file's 64-slot MIDI channel table, returning each channel's
program number and discarding its volume\/balance\/chorus\/reverb\/
phaser\/tremolo bytes. 'getTrack' looks up a track's program in this
list by its 1-based channel index.
-}
getChannels :: Get [Int]
getChannels :: Get [Int]
getChannels = Int -> Get Int -> Get [Int]
forall (m :: * -> *) a. Applicative m => Int -> m a -> m [a]
replicateM Int
64 Get Int
readChannel
  where
    readChannel :: Get Int
readChannel = do
        program <- Get Int
getInt32le'
        skip 7 -- volume, balance, chorus, reverb, phaser, tremolo (6 bytes) + padding (1 byte)
        return program

{- | Read one measure header. @idx@ is this measure's 0-based position
(needed only to detect the first measure's extra trailing byte); the
@(prevNum, prevDen)@ time signature and @(prevRoot, prevMode)@ key
signature are the previous measure's /effective/ values, reused whenever
this header's flag bits say the signature didn't change. Caller
("Termtab.Parser.GP5.parseMeasureHeaders") is responsible for threading
each result's values into the next call.
-}
getMeasureHeader :: Int -> (Int, Int) -> (Int, Int) -> Get GP5MeasureHeader
getMeasureHeader :: Int -> (Int, Int) -> (Int, Int) -> Get GP5MeasureHeader
getMeasureHeader Int
idx (Int
prevNum, Int
prevDen) (Int
prevRoot, Int
prevMode) = do
    flags <- Get Int
getU8
    (timeNum, timeDen) <-
        if flags `testBit` 0
            then do
                n <- getU8
                d <- getU8
                return (n, d)
            else return (prevNum, prevDen)
    when (flags `testBit` 2) (skip 1) -- repeat end count
    when (flags `testBit` 3) (skip 1) -- alternate ending
    (keyRoot, keyMode) <-
        if flags `testBit` 4
            then do
                r <- getInt8'
                m <- getU8
                return (r, m)
            else return (prevRoot, prevMode)
    -- GP5 has an extra byte after the first measure header and when marker is present
    when (idx == 0) (skip 1)
    return
        GP5MeasureHeader
            { gmhTimeNum = timeNum
            , gmhTimeDen = timeDen
            , gmhKeyRoot = keyRoot
            , gmhKeyMode = keyMode
            }

{- | Read one track header. @programs@ is the channel-to-program table from
'getChannels', used to resolve this track's channel index into an actual
MIDI program. The file always reserves 7 string-tuning slots regardless
of instrument; only the first @strCount@ are kept.
-}
getTrack :: [Int] -> Get GP5Track
getTrack :: [Int] -> Get GP5Track
getTrack [Int]
programs = do
    flags <- Get Int
getU8
    name <- getByteSizeString 40
    strCount <- getInt32le'
    -- 7 string slots, each 4 bytes
    allPitches <- replicateM 7 getInt32le'
    let pitches = Int -> [Int] -> [Int]
forall a. Int -> [a] -> [a]
take Int
strCount [Int]
allPitches
    _port <- getInt32le'
    chanIdx <- getInt32le'
    _chanFx <- getInt32le'
    _frets <- getInt32le'
    _height <- getInt32le'
    skip 4 -- color
    let prog =
            if Int
chanIdx Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
>= Int
1 Bool -> Bool -> Bool
&& Int
chanIdx Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= [Int] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [Int]
programs
                then [Int]
programs [Int] -> Int -> Int
forall a. HasCallStack => [a] -> Int -> a
!! (Int
chanIdx Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
1)
                else Int
0
    return
        GP5Track
            { gtName = name
            , gtStrings = pitches
            , gtChannel = chanIdx
            , gtProgram = prog
            , gtIsDrums = flags `testBit` 0
            }

{- | Read one beat: its rest flag and raw duration code, then optionally
skip a chord diagram, beat text, beat effects, and\/or a mix table
change depending on which of its flag bits are set, then read one
'GP5Note' for each string flagged present in the trailing string
bitmask.
-}
getBeat :: Get GP5Beat
getBeat :: Get GP5Beat
getBeat = do
    flags <- Get Int
getU8
    isRest <-
        if flags `testBit` 6 -- 0x40
            then (\Int
s -> Int
s Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
0x02) <$> getU8
            else return False
    durRaw <- getInt8'
    when (flags `testBit` 5) (skip 1) -- 0x20: tuplet
    when (flags `testBit` 1) skipChord -- 0x02: chord diagram
    when (flags `testBit` 2) (getIntSizeString >> return ()) -- 0x04: text
    when (flags `testBit` 3) skipBeatEffects -- 0x08: beat effects
    when (flags `testBit` 4) skipMixTable -- 0x10: mix table
    stringMask <- getU8
    let strNums = [Int
s | Int
s <- [Int
1 .. Int
7], Int
stringMask Int -> Int -> Bool
forall a. Bits a => a -> Int -> Bool
`testBit` (Int
s Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
1)]
    notes <- mapM getNote strNums
    return
        GP5Beat
            { gbDuration = durRaw
            , gbIsRest = isRest
            , gbNotes = notes
            }

{- | Read one note on string @strNum@ (the string number is passed in from
the beat's string bitmask — the on-disk \"string index\" byte read here
is redundant and discarded). Optionally skips a bend and\/or grace-note
block depending on flag bits; other effect types on a note (slides,
hammer-ons, harmonics — signaled at the beat-effects level, see
'skipBeatEffects') are not retained here at all.
-}
getNote :: Int -> Get GP5Note
getNote :: Int -> Get GP5Note
getNote Int
strNum = do
    flags <- Get Int
getU8
    _strIdx <- getU8 -- redundant string number
    fret <- getU8
    dynamic <- getU8
    when (flags `testBit` 3) skipBend -- 0x08: bend
    when (flags `testBit` 4) (skip 4) -- 0x10: grace note
    return
        GP5Note
            { gnString = strNum
            , gnFret = fret
            , gnDynamic = fromIntegral dynamic
            }

{- | Read one bar (one measure of one track). GP5 stores two independent
voices per bar; only the first (@voice0@) is kept, so a second voice
layered into the same measure in the original file is silently dropped.
-}
getBar :: Get GP5Bar
getBar :: Get GP5Bar
getBar = do
    voice0 <- Get [GP5Beat]
getVoice
    _voice1 <- getVoice
    return GP5Bar{gbarBeats = voice0}

-- | Read one voice: a 4-byte beat count followed by that many 'GP5Beat's.
getVoice :: Get [GP5Beat]
getVoice :: Get [GP5Beat]
getVoice = do
    beatCount <- Get Int
getInt32le'
    replicateM beatCount getBeat

{- | Skip a chord diagram attached to a beat. GP5 has two on-disk chord
formats distinguished by a leading flag byte (\"new\" vs. \"old\" format),
each with a different fixed layout; chord diagrams aren't represented in
the domain model at all.
-}
skipChord :: Get ()
skipChord :: Get ()
skipChord = do
    newFmt <- Get Int
getU8
    if newFmt == 1
        then do
            skip 16
            _ <- getByteSizeString 20
            skip 4
            replicateM_ 7 (skip 4)
            skip 32
        else do
            _ <- getByteSizeString 8
            skip 2
            replicateM_ 6 (getIntByteSizeString >> return ())

{- | Skip a beat's effects block (vibrato bar, tap\/slap\/pop, pick stroke
direction, tremolo picking, slide, harmonic), each field conditional on
its own flag bit across two flag bytes. None of these effects make it
into 'GP5Beat' — 'Termtab.Types.NoteEffect' values are not currently
populated by this parser.
-}
skipBeatEffects :: Get ()
skipBeatEffects :: Get ()
skipBeatEffects = do
    flags1 <- Get Int
getU8
    flags2 <- getU8
    when (flags1 `testBit` 0) skipBend -- vibrato bar
    when (flags1 `testBit` 1) (skip 1) -- tap/slap/pop type
    when (flags1 `testBit` 2) (skip 2) -- stroke down+up values
    when (flags2 `testBit` 5) (skip 1) -- tremolo picking duration
    when (flags2 `testBit` 6) (skip 1) -- slide type
    when (flags2 `testBit` 7) skipHarmonic -- harmonic type

{- | Skip a mix table change block (instrument, RSE info, and six mixer
parameters plus a display tempo), each parameter followed by its own
transition-duration byte when that parameter is actually present (a
value @>= 0@ signals \"present\" for these fields).
-}
skipMixTable :: Get ()
skipMixTable :: Get ()
skipMixTable = do
    instrument <- Get Int
getInt8'
    skip 16 -- RSE instrument info
    volume <- getInt8'
    balance <- getInt8'
    chorus <- getInt8'
    reverb <- getInt8'
    phaser <- getInt8'
    tremolo <- getInt8'
    _tempoStr <- getIntByteSizeString
    tempo <- getInt32le'
    when (instrument >= 0) (skip 1)
    when (volume >= 0) (skip 1)
    when (balance >= 0) (skip 1)
    when (chorus >= 0) (skip 1)
    when (reverb >= 0) (skip 1)
    when (phaser >= 0) (skip 1)
    when (tremolo >= 0) (skip 1)
    when (tempo >= 0) (skip 1)
    skip 1 -- use RSE flag

{- | Skip a bend (or vibrato bar) definition: a fixed 5-byte header
(type, value, and an unused byte) followed by a variable number of
11-byte bend points.
-}
skipBend :: Get ()
skipBend :: Get ()
skipBend = do
    Int -> Get ()
skip Int
5 -- type + value + unknown
    pointCount <- Get Int
getInt32le'
    skip (pointCount * 11)

{- | Skip a harmonic marker; only 'natural' harmonics have no extra payload
— artificial (semitone\/accidental\/octave) and tapped harmonics each
have their own fixed trailing size, selected by the harmonic type byte.
-}
skipHarmonic :: Get ()
skipHarmonic :: Get ()
skipHarmonic = do
    htype <- Get Int
getU8
    case htype of
        Int
2 -> Int -> Get ()
skip Int
3 -- semitone + accidental + octave
        Int
3 -> Int -> Get ()
skip Int
1 -- tapping harmonic fret
        Int
_ -> () -> Get ()
forall a. a -> Get a
forall (m :: * -> *) a. Monad m => a -> m a
return ()