From 1413086e90c001f923f757abf01ea16aaf941ca9 Mon Sep 17 00:00:00 2001 From: WeebDataHoarder <57538841+WeebDataHoarder@users.noreply.github.com> Date: Thu, 28 Jul 2022 14:31:28 +0200 Subject: [PATCH] Added CBR / VBR encoder --- decoder.go | 5 - decoder_test.go | 2 + encoder.go | 265 ++++++++++++++++++++++++++++++++++++++++++++++++ encoder_test.go | 111 ++++++++++++++++++++ 4 files changed, 378 insertions(+), 5 deletions(-) create mode 100644 encoder.go create mode 100644 encoder_test.go diff --git a/decoder.go b/decoder.go index ca8c850..32d1c16 100644 --- a/decoder.go +++ b/decoder.go @@ -188,11 +188,6 @@ func (d *Decoder) ReadFloat() ([]float32, int, error) { buf[i*channelCount+ch] = sample } } - for i := 0; i < int(ret); i++ { - for ch := 0; ch < channelCount; ch++ { - - } - } return buf, channelCount, nil } diff --git a/decoder_test.go b/decoder_test.go index 819f815..3ad91b8 100644 --- a/decoder_test.go +++ b/decoder_test.go @@ -8,6 +8,7 @@ import ( ) func TestDecoder(t *testing.T) { + t.Parallel() f, err := os.Open("test.vorbis") if err != nil { t.Error(err) @@ -43,6 +44,7 @@ func TestDecoder(t *testing.T) { } func TestDecoderFloat(t *testing.T) { + t.Parallel() f, err := os.Open("test.vorbis") if err != nil { t.Error(err) diff --git a/encoder.go b/encoder.go new file mode 100644 index 0000000..a42dbe3 --- /dev/null +++ b/encoder.go @@ -0,0 +1,265 @@ +package go_vorbis + +/* +#cgo pkg-config: vorbisenc +#cgo pkg-config: vorbis +#cgo pkg-config: ogg +#include + +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 + } +} diff --git a/encoder_test.go b/encoder_test.go new file mode 100644 index 0000000..a8e8c40 --- /dev/null +++ b/encoder_test.go @@ -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() +}