Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement seek #44

Merged
merged 15 commits into from
Jan 23, 2021
242 changes: 214 additions & 28 deletions flac.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ package flac
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
Expand All @@ -47,6 +48,15 @@ type Stream struct {
Info *meta.StreamInfo
// Zero or more metadata blocks.
Blocks []*meta.Block

// seekTable contains one or more pre-calculated audio frame seek points of the stream; nil if uninitialized.
seekTable *meta.SeekTable
// seekTableSize determines how many seek points the seekTable should have if the flac file does not include one
// in the metadata.
seekTableSize int
// dataStart is the offset of the first frame header since SeekPoint.Offset is relative to this position.
dataStart int64

// Underlying io.Reader.
r io.Reader
// Underlying io.Closer of file if opened with Open and ParseFile, and nil
Expand All @@ -64,79 +74,122 @@ func New(r io.Reader) (stream *Stream, err error) {
// Verify FLAC signature and parse the StreamInfo metadata block.
br := bufio.NewReader(r)
stream = &Stream{r: br}
isLast, err := stream.parseStreamInfo()
block, err := stream.parseStreamInfo()
if err != nil {
return nil, err
}

// Skip the remaining metadata blocks.
for !isLast {
block, err := meta.New(br)
for !block.IsLast {
block, err = meta.New(br)
if err != nil && err != meta.ErrReservedType {
return stream, err
}
if err = block.Skip(); err != nil {
return stream, err
}
isLast = block.IsLast
}

return stream, nil
}

// flacSignature marks the beginning of a FLAC stream.
var flacSignature = []byte("fLaC")
// NewSeek returns a Stream that has seeking enabled. The incoming
// io.ReadSeeker will not be buffered, which might result in performance issues.
// Using an in-memory buffer like *bytes.Reader should work well.
func NewSeek(r io.Reader) (stream *Stream, err error) {
rs, ok := r.(io.ReadSeeker)
if !ok {
return stream, ErrNoSeeker
}

stream = &Stream{r: r, seekTableSize: 100}
cswank marked this conversation as resolved.
Show resolved Hide resolved

// Verify FLAC signature and parse the StreamInfo metadata block.
block, err := stream.parseStreamInfo()
if err != nil {
return stream, err
}

for !block.IsLast {
block, err = meta.Parse(stream.r)
if err != nil {
if err != meta.ErrReservedType {
return stream, err
} else {
if err = block.Skip(); err != nil {
return stream, err
}
}
}

if block.Header.Type == meta.TypeSeekTable {
stream.seekTable = block.Body.(*meta.SeekTable)
}
}

stream.dataStart, err = rs.Seek(0, io.SeekCurrent)
cswank marked this conversation as resolved.
Show resolved Hide resolved
return stream, err
}

// id3Signature marks the beginning of an ID3 stream, used to skip over ID3 data.
var id3Signature = []byte("ID3")
var (
// flacSignature marks the beginning of a FLAC stream.
flacSignature = []byte("fLaC")

// id3Signature marks the beginning of an ID3 stream, used to skip over ID3 data.
id3Signature = []byte("ID3")

ErrInvalidSeek = errors.New("stream.Seek: out of stream seek")
ErrNoSeeker = errors.New("stream.Seek: no a Seeker")
cswank marked this conversation as resolved.
Show resolved Hide resolved
ErrInvalidOption = errors.New("stream.New: invalid Option")
cswank marked this conversation as resolved.
Show resolved Hide resolved
)

// parseStreamInfo verifies the signature which marks the beginning of a FLAC
// stream, and parses the StreamInfo metadata block. It returns a boolean value
// which specifies if the StreamInfo block was the last metadata block of the
// FLAC stream.
func (stream *Stream) parseStreamInfo() (isLast bool, err error) {
func (stream *Stream) parseStreamInfo() (block *meta.Block, err error) {
// Verify FLAC signature.
r := stream.r
var buf [4]byte
if _, err = io.ReadFull(r, buf[:]); err != nil {
return false, err
return block, err
}

// Skip prepended ID3v2 data.
if bytes.Equal(buf[:3], id3Signature) {
if err := stream.skipID3v2(); err != nil {
return false, err
return block, err
}

// Second attempt at verifying signature.
if _, err = io.ReadFull(r, buf[:]); err != nil {
return false, err
return block, err
}
}

if !bytes.Equal(buf[:], flacSignature) {
return false, fmt.Errorf("flac.parseStreamInfo: invalid FLAC signature; expected %q, got %q", flacSignature, buf)
return block, fmt.Errorf("flac.parseStreamInfo: invalid FLAC signature; expected %q, got %q", flacSignature, buf)
}

// Parse StreamInfo metadata block.
block, err := meta.Parse(r)
block, err = meta.Parse(r)
if err != nil {
return false, err
return block, err
}
si, ok := block.Body.(*meta.StreamInfo)
if !ok {
return false, fmt.Errorf("flac.parseStreamInfo: incorrect type of first metadata block; expected *meta.StreamInfo, got %T", si)
return block, fmt.Errorf("flac.parseStreamInfo: incorrect type of first metadata block; expected *meta.StreamInfo, got %T", si)
}
stream.Info = si
return block.IsLast, nil
return block, nil
}

// skipID3v2 skips ID3v2 data prepended to flac files.
func (stream *Stream) skipID3v2() error {
r := bufio.NewReader(stream.r)

// Discard unnecessary data from the ID3v2 header.
if _, err := r.Discard(2); err != nil {
mewmew marked this conversation as resolved.
Show resolved Hide resolved
r := stream.r
var buf [2]byte
if _, err := io.ReadFull(r, buf[:]); err != nil {
return err
}

Expand All @@ -148,7 +201,8 @@ func (stream *Stream) skipID3v2() error {
// The size is encoded as a synchsafe integer.
size := int(sizeBuf[0])<<21 | int(sizeBuf[1])<<14 | int(sizeBuf[2])<<7 | int(sizeBuf[3])

_, err := r.Discard(size)
disc := make([]byte, size)
_, err := io.ReadFull(r, disc)
return err
}

Expand All @@ -161,14 +215,14 @@ func Parse(r io.Reader) (stream *Stream, err error) {
// Verify FLAC signature and parse the StreamInfo metadata block.
br := bufio.NewReader(r)
stream = &Stream{r: br}
isLast, err := stream.parseStreamInfo()
block, err := stream.parseStreamInfo()
if err != nil {
return nil, err
}

// Parse the remaining metadata blocks.
for !isLast {
block, err := meta.Parse(br)
for !block.IsLast {
block, err = meta.Parse(br)
if err != nil {
if err != meta.ErrReservedType {
return stream, err
Expand All @@ -182,7 +236,6 @@ func Parse(r io.Reader) (stream *Stream, err error) {
}
}
stream.Blocks = append(stream.Blocks, block)
isLast = block.IsLast
}

return stream, nil
Expand All @@ -201,6 +254,7 @@ func Open(path string) (stream *Stream, err error) {
if err != nil {
return nil, err
}

stream, err = New(f)
if err != nil {
return nil, err
Expand Down Expand Up @@ -253,7 +307,139 @@ func (stream *Stream) ParseNext() (f *frame.Frame, err error) {
return frame.Parse(stream.r)
}

// Seek is not implement yet.
func (stream *Stream) Seek(offset int64, whence int) (read int64, err error) {
return 0, nil
// Seek to a specific sample number in the flac stream.
//
// sample is valid if:
// whence == io.SeekEnd and sample is negative
// whence == io.SeekStart and sample is positive
// whence == io.SeekCurrent and sample + current sample > 0 and < stream.Info.NSamples
//
// If sample does not match one of the above conditions then the result will
// probably be seeking to the beginning or very end of the data and no error
// will be returned.
func (stream *Stream) Seek(sample int64, whence int) (read int64, err error) {
cswank marked this conversation as resolved.
Show resolved Hide resolved
if stream.seekTable == nil && stream.seekTableSize > 0 {
if err := stream.makeSeekTable(); err != nil {
return 0, err
}
}

rs := stream.r.(io.ReadSeeker)

var point meta.SeekPoint
switch whence {
case io.SeekStart:
point = stream.searchFromStart(sample)
case io.SeekCurrent:
point, err = stream.searchFromCurrent(sample, rs)
case io.SeekEnd:
point = stream.searchFromEnd(sample)
default:
return 0, ErrInvalidSeek
}

if err != nil {
return 0, err
}

_, err = rs.Seek(stream.dataStart+int64(point.Offset), io.SeekStart)
return int64(point.SampleNum), err
}

func (stream *Stream) searchFromCurrent(sample int64, rs io.ReadSeeker) (p meta.SeekPoint, err error) {
o, err := rs.Seek(0, io.SeekCurrent)
if err != nil {
return p, err
}

offset := o - stream.dataStart
for _, p = range stream.seekTable.Points {
if int64(p.Offset) >= offset {
return stream.searchFromStart(int64(p.SampleNum) + sample), nil
}
}
return p, nil
}

// searchFromEnd expects sample to be negative.
// If it is positive, it's ok, the last seek point will be returned.
func (stream *Stream) searchFromEnd(sample int64) (p meta.SeekPoint) {
return stream.searchFromStart(int64(stream.Info.NSamples) + sample)
}

func (stream *Stream) searchFromStart(sample int64) (p meta.SeekPoint) {
cswank marked this conversation as resolved.
Show resolved Hide resolved
var last meta.SeekPoint
var i int
for i, p = range stream.seekTable.Points {
if int64(p.SampleNum) >= sample {
if i == 0 {
return p
}
return last
}
last = p
}
return p
}

func (stream *Stream) makeSeekTable() (err error) {
r, ok := stream.r.(io.ReadSeeker)
cswank marked this conversation as resolved.
Show resolved Hide resolved
if !ok {
return ErrNoSeeker
}

pos, err := r.Seek(0, io.SeekCurrent)
if err != nil {
return err
}

_, err = r.Seek(stream.dataStart, io.SeekStart)
if err != nil {
return err
}

var i int
var sampleNum uint64
var tmp []meta.SeekPoint
for {
f, err := stream.ParseNext()
if err == io.EOF {
break
}

if err != nil {
return err
}

o, err := r.Seek(0, io.SeekCurrent)
if err != nil {
return err
}

tmp = append(tmp, meta.SeekPoint{
SampleNum: sampleNum,
Offset: uint64(o - stream.dataStart),
NSamples: f.BlockSize,
})

sampleNum += uint64(f.BlockSize)
i++
}

// reduce the number of seek points down to the specified resolution
m := 1
if len(tmp) > stream.seekTableSize {
m = len(tmp) / stream.seekTableSize
}
points := make([]meta.SeekPoint, 0, stream.seekTableSize+1)
for i, p := range tmp {
if i%m == 0 {
points = append(points, p)
}
}

stream.seekTable = &meta.SeekTable{Points: points}

_, err = r.Seek(pos, io.SeekStart)
return err
}
Loading