This commit is contained in:
2025-04-19 22:56:37 +08:00
commit ceca244eaf
50 changed files with 7321 additions and 0 deletions

38
meta/application.go Executable file
View File

@@ -0,0 +1,38 @@
package meta
import (
"encoding/binary"
"io/ioutil"
)
// Application contains third party application specific data.
//
// ref: https://www.xiph.org/flac/format.html#metadata_block_application
type Application struct {
// Registered application ID.
//
// ref: https://www.xiph.org/flac/id.html
ID uint32
// Application data.
Data []byte
}
// parseApplication reads and parses the body of an Application metadata block.
func (block *Block) parseApplication() error {
// 32 bits: ID.
app := new(Application)
block.Body = app
err := binary.Read(block.lr, binary.BigEndian, &app.ID)
if err != nil {
return unexpected(err)
}
// Check if the Application block only contains an ID.
if block.Length == 4 {
return nil
}
// (block length)-4 bytes: Data.
app.Data, err = ioutil.ReadAll(block.lr)
return unexpected(err)
}

242
meta/cuesheet.go Executable file
View File

@@ -0,0 +1,242 @@
package meta
import (
"encoding/binary"
"errors"
"fmt"
"io"
"io/ioutil"
"strings"
)
// 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.
szMCN, err := readString(block.lr, 128)
if err != nil {
return unexpected(err)
}
cs := &CueSheet{
MCN: stringFromSZ(szMCN),
}
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.
szISRC, err := readString(block.lr, 12)
if err != nil {
return unexpected(err)
}
track.ISRC = stringFromSZ(szISRC)
// 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 returns a copy of the given string terminated at the first
// occurrence of a NULL character.
func stringFromSZ(szStr string) string {
pos := strings.IndexByte(szStr, '\x00')
if pos == -1 {
return szStr
}
return string(szStr[: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
}

210
meta/meta.go Executable file
View File

@@ -0,0 +1,210 @@
// Package meta implements access to FLAC metadata blocks.
//
// A brief introduction of the FLAC metadata format [1] follows. FLAC metadata
// is stored in blocks; each block contains a header followed by a body. The
// block header describes the type of the block body, its length in bytes, and
// specifies if the block was the last metadata block in a FLAC stream. The
// contents of the block body depends on the type specified in the block header.
//
// At the time of this writing, the FLAC metadata format defines seven different
// metadata block types, namely:
// - StreamInfo [2]
// - Padding [3]
// - Application [4]
// - SeekTable [5]
// - VorbisComment [6]
// - CueSheet [7]
// - Picture [8]
//
// Please refer to their respective documentation for further information.
//
// [1]: https://www.xiph.org/flac/format.html#format_overview
// [2]: https://godoc.org/github.com/mewkiz/flac/meta#StreamInfo
// [3]: https://www.xiph.org/flac/format.html#metadata_block_padding
// [4]: https://godoc.org/github.com/mewkiz/flac/meta#Application
// [5]: https://godoc.org/github.com/mewkiz/flac/meta#SeekTable
// [6]: https://godoc.org/github.com/mewkiz/flac/meta#VorbisComment
// [7]: https://godoc.org/github.com/mewkiz/flac/meta#CueSheet
// [8]: https://godoc.org/github.com/mewkiz/flac/meta#Picture
package meta
import (
"errors"
"io"
"io/ioutil"
"github.com/mewkiz/flac/internal/bits"
)
// A Block contains the header and body of a metadata block.
//
// ref: https://www.xiph.org/flac/format.html#metadata_block
type Block struct {
// Metadata block header.
Header
// Metadata block body of type *StreamInfo, *Application, ... etc. Body is
// initially nil, and gets populated by a call to Block.Parse.
Body interface{}
// Underlying io.Reader; limited by the length of the block body.
lr io.Reader
}
// New creates a new Block for accessing the metadata of r. It reads and parses
// a metadata block header.
//
// Call Block.Parse to parse the metadata block body, and call Block.Skip to
// ignore it.
func New(r io.Reader) (block *Block, err error) {
block = new(Block)
if err = block.parseHeader(r); err != nil {
return block, err
}
block.lr = io.LimitReader(r, block.Length)
return block, nil
}
// Parse reads and parses the header and body of a metadata block. Use New for
// additional granularity.
func Parse(r io.Reader) (block *Block, err error) {
block, err = New(r)
if err != nil {
return block, err
}
if err = block.Parse(); err != nil {
return block, err
}
return block, nil
}
// Errors returned by Parse.
var (
ErrReservedType = errors.New("meta.Block.Parse: reserved block type")
ErrInvalidType = errors.New("meta.Block.Parse: invalid block type")
)
// Parse reads and parses the metadata block body.
func (block *Block) Parse() error {
switch block.Type {
case TypeStreamInfo:
return block.parseStreamInfo()
case TypePadding:
return block.verifyPadding()
case TypeApplication:
return block.parseApplication()
case TypeSeekTable:
return block.parseSeekTable()
case TypeVorbisComment:
return block.parseVorbisComment()
case TypeCueSheet:
return block.parseCueSheet()
case TypePicture:
return block.parsePicture()
}
if block.Type >= 7 && block.Type <= 126 {
return ErrReservedType
}
return ErrInvalidType
}
// Skip ignores the contents of the metadata block body.
func (block *Block) Skip() error {
if sr, ok := block.lr.(io.Seeker); ok {
_, err := sr.Seek(0, io.SeekEnd)
return err
}
_, err := io.Copy(ioutil.Discard, block.lr)
return err
}
// A Header contains information about the type and length of a metadata block.
//
// ref: https://www.xiph.org/flac/format.html#metadata_block_header
type Header struct {
// Metadata block body type.
Type Type
// Length of body data in bytes.
Length int64
// IsLast specifies if the block is the last metadata block.
IsLast bool
}
// parseHeader reads and parses the header of a metadata block.
func (block *Block) parseHeader(r io.Reader) error {
// 1 bit: IsLast.
br := bits.NewReader(r)
x, err := br.Read(1)
if err != nil {
// This is the only place a metadata block may return io.EOF, which
// signals a graceful end of a FLAC stream (from a metadata point of
// view).
//
// Note that valid FLAC streams always contain at least one audio frame
// after the last metadata block. Therefore an io.EOF error at this
// location is always invalid. This logic is to be handled by the flac
// package however.
return err
}
if x != 0 {
block.IsLast = true
}
// 7 bits: Type.
x, err = br.Read(7)
if err != nil {
return unexpected(err)
}
block.Type = Type(x)
// 24 bits: Length.
x, err = br.Read(24)
if err != nil {
return unexpected(err)
}
block.Length = int64(x)
return nil
}
// Type represents the type of a metadata block body.
type Type uint8
// Metadata block body types.
const (
TypeStreamInfo Type = 0
TypePadding Type = 1
TypeApplication Type = 2
TypeSeekTable Type = 3
TypeVorbisComment Type = 4
TypeCueSheet Type = 5
TypePicture Type = 6
)
func (t Type) String() string {
switch t {
case TypeStreamInfo:
return "stream info"
case TypePadding:
return "padding"
case TypeApplication:
return "application"
case TypeSeekTable:
return "seek table"
case TypeVorbisComment:
return "vorbis comment"
case TypeCueSheet:
return "cue sheet"
case TypePicture:
return "picture"
default:
return "<unknown block type>"
}
}
// unexpected returns io.ErrUnexpectedEOF if err is io.EOF, and returns err
// otherwise.
func unexpected(err error) error {
if err == io.EOF {
return io.ErrUnexpectedEOF
}
return err
}

260
meta/meta_test.go Executable file
View File

@@ -0,0 +1,260 @@
package meta_test
import (
"bytes"
"io/ioutil"
"reflect"
"testing"
"github.com/mewkiz/flac"
"github.com/mewkiz/flac/meta"
)
var golden = []struct {
path string
info *meta.StreamInfo
blocks []*meta.Block
}{
{
path: "../testdata/59996.flac",
info: &meta.StreamInfo{BlockSizeMin: 0x1000, BlockSizeMax: 0x1000, FrameSizeMin: 0x44c5, FrameSizeMax: 0x4588, SampleRate: 0xac44, NChannels: 0x2, BitsPerSample: 0x18, NSamples: 0x2000, MD5sum: [16]uint8{0x95, 0xba, 0xe5, 0xe2, 0xc7, 0x45, 0xbb, 0x3c, 0xa9, 0x5c, 0xa3, 0xb1, 0x35, 0xc9, 0x43, 0xf4}},
blocks: []*meta.Block{
{
Header: meta.Header{Type: 0x4, Length: 202, IsLast: true},
Body: &meta.VorbisComment{Vendor: "reference libFLAC 1.2.1 20070917", Tags: [][2]string{{"Description", "Waving a bamboo staff"}, {"YEAR", "2008"}, {"ARTIST", "qubodup aka Iwan Gabovitch | qubodup@gmail.com"}, {"COMMENTS", "I release this file into the public domain"}}},
},
},
},
{
path: "../testdata/172960.flac",
info: &meta.StreamInfo{BlockSizeMin: 0x1000, BlockSizeMax: 0x1000, FrameSizeMin: 0xb7c, FrameSizeMax: 0x256b, SampleRate: 0x17700, NChannels: 0x2, BitsPerSample: 0x10, NSamples: 0xaaa3, MD5sum: [16]uint8{0x76, 0x3d, 0xa8, 0xa5, 0xb7, 0x58, 0xe6, 0x2, 0x61, 0xb4, 0xd4, 0xc2, 0x88, 0x4d, 0x8e, 0xe}},
blocks: []*meta.Block{
{
Header: meta.Header{Type: 0x4, Length: 180, IsLast: true},
Body: &meta.VorbisComment{Vendor: "reference libFLAC 1.2.1 20070917", Tags: [][2]string{{"GENRE", "Sound Clip"}, {"ARTIST", "Iwan 'qubodup' Gabovitch"}, {"Artist Homepage", "http://qubodup.net"}, {"Artist Email", "qubodup@gmail.com"}, {"DATE", "2012"}}},
},
},
},
{
path: "../testdata/189983.flac",
info: &meta.StreamInfo{BlockSizeMin: 0x1200, BlockSizeMax: 0x1200, FrameSizeMin: 0x94d, FrameSizeMax: 0x264a, SampleRate: 0xac44, NChannels: 0x2, BitsPerSample: 0x10, NSamples: 0x50f4, MD5sum: [16]uint8{0x63, 0x28, 0xed, 0x6d, 0xd3, 0xe, 0x55, 0xfb, 0xa5, 0x73, 0x69, 0x2b, 0xb7, 0x35, 0x73, 0xb7}},
blocks: []*meta.Block{
{
Header: meta.Header{Type: 0x4, Length: 40, IsLast: true},
Body: &meta.VorbisComment{Vendor: "reference libFLAC 1.2.1 20070917", Tags: nil},
},
},
},
{
path: "testdata/input-SCPAP.flac",
info: &meta.StreamInfo{BlockSizeMin: 0x1200, BlockSizeMax: 0x1200, FrameSizeMin: 0xe, FrameSizeMax: 0x10, SampleRate: 0xac44, NChannels: 0x2, BitsPerSample: 0x10, NSamples: 0x16f8, MD5sum: [16]uint8{0x74, 0xff, 0xd4, 0x73, 0x7e, 0xb5, 0x48, 0x8d, 0x51, 0x2b, 0xe4, 0xaf, 0x58, 0x94, 0x33, 0x62}},
blocks: []*meta.Block{
{
Header: meta.Header{Type: 0x3, Length: 180, IsLast: false},
Body: &meta.SeekTable{Points: []meta.SeekPoint{{SampleNum: 0x0, Offset: 0x0, NSamples: 0x1200}, {SampleNum: 0x1200, Offset: 0xe, NSamples: 0x4f8}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}}},
},
{
Header: meta.Header{Type: 0x5, Length: 540, IsLast: false},
Body: &meta.CueSheet{MCN: "1234567890123", NLeadInSamples: 0x15888, IsCompactDisc: true, Tracks: []meta.CueSheetTrack{{Offset: 0x0, Num: 0x1, ISRC: "", IsAudio: true, HasPreEmphasis: false, Indicies: []meta.CueSheetTrackIndex{{Offset: 0x0, Num: 0x1}, {Offset: 0x24c, Num: 0x2}}}, {Offset: 0xb7c, Num: 0x2, ISRC: "", IsAudio: true, HasPreEmphasis: false, Indicies: []meta.CueSheetTrackIndex{{Offset: 0x0, Num: 0x1}}}, {Offset: 0x16f8, Num: 0xaa, ISRC: "", IsAudio: true, HasPreEmphasis: false, Indicies: []meta.CueSheetTrackIndex(nil)}}},
},
{
Header: meta.Header{Type: 0x1, Length: 4, IsLast: false},
Body: nil,
},
{
Header: meta.Header{Type: 0x2, Length: 4, IsLast: false},
Body: &meta.Application{ID: 0x66616b65, Data: nil},
},
{
Header: meta.Header{Type: 0x1, Length: 3201, IsLast: true},
Body: nil,
},
},
},
{
path: "testdata/input-SCVA.flac",
info: &meta.StreamInfo{BlockSizeMin: 0x1200, BlockSizeMax: 0x1200, FrameSizeMin: 0xe, FrameSizeMax: 0x10, SampleRate: 0xac44, NChannels: 0x2, BitsPerSample: 0x10, NSamples: 0x16f8, MD5sum: [16]uint8{0x74, 0xff, 0xd4, 0x73, 0x7e, 0xb5, 0x48, 0x8d, 0x51, 0x2b, 0xe4, 0xaf, 0x58, 0x94, 0x33, 0x62}},
blocks: []*meta.Block{
{
Header: meta.Header{Type: 0x3, Length: 180, IsLast: false},
Body: &meta.SeekTable{Points: []meta.SeekPoint{{SampleNum: 0x0, Offset: 0x0, NSamples: 0x1200}, {SampleNum: 0x1200, Offset: 0xe, NSamples: 0x4f8}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}}},
},
{
Header: meta.Header{Type: 0x5, Length: 540, IsLast: false},
Body: &meta.CueSheet{MCN: "1234567890123", NLeadInSamples: 0x15888, IsCompactDisc: true, Tracks: []meta.CueSheetTrack{{Offset: 0x0, Num: 0x1, ISRC: "", IsAudio: true, HasPreEmphasis: false, Indicies: []meta.CueSheetTrackIndex{{Offset: 0x0, Num: 0x1}, {Offset: 0x24c, Num: 0x2}}}, {Offset: 0xb7c, Num: 0x2, ISRC: "", IsAudio: true, HasPreEmphasis: false, Indicies: []meta.CueSheetTrackIndex{{Offset: 0x0, Num: 0x1}}}, {Offset: 0x16f8, Num: 0xaa, ISRC: "", IsAudio: true, HasPreEmphasis: false, Indicies: []meta.CueSheetTrackIndex(nil)}}},
},
{
Header: meta.Header{Type: 0x4, Length: 203, IsLast: false},
Body: &meta.VorbisComment{Vendor: "reference libFLAC 1.1.3 20060805", Tags: [][2]string{{"REPLAYGAIN_TRACK_PEAK", "0.99996948"}, {"REPLAYGAIN_TRACK_GAIN", "-7.89 dB"}, {"REPLAYGAIN_ALBUM_PEAK", "0.99996948"}, {"REPLAYGAIN_ALBUM_GAIN", "-7.89 dB"}, {"artist", "1"}, {"title", "2"}}},
},
{
Header: meta.Header{Type: 0x2, Length: 4, IsLast: true},
Body: &meta.Application{ID: 0x66616b65, Data: nil},
},
},
},
{
path: "testdata/input-SCVAUP.flac",
info: &meta.StreamInfo{BlockSizeMin: 0x1200, BlockSizeMax: 0x1200, FrameSizeMin: 0xe, FrameSizeMax: 0x10, SampleRate: 0xac44, NChannels: 0x2, BitsPerSample: 0x10, NSamples: 0x16f8, MD5sum: [16]uint8{0x74, 0xff, 0xd4, 0x73, 0x7e, 0xb5, 0x48, 0x8d, 0x51, 0x2b, 0xe4, 0xaf, 0x58, 0x94, 0x33, 0x62}},
blocks: []*meta.Block{
{
Header: meta.Header{Type: 0x3, Length: 180, IsLast: false},
Body: &meta.SeekTable{Points: []meta.SeekPoint{{SampleNum: 0x0, Offset: 0x0, NSamples: 0x1200}, {SampleNum: 0x1200, Offset: 0xe, NSamples: 0x4f8}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}}},
},
{
Header: meta.Header{Type: 0x5, Length: 540, IsLast: false},
Body: &meta.CueSheet{MCN: "1234567890123", NLeadInSamples: 0x15888, IsCompactDisc: true, Tracks: []meta.CueSheetTrack{{Offset: 0x0, Num: 0x1, ISRC: "", IsAudio: true, HasPreEmphasis: false, Indicies: []meta.CueSheetTrackIndex{{Offset: 0x0, Num: 0x1}, {Offset: 0x24c, Num: 0x2}}}, {Offset: 0xb7c, Num: 0x2, ISRC: "", IsAudio: true, HasPreEmphasis: false, Indicies: []meta.CueSheetTrackIndex{{Offset: 0x0, Num: 0x1}}}, {Offset: 0x16f8, Num: 0xaa, ISRC: "", IsAudio: true, HasPreEmphasis: false, Indicies: []meta.CueSheetTrackIndex(nil)}}},
},
{
Header: meta.Header{Type: 0x4, Length: 203, IsLast: false},
Body: &meta.VorbisComment{Vendor: "reference libFLAC 1.1.3 20060805", Tags: [][2]string{{"REPLAYGAIN_TRACK_PEAK", "0.99996948"}, {"REPLAYGAIN_TRACK_GAIN", "-7.89 dB"}, {"REPLAYGAIN_ALBUM_PEAK", "0.99996948"}, {"REPLAYGAIN_ALBUM_GAIN", "-7.89 dB"}, {"artist", "1"}, {"title", "2"}}},
},
{
Header: meta.Header{Type: 0x2, Length: 4, IsLast: false},
Body: &meta.Application{ID: 0x66616b65, Data: nil},
},
{
Header: meta.Header{Type: 0x7e, Length: 0, IsLast: false},
Body: nil,
},
{
Header: meta.Header{Type: 0x1, Length: 3201, IsLast: true},
Body: nil,
},
},
},
{
path: "testdata/input-SCVPAP.flac",
info: &meta.StreamInfo{BlockSizeMin: 0x1200, BlockSizeMax: 0x1200, FrameSizeMin: 0xe, FrameSizeMax: 0x10, SampleRate: 0xac44, NChannels: 0x2, BitsPerSample: 0x10, NSamples: 0x16f8, MD5sum: [16]uint8{0x74, 0xff, 0xd4, 0x73, 0x7e, 0xb5, 0x48, 0x8d, 0x51, 0x2b, 0xe4, 0xaf, 0x58, 0x94, 0x33, 0x62}},
blocks: []*meta.Block{
{
Header: meta.Header{Type: 0x3, Length: 180, IsLast: false},
Body: &meta.SeekTable{Points: []meta.SeekPoint{{SampleNum: 0x0, Offset: 0x0, NSamples: 0x1200}, {SampleNum: 0x1200, Offset: 0xe, NSamples: 0x4f8}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}}},
},
{
Header: meta.Header{Type: 0x5, Length: 540, IsLast: false},
Body: &meta.CueSheet{MCN: "1234567890123", NLeadInSamples: 0x15888, IsCompactDisc: true, Tracks: []meta.CueSheetTrack{{Offset: 0x0, Num: 0x1, ISRC: "", IsAudio: true, HasPreEmphasis: false, Indicies: []meta.CueSheetTrackIndex{{Offset: 0x0, Num: 0x1}, {Offset: 0x24c, Num: 0x2}}}, {Offset: 0xb7c, Num: 0x2, ISRC: "", IsAudio: true, HasPreEmphasis: false, Indicies: []meta.CueSheetTrackIndex{{Offset: 0x0, Num: 0x1}}}, {Offset: 0x16f8, Num: 0xaa, ISRC: "", IsAudio: true, HasPreEmphasis: false, Indicies: []meta.CueSheetTrackIndex(nil)}}},
},
{
Header: meta.Header{Type: 0x4, Length: 203, IsLast: false},
Body: &meta.VorbisComment{Vendor: "reference libFLAC 1.1.3 20060805", Tags: [][2]string{{"REPLAYGAIN_TRACK_PEAK", "0.99996948"}, {"REPLAYGAIN_TRACK_GAIN", "-7.89 dB"}, {"REPLAYGAIN_ALBUM_PEAK", "0.99996948"}, {"REPLAYGAIN_ALBUM_GAIN", "-7.89 dB"}, {"artist", "1"}, {"title", "2"}}},
},
{
Header: meta.Header{Type: 0x1, Length: 4, IsLast: false},
Body: nil,
},
{
Header: meta.Header{Type: 0x2, Length: 4, IsLast: false},
Body: &meta.Application{ID: 0x66616b65, Data: nil},
},
{
Header: meta.Header{Type: 0x1, Length: 3201, IsLast: true},
Body: nil,
},
},
},
{
path: "testdata/input-SVAUP.flac",
info: &meta.StreamInfo{BlockSizeMin: 0x1200, BlockSizeMax: 0x1200, FrameSizeMin: 0xe, FrameSizeMax: 0x10, SampleRate: 0xac44, NChannels: 0x2, BitsPerSample: 0x10, NSamples: 0x16f8, MD5sum: [16]uint8{0x74, 0xff, 0xd4, 0x73, 0x7e, 0xb5, 0x48, 0x8d, 0x51, 0x2b, 0xe4, 0xaf, 0x58, 0x94, 0x33, 0x62}},
blocks: []*meta.Block{
{
Header: meta.Header{Type: 0x3, Length: 180, IsLast: false},
Body: &meta.SeekTable{Points: []meta.SeekPoint{{SampleNum: 0x0, Offset: 0x0, NSamples: 0x1200}, {SampleNum: 0x1200, Offset: 0xe, NSamples: 0x4f8}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}, {SampleNum: 0xffffffffffffffff, Offset: 0x0, NSamples: 0x0}}},
},
{
Header: meta.Header{Type: 0x4, Length: 203, IsLast: false},
Body: &meta.VorbisComment{Vendor: "reference libFLAC 1.1.3 20060805", Tags: [][2]string{{"REPLAYGAIN_TRACK_PEAK", "0.99996948"}, {"REPLAYGAIN_TRACK_GAIN", "-7.89 dB"}, {"REPLAYGAIN_ALBUM_PEAK", "0.99996948"}, {"REPLAYGAIN_ALBUM_GAIN", "-7.89 dB"}, {"artist", "1"}, {"title", "2"}}},
},
{
Header: meta.Header{Type: 0x2, Length: 4, IsLast: false},
Body: &meta.Application{ID: 0x66616b65, Data: nil},
},
{
Header: meta.Header{Type: 0x7e, Length: 0, IsLast: false},
Body: nil,
},
{
Header: meta.Header{Type: 0x1, Length: 3201, IsLast: true},
Body: nil,
},
},
},
{
path: "testdata/input-VA.flac",
info: &meta.StreamInfo{BlockSizeMin: 0x1200, BlockSizeMax: 0x1200, FrameSizeMin: 0xe, FrameSizeMax: 0x10, SampleRate: 0xac44, NChannels: 0x2, BitsPerSample: 0x10, NSamples: 0x16f8, MD5sum: [16]uint8{0x74, 0xff, 0xd4, 0x73, 0x7e, 0xb5, 0x48, 0x8d, 0x51, 0x2b, 0xe4, 0xaf, 0x58, 0x94, 0x33, 0x62}},
blocks: []*meta.Block{
{
Header: meta.Header{Type: 0x4, Length: 203, IsLast: false},
Body: &meta.VorbisComment{Vendor: "reference libFLAC 1.1.3 20060805", Tags: [][2]string{{"REPLAYGAIN_TRACK_PEAK", "0.99996948"}, {"REPLAYGAIN_TRACK_GAIN", "-7.89 dB"}, {"REPLAYGAIN_ALBUM_PEAK", "0.99996948"}, {"REPLAYGAIN_ALBUM_GAIN", "-7.89 dB"}, {"artist", "1"}, {"title", "2"}}},
},
{
Header: meta.Header{Type: 0x2, Length: 4, IsLast: true},
Body: &meta.Application{ID: 0x66616b65, Data: nil},
},
},
},
}
func TestParseBlocks(t *testing.T) {
for _, g := range golden {
stream, err := flac.ParseFile(g.path)
if err != nil {
t.Fatal(err)
}
defer stream.Close()
blocks := stream.Blocks
if len(blocks) != len(g.blocks) {
t.Errorf("path=%q: invalid number of metadata blocks; expected %d, got %d", g.path, len(g.blocks), len(blocks))
continue
}
got := stream.Info
want := g.info
if !reflect.DeepEqual(got, want) {
t.Errorf("path=%q: metadata StreamInfo block bodies differ; expected %#v, got %#v", g.path, want, got)
}
for blockNum, got := range blocks {
want := g.blocks[blockNum]
if !reflect.DeepEqual(got.Header, want.Header) {
t.Errorf("path=%q, blockNum=%d: metadata block headers differ; expected %#v, got %#v", g.path, blockNum, want.Header, got.Header)
}
if !reflect.DeepEqual(got.Body, want.Body) {
t.Errorf("path=%q, blockNum=%d: metadata block bodies differ; expected %#v, got %#v", g.path, blockNum, want.Body, got.Body)
}
}
}
}
func TestParsePicture(t *testing.T) {
stream, err := flac.ParseFile("testdata/silence.flac")
if err != nil {
t.Fatal(err)
}
defer stream.Close()
want, err := ioutil.ReadFile("testdata/silence.jpg")
if err != nil {
t.Fatal(err)
}
for _, block := range stream.Blocks {
if block.Type == meta.TypePicture {
pic := block.Body.(*meta.Picture)
got := pic.Data
if !bytes.Equal(got, want) {
t.Errorf("picture data differ; expected %v, got %v", want, got)
}
break
}
}
}
// TODO: better error verification than string-based comparisons.
func TestMissingValue(t *testing.T) {
_, err := flac.ParseFile("testdata/missing-value.flac")
if err.Error() != `meta.Block.parseVorbisComment: unable to locate '=' in vector "title 2"` {
t.Fatal(err)
}
}

