Absolute seek (#46)

* frame: Add frame.SampleNumber()

* flac/seek: Make stream.Seek seek to absolute sample numbers

* seek/test: Update test case for absolute seeking

* Update flac.go

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

* Update flac.go

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

* Update flac.go

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

* Update flac.go

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

* Update flac_test.go

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

* Update frame/frame.go

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

* flac: Improve code styling

* flac: Add ErrNoSeeker documentation comment

* flac: Make searchFromStart return an error instead of panic

* Update flac.go

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

* Update flac.go

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

Co-authored-by: Robin <mewmew@users.noreply.github.com>
This commit is contained in:
Henry Eklind 2021-01-28 00:23:27 +01:00 committed by GitHub
parent bb157a5067
commit 11466d8af8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 80 additions and 101 deletions

144
flac.go
View file

@ -110,10 +110,9 @@ func NewSeek(rs io.ReadSeeker) (stream *Stream, err error) {
if err != nil {
if err != meta.ErrReservedType {
return stream, err
} else {
if err = block.Skip(); err != nil {
return stream, err
}
}
if err = block.Skip(); err != nil {
return stream, err
}
}
@ -134,8 +133,13 @@ var (
// id3Signature marks the beginning of an ID3 stream, used to skip over ID3 data.
id3Signature = []byte("ID3")
ErrInvalidSeek = errors.New("stream.Seek: invalid seek out of stream")
ErrNoSeeker = errors.New("stream.Seek: reader does not implement io.Seeker")
// ErrNoSeeker reports that flac.NewSeek was called with an io.Reader not
// implementing io.Seeker, and thus does not allow for seeking.
ErrNoSeeker = errors.New("stream.Seek: reader does not implement io.Seeker")
// ErrNoSeektable reports that no seektable has been generated. Therefore,
// it is not possible to seek in the stream.
ErrNoSeektable = errors.New("stream.searchFromStart: no seektable exists")
)
const (
@ -305,20 +309,10 @@ func (stream *Stream) ParseNext() (f *frame.Frame, err error) {
return frame.Parse(stream.r)
}
// 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) {
// Seek seeks to the frame containing the given absolute sample number. The
// return value specifies the first sample number of the frame containing
// sampleNum.
func (stream *Stream) Seek(sampleNum uint64) (uint64, error) {
if stream.seekTable == nil && stream.seekTableSize > 0 {
if err := stream.makeSeekTable(); err != nil {
return 0, err
@ -327,62 +321,54 @@ func (stream *Stream) Seek(sampleNum int64, whence int) (result int64, err error
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
isBiggerThanStream := stream.Info.NSamples != 0 && sampleNum > stream.Info.NSamples
if isBiggerThanStream || sampleNum < 0 {
return 0, fmt.Errorf("unable to seek to sample number %d", sampleNum)
}
point, err := stream.searchFromStart(sampleNum)
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
if _, err := rs.Seek(stream.dataStart+int64(point.Offset), io.SeekStart); err != nil {
return 0, err
}
offset := o - stream.dataStart
for _, p = range stream.seekTable.Points {
if int64(p.Offset) >= offset {
return stream.searchFromStart(int64(p.SampleNum) + sample), nil
for {
// Record seek offset to start of frame.
offset, err := rs.Seek(0, io.SeekCurrent)
if err != nil {
return 0, err
}
frame, err := stream.ParseNext()
if err != nil {
return 0, err
}
if frame.SampleNumber()+uint64(frame.BlockSize) >= sampleNum {
// Restore seek offset to the start of the frame containing the specified sample number.
_, err := rs.Seek(offset, io.SeekStart)
return frame.SampleNumber(), err
}
}
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)
}
// TODO(_): Utilize binary search.
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
//
func (stream *Stream) searchFromStart(sample uint64) (meta.SeekPoint, error) {
if len(stream.seekTable.Points) == 0 {
return meta.SeekPoint{}, ErrNoSeektable
}
return p
prev := stream.seekTable.Points[0]
for _, p := range stream.seekTable.Points {
if p.SampleNum+uint64(p.NSamples) >= sample {
return prev, nil
}
prev = p
}
return prev
}
// makeSeekTable creates a seek table with seek points to each frame of the FLAC stream.
func (stream *Stream) makeSeekTable() (err error) {
rs, ok := stream.r.(io.ReadSeeker)
if !ok {
@ -401,23 +387,21 @@ func (stream *Stream) makeSeekTable() (err error) {
var i int
var sampleNum uint64
var tmp []meta.SeekPoint
var points []meta.SeekPoint
for {
f, err := stream.ParseNext()
if err == io.EOF {
break
}
if err != nil {
return err
}
// Record seek offset to start of frame.
o, err := rs.Seek(0, io.SeekCurrent)
if err != nil {
return err
}
tmp = append(tmp, meta.SeekPoint{
f, err := stream.ParseNext()
if err != nil {
if err == io.EOF {
break
}
return err
}
points = append(points, meta.SeekPoint{
SampleNum: sampleNum,
Offset: uint64(o - stream.dataStart),
NSamples: f.BlockSize,
@ -427,18 +411,6 @@ func (stream *Stream) makeSeekTable() (err error) {
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)

View file

@ -37,20 +37,17 @@ func TestSeek(t *testing.T) {
// {SampleNum:40960 Offset:89596 NSamples:2723}
testPos := []struct {
seek int64
whence int
expected int64
seek uint64
expected uint64
err string
}{
{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},
{seek: 0, expected: 0},
{seek: 9000, expected: 8192},
{seek: 0, expected: 0},
{seek: 8000, expected: 4096},
{seek: 0, expected: 0},
{seek: 50000, expected: 0, err: "unable to seek to sample number 50000"},
{seek: 100, expected: 0},
}
stream, err := flac.NewSeek(f)
@ -60,9 +57,11 @@ func TestSeek(t *testing.T) {
for i, pos := range testPos {
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
p, err := stream.Seek(pos.seek, pos.whence)
p, err := stream.Seek(pos.seek)
if err != nil {
t.Fatal(err)
if err.Error() != pos.err {
t.Fatal(err)
}
}
if p != pos.expected {

View file

@ -624,6 +624,14 @@ func (frame *Frame) correlate() {
}
}
// SampleNumber returns the first sample number contained within the frame.
func (frame *Frame) SampleNumber() uint64 {
if frame.HasFixedBlockSize {
return frame.Num * uint64(frame.BlockSize)
}
return frame.Num
}
// unexpected returns io.ErrUnexpectedEOF if err is io.EOF, and returns err
// otherwise.
func unexpected(err error) error {