package meta import ( "bytes" "encoding/binary" "errors" "fmt" "io" "io/ioutil" ) // A CueSheet describes how tracks are laid out within a FLAC stream. // // ref: https://www.xiph.org/flac/format.html#metadata_block_cuesheet type CueSheet struct { // Media catalog number. MCN string // Number of lead-in samples. This field only has meaning for CD-DA cue // sheets; for other uses it should be 0. Refer to the spec for additional // information. NLeadInSamples uint64 // Specifies if the cue sheet corresponds to a Compact Disc. IsCompactDisc bool // One or more tracks. The last track of a cue sheet is always the lead-out // track. Tracks []CueSheetTrack } // parseCueSheet reads and parses the body of a CueSheet metadata block. func (block *Block) parseCueSheet() error { // Parse cue sheet. // 128 bytes: MCN. buf, err := readBytes(block.lr, 128) if err != nil { return unexpected(err) } cs := &CueSheet{ MCN: stringFromSZ(buf), } block.Body = cs // 64 bits: NLeadInSamples. if err = binary.Read(block.lr, binary.BigEndian, &cs.NLeadInSamples); err != nil { return unexpected(err) } // 1 bit: IsCompactDisc. var x uint8 if err := binary.Read(block.lr, binary.BigEndian, &x); err != nil { return unexpected(err) } // mask = 10000000 if x&0x80 != 0 { cs.IsCompactDisc = true } // 7 bits and 258 bytes: reserved. // mask = 01111111 if x&0x7F != 0 { return ErrInvalidPadding } lr := io.LimitReader(block.lr, 258) zr := zeros{r: lr} if _, err := io.Copy(ioutil.Discard, zr); err != nil { return err } // Parse cue sheet tracks. // 8 bits: (number of tracks) if err := binary.Read(block.lr, binary.BigEndian, &x); err != nil { return unexpected(err) } if x < 1 { return errors.New("meta.Block.parseCueSheet: at least one track required") } if cs.IsCompactDisc && x > 100 { return fmt.Errorf("meta.Block.parseCueSheet: number of CD-DA tracks (%d) exceeds 100", x) } cs.Tracks = make([]CueSheetTrack, x) // Each track number within a cue sheet must be unique; use uniq to keep // track. uniq := make(map[uint8]struct{}) for i := range cs.Tracks { if err := block.parseTrack(cs, i, uniq); err != nil { return err } } return nil } // parseTrack parses the i:th cue sheet track, and ensures that its track number // is unique. func (block *Block) parseTrack(cs *CueSheet, i int, uniq map[uint8]struct{}) error { track := &cs.Tracks[i] // 64 bits: Offset. if err := binary.Read(block.lr, binary.BigEndian, &track.Offset); err != nil { return unexpected(err) } if cs.IsCompactDisc && track.Offset%588 != 0 { return fmt.Errorf("meta.Block.parseCueSheet: CD-DA track offset (%d) must be evenly divisible by 588", track.Offset) } // 8 bits: Num. if err := binary.Read(block.lr, binary.BigEndian, &track.Num); err != nil { return unexpected(err) } if _, ok := uniq[track.Num]; ok { return fmt.Errorf("meta.Block.parseCueSheet: duplicated track number %d", track.Num) } uniq[track.Num] = struct{}{} if track.Num == 0 { return errors.New("meta.Block.parseCueSheet: invalid track number (0)") } isLeadOut := i == len(cs.Tracks)-1 if cs.IsCompactDisc { if !isLeadOut { if track.Num >= 100 { return fmt.Errorf("meta.Block.parseCueSheet: CD-DA track number (%d) exceeds 99", track.Num) } } else { if track.Num != 170 { return fmt.Errorf("meta.Block.parseCueSheet: invalid lead-out CD-DA track number; expected 170, got %d", track.Num) } } } else { if isLeadOut && track.Num != 255 { return fmt.Errorf("meta.Block.parseCueSheet: invalid lead-out track number; expected 255, got %d", track.Num) } } // 12 bytes: ISRC. buf, err := readBytes(block.lr, 12) if err != nil { return unexpected(err) } track.ISRC = stringFromSZ(buf) // 1 bit: IsAudio. var x uint8 if err = binary.Read(block.lr, binary.BigEndian, &x); err != nil { return unexpected(err) } // mask = 10000000 if x&0x80 == 0 { track.IsAudio = true } // 1 bit: HasPreEmphasis. // mask = 01000000 if x&0x40 != 0 { track.HasPreEmphasis = true } // 6 bits and 13 bytes: reserved. // mask = 00111111 if x&0x3F != 0 { return ErrInvalidPadding } lr := io.LimitReader(block.lr, 13) zr := zeros{r: lr} _, err = io.Copy(ioutil.Discard, zr) if err != nil { return err } // Parse indicies. // 8 bits: (number of indicies) if err = binary.Read(block.lr, binary.BigEndian, &x); err != nil { return unexpected(err) } if x < 1 { if !isLeadOut { return errors.New("meta.Block.parseCueSheet: at least one track index required") } // Lead-out track has no track indices to parse; return early. return nil } track.Indicies = make([]CueSheetTrackIndex, x) for i := range track.Indicies { index := &track.Indicies[i] // 64 bits: Offset. if err = binary.Read(block.lr, binary.BigEndian, &index.Offset); err != nil { return unexpected(err) } // 8 bits: Num. if err = binary.Read(block.lr, binary.BigEndian, &index.Num); err != nil { return unexpected(err) } // 3 bytes: reserved. lr = io.LimitReader(block.lr, 3) zr = zeros{r: lr} _, err = io.Copy(ioutil.Discard, zr) if err != nil { return err } } return nil } // stringFromSZ converts the provided byte slice to a string after terminating // it at the first occurrence of a NULL character. func stringFromSZ(buf []byte) string { pos := bytes.IndexByte(buf, 0) if pos == -1 { return string(buf) } return string(buf[:pos]) } // CueSheetTrack contains the start offset of a track and other track specific // metadata. type CueSheetTrack struct { // Track offset in samples, relative to the beginning of the FLAC audio // stream. Offset uint64 // Track number; never 0, always unique. Num uint8 // International Standard Recording Code; empty string if not present. // // ref: http://isrc.ifpi.org/ ISRC string // Specifies if the track contains audio or data. IsAudio bool // Specifies if the track has been recorded with pre-emphasis HasPreEmphasis bool // Every track has one or more track index points, except for the lead-out // track which has zero. Each index point specifies a position within the // track. Indicies []CueSheetTrackIndex } // A CueSheetTrackIndex specifies a position within a track. type CueSheetTrackIndex struct { // Index point offset in samples, relative to the track offset. Offset uint64 // Index point number; subsequently incrementing by 1 and always unique // within a track. Num uint8 }