39
meta/padding.go Executable file
View File

@@ -0,0 +1,39 @@
package meta
import (
"errors"
"io"
"io/ioutil"
)
// verifyPadding verifies the body of a Padding metadata block. It should only
// contain zero-padding.
//
// ref: https://www.xiph.org/flac/format.html#metadata_block_padding
func (block *Block) verifyPadding() error {
zr := zeros{r: block.lr}
_, err := io.Copy(ioutil.Discard, zr)
return err
}
// Errors returned by zeros.Read.
var (
ErrInvalidPadding = errors.New("invalid padding")
)
// zeros implements an io.Reader, with a Read method which returns an error if
// any byte read isn't zero.
type zeros struct {
r io.Reader
}
// Read returns an error if any byte read isn't zero.
func (zr zeros) Read(p []byte) (n int, err error) {
n, err = zr.r.Read(p)
for i := 0; i < n; i++ {
if p[i] != 0 {
return n, ErrInvalidPadding
}
}
return n, err
}

120
meta/picture.go Executable file
View File

@@ -0,0 +1,120 @@
package meta
import (
"encoding/binary"
"io"
)
// Picture contains the image data of an embedded picture.
//
// ref: https://www.xiph.org/flac/format.html#metadata_block_picture
type Picture struct {
// Picture type according to the ID3v2 APIC frame:
//
// 0: Other
// 1: 32x32 pixels 'file icon' (PNG only)
// 2: Other file icon
// 3: Cover (front)
// 4: Cover (back)
// 5: Leaflet page
// 6: Media (e.g. label side of CD)
// 7: Lead artist/lead performer/soloist
// 8: Artist/performer
// 9: Conductor
// 10: Band/Orchestra
// 11: Composer
// 12: Lyricist/text writer
// 13: Recording Location
// 14: During recording
// 15: During performance
// 16: Movie/video screen capture
// 17: A bright coloured fish
// 18: Illustration
// 19: Band/artist logotype
// 20: Publisher/Studio logotype
//
// ref: http://id3.org/id3v2.4.0-frames
Type uint32
// MIME type string. The MIME type "-->" specifies that the picture data is
// to be interpreted as an URL instead of image data.
MIME string
// Description of the picture.
Desc string
// Image dimensions.
Width, Height uint32
// Color depth in bits-per-pixel.
Depth uint32
// Number of colors in palette; 0 for non-indexed images.
NPalColors uint32
// Image data.
Data []byte
}
// parsePicture reads and parses the body of a Picture metadata block.
func (block *Block) parsePicture() error {
// 32 bits: Type.
pic := new(Picture)
block.Body = pic
err := binary.Read(block.lr, binary.BigEndian, &pic.Type)
if err != nil {
return unexpected(err)
}
// 32 bits: (MIME type length).
var x uint32
if err = binary.Read(block.lr, binary.BigEndian, &x); err != nil {
return unexpected(err)
}
// (MIME type length) bytes: MIME.
mime, err := readString(block.lr, int(x))
if err != nil {
return unexpected(err)
}
pic.MIME = mime
// 32 bits: (description length).
if err = binary.Read(block.lr, binary.BigEndian, &x); err != nil {
return unexpected(err)
}
// (description length) bytes: Desc.
desc, err := readString(block.lr, int(x))
if err != nil {
return unexpected(err)
}
pic.Desc = desc
// 32 bits: Width.
if err = binary.Read(block.lr, binary.BigEndian, &pic.Width); err != nil {
return unexpected(err)
}
// 32 bits: Height.
if err = binary.Read(block.lr, binary.BigEndian, &pic.Height); err != nil {
return unexpected(err)
}
// 32 bits: Depth.
if err = binary.Read(block.lr, binary.BigEndian, &pic.Depth); err != nil {
return unexpected(err)
}
// 32 bits: NPalColors.
if err = binary.Read(block.lr, binary.BigEndian, &pic.NPalColors); err != nil {
return unexpected(err)
}
// 32 bits: (data length).
if err = binary.Read(block.lr, binary.BigEndian, &x); err != nil {
return unexpected(err)
}
if x == 0 {
return nil
}
// (data length) bytes: Data.
pic.Data = make([]byte, x)
_, err = io.ReadFull(block.lr, pic.Data)
return unexpected(err)
}

