commit 88cc70a6e35f07499bfddf40ae2281ebde0358d6 Author: Sean DuBois Date: Thu Jun 2 19:14:23 2022 -0400 Initial commit diff --git a/bandtype.go b/bandtype.go new file mode 100644 index 0000000..f9d9836 --- /dev/null +++ b/bandtype.go @@ -0,0 +1,12 @@ +package main + +//TODO right name? +type bandType int + +const ( + narrowBandType bandType = iota + 1 + mediumBandType + wideBandType + superWideBandType + fullBandType +) diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..dbee98c --- /dev/null +++ b/errors.go @@ -0,0 +1,7 @@ +package main + +import "errors" + +var ( + errTooShortForTableOfContentsHeader = errors.New("Packet is too short to contain table of contents header") +) diff --git a/frame.go b/frame.go new file mode 100644 index 0000000..1b0515a --- /dev/null +++ b/frame.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0ebbd0e --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/pion/opus + +go 1.18 diff --git a/internal/oggreader/oggreader.go b/internal/oggreader/oggreader.go new file mode 100644 index 0000000..cd14a01 --- /dev/null +++ b/internal/oggreader/oggreader.go @@ -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 +} diff --git a/internal/oggreader/oggreader_test.go b/internal/oggreader/oggreader_test.go new file mode 100644 index 0000000..1a75ca6 --- /dev/null +++ b/internal/oggreader/oggreader_test.go @@ -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) + }) +} diff --git a/opus.go b/opus.go new file mode 100644 index 0000000..80060e6 --- /dev/null +++ b/opus.go @@ -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)) + } +} diff --git a/table_of_contents_header.go b/table_of_contents_header.go new file mode 100644 index 0000000..a624fcf --- /dev/null +++ b/table_of_contents_header.go @@ -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 + } +}