callback_proxy.c | 13 +++++++++++++ callback_proxy.h | 3 +++ encoder.go | 5 ++--- errors.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ opus.go | 13 ++++--------- stream.go | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++ diff --git a/callback_proxy.c b/callback_proxy.c new file mode 100644 index 0000000000000000000000000000000000000000..682debf27d3b9b8df314b4d4da8f605640a3a841 --- /dev/null +++ b/callback_proxy.c @@ -0,0 +1,13 @@ +/* + * Named proxy callback function, see + * + * https://code.google.com/p/go-wiki/wiki/cgo + */ + +int go_readproxy(void *p, unsigned char *buf, int nbytes); + +int +c_readproxy(void *p, unsigned char *buf, int nbytes) +{ + return go_readproxy(p, buf, nbytes); +} diff --git a/callback_proxy.h b/callback_proxy.h new file mode 100644 index 0000000000000000000000000000000000000000..f8456c80ca941ad9456560b883f016b4793a8c51 --- /dev/null +++ b/callback_proxy.h @@ -0,0 +1,3 @@ +#pragma once + +int c_readproxy(void *p, unsigned char *buf, int nbytes); diff --git a/encoder.go b/encoder.go index 54383730ee8fbf620ee68f119565e9ca9c8be384..700d51563c7d2900b6f44b1918ef1d28a820b575 100644 --- a/encoder.go +++ b/encoder.go @@ -67,8 +67,7 @@ } if pcm == nil || len(pcm) == 0 { return nil, fmt.Errorf("opus: no data supplied") } - // I never know how much to allocate - data := make([]byte, 10000) + data := make([]byte, maxEncodedFrameSize) n := int(C.opus_encode( enc.p, (*C.opus_int16)(&pcm[0]), @@ -88,7 +87,7 @@ } if pcm == nil || len(pcm) == 0 { return nil, fmt.Errorf("opus: no data supplied") } - data := make([]byte, 10000) + data := make([]byte, maxEncodedFrameSize) n := int(C.opus_encode_float( enc.p, (*C.float)(&pcm[0]), diff --git a/errors.go b/errors.go new file mode 100644 index 0000000000000000000000000000000000000000..3ddfab3f12dc77a34703d021229078985e1d5207 --- /dev/null +++ b/errors.go @@ -0,0 +1,45 @@ +package opus + +import ( + "errors" + "fmt" +) + +/* +#cgo CFLAGS: -std=c99 -Wall -Werror -pedantic -Ibuild/include +#include +*/ +import "C" + +var opusfileErrcodes = map[C.int]error{ + -1: errors.New("OP_FALSE"), + -2: errors.New("OP_EOF"), + -3: errors.New("OP_HOLE"), + -128: errors.New("OP_EREAD"), + -129: errors.New("OP_EFAULT"), + -130: errors.New("OP_EIMPL"), + -131: errors.New("OP_EINVAL"), + -132: errors.New("OP_ENOTFORMAT"), + -133: errors.New("OP_EBADHEADER"), + -134: errors.New("OP_EVERSION"), + -135: errors.New("OP_ENOTAUDIO"), + -136: errors.New("OP_EBADPACKET"), + -137: errors.New("OP_EBADLINK"), + -138: errors.New("OP_ENOSEEK"), + -139: errors.New("OP_EBADTIMESTAMP"), +} + +// opusfileerr maps libopusfile error codes to human readable strings +func opusfileerr(code C.int) error { + err, ok := opusfileErrcodes[code] + if ok { + return err + } + return fmt.Errorf("libopus error: %d (unknown code)", int(code)) +} + +// opuserr translates libopus (not libopusfile) error codes to human readable +// strings +func opuserr(code int) error { + return fmt.Errorf("opus: %s", C.GoString(C.opus_strerror(C.int(code)))) +} diff --git a/opus.go b/opus.go index 518399ddcdaccdb2cc3f7c5d059a23951be906ce..47caf59c74f8dbc90da8d99f7473610a653d135f 100644 --- a/opus.go +++ b/opus.go @@ -4,10 +4,6 @@ // License for use of this code is detailed in the LICENSE file package opus -import ( - "fmt" -) - /* // Statically link libopus. Requires a libopus.a in every directory you use this // as a dependency in. Not great, but CGO doesn't offer anything better, right @@ -18,7 +14,7 @@ // the pre-built lib in here. At least it will be up to the package maintainer, // not the user. // // If I missed something, and somebody knows a better way: please let me know. -#cgo LDFLAGS: libopus.a -lm +#cgo LDFLAGS: libopusfile.a libopus.a -logg -lm #cgo CFLAGS: -std=c99 -Wall -Werror -pedantic -Ibuild/include #include */ @@ -41,12 +37,11 @@ const ( xMAX_BITRATE = 48000 xMAX_FRAME_SIZE_MS = 60 xMAX_FRAME_SIZE = xMAX_BITRATE * xMAX_FRAME_SIZE_MS / 1000 + // Maximum size of an encoded frame. I actually have no idea, but this + // looks like it's big enough. + maxEncodedFrameSize = 10000 ) func Version() string { return C.GoString(C.opus_get_version_string()) } - -func opuserr(code int) error { - return fmt.Errorf("opus: %s", C.GoString(C.opus_strerror(C.int(code)))) -} diff --git a/stream.go b/stream.go new file mode 100644 index 0000000000000000000000000000000000000000..31f8dae3a3c7320b452e3aa00ee929a2a2fc1526 --- /dev/null +++ b/stream.go @@ -0,0 +1,94 @@ +package opus + +import ( + "fmt" + "io" + "unsafe" +) + +/* +#cgo CFLAGS: -std=c99 -Wall -Werror -pedantic -Ibuild/include/opus +#include +#include +#include "callback_proxy.h" +*/ +import "C" + +type Stream struct { + oggfile *C.OggOpusFile + read io.Reader + // Preallocated buffer to pass to the reader + buf []byte +} + +//export go_readproxy +func go_readproxy(p unsafe.Pointer, cbuf *C.uchar, cmaxbytes C.int) C.int { + stream := (*Stream)(p) + maxbytes := int(cmaxbytes) + if maxbytes > cap(stream.buf) { + maxbytes = cap(stream.buf) + } + // Don't bother cleaning up old data because that's not required by the + // io.Reader API. + n, err := stream.read.Read(stream.buf[:maxbytes]) + if err != nil || n == 0 { + return 0 + } + C.memcpy(unsafe.Pointer(cbuf), unsafe.Pointer(&stream.buf[0]), C.size_t(n)) + return C.int(n) +} + +var callbacks = C.struct_OpusFileCallbacks{ + read: C.op_read_func(C.c_readproxy), + seek: nil, + tell: nil, + close: nil, +} + +func NewStream(read io.Reader) (*Stream, error) { + var s Stream + err := s.Init(read) + if err != nil { + return nil, err + } + return &s, nil +} + +// Init initializes a stream with an io.Reader to fetch opus encoded data from +// on demand. Errors from the reader are all transformed to an EOF, any actual +// error information is lost. The same happens when a read returns succesfully, +// but with zero bytes. +func (s *Stream) Init(read io.Reader) error { + if s.oggfile != nil { + return fmt.Errorf("opus stream is already initialized") + } + if read == nil { + return fmt.Errorf("Reader function must be non-nil") + } + var errno C.int + oggfile := C.op_open_callbacks( + unsafe.Pointer(s), + &callbacks, + nil, + 0, + &errno) + if errno != 0 { + return opusfileerr(errno) + } + s.oggfile = oggfile + s.buf = make([]byte, maxEncodedFrameSize) + return nil +} + +func (s *Stream) Read() ([]int16, error) { + pcm := make([]int16, xMAX_FRAME_SIZE) + n := C.op_read( + s.oggfile, + (*C.opus_int16)(&pcm[0]), + C.int(cap(pcm)), + nil) + if n < 0 { + return nil, opusfileerr(n) + } + return pcm[:n], nil +}