Added CBR / VBR encoder
This commit is contained in:
parent
af4ded4c0a
commit
1413086e90
|
@ -188,11 +188,6 @@ func (d *Decoder) ReadFloat() ([]float32, int, error) {
|
||||||
buf[i*channelCount+ch] = sample
|
buf[i*channelCount+ch] = sample
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i := 0; i < int(ret); i++ {
|
|
||||||
for ch := 0; ch < channelCount; ch++ {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf, channelCount, nil
|
return buf, channelCount, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDecoder(t *testing.T) {
|
func TestDecoder(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
f, err := os.Open("test.vorbis")
|
f, err := os.Open("test.vorbis")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
|
@ -43,6 +44,7 @@ func TestDecoder(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDecoderFloat(t *testing.T) {
|
func TestDecoderFloat(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
f, err := os.Open("test.vorbis")
|
f, err := os.Open("test.vorbis")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
|
|
265
encoder.go
Normal file
265
encoder.go
Normal file
|
@ -0,0 +1,265 @@
|
||||||
|
package go_vorbis
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo pkg-config: vorbisenc
|
||||||
|
#cgo pkg-config: vorbis
|
||||||
|
#cgo pkg-config: ogg
|
||||||
|
#include <vorbis/vorbisenc.h>
|
||||||
|
|
||||||
|
typedef size_t (*vorbisfile_read_func)(void *ptr, size_t size, size_t nmemb, void *datasource);
|
||||||
|
typedef int (*vorbisfile_seek_func)(void *datasource, ogg_int64_t offset, int whence);
|
||||||
|
typedef long (*vorbisfile_tell_func)(void *datasource);
|
||||||
|
typedef int (*vorbisfile_close_func)(void *datasource);
|
||||||
|
|
||||||
|
size_t cgoReadCallback(void *ptr, size_t size, size_t nmemb, void *datasource);
|
||||||
|
int cgoSeekCallback(void *datasource, ogg_int64_t offset, int whence);
|
||||||
|
long cgoTellCallback(void *datasource);
|
||||||
|
int cgoCloseCallback(void *datasource);
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"math/rand"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cStr = C.CString("ENCODER")
|
||||||
|
|
||||||
|
type Encoder struct {
|
||||||
|
vorbisInfo *C.vorbis_info
|
||||||
|
//allocated vorbisInfo memory
|
||||||
|
vorbisInfoMem []byte
|
||||||
|
|
||||||
|
vorbisComment *C.vorbis_comment
|
||||||
|
//allocated vorbisComment memory
|
||||||
|
vorbisCommentMem []byte
|
||||||
|
|
||||||
|
vorbisDspState *C.vorbis_dsp_state
|
||||||
|
//allocated vorbisDspState memory
|
||||||
|
vorbisDspStateMem []byte
|
||||||
|
|
||||||
|
vorbisBlock *C.vorbis_block
|
||||||
|
//allocated vorbisDspState memory
|
||||||
|
vorbisBlockMem []byte
|
||||||
|
|
||||||
|
oggPage *C.ogg_page
|
||||||
|
//allocated oggPage memory
|
||||||
|
oggPageMem []byte
|
||||||
|
|
||||||
|
oggPacket *C.ogg_packet
|
||||||
|
//allocated oggPage memory
|
||||||
|
oggPacketMem []byte
|
||||||
|
|
||||||
|
oggStreamState *C.ogg_stream_state
|
||||||
|
//allocated oggStreamState memory
|
||||||
|
oggStreamStateMem []byte
|
||||||
|
|
||||||
|
channels int
|
||||||
|
|
||||||
|
writer io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewEncoderCBR Creates a new encoder instance. maxBitRate, nominalBitRate, minBitrate can be set to -1 to let encoder pick.
|
||||||
|
func NewEncoderCBR(writer io.Writer, channels, sampleRate int, maxBitRate, nominalBitRate, minBitrate int64) (*Encoder, error) {
|
||||||
|
e := &Encoder{
|
||||||
|
writer: writer,
|
||||||
|
channels: channels,
|
||||||
|
}
|
||||||
|
e.initStructs()
|
||||||
|
|
||||||
|
if ret := C.vorbis_encode_init(e.vorbisInfo, C.long(channels), C.long(sampleRate), C.long(maxBitRate), C.long(nominalBitRate), C.long(minBitrate)); ret != 0 {
|
||||||
|
switch ret {
|
||||||
|
case C.OV_EFAULT:
|
||||||
|
return nil, errors.New("OV_EFAULT")
|
||||||
|
case C.OV_EINVAL:
|
||||||
|
return nil, errors.New("OV_EINVAL")
|
||||||
|
case C.OV_EIMPL:
|
||||||
|
return nil, errors.New("OV_EIMPL")
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unknown error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e.initStructs()
|
||||||
|
|
||||||
|
e.start()
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//NewEncoderVBR Creates a new encoder instance.
|
||||||
|
func NewEncoderVBR(writer io.Writer, channels, sampleRate int, quality float32) (*Encoder, error) {
|
||||||
|
e := &Encoder{
|
||||||
|
writer: writer,
|
||||||
|
channels: channels,
|
||||||
|
}
|
||||||
|
e.initStructs()
|
||||||
|
|
||||||
|
if ret := C.vorbis_encode_init_vbr(e.vorbisInfo, C.long(channels), C.long(sampleRate), C.float(quality)); ret != 0 {
|
||||||
|
switch ret {
|
||||||
|
case C.OV_EFAULT:
|
||||||
|
return nil, errors.New("OV_EFAULT")
|
||||||
|
case C.OV_EINVAL:
|
||||||
|
return nil, errors.New("OV_EINVAL")
|
||||||
|
case C.OV_EIMPL:
|
||||||
|
return nil, errors.New("OV_EIMPL")
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unknown error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e.initStructs()
|
||||||
|
|
||||||
|
e.start()
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) start() {
|
||||||
|
C.vorbis_comment_add_tag(e.vorbisComment, cStr, C.vorbis_version_string())
|
||||||
|
|
||||||
|
var header, headerComm, headerCode C.ogg_packet
|
||||||
|
|
||||||
|
C.vorbis_analysis_headerout(e.vorbisDspState, e.vorbisComment, &header, &headerComm, &headerCode)
|
||||||
|
C.ogg_stream_packetin(e.oggStreamState, &header)
|
||||||
|
C.ogg_stream_packetin(e.oggStreamState, &headerComm)
|
||||||
|
C.ogg_stream_packetin(e.oggStreamState, &headerCode)
|
||||||
|
|
||||||
|
for {
|
||||||
|
if C.ogg_stream_flush(e.oggStreamState, e.oggPage) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
e.pageWrite()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) WriteFloat(buf []float32) error {
|
||||||
|
if (len(buf) % e.channels) != 0 {
|
||||||
|
return errors.New("non-divisible buffer length")
|
||||||
|
}
|
||||||
|
samples := len(buf) / e.channels
|
||||||
|
target := C.vorbis_analysis_buffer(e.vorbisDspState, C.int(samples))
|
||||||
|
|
||||||
|
ptrSlice := unsafe.Slice(target, e.channels)
|
||||||
|
for ch, ptr := range ptrSlice {
|
||||||
|
floatSlice := unsafe.Slice((*float32)(ptr), samples)
|
||||||
|
for i := range floatSlice {
|
||||||
|
floatSlice[i] = buf[i*e.channels+ch]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
C.vorbis_analysis_wrote(e.vorbisDspState, C.int(samples))
|
||||||
|
|
||||||
|
e.process()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) Flush() {
|
||||||
|
C.vorbis_analysis_wrote(e.vorbisDspState, 0)
|
||||||
|
e.process()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) process() {
|
||||||
|
for C.vorbis_analysis_blockout(e.vorbisDspState, e.vorbisBlock) == 1 {
|
||||||
|
C.vorbis_analysis(e.vorbisBlock, nil)
|
||||||
|
C.vorbis_bitrate_addblock(e.vorbisBlock)
|
||||||
|
|
||||||
|
for {
|
||||||
|
ret := C.vorbis_bitrate_flushpacket(e.vorbisDspState, e.oggPacket)
|
||||||
|
if ret == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
C.ogg_stream_packetin(e.oggStreamState, e.oggPacket)
|
||||||
|
|
||||||
|
for {
|
||||||
|
if C.ogg_stream_pageout(e.oggStreamState, e.oggPage) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
e.pageWrite()
|
||||||
|
|
||||||
|
if C.ogg_page_eos(e.oggPage) > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) pageWrite() {
|
||||||
|
e.writer.Write(unsafe.Slice((*byte)(unsafe.Pointer(e.oggPage.header)), int(e.oggPage.header_len)))
|
||||||
|
e.writer.Write(unsafe.Slice((*byte)(unsafe.Pointer(e.oggPage.body)), int(e.oggPage.body_len)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) initStructs() {
|
||||||
|
|
||||||
|
if e.vorbisDspState == nil && e.vorbisInfo != nil {
|
||||||
|
e.vorbisDspStateMem = make([]byte, unsafe.Sizeof(C.vorbis_dsp_state{}))
|
||||||
|
e.vorbisDspState = (*C.vorbis_dsp_state)(unsafe.Pointer(&e.vorbisDspStateMem[0]))
|
||||||
|
C.vorbis_analysis_init(e.vorbisDspState, e.vorbisInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.vorbisBlock == nil && e.vorbisInfo != nil {
|
||||||
|
e.vorbisBlockMem = make([]byte, unsafe.Sizeof(C.vorbis_block{}))
|
||||||
|
e.vorbisBlock = (*C.vorbis_block)(unsafe.Pointer(&e.vorbisBlockMem[0]))
|
||||||
|
C.vorbis_block_init(e.vorbisDspState, e.vorbisBlock)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.vorbisInfo == nil {
|
||||||
|
e.vorbisInfoMem = make([]byte, unsafe.Sizeof(C.vorbis_info{}))
|
||||||
|
e.vorbisInfo = (*C.vorbis_info)(unsafe.Pointer(&e.vorbisInfoMem[0]))
|
||||||
|
C.vorbis_info_init(e.vorbisInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.vorbisComment == nil {
|
||||||
|
e.vorbisCommentMem = make([]byte, unsafe.Sizeof(C.vorbis_comment{}))
|
||||||
|
e.vorbisComment = (*C.vorbis_comment)(unsafe.Pointer(&e.vorbisCommentMem[0]))
|
||||||
|
C.vorbis_comment_init(e.vorbisComment)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.oggStreamState == nil {
|
||||||
|
e.oggStreamStateMem = make([]byte, unsafe.Sizeof(C.ogg_stream_state{}))
|
||||||
|
e.oggStreamState = (*C.ogg_stream_state)(unsafe.Pointer(&e.oggStreamStateMem[0]))
|
||||||
|
C.ogg_stream_init(e.oggStreamState, C.int(rand.Int()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.oggPage == nil {
|
||||||
|
e.oggPageMem = make([]byte, unsafe.Sizeof(C.ogg_page{}))
|
||||||
|
e.oggPage = (*C.ogg_page)(unsafe.Pointer(&e.oggPageMem[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.oggPacket == nil {
|
||||||
|
e.oggPacketMem = make([]byte, unsafe.Sizeof(C.ogg_packet{}))
|
||||||
|
e.oggPacket = (*C.ogg_packet)(unsafe.Pointer(&e.oggPacketMem[0]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Encoder) Close() {
|
||||||
|
if e.oggStreamState != nil {
|
||||||
|
C.ogg_stream_clear(e.oggStreamState)
|
||||||
|
e.oggStreamState = nil
|
||||||
|
}
|
||||||
|
if e.vorbisBlock != nil {
|
||||||
|
C.vorbis_block_clear(e.vorbisBlock)
|
||||||
|
e.vorbisBlock = nil
|
||||||
|
}
|
||||||
|
if e.vorbisDspState != nil {
|
||||||
|
C.vorbis_dsp_clear(e.vorbisDspState)
|
||||||
|
e.vorbisDspState = nil
|
||||||
|
}
|
||||||
|
if e.vorbisInfo != nil {
|
||||||
|
C.vorbis_info_clear(e.vorbisInfo)
|
||||||
|
e.vorbisInfo = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.vorbisComment != nil {
|
||||||
|
C.vorbis_comment_clear(e.vorbisComment)
|
||||||
|
e.vorbisComment = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.vorbisInfo != nil {
|
||||||
|
C.vorbis_info_clear(e.vorbisInfo)
|
||||||
|
e.vorbisInfo = nil
|
||||||
|
}
|
||||||
|
}
|
111
encoder_test.go
Normal file
111
encoder_test.go
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
package go_vorbis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncoderCBR(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
f, err := os.Open("test.vorbis")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
decoder, err := NewSeekCloserDecoder(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer decoder.Close()
|
||||||
|
|
||||||
|
target, err := os.CreateTemp("/tmp", "encode_test_*.vorbis")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
name := target.Name()
|
||||||
|
target.Close()
|
||||||
|
os.Remove(name)
|
||||||
|
}()
|
||||||
|
|
||||||
|
encoder, err := NewEncoderCBR(target, decoder.Information().Channels, int(decoder.Information().Rate), -1, 128*1000, -1)
|
||||||
|
defer encoder.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
buf, _, err := decoder.ReadFloat()
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = encoder.WriteFloat(buf); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncoderVBR(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
f, err := os.Open("test.vorbis")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
decoder, err := NewSeekCloserDecoder(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer decoder.Close()
|
||||||
|
|
||||||
|
target, err := os.CreateTemp("/tmp", "encode_test_*.vorbis")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
name := target.Name()
|
||||||
|
target.Close()
|
||||||
|
os.Remove(name)
|
||||||
|
}()
|
||||||
|
|
||||||
|
encoder, err := NewEncoderVBR(target, decoder.Information().Channels, int(decoder.Information().Rate), 0.5)
|
||||||
|
defer encoder.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
buf, _, err := decoder.ReadFloat()
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = encoder.WriteFloat(buf); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder.Flush()
|
||||||
|
}
|
Loading…
Reference in a new issue