24
meta/reader.go Executable file
View File

@@ -0,0 +1,24 @@
package meta
import "io"
// readString reads and returns exactly n bytes from the provided io.Reader.
//
// The error is io.EOF only if no bytes were read. If an io.EOF happens after
// reading some but not all the bytes, ReadFull returns io.ErrUnexpectedEOF. On
// return, n == len(buf) if and only if err == nil.
func readString(r io.Reader, n int) (string, error) {
// readBuf is the local buffer used by readBytes.
var backingArray [4096]byte // hopefully allocated on stack.
readBuf := backingArray[:]
if n > len(readBuf) {
// The local buffer is initially 4096 bytes and will grow automatically if
// so required.
readBuf = make([]byte, n)
}
_, err := io.ReadFull(r, readBuf[:n])
if err != nil {
return "", err
}
return string(readBuf[:n]), nil
}

67
meta/seektable.go Executable file
View File

@@ -0,0 +1,67 @@
package meta
import (
"encoding/binary"
"errors"
"fmt"
)
// SeekTable contains one or more pre-calculated audio frame seek points.
//
// ref: https://www.xiph.org/flac/format.html#metadata_block_seektable
type SeekTable struct {
// One or more seek points.
Points []SeekPoint
}
// parseSeekTable reads and parses the body of a SeekTable metadata block.
func (block *Block) parseSeekTable() error {
// The number of seek points is derived from the header length, divided by
// the size of a SeekPoint; which is 18 bytes.
n := block.Length / 18
if n < 1 {
return errors.New("meta.Block.parseSeekTable: at least one seek point is required")
}
table := &SeekTable{Points: make([]SeekPoint, n)}
block.Body = table
var prev uint64
for i := range table.Points {
point := &table.Points[i]
err := binary.Read(block.lr, binary.BigEndian, point)
if err != nil {
return unexpected(err)
}
// Seek points within a table must be sorted in ascending order by sample
// number. Each seek point must have a unique sample number, except for
// placeholder points.
sampleNum := point.SampleNum
if i != 0 && sampleNum != PlaceholderPoint {
switch {
case sampleNum < prev:
return fmt.Errorf("meta.Block.parseSeekTable: invalid seek point order; sample number (%d) < prev (%d)", sampleNum, prev)
case sampleNum == prev:
return fmt.Errorf("meta.Block.parseSeekTable: duplicate seek point with sample number (%d)", sampleNum)
}
}
}
return nil
}
// A SeekPoint specifies the byte offset and initial sample number of a given
// target frame.
//
// ref: https://www.xiph.org/flac/format.html#seekpoint
type SeekPoint struct {
// Sample number of the first sample in the target frame, or
// 0xFFFFFFFFFFFFFFFF for a placeholder point.
SampleNum uint64
// Offset in bytes from the first byte of the first frame header to the first
// byte of the target frame's header.
Offset uint64
// Number of samples in the target frame.
NSamples uint16
}
// PlaceholderPoint represent the sample number used to specify placeholder seek
// points.
const PlaceholderPoint = 0xFFFFFFFFFFFFFFFF

