Skip to content

Commit

Permalink
V2
Browse files Browse the repository at this point in the history
  • Loading branch information
bsponge committed Mar 5, 2022
1 parent 6035aa9 commit 4baf2a5
Show file tree
Hide file tree
Showing 16 changed files with 945 additions and 285 deletions.
10 changes: 7 additions & 3 deletions AUTHORS
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
Authors and copyright holders of this package in no particular order. By adding
yourself to this list you agree to license your contributions under the
relevant license (see the LICENSE file).
All code and content in this project is Copyright © 2015-2022 Go Opus Authors

Go Opus Authors and copyright holders of this package are listed below, in no
particular order. By adding yourself to this list you agree to license your
contributions under the relevant license (see the LICENSE file).

Hraban Luyat <[email protected]>
Dejian Xu <[email protected]>
Tobias Wellnitz <[email protected]>
Elinor Natanzon <[email protected]>
Victor Gaydov <[email protected]>
Randy Reddig <[email protected]>
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright © 2015-2016 Go Opus Authors (see AUTHORS file)
Copyright © 2015-2022 Go Opus Authors (see AUTHORS file)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
90 changes: 78 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[![travis-ci status](https://api.travis-ci.org/travis-ci/travis-web.svg?branch=master "tarvis-ci build status")](https://travis-ci.org/hraban/opus)
[![Test](https://github.com/hraban/opus/workflows/Test/badge.svg)](https://github.com/hraban/opus/actions?query=workflow%3ATest)

## Go wrapper for Opus

Expand All @@ -8,6 +8,23 @@ libopusfile.
The C libraries and docs are hosted at https://opus-codec.org/. This package
just handles the wrapping in Go, and is unaffiliated with xiph.org.

Features:

- ✅ encode and decode raw PCM data to raw Opus data
- ✅ useful when you control the recording device, _and_ the playback
- ✅ decode .opus and .ogg files into raw audio data ("PCM")
- ✅ reuse the system libraries for opus decoding (libopus)
- ✅ works easily on Linux, Mac and Docker; needs libs on Windows
- ❌ does not _create_ .opus or .ogg files (but feel free to send a PR)
- ❌ does not work with .wav files (you need a separate .wav library for that)
- ❌ no self-contained binary (you need the xiph.org libopus lib, e.g. through a package manager)
- ❌ no cross compiling (because it uses CGo)

Good use cases:

- 👍 you are writing a music player app in Go, and you want to play back .opus files
- 👍 you record raw wav in a web app or mobile app, you encode it as Opus on the client, you send the opus to a remote webserver written in Go, and you want to decode it back to raw audio data on that server

## Details

This wrapper provides a Go translation layer for three elements from the
Expand All @@ -17,6 +34,12 @@ xiph.org opus libs:
* decoders
* files & streams

### Import

```go
import "gopkg.in/hraban/opus.v2"
```

### Encoding

To encode raw audio to the Opus format, create an encoder first:
Expand Down Expand Up @@ -85,7 +108,7 @@ Now pass it the opus bytes, and a buffer to store the PCM sound in:
```go
var frameSizeMs float32 = ... // if you don't know, go with 60 ms.
frameSize := channels * frameSizeMs * sampleRate / 1000
pcm := make([]byte, int(frameSize))
pcm := make([]int16, int(frameSize))
n, err := dec.Decode(data, pcm)
if err != nil {
...
Expand All @@ -94,15 +117,20 @@ if err != nil {
// To get all samples (interleaved if multiple channels):
pcm = pcm[:n*channels] // only necessary if you didn't know the right frame size

// or access directly:
// or access sample per sample, directly:
for i := 0; i < n; i++ {
ch1 := pcm[i*channels+0]
// if stereo:
ch2 := pcm[i*channels+1]
// For stereo output: copy ch1 into ch2 in mono mode, or deinterleave stereo
ch2 := pcm[(i*channels)+(channels-1)]
}
```

### Streams (and files)
To handle packet loss from an unreliable network, see the
[DecodePLC](https://godoc.org/gopkg.in/hraban/opus.v2#Decoder.DecodePLC) and
[DecodeFEC](https://godoc.org/gopkg.in/hraban/opus.v2#Decoder.DecodeFEC)
options.

### Streams (and Files)

To decode a .opus file (or .ogg with Opus data), or to decode a "Opus stream"
(which is a Ogg stream with Opus data), use the `Stream` interface. It wraps an
Expand All @@ -120,34 +148,53 @@ if err != nil {
...
}
defer s.Close()
buf := make([]byte, 16384)
pcmbuf := make([]int16, 16384)
for {
n, err = s.Read(buf)
n, err = s.Read(pcmbuf)
if err == io.EOF {
break
} else if err != nil {
...
}
pcm := buf[:n*channels]
pcm := pcmbuf[:n*channels]

// send pcm to audio device here, or write to a .wav file

}
```

See https://godoc.org/gopkg.in/hraban/opus.v1#Stream for further info.
See https://godoc.org/gopkg.in/hraban/opus.v2#Stream for further info.

### "My .ogg/.opus file doesn't play!" or "How do I play Opus in VLC / mplayer / ...?"

Note: this package only does _encoding_ of your audio, to _raw opus data_. You can't just dump those all in one big file and play it back. You need extra info. First of all, you need to know how big each individual block is. Remember: opus data is a stream of encoded separate blocks, not one big stream of bytes. Second, you need meta-data: how many channels? What's the sampling rate? Frame size? Etc.

Look closely at the decoding sample code (not stream), above: we're passing all that meta-data in, hard-coded. If you just put all your encoded bytes in one big file and gave that to a media player, it wouldn't know what to do with it. It wouldn't even know that it's Opus data. It would just look like `/dev/random`.

What you need is a [container format](https://en.wikipedia.org/wiki/Container_format_(computing)).

Compare it to video:

* Encodings: MPEG[1234], VP9, H26[45], AV1
* Container formats: .mkv, .avi, .mov, .ogv

For Opus audio, the most common container format is OGG, aka .ogg or .opus. You'll know OGG from OGG/Vorbis: that's [Vorbis](https://xiph.org/vorbis/) encoded audio in an OGG container. So for Opus, you'd call it OGG/Opus. But technically you could stick opus data in any container format that supports it, including e.g. Matroska (.mka for audio, you probably know it from .mkv for video).

Note: libopus, the C library that this wraps, technically comes with libopusfile, which can help with the creation of OGG/Opus streams from raw audio data. I just never needed it myself, so I haven't added the necessary code for it. If you find yourself adding it: send me a PR and we'll get it merged.

This libopus wrapper _does_ come with code for _decoding_ an OGG/Opus stream. Just not for writing one.

### API Docs

Go wrapper API reference:
https://godoc.org/gopkg.in/hraban/opus.v1
https://godoc.org/gopkg.in/hraban/opus.v2

Full libopus C API reference:
https://www.opus-codec.org/docs/opus_api-1.1.3/

For more examples, see the `_test.go` files.

## Build & installation
## Build & Installation

This package requires libopus and libopusfile development packages to be
installed on your system. These are available on Debian based systems from
Expand All @@ -165,6 +212,25 @@ Mac:
brew install pkg-config opus opusfile
```

### Building Without `libopusfile`

This package can be built without `libopusfile` by using the build tag `nolibopusfile`.
This enables the compilation of statically-linked binaries with no external
dependencies on operating systems without a static `libopusfile`, such as
[Alpine Linux](https://pkgs.alpinelinux.org/contents?branch=edge&name=opusfile-dev&arch=x86_64&repo=main).

**Note:** this will disable all file and `Stream` APIs.

To enable this feature, add `-tags nolibopusfile` to your `go build` or `go test` commands:

```sh
# Build
go build -tags nolibopusfile ...

# Test
go test -tags nolibopusfile ./...
```

### Using in Docker

If your Dockerized app has this library as a dependency (directly or
Expand Down
20 changes: 16 additions & 4 deletions callbacks.c
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
// Copyright © 2015, 2016 Authors (see AUTHORS file)
// +build !nolibopusfile

// Copyright © Go Opus Authors (see AUTHORS file)
//
// License for use of this code is detailed in the LICENSE file

// Allocate callback struct in C to ensure it's not managed by the Go GC. This
// plays nice with the CGo rules and avoids any confusion.

#include <opusfile.h>
#include <stdint.h>

// Defined in Go. Uses the same signature as Go, no need for proxy function.
int go_readcallback(void *p, unsigned char *buf, int nbytes);

// Allocated once, never moved. Pointer to this is safe for passing around
// between Go and C.
struct OpusFileCallbacks callbacks = {
static struct OpusFileCallbacks callbacks = {
.read = go_readcallback,
};

// Proxy function for op_open_callbacks, because it takes a void * context but
// we want to pass it non-pointer data, namely an arbitrary uintptr_t
// value. This is legal C, but go test -race (-d=checkptr) complains anyway. So
// we have this wrapper function to shush it.
// https://groups.google.com/g/golang-nuts/c/995uZyRPKlU
OggOpusFile *
my_open_callbacks(uintptr_t p, int *error)
{
return op_open_callbacks((void *)p, &callbacks, NULL, 0, error);
}
151 changes: 149 additions & 2 deletions decoder.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2015-2017 Go Opus Authors (see AUTHORS file)
// Copyright © Go Opus Authors (see AUTHORS file)
//
// License for use of this code is detailed in the LICENSE file

Expand All @@ -11,7 +11,13 @@ import (

/*
#cgo pkg-config: opus
#include <opus/opus.h>
#include <opus.h>
int
bridge_decoder_get_last_packet_duration(OpusDecoder *st, opus_int32 *samples)
{
return opus_decoder_ctl(st, OPUS_GET_LAST_PACKET_DURATION(samples));
}
*/
import "C"

Expand Down Expand Up @@ -113,3 +119,144 @@ func (dec *Decoder) DecodeFloat32(data []byte, pcm []float32) (int, error) {
}
return n, nil
}

// DecodeFEC encoded Opus data into the supplied buffer with forward error
// correction.
//
// It is to be used on the packet directly following the lost one. The supplied
// buffer needs to be exactly the duration of audio that is missing
//
// When a packet is considered "lost", DecodeFEC can be called on the next
// packet in order to try and recover some of the lost data. The PCM needs to be
// exactly the duration of audio that is missing. `LastPacketDuration()` can be
// used on the decoder to get the length of the last packet. Note also that in
// order to use this feature the encoder needs to be configured with
// SetInBandFEC(true) and SetPacketLossPerc(x) options.
//
// Note that DecodeFEC automatically falls back to PLC when no FEC data is
// available in the provided packet.
func (dec *Decoder) DecodeFEC(data []byte, pcm []int16) error {
if dec.p == nil {
return errDecUninitialized
}
if len(data) == 0 {
return fmt.Errorf("opus: no data supplied")
}
if len(pcm) == 0 {
return fmt.Errorf("opus: target buffer empty")
}
if cap(pcm)%dec.channels != 0 {
return fmt.Errorf("opus: target buffer capacity must be multiple of channels")
}
n := int(C.opus_decode(
dec.p,
(*C.uchar)(&data[0]),
C.opus_int32(len(data)),
(*C.opus_int16)(&pcm[0]),
C.int(cap(pcm)/dec.channels),
1))
if n < 0 {
return Error(n)
}
return nil
}

// DecodeFECFloat32 encoded Opus data into the supplied buffer with forward error
// correction. It is to be used on the packet directly following the lost one.
// The supplied buffer needs to be exactly the duration of audio that is missing
func (dec *Decoder) DecodeFECFloat32(data []byte, pcm []float32) error {
if dec.p == nil {
return errDecUninitialized
}
if len(data) == 0 {
return fmt.Errorf("opus: no data supplied")
}
if len(pcm) == 0 {
return fmt.Errorf("opus: target buffer empty")
}
if cap(pcm)%dec.channels != 0 {
return fmt.Errorf("opus: target buffer capacity must be multiple of channels")
}
n := int(C.opus_decode_float(
dec.p,
(*C.uchar)(&data[0]),
C.opus_int32(len(data)),
(*C.float)(&pcm[0]),
C.int(cap(pcm)/dec.channels),
1))
if n < 0 {
return Error(n)
}
return nil
}

// DecodePLC recovers a lost packet using Opus Packet Loss Concealment feature.
//
// The supplied buffer needs to be exactly the duration of audio that is missing.
// When a packet is considered "lost", `DecodePLC` and `DecodePLCFloat32` methods
// can be called in order to obtain something better sounding than just silence.
// The PCM needs to be exactly the duration of audio that is missing.
// `LastPacketDuration()` can be used on the decoder to get the length of the
// last packet.
//
// This option does not require any additional encoder options. Unlike FEC,
// PLC does not introduce additional latency. It is calculated from the previous
// packet, not from the next one.
func (dec *Decoder) DecodePLC(pcm []int16) error {
if dec.p == nil {
return errDecUninitialized
}
if len(pcm) == 0 {
return fmt.Errorf("opus: target buffer empty")
}
if cap(pcm)%dec.channels != 0 {
return fmt.Errorf("opus: output buffer capacity must be multiple of channels")
}
n := int(C.opus_decode(
dec.p,
nil,
0,
(*C.opus_int16)(&pcm[0]),
C.int(cap(pcm)/dec.channels),
0))
if n < 0 {
return Error(n)
}
return nil
}

// DecodePLCFloat32 recovers a lost packet using Opus Packet Loss Concealment feature.
// The supplied buffer needs to be exactly the duration of audio that is missing.
func (dec *Decoder) DecodePLCFloat32(pcm []float32) error {
if dec.p == nil {
return errDecUninitialized
}
if len(pcm) == 0 {
return fmt.Errorf("opus: target buffer empty")
}
if cap(pcm)%dec.channels != 0 {
return fmt.Errorf("opus: output buffer capacity must be multiple of channels")
}
n := int(C.opus_decode_float(
dec.p,
nil,
0,
(*C.float)(&pcm[0]),
C.int(cap(pcm)/dec.channels),
0))
if n < 0 {
return Error(n)
}
return nil
}

// LastPacketDuration gets the duration (in samples)
// of the last packet successfully decoded or concealed.
func (dec *Decoder) LastPacketDuration() (int, error) {
var samples C.opus_int32
res := C.bridge_decoder_get_last_packet_duration(dec.p, &samples)
if res != C.OPUS_OK {
return 0, Error(res)
}
return int(samples), nil
}
Loading

0 comments on commit 4baf2a5

Please sign in to comment.