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:
parent
0b3d4b0db5
commit
052c3e4f58
240
flac.go
240
flac.go
|
@ -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
|
||||
}
|
||||
|
|
119
flac_test.go
119
flac_test.go
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue