Initial commit

This commit is contained in:
Sean DuBois 2022-06-02 19:14:23 -04:00
commit 88cc70a6e3
8 changed files with 429 additions and 0 deletions

12
bandtype.go Normal file
View file

@ -0,0 +1,12 @@
package main
//TODO right name?
type bandType int
const (
narrowBandType bandType = iota + 1
mediumBandType
wideBandType
superWideBandType
fullBandType
)

7
errors.go Normal file
View file

@ -0,0 +1,7 @@
package main
import "errors"
var (
errTooShortForTableOfContentsHeader = errors.New("Packet is too short to contain table of contents header")
)

13
frame.go Normal file
View file

@ -0,0 +1,13 @@
package main
func parsePacket(in []byte) (c configuration, isStereo bool, frames [][]byte, err error) {
if len(in) < 1 {
err = errTooShortForTableOfContentsHeader
return
}
tocHeader := tableOfContentsHeader(in[0])
c = tocHeader.configuration()
return
}

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module github.com/pion/opus
go 1.18

View file

@ -0,0 +1,215 @@
// 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) {
payload, 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(payload) != idPagePayloadLength {
return nil, errBadIDPageLength
}
if s := string(payload[:8]); s != idPageSignature {
return nil, errBadIDPagePayloadSignature
}
header.Version = payload[8]
header.Channels = payload[9]
header.PreSkip = binary.LittleEndian.Uint16(payload[10:12])
header.SampleRate = binary.LittleEndian.Uint32(payload[12:16])
header.OutputGain = binary.LittleEndian.Uint16(payload[16:18])
header.ChannelMap = payload[18]
return header, nil
}
// ParseNextPage reads from stream and returns Ogg page payload, 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
}
payloadSize := 0
for _, s := range sizeBuffer {
payloadSize += int(s)
}
payload := make([]byte, payloadSize)
if _, err = io.ReadFull(o.stream, payload); err != nil {
return nil, nil, err
}
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 index := range payload {
updateChecksum(payload[index])
}
if binary.LittleEndian.Uint32(h[22:22+4]) != checksum {
return nil, nil, errChecksumMismatch
}
}
return payload, 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
}

View file

@ -0,0 +1,100 @@
package oggreader
import (
"bytes"
"io"
"testing"
"github.com/stretchr/testify/assert"
)
// buildOggFile generates a valid oggfile that can
// be used for tests
func buildOggContainer() []byte {
return []byte{
0x4f, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x8e, 0x9b, 0x20, 0xaa, 0x00, 0x00,
0x00, 0x00, 0x61, 0xee, 0x61, 0x17, 0x01, 0x13, 0x4f, 0x70,
0x75, 0x73, 0x48, 0x65, 0x61, 0x64, 0x01, 0x02, 0x00, 0x0f,
0x80, 0xbb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4f, 0x67, 0x67,
0x53, 0x00, 0x00, 0xda, 0x93, 0xc2, 0xd9, 0x00, 0x00, 0x00,
0x00, 0x8e, 0x9b, 0x20, 0xaa, 0x02, 0x00, 0x00, 0x00, 0x49,
0x97, 0x03, 0x37, 0x01, 0x05, 0x98, 0x36, 0xbe, 0x88, 0x9e,
}
}
func TestOggReader_ParseValidHeader(t *testing.T) {
reader, header, err := NewWith(bytes.NewReader(buildOggContainer()))
assert.NoError(t, err)
assert.NotNil(t, reader)
assert.NotNil(t, header)
assert.EqualValues(t, header.ChannelMap, 0)
assert.EqualValues(t, header.Channels, 2)
assert.EqualValues(t, header.OutputGain, 0)
assert.EqualValues(t, header.PreSkip, 0xf00)
assert.EqualValues(t, header.SampleRate, 48000)
assert.EqualValues(t, header.Version, 1)
}
func TestOggReader_ParseNextPage(t *testing.T) {
ogg := bytes.NewReader(buildOggContainer())
reader, _, err := NewWith(ogg)
assert.NoError(t, err)
assert.NotNil(t, reader)
payload, _, err := reader.ParseNextPage()
assert.Equal(t, []byte{0x98, 0x36, 0xbe, 0x88, 0x9e}, payload)
assert.NoError(t, err)
_, _, err = reader.ParseNextPage()
assert.Equal(t, err, io.EOF)
}
func TestOggReader_ParseErrors(t *testing.T) {
t.Run("Assert that Reader isn't nil", func(t *testing.T) {
_, _, err := NewWith(nil)
assert.Equal(t, err, errNilStream)
})
t.Run("Invalid ID Page Header Signature", func(t *testing.T) {
ogg := buildOggContainer()
ogg[0] = 0
_, _, err := newWith(bytes.NewReader(ogg), false)
assert.Equal(t, err, errBadIDPageSignature)
})
t.Run("Invalid ID Page Header Type", func(t *testing.T) {
ogg := buildOggContainer()
ogg[5] = 0
_, _, err := newWith(bytes.NewReader(ogg), false)
assert.Equal(t, err, errBadIDPageType)
})
t.Run("Invalid ID Page Payload Length", func(t *testing.T) {
ogg := buildOggContainer()
ogg[27] = 0
_, _, err := newWith(bytes.NewReader(ogg), false)
assert.Equal(t, err, errBadIDPageLength)
})
t.Run("Invalid ID Page Payload Length", func(t *testing.T) {
ogg := buildOggContainer()
ogg[35] = 0
_, _, err := newWith(bytes.NewReader(ogg), false)
assert.Equal(t, err, errBadIDPagePayloadSignature)
})
t.Run("Invalid Page Checksum", func(t *testing.T) {
ogg := buildOggContainer()
ogg[22] = 0
_, _, err := NewWith(bytes.NewReader(ogg))
assert.Equal(t, err, errChecksumMismatch)
})
}

40
opus.go Normal file
View file

@ -0,0 +1,40 @@
package main
import (
"errors"
"fmt"
"io"
"os"
"github.com/pion/opus/internal/oggreader"
)
func main() {
file, err := os.Open("output.ogg")
if err != nil {
panic(err)
}
ogg, _, err := oggreader.NewWith(file)
if err != nil {
panic(err)
}
for {
pageData, _, err := ogg.ParseNextPage()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
panic(err)
}
config, isStereo, frames, err := parsePacket(pageData)
if err != nil {
panic(err)
}
fmt.Printf("Mode(%d) isStereo(%t) framesCount(%d)\n", config.mode(), isStereo, len(frames))
}
}

View file

@ -0,0 +1,39 @@
package main
type (
tableOfContentsHeader byte
configuration byte
configurationMode byte
)
func (t tableOfContentsHeader) configuration() configuration {
return configuration(t >> 3)
}
func (t tableOfContentsHeader) isStereo() bool {
return false
}
func (t tableOfContentsHeader) numberOfFrames() byte {
return 0
}
const (
configurationModeSilkOnly configurationMode = iota + 1
configurationModeCELTOnly
configurationModeHybrid
)
func (c configuration) mode() configurationMode {
switch {
case c >= 0 && c <= 11:
return configurationModeSilkOnly
case c >= 12 && c <= 15:
return configurationModeHybrid
case c >= 16 && c <= 31:
return configurationModeCELTOnly
default:
return 0
}
}