diff --git a/dsf.go b/dsf.go index 8ca34f2..d598e97 100644 --- a/dsf.go +++ b/dsf.go @@ -7,12 +7,13 @@ package tag import ( "errors" "io" + "time" ) -// ReadDSFTags reads DSF metadata from the io.ReadSeeker, returning the resulting +// ReadDSFMeta reads DSF metadata from the io.ReadSeeker, returning the resulting // metadata in a Metadata implementation, or non-nil error if there was a problem. // samples: http://www.2l.no/hires/index.html -func ReadDSFTags(r io.ReadSeeker) (Metadata, error) { +func ReadDSFMeta(r io.ReadSeeker) (Metadata, error) { dsd, err := readString(r, 4) if err != nil { return nil, err @@ -31,79 +32,53 @@ func ReadDSFTags(r io.ReadSeeker) (Metadata, error) { return nil, err } - _, err = r.Seek(int64(id3Pointer), io.SeekStart) + _, err = r.Seek(int64(28), io.SeekCurrent) if err != nil { return nil, err } - id3, err := ReadID3v2Tags(r) + sampleRate, err := readUint32LittleEndian(r) if err != nil { return nil, err } - return metadataDSF{id3}, nil -} - -type metadataDSF struct { - id3 Metadata -} - -func (m metadataDSF) Format() Format { - return m.id3.Format() -} - -func (m metadataDSF) FileType() FileType { - return DSF -} - -func (m metadataDSF) Title() string { - return m.id3.Title() -} - -func (m metadataDSF) Album() string { - return m.id3.Album() -} - -func (m metadataDSF) Artist() string { - return m.id3.Artist() -} - -func (m metadataDSF) AlbumArtist() string { - return m.id3.AlbumArtist() -} - -func (m metadataDSF) Composer() string { - return m.id3.Composer() -} + _, err = r.Seek(int64(4), io.SeekCurrent) + if err != nil { + return nil, err + } -func (m metadataDSF) Year() int { - return m.id3.Year() -} + sampleNum, err := readUint64LittleEndian(r) + if err != nil { + return nil, err + } -func (m metadataDSF) Genre() string { - return m.id3.Genre() -} + duration := time.Second * (time.Duration(sampleNum) / time.Duration(sampleRate)) -func (m metadataDSF) Track() (int, int) { - return m.id3.Track() -} + _, err = r.Seek(int64(id3Pointer), io.SeekStart) + if err != nil { + return nil, err + } -func (m metadataDSF) Disc() (int, int) { - return m.id3.Disc() -} + id3, err := ReadID3v2Tags(r) + if err != nil { + return nil, err + } -func (m metadataDSF) Picture() *Picture { - return m.id3.Picture() + return metadataDSF{ + metadataID3v2: id3, + duration: duration, + }, nil } -func (m metadataDSF) Lyrics() string { - return m.id3.Lyrics() +type metadataDSF struct { + *metadataID3v2 + duration time.Duration } -func (m metadataDSF) Comment() string { - return m.id3.Comment() +func (m metadataDSF) FileType() FileType { + return DSF } -func (m metadataDSF) Raw() map[string]interface{} { - return m.id3.Raw() +func (m metadataDSF) Duration() time.Duration { + return m.duration } diff --git a/flac.go b/flac.go index c370467..b1cae9a 100644 --- a/flac.go +++ b/flac.go @@ -6,7 +6,9 @@ package tag import ( "errors" + "fmt" "io" + "time" ) // blockType is a type which represents an enumeration of valid FLAC blocks @@ -14,18 +16,18 @@ type blockType byte // FLAC block types. const ( - // Stream Info Block 0 // Padding Block 1 // Application Block 2 // Seektable Block 3 // Cue Sheet Block 5 + streamInfoBlock blockType = 0 vorbisCommentBlock blockType = 4 pictureBlock blockType = 6 ) -// ReadFLACTags reads FLAC metadata from the io.ReadSeeker, returning the resulting +// ReadFLACMeta reads FLAC metadata from the io.ReadSeeker, returning the resulting // metadata in a Metadata implementation, or non-nil error if there was a problem. -func ReadFLACTags(r io.ReadSeeker) (Metadata, error) { +func ReadFLACMeta(r io.ReadSeeker) (Metadata, error) { flac, err := readString(r, 4) if err != nil { return nil, err @@ -35,11 +37,11 @@ func ReadFLACTags(r io.ReadSeeker) (Metadata, error) { } m := &metadataFLAC{ - newMetadataVorbis(), + metadataVorbis: newMetadataVorbis(), } for { - last, err := m.readFLACMetadataBlock(r) + last, err := m.readFLACBlock(r) if err != nil { return nil, err } @@ -53,9 +55,10 @@ func ReadFLACTags(r io.ReadSeeker) (Metadata, error) { type metadataFLAC struct { *metadataVorbis + duration time.Duration } -func (m *metadataFLAC) readFLACMetadataBlock(r io.ReadSeeker) (last bool, err error) { +func (m *metadataFLAC) readFLACBlock(r io.ReadSeeker) (last bool, err error) { blockHeader, err := readBytes(r, 1) if err != nil { return @@ -78,12 +81,40 @@ func (m *metadataFLAC) readFLACMetadataBlock(r io.ReadSeeker) (last bool, err er case pictureBlock: err = m.readPictureBlock(r) + case streamInfoBlock: + err = m.readStreamingInfoBlock(r, blockLen) + default: _, err = r.Seek(int64(blockLen), io.SeekCurrent) } return } +func (m *metadataFLAC) readStreamingInfoBlock(r io.Reader, len int) error { + data := make([]byte, len) + if _, err := r.Read(data); err != nil { + return err + } + + sampleRate, err := cutBits(data, 80, 20) + if err != nil { + return fmt.Errorf("reading sample rate: %w", err) + } + + sampleNum, err := cutBits(data, 108, 36) + if err != nil { + return fmt.Errorf("reading sample number: %w", err) + } + + m.duration = time.Second * (time.Duration(sampleNum) / time.Duration(sampleRate)) + + return nil +} + func (m *metadataFLAC) FileType() FileType { return FLAC } + +func (m *metadataFLAC) Duration() time.Duration { + return m.duration +} diff --git a/go.mod b/go.mod index e1aa28a..2985429 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/dhowden/tag -go 1.20 +go 1.22 require github.com/dhowden/itl v0.0.0-20170329215456-9fbe21093131 diff --git a/id3v1.go b/id3v1.go index 0953f0b..e28f55d 100644 --- a/id3v1.go +++ b/id3v1.go @@ -42,7 +42,7 @@ var ErrNotID3v1 = errors.New("invalid ID3v1 header") // ReadID3v1Tags reads ID3v1 tags from the io.ReadSeeker. Returns ErrNotID3v1 // if there are no ID3v1 tags, otherwise non-nil error if there was a problem. -func ReadID3v1Tags(r io.ReadSeeker) (Metadata, error) { +func ReadID3v1Tags(r io.ReadSeeker) (metadataID3v1, error) { _, err := r.Seek(-128, io.SeekEnd) if err != nil { return nil, err diff --git a/id3v2.go b/id3v2.go index fe33685..60533fb 100644 --- a/id3v2.go +++ b/id3v2.go @@ -60,6 +60,7 @@ type id3v2Header struct { Unsynchronisation bool ExtendedHeader bool Experimental bool + FooterPresent bool Size uint } @@ -97,6 +98,7 @@ func readID3v2Header(r io.Reader) (h *id3v2Header, offset uint, err error) { Unsynchronisation: getBit(b[2], 7), ExtendedHeader: getBit(b[2], 6), Experimental: getBit(b[2], 5), + FooterPresent: getBit(b[2], 4), Size: uint(get7BitChunkedInt(b[3:7])), } @@ -425,7 +427,7 @@ func (r *unsynchroniser) Read(p []byte) (int, error) { // ReadID3v2Tags parses ID3v2.{2,3,4} tags from the io.ReadSeeker into a Metadata, returning // non-nil error on failure. -func ReadID3v2Tags(r io.ReadSeeker) (Metadata, error) { +func ReadID3v2Tags(r io.ReadSeeker) (*metadataID3v2, error) { h, offset, err := readID3v2Header(r) if err != nil { return nil, err @@ -440,12 +442,13 @@ func ReadID3v2Tags(r io.ReadSeeker) (Metadata, error) { if err != nil { return nil, err } - return metadataID3v2{header: h, frames: f}, nil + + return &metadataID3v2{header: h, frames: f}, nil } var id3v2genreRe = regexp.MustCompile(`(.*[^(]|.* |^)\(([0-9]+)\) *(.*)$`) -// id3v2genre parse a id3v2 genre tag and expand the numeric genres +// id3v2genre parse a id3v2 genre tag and expand the numeric genres func id3v2genre(genre string) string { c := true for c { diff --git a/mp3.go b/mp3.go new file mode 100644 index 0000000..04c8eda --- /dev/null +++ b/mp3.go @@ -0,0 +1,220 @@ +package tag + +import ( + "fmt" + "io" + "math" + "time" +) + +type mpegVersion int + +const ( + mpeg25 mpegVersion = iota + mpegReserved + mpeg2 + mpeg1 + mpegMax +) + +type mpegLayer int + +const ( + layerReserved mpegLayer = iota + layer3 + layer2 + layer1 + layerMax +) + +// Took from: https://github.com/tcolgate/mp3/blob/master/frames.go +var ( + bitrates = [mpegMax][layerMax][15]int{ + { // MPEG 2.5 + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // LayerReserved + {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160}, // Layer3 + {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160}, // Layer2 + {0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256}, // Layer1 + }, + { // Reserved + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // LayerReserved + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Layer3 + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Layer2 + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // Layer1 + }, + { // MPEG 2 + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // LayerReserved + {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160}, // Layer3 + {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160}, // Layer2 + {0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256}, // Layer1 + }, + { // MPEG 1 + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // LayerReserved + {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320}, // Layer3 + {0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384}, // Layer2 + {0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448}, // Layer1 + }, + } + sampleRates = [int(mpegMax)][3]int{ + {11025, 12000, 8000}, //MPEG25 + {0, 0, 0}, //MPEGReserved + {22050, 24000, 16000}, //MPEG2 + {44100, 48000, 32000}, //MPEG1 + } + + samplesPerFrame = [mpegMax][layerMax]int{ + { // MPEG25 + 0, + 576, + 1152, + 384, + }, + { // Reserved + 0, + 0, + 0, + 0, + }, + { // MPEG2 + 0, + 576, + 1152, + 384, + }, + { // MPEG1 + 0, + 1152, + 1152, + 384, + }, + } + slotSize = [layerMax]int{ + 0, // LayerReserved + 1, // Layer3 + 1, // Layer2 + 4, // Layer1 + } +) + +type metadataV2MP3 struct { + *metadataID3v2 + duration time.Duration +} + +type metadataV1MP3 struct { + *metadataID3v1 + duration time.Duration +} + +func getMP3Duration(header []byte, strippedSize int64) (time.Duration, error) { + version, err := cutBits(header, 11, 2) + if err != nil { + return 0, fmt.Errorf("reading mpeg version: %w", err) + } + layer, err := cutBits(header, 13, 2) + if err != nil { + return 0, fmt.Errorf("reading mpeg layer: %w", err) + } + protection, err := cutBits(header, 15, 1) + if err != nil { + return 0, fmt.Errorf("reading mpeg protection: %w", err) + } + bitrateIndex, err := cutBits(header, 16, 4) + if err != nil { + return 0, fmt.Errorf("reading mpeg bitrate index: %w", err) + } + samplerateIndex, err := cutBits(header, 20, 2) + if err != nil { + return 0, fmt.Errorf("reading mpeg samplerate index: %w", err) + } + padding, err := cutBits(header, 21, 1) + if err != nil { + return 0, fmt.Errorf("reading mpeg padding bit: %w", err) + } + frameSampleNum := samplesPerFrame[version][layer] + frameDuration := float64(frameSampleNum) / float64(sampleRates[version][samplerateIndex]) + frameSize := math.Floor(((frameDuration * float64(bitrates[version][layer][bitrateIndex])) * 1000) / 8) + if padding == 1 { + frameSize += float64(slotSize[layer]) + } + if protection == 1 { + frameSize += 2 + } + // add the header length + frameSize += 4 + duration := time.Second * time.Duration(math.Round((float64(strippedSize)/float64(frameSize))*frameDuration)) + + return duration, nil +} + +func ReadV2MP3Meta(r io.ReadSeeker, size int64) (Metadata, error) { + tagMeta, err := ReadID3v2Tags(r) + if err != nil { + return nil, fmt.Errorf("reading id3v2 tags: %w", err) + } + + id3Size := tagMeta.header.Size + 10 + if tagMeta.header.FooterPresent { + id3Size += 10 + } + _, err = r.Seek(int64(id3Size), io.SeekStart) + if err != nil { + return nil, fmt.Errorf("seeking to skip id3v2: %w", err) + + } + + header := make([]byte, 4) + _, err = io.ReadFull(r, header) + if err != nil { + return nil, fmt.Errorf("reading first frame header: %w", err) + } + + duration, err := getMP3Duration(header, size-int64(id3Size)) + if err != nil { + return nil, fmt.Errorf("reading the mp3 duration: %w", err) + } + + return &metadataV2MP3{ + metadataID3v2: tagMeta, + duration: duration, + }, nil + +} + +func ReadV1MP3Meta(r io.ReadSeeker, size int64) (Metadata, error) { + tagMeta, err := ReadID3v1Tags(r) + if err != nil { + return nil, fmt.Errorf("reading id3v2 tags: %w", err) + } + + _, err = r.Seek(0, io.SeekStart) + if err != nil { + return nil, fmt.Errorf("seeking to the start: %w", err) + + } + + header := make([]byte, 4) + _, err = io.ReadFull(r, header) + if err != nil { + return nil, fmt.Errorf("reading first frame header: %w", err) + } + + duration, err := getMP3Duration(header, size-128) + if err != nil { + return nil, fmt.Errorf("reading the mp3 duration: %w", err) + } + + return &metadataV1MP3{ + metadataID3v1: &tagMeta, + duration: duration, + }, nil + +} + +func (m *metadataV2MP3) Duration() time.Duration { + return m.duration +} + +func (m *metadataV1MP3) Duration() time.Duration { + return m.duration +} diff --git a/mp4.go b/mp4.go index 19c1a81..8f8def1 100644 --- a/mp4.go +++ b/mp4.go @@ -12,6 +12,7 @@ import ( "io" "strconv" "strings" + "time" ) var atomTypes = map[int]string{ @@ -70,6 +71,7 @@ func (f atomNames) Name(n string) []string { type metadataMP4 struct { fileType FileType data map[string]interface{} + duration time.Duration } // ReadAtoms reads MP4 metadata atoms from the io.ReadSeeker into a Metadata, returning @@ -83,7 +85,7 @@ func ReadAtoms(r io.ReadSeeker) (Metadata, error) { return m, err } -func (m metadataMP4) readAtoms(r io.ReadSeeker) error { +func (m *metadataMP4) readAtoms(r io.ReadSeeker) error { for { name, size, err := readAtomHeader(r) if err != nil { @@ -104,6 +106,26 @@ func (m metadataMP4) readAtoms(r io.ReadSeeker) error { case "moov", "udta", "ilst": return m.readAtoms(r) + case "mvhd": + _, err = r.Seek(12, io.SeekCurrent) + if err != nil { + return err + } + sampleRate, err := readUint32BigEndian(r) + if err != nil { + return err + } + sampleNum, err := readUint32BigEndian(r) + if err != nil { + return err + } + m.duration = time.Second * (time.Duration(sampleNum) / time.Duration(sampleRate)) + + _, err = r.Seek(int64(size-8-12-8), io.SeekCurrent) + if err != nil { + return err + } + continue } _, ok := atoms[name] @@ -135,7 +157,7 @@ func (m metadataMP4) readAtoms(r io.ReadSeeker) error { } } -func (m metadataMP4) readAtomData(r io.ReadSeeker, name string, size uint32, processedData []string) error { +func (m *metadataMP4) readAtomData(r io.ReadSeeker, name string, size uint32, processedData []string) error { var b []byte var err error var contentType string @@ -377,3 +399,7 @@ func (m metadataMP4) Picture() *Picture { p, _ := v.(*Picture) return p } + +func (m metadataMP4) Duration() time.Duration { + return m.duration +} diff --git a/ogg.go b/ogg.go index f5c4770..db32f5e 100644 --- a/ogg.go +++ b/ogg.go @@ -10,11 +10,13 @@ import ( "errors" "fmt" "io" + "time" ) var ( - vorbisCommentPrefix = []byte("\x03vorbis") - opusTagsPrefix = []byte("OpusTags") + vorbisIdentificationPrefix = []byte("\x01vorbis") + vorbisCommentPrefix = []byte("\x03vorbis") + opusTagsPrefix = []byte("OpusTags") ) var oggCRC32Poly04c11db7 = oggCRCTable(0x04c11db7) @@ -63,21 +65,21 @@ type oggDemuxer struct { // Read ogg packets, can return empty slice of packets and nil err // if more data is needed -func (o *oggDemuxer) Read(r io.Reader) ([][]byte, error) { +func (o *oggDemuxer) Read(r io.Reader) ([][]byte, int, error) { headerBuf := &bytes.Buffer{} var oh oggPageHeader if err := binary.Read(io.TeeReader(r, headerBuf), binary.LittleEndian, &oh); err != nil { - return nil, err + return nil, 0, err } if bytes.Compare(oh.Magic[:], []byte("OggS")) != 0 { // TODO: seek for syncword? - return nil, errors.New("expected 'OggS'") + return nil, 0, errors.New("expected 'OggS'") } segmentTable := make([]byte, oh.Segments) if _, err := io.ReadFull(r, segmentTable); err != nil { - return nil, err + return nil, 0, err } var segmentsSize int64 for _, s := range segmentTable { @@ -85,7 +87,7 @@ func (o *oggDemuxer) Read(r io.Reader) ([][]byte, error) { } segmentsData := make([]byte, segmentsSize) if _, err := io.ReadFull(r, segmentsData); err != nil { - return nil, err + return nil, 0, err } headerBytes := headerBuf.Bytes() @@ -98,7 +100,7 @@ func (o *oggDemuxer) Read(r io.Reader) ([][]byte, error) { crc = oggCRCUpdate(crc, oggCRC32Poly04c11db7, segmentTable) crc = oggCRCUpdate(crc, oggCRC32Poly04c11db7, segmentsData) if crc != oh.CRC { - return nil, fmt.Errorf("expected crc %x != %x", oh.CRC, crc) + return nil, 0, fmt.Errorf("expected crc %x != %x", oh.CRC, crc) } if o.packetBufs == nil { @@ -111,7 +113,7 @@ func (o *oggDemuxer) Read(r io.Reader) ([][]byte, error) { if b, ok := o.packetBufs[oh.SerialNumber]; ok { packetBuf = b } else { - return nil, fmt.Errorf("could not find continued packet %d", oh.SerialNumber) + return nil, 0, fmt.Errorf("could not find continued packet %d", oh.SerialNumber) } } else { packetBuf = &bytes.Buffer{} @@ -130,35 +132,50 @@ func (o *oggDemuxer) Read(r io.Reader) ([][]byte, error) { o.packetBufs[oh.SerialNumber] = packetBuf - return packets, nil + return packets, int(oh.GranulePosition), nil } -// ReadOGGTags reads OGG metadata from the io.ReadSeeker, returning the resulting +// ReadOGGMeta reads OGG metadata from the io.ReadSeeker, returning the resulting // metadata in a Metadata implementation, or non-nil error if there was a problem. // See http://www.xiph.org/vorbis/doc/Vorbis_I_spec.html // and http://www.xiph.org/ogg/doc/framing.html for details. // For Opus see https://tools.ietf.org/html/rfc7845 -func ReadOGGTags(r io.Reader) (Metadata, error) { +func ReadOGGMeta(r io.Reader) (Metadata, error) { od := &oggDemuxer{} + metaExtracted := false + m := &metadataOGG{ + metadataVorbis: newMetadataVorbis(), + } + prevPos := 0 for { - bs, err := od.Read(r) - if err != nil { + bs, pos, err := od.Read(r) + if err != nil && !errors.Is(err, io.EOF) { return nil, err } + if errors.Is(err, io.EOF) { + if !metaExtracted { + return nil, ErrNoTagsFound + } + if m.sampleRate > 0 { + m.duration = time.Second * (time.Duration(prevPos) / time.Duration(m.sampleRate)) + } + return m, nil + } + prevPos = pos for _, b := range bs { switch { case bytes.HasPrefix(b, vorbisCommentPrefix): - m := &metadataOGG{ - newMetadataVorbis(), - } + metaExtracted = true err = m.readVorbisComment(bytes.NewReader(b[len(vorbisCommentPrefix):])) - return m, err case bytes.HasPrefix(b, opusTagsPrefix): - m := &metadataOGG{ - newMetadataVorbis(), - } + metaExtracted = true err = m.readVorbisComment(bytes.NewReader(b[len(opusTagsPrefix):])) + m.sampleRate = 48000 + case bytes.HasPrefix(b, vorbisIdentificationPrefix): + err = m.readVorbisIdentification(bytes.NewReader(b[len(vorbisIdentificationPrefix):])) + } + if err != nil { return m, err } } @@ -167,8 +184,26 @@ func ReadOGGTags(r io.Reader) (Metadata, error) { type metadataOGG struct { *metadataVorbis + sampleRate uint32 + duration time.Duration } func (m *metadataOGG) FileType() FileType { return OGG } + +func (m *metadataOGG) Duration() time.Duration { + return m.duration +} + +func (m *metadataOGG) readVorbisIdentification(r io.ReadSeeker) error { + _, err := r.Seek(5, io.SeekCurrent) + if err != nil { + return err + } + m.sampleRate, err = readUint32LittleEndian(r) + if err != nil { + return err + } + return nil +} diff --git a/tag.go b/tag.go index 306f1d7..89d0991 100644 --- a/tag.go +++ b/tag.go @@ -6,18 +6,20 @@ // parsing and artwork extraction. // // Detect and parse tag metadata from an io.ReadSeeker (i.e. an *os.File): -// m, err := tag.ReadFrom(f) -// if err != nil { -// log.Fatal(err) -// } -// log.Print(m.Format()) // The detected format. -// log.Print(m.Title()) // The title of the track (see Metadata interface for more details). +// +// m, err := tag.ReadFrom(f) +// if err != nil { +// log.Fatal(err) +// } +// log.Print(m.Format()) // The detected format. +// log.Print(m.Title()) // The title of the track (see Metadata interface for more details). package tag import ( "errors" "fmt" "io" + "time" ) // ErrNoTagsFound is the error returned by ReadFrom when the metadata format @@ -40,29 +42,53 @@ func ReadFrom(r io.ReadSeeker) (Metadata, error) { switch { case string(b[0:4]) == "fLaC": - return ReadFLACTags(r) + return ReadFLACMeta(r) case string(b[0:4]) == "OggS": - return ReadOGGTags(r) + return ReadOGGMeta(r) case string(b[4:8]) == "ftyp": return ReadAtoms(r) case string(b[0:3]) == "ID3": - return ReadID3v2Tags(r) + size, err := getFileSize(r) + if err != nil { + return nil, fmt.Errorf("could not get file size: %w", err) + } + return ReadV2MP3Meta(r, size) + + case b[0] == 0xff && (b[1] == 0xfb || b[2] == 0xf3 || b[3] == 0xf2): + size, err := getFileSize(r) + if err != nil { + return nil, fmt.Errorf("could not get file size: %w", err) + } + return ReadV1MP3Meta(r, size) case string(b[0:4]) == "DSD ": - return ReadDSFTags(r) + return ReadDSFMeta(r) } - m, err := ReadID3v1Tags(r) + return nil, errors.ErrUnsupported +} + +func getFileSize(r io.ReadSeeker) (int64, error) { + current, err := r.Seek(0, io.SeekCurrent) if err != nil { - if err == ErrNotID3v1 { - err = ErrNoTagsFound - } - return nil, err + return 0, fmt.Errorf("could not get current pos: %w", err) } - return m, nil + + size, err := r.Seek(0, io.SeekEnd) + if err != nil { + return 0, fmt.Errorf("could not seek to end pos: %w", err) + } + + _, err = r.Seek(current, io.SeekStart) + if err != nil { + return 0, fmt.Errorf("could not reset current pos: %w", err) + } + + return size, nil + } // Format is an enumeration of metadata types supported by this package. @@ -144,4 +170,6 @@ type Metadata interface { // Raw returns the raw mapping of retrieved tag names and associated values. // NB: tag/atom names are not standardised between formats. Raw() map[string]interface{} + + Duration() time.Duration } diff --git a/util.go b/util.go index c738d2f..0b4556f 100644 --- a/util.go +++ b/util.go @@ -7,6 +7,7 @@ package tag import ( "bytes" "encoding/binary" + "errors" "io" ) @@ -100,3 +101,45 @@ func readUint32LittleEndian(r io.Reader) (uint32, error) { } return binary.LittleEndian.Uint32(b), nil } + +func readUint32BigEndian(r io.Reader) (uint32, error) { + b, err := readBytes(r, 4) + if err != nil { + return 0, err + } + return binary.BigEndian.Uint32(b), nil +} + +func cutBits(in []byte, offset, n uint) (uint64, error) { + if n > 64 { + return 0, errors.New("n exceeds maximum value of 64") + } + if len(in)*8 < int(offset+n) { + return 0, errors.New("out of bounds read") + } + var res uint64 + var bitsRead uint + if splitStart := offset % 8; splitStart > 0 { + remaining := 8 - splitStart + splitByte := uint64(in[int(offset/8)]) & ((1 << remaining) - 1) + if n <= remaining { + return uint64(splitByte) >> uint64(remaining-n), nil + } + bitsRead = remaining + res |= splitByte + } + + wholeBytes := int((n - bitsRead) / 8) + start := int((offset + bitsRead) / 8) + for i := range wholeBytes { + res <<= 8 + res |= uint64(in[start+i]) + bitsRead += 8 + } + + if remaining := n - bitsRead; remaining > 0 { + res <<= remaining + res |= uint64(in[start+wholeBytes]) >> (8 - remaining) + } + return res, nil +}