Implement seek (#44)

* Implement seek

* update tests

* fixed io.SeekCurrent seek

* rename test

* Tweak seekTable reserved slice size

* fixed a seek bug

* brush up comment

* a little cleanup

* Update flac.go

Co-authored-by: Robin <mewmew@users.noreply.github.com>

* Update flac.go

Co-authored-by: Robin <mewmew@users.noreply.github.com>

* CR changes

* remove unused Err

* try to make comment easier to understand

* comment edit

Co-authored-by: Robin <mewmew@users.noreply.github.com>
This commit is contained in:
Craig Swank 2021-01-23 09:38:38 -07:00 committed by GitHub
parent 0b3d4b0db5
commit 052c3e4f58
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 335 additions and 24 deletions

240
flac.go
View file

@ -29,6 +29,7 @@ package flac
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
@ -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
@ -64,71 +74,118 @@ 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
}
// id3Signature marks the beginning of an ID3 stream, used to skip over ID3 data.
var id3Signature = []byte("ID3")
stream = &Stream{r: rs, seekTableSize: defaultSeekTableSize}
// 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)
}
}
// Record file offset of the first frame header.
stream.dataStart, err = rs.Seek(0, io.SeekCurrent)
return stream, err
}
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: not a Seeker")
)
const (
defaultSeekTableSize = 100
)
// 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.
@ -161,14 +218,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
@ -182,7 +239,6 @@ func Parse(r io.Reader) (stream *Stream, err error) {
}
}
stream.Blocks = append(stream.Blocks, block)
isLast = block.IsLast
}
return stream, nil
@ -201,6 +257,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
@ -253,7 +310,142 @@ 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.
//
// The returned value, result, represents the closest match to sampleNum from the seek table.
// Note that result will always be >= sampleNum
func (stream *Stream) Seek(sampleNum int64, whence int) (result int64, err error) {
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(sampleNum)
case io.SeekCurrent:
point, err = stream.searchFromCurrent(sampleNum, rs)
case io.SeekEnd:
point = stream.searchFromEnd(sampleNum)
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) {
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) {
rs, ok := stream.r.(io.ReadSeeker)
if !ok {
return ErrNoSeeker
}
pos, err := rs.Seek(0, io.SeekCurrent)
if err != nil {
return err
}
_, err = rs.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 := rs.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 = rs.Seek(pos, io.SeekStart)
return err
}

View file

@ -1,6 +1,9 @@
package flac_test
import (
"fmt"
"io"
"os"
"testing"
"github.com/mewkiz/flac"
@ -11,3 +14,119 @@ func TestSkipID3v2(t *testing.T) {
t.Fatal(err)
}
}
func TestSeek(t *testing.T) {
f, err := os.Open("testdata/172960.flac")
if err != nil {
t.Fatal(err)
}
defer f.Close()
//Seek Table:
// {SampleNum:0 Offset:8283 NSamples:4096}
// {SampleNum:4096 Offset:17777 NSamples:4096}
// {SampleNum:8192 Offset:27141 NSamples:4096}
// {SampleNum:12288 Offset:36665 NSamples:4096}
// {SampleNum:16384 Offset:46179 NSamples:4096}
// {SampleNum:20480 Offset:55341 NSamples:4096}
// {SampleNum:24576 Offset:64690 NSamples:4096}
// {SampleNum:28672 Offset:74269 NSamples:4096}
// {SampleNum:32768 Offset:81984 NSamples:4096}
// {SampleNum:36864 Offset:86656 NSamples:4096}
// {SampleNum:40960 Offset:89596 NSamples:2723}
testPos := []struct {
seek int64
whence int
expected int64
}{
{seek: 0, whence: io.SeekStart, expected: 0},
{seek: 9000, whence: io.SeekStart, expected: 8192},
{seek: 0, whence: io.SeekStart, expected: 0},
{seek: -6000, whence: io.SeekEnd, expected: 36864},
{seek: -8000, whence: io.SeekCurrent, expected: 32768},
{seek: 8000, whence: io.SeekCurrent, expected: 40960},
{seek: 0, whence: io.SeekEnd, expected: 40960},
{seek: 50000, whence: io.SeekStart, expected: 40960},
{seek: 100, whence: io.SeekEnd, expected: 40960},
{seek: -100, whence: io.SeekStart, expected: 0},
}
stream, err := flac.NewSeek(f)
if err != nil {
t.Fatal(err)
}
for i, pos := range testPos {
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
p, err := stream.Seek(pos.seek, pos.whence)
if err != nil {
t.Fatal(err)
}
if p != pos.expected {
t.Fatalf("pos %d does not equal %d", p, pos.expected)
}
_, err = stream.ParseNext()
if err != nil && err != io.EOF {
t.Fatal(err)
}
})
}
}
func TestDecode(t *testing.T) {
paths := []string{
"meta/testdata/input-SCPAP.flac",
"meta/testdata/input-SCVA.flac",
"meta/testdata/input-SCVPAP.flac",
"meta/testdata/input-VA.flac",
"meta/testdata/silence.flac",
"testdata/19875.flac",
"testdata/44127.flac",
"testdata/59996.flac",
"testdata/80574.flac",
"testdata/172960.flac",
"testdata/189983.flac",
"testdata/191885.flac",
"testdata/212768.flac",
"testdata/220014.flac",
"testdata/243749.flac",
"testdata/256529.flac",
"testdata/257344.flac",
"testdata/8297-275156-0011.flac",
"testdata/love.flac",
}
funcs := map[string]func(io.Reader) (*flac.Stream, error){
"new": flac.New,
"newSeek": flac.NewSeek,
"parse": flac.Parse,
}
for _, path := range paths {
for k, f := range funcs {
t.Run(fmt.Sprintf("%s/%s", k, path), func(t *testing.T) {
file, err := os.Open(path)
if err != nil {
t.Fatal(err)
}
stream, err := f(file)
if err != nil {
t.Fatal(err)
}
_, err = stream.ParseNext()
if err != nil {
t.Fatal(err)
}
file.Close()
})
}
}
}