116
meta/streaminfo.go Executable file
View File

@@ -0,0 +1,116 @@
package meta
import (
"crypto/md5"
"errors"
"fmt"
"io"
"github.com/mewkiz/flac/internal/bits"
)
// StreamInfo contains the basic properties of a FLAC audio stream, such as its
// sample rate and channel count. It is the only mandatory metadata block and
// must be present as the first metadata block of a FLAC stream.
//
// ref: https://www.xiph.org/flac/format.html#metadata_block_streaminfo
type StreamInfo struct {
// Minimum block size (in samples) used in the stream; between 16 and 65535
// samples.
BlockSizeMin uint16
// Maximum block size (in samples) used in the stream; between 16 and 65535
// samples.
BlockSizeMax uint16
// Minimum frame size in bytes; a 0 value implies unknown.
FrameSizeMin uint32
// Maximum frame size in bytes; a 0 value implies unknown.
FrameSizeMax uint32
// Sample rate in Hz; between 1 and 655350 Hz.
SampleRate uint32
// Number of channels; between 1 and 8 channels.
NChannels uint8
// Sample size in bits-per-sample; between 4 and 32 bits.
BitsPerSample uint8
// Total number of inter-channel samples in the stream. One second of 44.1
// KHz audio will have 44100 samples regardless of the number of channels. A
// 0 value implies unknown.
NSamples uint64
// MD5 checksum of the unencoded audio data.
MD5sum [md5.Size]uint8
}
// parseStreamInfo reads and parses the body of a StreamInfo metadata block.
func (block *Block) parseStreamInfo() error {
// 16 bits: BlockSizeMin.
br := bits.NewReader(block.lr)
x, err := br.Read(16)
if err != nil {
return unexpected(err)
}
if x < 16 {
return fmt.Errorf("meta.Block.parseStreamInfo: invalid minimum block size (%d); expected >= 16", x)
}
si := new(StreamInfo)
block.Body = si
si.BlockSizeMin = uint16(x)
// 16 bits: BlockSizeMax.
x, err = br.Read(16)
if err != nil {
return unexpected(err)
}
if x < 16 {
return fmt.Errorf("meta.Block.parseStreamInfo: invalid maximum block size (%d); expected >= 16", x)
}
si.BlockSizeMax = uint16(x)
// 24 bits: FrameSizeMin.
x, err = br.Read(24)
if err != nil {
return unexpected(err)
}
si.FrameSizeMin = uint32(x)
// 24 bits: FrameSizeMax.
x, err = br.Read(24)
if err != nil {
return unexpected(err)
}
si.FrameSizeMax = uint32(x)
// 20 bits: SampleRate.
x, err = br.Read(20)
if err != nil {
return unexpected(err)
}
if x == 0 {
return errors.New("meta.Block.parseStreamInfo: invalid sample rate (0)")
}
si.SampleRate = uint32(x)
// 3 bits: NChannels.
x, err = br.Read(3)
if err != nil {
return unexpected(err)
}
// x contains: (number of channels) - 1
si.NChannels = uint8(x + 1)
// 5 bits: BitsPerSample.
x, err = br.Read(5)
if err != nil {
return unexpected(err)
}
// x contains: (bits-per-sample) - 1
si.BitsPerSample = uint8(x + 1)
// 36 bits: NSamples.
si.NSamples, err = br.Read(36)
if err != nil {
return unexpected(err)
}
// 16 bytes: MD5sum.
_, err = io.ReadFull(block.lr, si.MD5sum[:])
return unexpected(err)
}

