Initial commit
This commit is contained in:
commit
88cc70a6e3
12
bandtype.go
Normal file
12
bandtype.go
Normal 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
7
errors.go
Normal 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
13
frame.go
Normal 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
|
||||
}
|
215
internal/oggreader/oggreader.go
Normal file
215
internal/oggreader/oggreader.go
Normal 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
|
||||
}
|
100
internal/oggreader/oggreader_test.go
Normal file
100
internal/oggreader/oggreader_test.go
Normal 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
40
opus.go
Normal 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))
|
||||
}
|
||||
}
|
39
table_of_contents_header.go
Normal file
39
table_of_contents_header.go
Normal 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
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue