-
Notifications
You must be signed in to change notification settings - Fork 44
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
Seek #43
Seek #43
Conversation
Hi cswank! Glad to see someone giving seeking some well-earned love. I'll check the PR tomorrow 👍 |
TODO: handle cases where sample is invalid: whence == io.SeekEnd && sample is not negative whence == io.SeekStart && sample is negative whence == io.SeekCurrent and sample + current sample < 0 or > stream.Info.NSamples
The last commit supports io.SeekCurrent and io.SeekEnd |
return 0, nil | ||
// Seek to offset where offset represents the sample number in the flac file | ||
// | ||
// TODO: return an error if sample is invalid? Do nothing? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do you think invalid input to Seek should be handled?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your comment about when the sample is invalid is perfect. I think the best would be to create a new error type for those cases. For example:
ErrInvalidSeek = errors.New("stream.Seek: out of stream seek")
We have custom error types in meta.go
which are public so that library end-users may check against them.
flac.go
Outdated
return err | ||
} | ||
|
||
if i%10 == 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made the assumption that the number of seek points can be reduced in order to save memory and increase seek performance (don't have to search through so many points in the seek table) at the cost of the seeks having lower resolution. Does this seem appropriate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's good!
I don't have a good gut feeling what the perfect resolution is, since this library could possibly handle short sound segments (<2s) to long sound segments (>10h+). This could possibly be a configure option, but let's just go with %10
for now.
Or do you have any opinions @mewmew ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could use a sane default (e.g. 10%
as you suggested), and then at a later point introduce a public API for initializing a custom seek table at arbitrary resolution (that API point would essentially set the unexported seekTable
field).
flac.go
Outdated
} | ||
|
||
if seeker && stream.seekTable == nil { | ||
return stream, stream.makeSeekTable() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps makeSeekTable should be called when Seek is called for the first time?
For the flac file I've been playing makeSeekTable takes 1.1 seconds, which is kind of a lot.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good suggestion. Seek should probably be an opt-in feature, and we want to keep simple parsing as streamlined as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's easy to check if makeSeekTable
has been called by checking if stream.seekTable == nil
or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps makeSeekTable should be called when Seek is called for the first time?
This sounds like a good approach. And then if we introduce an API such as InitSeekTable
, then the user could ensure that the seek table is already initialized should they wish before the first call to Seek
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really like your implementation, just a few kinks we need to straighten out before we merge this. Thank you so much for contributing to our repo 🌷
Also, would it be possible to write a test for Seek
testing SeekStart
, SeekCurrent
and SeekEnd
with any of the flac files in this repo?
Note: I use the conventional comments format when I review, if you're confused just ask a question and I'll respond as soon as I can or read more about it here: https://conventionalcomments.org/
flac.go
Outdated
@@ -48,6 +48,11 @@ type Stream struct { | |||
// Zero or more metadata blocks. | |||
Blocks []*meta.Block | |||
// Underlying io.Reader. | |||
|
|||
seekTable *meta.SeekTable | |||
// offset where the music data begins since SeekTable.Offset seems to be relative to this position |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: Capital letter and punctuation for comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: rename start
to dataStart
or something more descriptive.
flac.go
Outdated
br := bufio.NewReader(r) | ||
stream = &Stream{r: br} | ||
isLast, err := stream.parseStreamInfo() | ||
//br := bufio.NewReader(r) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue(blocking): This is a breaking change, since parsing would no longer be buffered. This can cause performance and stability issues for users of this library.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reverting stream.r back to bufio.Reader means losing access to the Seeker (stream.r.(io.ReadSeeker) will fail). I'm trying to think of a way around this, so stay tuned.
@@ -94,41 +121,41 @@ var id3Signature = []byte("ID3") | |||
// 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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
praise: Nice refactor, block.IsLast
reads even better in New
and exposes SeekTable
👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thought: When I think about it, stream's parseStreamInfo
has a name conflict with block's parseStreamInfo
, maybe we should rename one of them. What do you think @mewmew?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thought: When I think about it, stream's parseStreamInfo has a name conflict with block's parseStreamInfo, maybe we should rename one of them. What do you think @mewmew?
Both are unexported so we can rename without changing exported API. Personally, I think it is fine as is, but would also be fine with changing the name of one to avoid confusion. The are distinct by one being a function, the other a method, so the call sites will be different. If we do change the name, e.g. of the method parseStreamInfo
the names of other methods parsing blocks should be updated for consistency.
flac.go
Outdated
br := bufio.NewReader(r) | ||
stream = &Stream{r: br} | ||
isLast, err := stream.parseStreamInfo() | ||
stream = &Stream{r: r} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue(blocking): This is a breaking change, since parsing would no longer be buffered. This can cause performance and stability issues for users of this library.
flac.go
Outdated
@@ -157,18 +181,17 @@ func (stream *Stream) skipID3v2() error { | |||
// | |||
// Call Stream.Next to parse the frame header of the next audio frame, and call | |||
// Stream.ParseNext to parse the entire next frame including audio samples. | |||
func Parse(r io.Reader) (stream *Stream, err error) { | |||
func Parse(r io.ReadSeeker) (stream *Stream, err error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue(blocking): Calls to flac.Parse
won't implement seeking. Since the metadata block will be parsed, but the SeekTable won't be created. The logic from New
is missing here to correctly implement seeking in both places.
End-users of this library, should be able to seek regardless if they initialized with New
or Parse
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thought: The names New
and Parse
don't accurately reflect what they do. At least the functionality is well explained in the documentation. What did we think here @mewmew haha?
flac.go
Outdated
return 0, nil | ||
} | ||
|
||
r := stream.r.(io.ReadSeeker) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: This shadows r, use rs
as you did in New
.
flac.go
Outdated
} | ||
|
||
func (stream *Stream) makeSeekTable() (err error) { | ||
r := stream.r.(io.ReadSeeker) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: This shadows r
, use rs
as you did in New
.
flac.go
Outdated
return err | ||
} | ||
|
||
if i%10 == 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's good!
I don't have a good gut feeling what the perfect resolution is, since this library could possibly handle short sound segments (<2s) to long sound segments (>10h+). This could possibly be a configure option, but let's just go with %10
for now.
Or do you have any opinions @mewmew ?
flac.go
Outdated
} | ||
|
||
if i%10 == 0 { | ||
o, err := r.Seek(0, io.SeekCurrent) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: Second time I see this pattern, wondering if it would be better to create a function called something like currentOffset
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, no problem if you really want this. However wouldn't that func have to do another stream.r.(io.ReadSeeker) assertion? So it'd either look like:
func (stream *Stream) currentOffset() (int64, error) { rs, ok := stream.r.(io.ReadSeeker) if !ok { return 0, ErrNoSeeker } return rs.Seek(0, io.SeekCurrent) }
Or
func (stream *Stream) currentOffset(rs io.ReadSeeker) (int64, error) { return rs.Seek(0, io.SeekCurrent) }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kind of like the way it is compared to either of those funcs, but am happy to make the change if you want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it doesn't look that much better. Keep it as it is! Thanks for writing it out :)
flac.go
Outdated
if seeker { | ||
stream.r = r | ||
} else { | ||
stream.r = bufio.NewReader(r) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about this change?
Here is the reasoning:
- if stream.r is *bufio.Reader then it is no longer possible to assert for io.ReadSeeker and so Seek is no longer possible.
- if r is io.ReadSeeker then r must be either *os.File or an in-memory buffer of some kind.
- if r is not io.ReadSeeker then it is probably a network stream, like from http.Request.Body???
My guess is that bufio.Reader is really only needed when the reader is streaming over a network. If reader is *os.File or it's an in memory buffer then bufio.Reader really isn't necessary (but you'll have a way better feel for this than I)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about this change?
The change looks good to me, and the reasoning valid.
My guess is that bufio.Reader is really only needed when the reader is streaming over a network. If reader is
*os.File
or it's an in memory buffer then bufio.Reader really isn't necessary.
I think we did some preliminary tests using *os.File
without buffered reading and it did (at least for an older version of mewkiz/flac
) impact the performance. This was the primary reason we used buffered reading as default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If *os.File did cause problems then it seems like explicitly opting in for the ability to Seek is the thing to do.
something like?
`type Option func(stream *Stream) (err error)
func New(r io.Reader, opts ...Option) (stream *Stream, err error) {
...
}
func EnableSeek(stream *Stream) (err error) {
// don't make stream.r a *bufio.Reader and look for seekTable in metadata
...
}
`
flac.go
Outdated
@@ -48,6 +49,11 @@ type Stream struct { | |||
// Zero or more metadata blocks. | |||
Blocks []*meta.Block | |||
// Underlying io.Reader. | |||
|
|||
seekTable *meta.SeekTable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seekTable *meta.SeekTable | |
// seekTable contains one or more pre-calculated audio frame seek points of the stream; nil if uninitialized. | |
seekTable *meta.SeekTable |
flac.go
Outdated
@@ -48,6 +49,11 @@ type Stream struct { | |||
// Zero or more metadata blocks. | |||
Blocks []*meta.Block | |||
// Underlying io.Reader. | |||
|
|||
seekTable *meta.SeekTable | |||
// Offset where the music data begins since SeekTable.Offset seems to be relative to this position. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since SeekTable.Offset seems to be relative to this position.
We should try to verify this against the official spec and then update the comment accordingly (e.g. remove "seems").
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have been testing with a single Flac file that has a seekTable and its first data point is meta.SeekPoint{SampleNum:0, Offset:0}.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Offset (in bytes) from the first byte of the first frame header to the first byte of the target frame's header."
from https://xiph.org/flac/format.html#metadata_block_seektable, so I removed 'seems'
flac.go
Outdated
} | ||
|
||
return stream, nil | ||
} | ||
|
||
// flacSignature marks the beginning of a FLAC stream. | ||
var flacSignature = []byte("fLaC") | ||
// func EnsureSeekTable(stream *Stream) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The final PR should probably remove this commented-out lines?
return p | ||
} | ||
} | ||
return p |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It wasn't immediately clear to me that this line returned the zero value. For clarity, consider returning meta.SeekPoint{}
which stands out as a zero value, and thus not the "intended" code path.
Then, the function would also be updated to not used named returned values. While this function is not exported it doesn't matter too much. For exported API however, one suggestion is to only used named returns when they help support documentation, e.g. when a function returns two integers (x, y int)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would return the last SeekPoint. I've been playing with ReadSeeker a bit and returning the last point actually seems to mimic the behavior seen here:
https://play.golang.org/p/WStOyCU2wU9
In other words, seeking before the start isn't an error seeking past the end isn't either.
This commit changes the way Stream is initialized quite a bit and maybe it is less readable? I personally think it's more readable because I really like the variadic argument pattern so I've used it quite a bit. I think it addresses some of the concerns: 1: Use *bufio.Reader as the default in order to match the current behaivor by calling stream, err = flac.New(r) 2: Make Seek explicitly optional by calling: stream, err = flac.New(r, flac.EnableSeek)
Hi @cswank, Just a quick heads up. My brother @karlek and I will look at this PR when we find the time to see each other (probably this weekend or next week). Now, we're both busy with work + Uni :) Thanks a lot for working on the seek support for FLAC, it's really been the last missing feature of the low level API for quite some time. This would mark the perfect transition for discussing the v2.0 API (#33), to split the API into two parts; one high-level user facing, and one low-level API of FLAC. After a brief discussion with Henry, I think we will try to limit the scope of this PR to functionality changes - in other words the core seek functionality - and then move extended API discussions (e.g. adding Cheers, |
Hi Robin, EDIT: I have a new branch that I haven't pushed up yet for a new PR. Craig |
Sounds great! |
WIP!!!
I got something working with seek table and fixed block sized flac. I'm making a PR now just to see if you think I'm on the right track before going too far.
If the Flac file doesn't have a seek table in its metadata then this builds one. I still need to make it work for FixedBlockSize=false Flac files (gotta find a file like this first).