opus-go/pkg/oggreader/oggreader.go
2022-09-20 22:29:27 -04:00

220 lines
5.3 KiB
Go

// Package oggreader implements the Ogg media container reader
package oggreader
import (
"encoding/binary"
"errors"
"io"
)
const (
pageHeaderTypeBeginningOfStream = 0x02
pageHeaderSignature = "OggS"
idPageSignature = "OpusHead"
pageHeaderLen = 27
idPagePayloadLength = 19
)
var (
errNilStream = errors.New("stream is nil")
errBadIDPageSignature = errors.New("bad header signature")
errBadIDPageType = errors.New("wrong header, expected beginning of stream")
errBadIDPageLength = errors.New("payload for id page must be 19 bytes")
errBadIDPagePayloadSignature = errors.New("bad payload signature")
errShortPageHeader = errors.New("not enough data for payload header")
errChecksumMismatch = errors.New("expected and actual checksum do not match")
)
// OggReader is used to read Ogg files and return page payloads
type OggReader struct {
stream io.Reader
bytesReadSuccesfully int64
checksumTable *[256]uint32
doChecksum bool
}
// OggHeader is the metadata from the first two pages
// in the file (ID and Comment)
//
// https://tools.ietf.org/html/rfc7845.html#section-3
type OggHeader struct {
ChannelMap uint8
Channels uint8
OutputGain uint16
PreSkip uint16
SampleRate uint32
Version uint8
}
// OggPageHeader is the metadata for a Page
// Pages are the fundamental unit of multiplexing in an Ogg stream
//
// https://tools.ietf.org/html/rfc7845.html#section-1
type OggPageHeader struct {
GranulePosition uint64
sig [4]byte
version uint8
headerType uint8
serial uint32
index uint32
segmentsCount uint8
}
// NewWith returns a new Ogg reader and Ogg header
// with an io.Reader input
func NewWith(in io.Reader) (*OggReader, *OggHeader, error) {
return newWith(in /* doChecksum */, true)
}
func newWith(in io.Reader, doChecksum bool) (*OggReader, *OggHeader, error) {
if in == nil {
return nil, nil, errNilStream
}
reader := &OggReader{
stream: in,
checksumTable: generateChecksumTable(),
doChecksum: doChecksum,
}
header, err := reader.readHeaders()
if err != nil {
return nil, nil, err
}
return reader, header, nil
}
func (o *OggReader) readHeaders() (*OggHeader, error) {
segments, pageHeader, err := o.ParseNextPage()
if err != nil {
return nil, err
}
header := &OggHeader{}
if string(pageHeader.sig[:]) != pageHeaderSignature {
return nil, errBadIDPageSignature
}
if pageHeader.headerType != pageHeaderTypeBeginningOfStream {
return nil, errBadIDPageType
}
if len(segments[0]) != idPagePayloadLength {
return nil, errBadIDPageLength
}
if s := string(segments[0][:8]); s != idPageSignature {
return nil, errBadIDPagePayloadSignature
}
header.Version = segments[0][8]
header.Channels = segments[0][9]
header.PreSkip = binary.LittleEndian.Uint16(segments[0][10:12])
header.SampleRate = binary.LittleEndian.Uint32(segments[0][12:16])
header.OutputGain = binary.LittleEndian.Uint16(segments[0][16:18])
header.ChannelMap = segments[0][18]
return header, nil
}
// ParseNextPage reads from stream and returns Ogg page segments, header,
// and an error if there is incomplete page data.
func (o *OggReader) ParseNextPage() ([][]byte, *OggPageHeader, error) {
h := make([]byte, pageHeaderLen)
n, err := io.ReadFull(o.stream, h)
if err != nil {
return nil, nil, err
} else if n < len(h) {
return nil, nil, errShortPageHeader
}
pageHeader := &OggPageHeader{
sig: [4]byte{h[0], h[1], h[2], h[3]},
}
pageHeader.version = h[4]
pageHeader.headerType = h[5]
pageHeader.GranulePosition = binary.LittleEndian.Uint64(h[6 : 6+8])
pageHeader.serial = binary.LittleEndian.Uint32(h[14 : 14+4])
pageHeader.index = binary.LittleEndian.Uint32(h[18 : 18+4])
pageHeader.segmentsCount = h[26]
sizeBuffer := make([]byte, pageHeader.segmentsCount)
if _, err = io.ReadFull(o.stream, sizeBuffer); err != nil {
return nil, nil, err
}
segments := [][]byte{}
for _, s := range sizeBuffer {
segment := make([]byte, int(s))
if _, err = io.ReadFull(o.stream, segment); err != nil {
return nil, nil, err
}
segments = append(segments, segment)
}
if o.doChecksum {
var checksum uint32
updateChecksum := func(v byte) {
checksum = (checksum << 8) ^ o.checksumTable[byte(checksum>>24)^v]
}
for index := range h {
// Don't include expected checksum in our generation
if index > 21 && index < 26 {
updateChecksum(0)
continue
}
updateChecksum(h[index])
}
for _, s := range sizeBuffer {
updateChecksum(s)
}
for i := range segments {
for index := range segments[i] {
updateChecksum(segments[i][index])
}
}
if binary.LittleEndian.Uint32(h[22:22+4]) != checksum {
return nil, nil, errChecksumMismatch
}
}
return segments, pageHeader, nil
}
// ResetReader resets the internal stream of OggReader. This is useful
// for live streams, where the end of the file might be read without the
// data being finished.
func (o *OggReader) ResetReader(reset func(bytesRead int64) io.Reader) {
o.stream = reset(o.bytesReadSuccesfully)
}
func generateChecksumTable() *[256]uint32 {
var table [256]uint32
const poly = 0x04c11db7
for i := range table {
r := uint32(i) << 24
for j := 0; j < 8; j++ {
if (r & 0x80000000) != 0 {
r = (r << 1) ^ poly
} else {
r <<= 1
}
table[i] = (r & 0xffffffff)
}
}
return &table
}