35
meta/testdata/README.md vendored Executable file
View File

@@ -0,0 +1,35 @@
# Testcase Licences
## BSD License
The following testcase sounds have been copied from the [reference implementation] library, which is released under a [BSD license].
* input-SCPAP.flac
* input-SCVA.flac
* input-SCVAUP.flac
* input-SCVPAP.flac
* input-SVAUP.flac
* input-VA.flac
* `missing-value.flac`, created using the following command.
```shell
sed 's/title=/title /' input-SCVA.flac > missing-value.flac
```
[reference implementation]: https://git.xiph.org/?p=flac.git
[BSD license]: https://git.xiph.org/?p=flac.git;a=blob_plain;f=COPYING.Xiph
## Public domain
The following testcase images and sounds have been released into the [public domain].
* [silence.jpg](http://www.pdpics.com/photo/2546-silence-word-magnified/)
* `silence.flac`, created using the following commands.
```shell
ffmpeg -f lavfi -i "aevalsrc=0|0:d=3" silence.wav
flac silence.wav
metaflac --import-picture=silence.jpg silence.flac
```
[public domain]: https://creativecommons.org/publicdomain/zero/1.0/

BIN
meta/testdata/silence.jpg vendored Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

69
meta/vorbiscomment.go Executable file
View File

@@ -0,0 +1,69 @@
package meta
import (
"encoding/binary"
"fmt"
"strings"
)
// VorbisComment contains a list of name-value pairs.
//
// ref: https://www.xiph.org/flac/format.html#metadata_block_vorbis_comment
type VorbisComment struct {
// Vendor name.
Vendor string
// A list of tags, each represented by a name-value pair.
Tags [][2]string
}
// parseVorbisComment reads and parses the body of a VorbisComment metadata
// block.
func (block *Block) parseVorbisComment() (err error) {
// 32 bits: vendor length.
var x uint32
if err = binary.Read(block.lr, binary.LittleEndian, &x); err != nil {
return unexpected(err)
}
// (vendor length) bits: Vendor.
vendor, err := readString(block.lr, int(x))
if err != nil {
return unexpected(err)
}
comment := new(VorbisComment)
block.Body = comment
comment.Vendor = vendor
// Parse tags.
// 32 bits: number of tags.
if err = binary.Read(block.lr, binary.LittleEndian, &x); err != nil {
return unexpected(err)
}
if x < 1 {
return nil
}
comment.Tags = make([][2]string, x)
for i := range comment.Tags {
// 32 bits: vector length
if err = binary.Read(block.lr, binary.LittleEndian, &x); err != nil {
return unexpected(err)
}
// (vector length): vector.
vector, err := readString(block.lr, int(x))
if err != nil {
return unexpected(err)
}
// Parse tag, which has the following format:
// NAME=VALUE
pos := strings.Index(vector, "=")
if pos == -1 {
return fmt.Errorf("meta.Block.parseVorbisComment: unable to locate '=' in vector %q", vector)
}
comment.Tags[i][0] = vector[:pos]
comment.Tags[i][1] = vector[pos+1:]
}
return nil
}