diff --git a/pkg/oggtag/oggtag.go b/pkg/oggtag/oggtag.go index 47d344a..d1aa0c2 100644 --- a/pkg/oggtag/oggtag.go +++ b/pkg/oggtag/oggtag.go @@ -6,9 +6,57 @@ for certain tag names ending with an = character, e.g. artist= and title=. import ( "bytes" + "io" + "os" "strings" + "time" ) +const bufferLength = 4096 +const OggMagicSequence = "OggS" +const OggVorbisSequence = "vorbis" + +// OggReadFile returns a head of an OGG file, and a buffer contains last frame. +func OggReadFile(path string) (buf_start, buf_end []byte, _ error) { + f, err := os.Open(path) + if err != nil { + return nil, nil, err + } + + buf_start = make([]byte, bufferLength) + + if _, err := f.Read(buf_start); err != nil { + return nil, nil, err + } + + buf_end = make([]byte, bufferLength) + + if _, err := f.Seek(-bufferLength, io.SeekEnd); err != nil { + return nil, nil, err + } + + fst, err := f.Stat() + if err != nil { + return nil, nil, err + } + + for offset := int64(bufferLength); offset <= fst.Size(); offset += bufferLength { + if _, err := f.Seek(-offset, io.SeekEnd); err != nil { + return nil, nil, err + } + + if _, err := f.Read(buf_end); err != nil { + return nil, nil, err + } + + if bytes.Contains(buf_end, []byte(OggMagicSequence)) { + break + } + } + + return +} + // OggGetTag is searching for a certain tag in a given buffer buf and returns // its value. // @@ -29,3 +77,22 @@ func OggGetTag(buf []byte, tag string) string { valLen := int(buf[tagIdx-4]) - tagNameLen return string(buf[valStart : valStart+valLen]) } + +// OggGetDuration returns song's duration in milliseconds. +func OggGetDuration(buf_start, buf_end []byte) time.Duration { + rateIdx := bytes.Index(buf_start, []byte(OggVorbisSequence)) + + len(OggVorbisSequence) + 5 + rateBytes := buf_start[rateIdx : rateIdx+4] + rate := int32(rateBytes[0]) + int32(rateBytes[1])<<8 + + int32(rateBytes[2])<<16 + int32(rateBytes[3])<<24 + + granuleIdx := bytes.LastIndex(buf_end, []byte(OggMagicSequence)) + + len(OggMagicSequence) + 2 + granuleBytes := buf_end[granuleIdx : granuleIdx+8] + granule := int64(granuleBytes[0]) + int64(granuleBytes[1])<<8 + + int64(granuleBytes[2])<<16 + int64(granuleBytes[3])<<24 + + int64(granuleBytes[4])<<32 + int64(granuleBytes[5])<<40 + + int64(granuleBytes[6])<<48 + int64(granuleBytes[7])<<56 + + return time.Duration(granule*1000/int64(rate)) * time.Millisecond +} diff --git a/pkg/oggtag/oggtag_test.go b/pkg/oggtag/oggtag_test.go index a98c23f..e5d51a3 100644 --- a/pkg/oggtag/oggtag_test.go +++ b/pkg/oggtag/oggtag_test.go @@ -1,6 +1,7 @@ package oggtag import ( + "bytes" "os" "testing" ) @@ -33,3 +34,31 @@ func BenchmarkOggGetTag(b *testing.B) { OggGetTag(buf, "artist") } } + +func TestOggReadFile(t *testing.T) { + bs, bf, err := OggReadFile(sampleSong) + if err != nil { + t.Error(err) + } + + t.Log("bs = ", len(bs), "; bf =", len(bf), "bf(OggS) =", bytes.LastIndex(bf, []byte("OggS"))) +} + +func BenchmarkOggGetDuration(b *testing.B) { + bs, bf, err := OggReadFile(sampleSong) + if err != nil { + b.Error(err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + OggGetDuration(bs, bf) + } +} + +func TestOggGetDuration(t *testing.T) { + bs, bf, err := OggReadFile(sampleSong) + if err != nil { + t.Error(err) + } + t.Log(OggGetDuration(bs, bf)) +}