Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions cmd/resampler/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,25 @@ func main() {
}
// Skip WAV file header in order to pass only the PCM data to the Resampler
if strings.ToLower(filepath.Ext(inputFile)) == ".wav" {
input.Seek(wavHeader, 0)
if _, err := input.Seek(wavHeader, 0); err != nil {
output.Close()
os.Remove(outputFile)
log.Fatalln("Failed to skip WAV header:", err)
}
}

// Read input and pass it to the Resampler in chunks
_, err = io.Copy(res, input)
// Close the Resampler and the output file. Clsoing the Resampler will flush any remaining data to the output file.
// Close the Resampler and the output file. Closing the Resampler will flush any remaining data to the output file.
// If the Resampler is not closed before the output file, any remaining data will be lost.
res.Close()
closeErr := res.Close()
output.Close()
if err != nil {
os.Remove(outputFile)
log.Fatalln(err)
}
if closeErr != nil {
os.Remove(outputFile)
log.Fatalln("Failed to flush resampler:", closeErr)
}
}
57 changes: 31 additions & 26 deletions resample.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import (
"errors"
"io"
"runtime"
"unsafe"
)

const (
Expand Down Expand Up @@ -73,15 +72,14 @@ func init() {
// sampling rates, the number of channels of the input data, the input format
// and the quality setting.
func New(writer io.Writer, inputRate, outputRate float64, channels, format, quality int) (*Resampler, error) {
var err error
var size int
if writer == nil {
return nil, errors.New("io.Writer is nil")
}
if inputRate <= 0 || outputRate <= 0 {
return nil, errors.New("invalid input or output sampling rates")
}
if channels == 0 {
if channels <= 0 {
return nil, errors.New("invalid channels number")
}
if quality < 0 || quality > 6 {
Expand All @@ -105,26 +103,27 @@ func New(writer io.Writer, inputRate, outputRate float64, channels, format, qual
runtimeSpec := C.soxr_runtime_spec(C.uint(threads))
soxr = C.soxr_create(C.double(inputRate), C.double(outputRate), C.uint(channels), &soxErr, &ioSpec, &qSpec, &runtimeSpec)
if C.GoString(soxErr) != "" && C.GoString(soxErr) != "0" {
err = errors.New(C.GoString(soxErr))
C.free(unsafe.Pointer(soxErr))
return nil, err
return nil, errors.New(C.GoString(soxErr))
}

r := Resampler{
r := &Resampler{
resampler: soxr,
inRate: inputRate,
outRate: outputRate,
channels: channels,
frameSize: size,
destination: writer,
}
C.free(unsafe.Pointer(soxErr))
return &r, err
runtime.SetFinalizer(r, (*Resampler).Close)
return r, nil
}

// Reset permits reusing a Resampler rather than allocating a new one.
func (r *Resampler) Reset(writer io.Writer) error {
var err error
if writer == nil {
return errors.New("io.Writer is nil")
}
if r.resampler == nil {
return errors.New("soxr resampler is nil")
}
Expand All @@ -145,6 +144,7 @@ func (r *Resampler) Close() error {
err = r.flush()
C.soxr_delete(r.resampler)
r.resampler = nil
runtime.SetFinalizer(r, nil)
return err
}

Expand All @@ -165,29 +165,27 @@ func (r *Resampler) Write(p []byte) (int, error) {
if framesIn == 0 {
return i, errors.New("incomplete input frame data")
}
framesOut := int(float64(framesIn) * (r.outRate / r.inRate))
if framesOut == 0 {
return i, errors.New("not enough input to generate output")
}
framesOut := int(float64(framesIn)*(r.outRate/r.inRate)) + 2
dataIn := C.CBytes(p)
dataOut := C.malloc(C.size_t(framesOut * r.channels * r.frameSize))
var soxErr C.soxr_error_t
var read, done C.size_t = 0, 0
soxErr = C.soxr_process(r.resampler, C.soxr_in_t(dataIn), C.size_t(framesIn), &read, C.soxr_out_t(dataOut), C.size_t(framesOut), &done)
var done C.size_t = 0
soxErr = C.soxr_process(r.resampler, C.soxr_in_t(dataIn), C.size_t(framesIn), nil, C.soxr_out_t(dataOut), C.size_t(framesOut), &done)
if C.GoString(soxErr) != "" && C.GoString(soxErr) != "0" {
err = errors.New(C.GoString(soxErr))
goto cleanup
}
_, err = r.destination.Write(C.GoBytes(dataOut, C.int(int(done)*r.channels*r.frameSize)))
// In many cases the resampler will not return the full data unless we flush it. Espasially if the input chunck is small
// As long as we close the resampler (Close() flushes all data) we don't need to worry about short writes, unless r.destination.Write() fails
// In many cases the resampler will not return the full data unless we flush it. Especially if the input chunk is small.
// As long as we close the resampler (Close() flushes all data) we don't need to worry about short writes, unless r.destination.Write() fails.
if err == nil {
i = len(p)
// Report the frame-aligned bytes we passed to soxr, excluding any
// trailing bytes that didn't form a complete frame.
i = framesIn * r.channels * r.frameSize
}
cleanup:
C.free(dataIn)
C.free(dataOut)
C.free(unsafe.Pointer(soxErr))
return i, err
}

Expand All @@ -199,14 +197,21 @@ func (r *Resampler) flush() error {
framesOut := 4096 * 16
dataOut := C.malloc(C.size_t(framesOut * r.channels * r.frameSize))
// Flush any pending output by calling soxr_process with no input data.
soxErr = C.soxr_process(r.resampler, nil, 0, nil, C.soxr_out_t(dataOut), C.size_t(framesOut), &done)
if C.GoString(soxErr) != "" && C.GoString(soxErr) != "0" {
err = errors.New(C.GoString(soxErr))
goto cleanup
// Loop until soxr has no more pending output.
for {
soxErr = C.soxr_process(r.resampler, nil, 0, nil, C.soxr_out_t(dataOut), C.size_t(framesOut), &done)
if C.GoString(soxErr) != "" && C.GoString(soxErr) != "0" {
err = errors.New(C.GoString(soxErr))
break
}
if done == 0 {
break
}
_, err = r.destination.Write(C.GoBytes(dataOut, C.int(int(done)*r.channels*r.frameSize)))
if err != nil {
break
}
}
_, err = r.destination.Write(C.GoBytes(dataOut, C.int(int(done)*r.channels*r.frameSize)))
cleanup:
C.free(dataOut)
C.free(unsafe.Pointer(soxErr))
return err
}
5 changes: 3 additions & 2 deletions resample_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ var NewTest = []struct {
{writer: io.Discard, inputRate: 16000.0, outputRate: 8000.0, channels: 2, format: I16, quality: VeryHighQ, err: ""},
{writer: nil, inputRate: 8000.0, outputRate: 8000.0, channels: 2, format: I16, quality: MediumQ, err: "io.Writer is nil"},
{writer: io.Discard, inputRate: 16000.0, outputRate: 8000.0, channels: 0, format: I16, quality: MediumQ, err: "invalid channels number"},
{writer: io.Discard, inputRate: 16000.0, outputRate: 8000.0, channels: -1, format: I16, quality: MediumQ, err: "invalid channels number"},
{writer: io.Discard, inputRate: 16000.0, outputRate: 0.0, channels: 0, format: I16, quality: MediumQ, err: "invalid input or output sampling rates"},
{writer: io.Discard, inputRate: 0.0, outputRate: 8000.0, channels: 0, format: I16, quality: MediumQ, err: "invalid input or output sampling rates"},
{writer: io.Discard, inputRate: 16000.0, outputRate: 8000.0, channels: 2, format: 10, quality: MediumQ, err: "invalid format setting"},
Expand Down Expand Up @@ -77,7 +78,7 @@ var WriteTest = []struct {
{[]byte{0x01}, 0, "incomplete input frame data"},
{[]byte{0x01, 0x00}, 2, ""},
{[]byte{0x01, 0x00, 0x7c, 0x7f, 0xd1, 0xd0, 0xd3, 0xd2, 0xdd, 0xdc, 0xdf, 0xde, 0x01, 0x00, 0x7c, 0x7f, 0xd1, 0xd0, 0xd3, 0xd2, 0xdd, 0xdc, 0xdf, 0xde}, 24, ""},
{[]byte{0x01, 0x00, 0x7c, 0x7f, 0xd1, 0xd0, 0xd3, 0xd2, 0xdd, 0xdc, 0xdf, 0xde, 0x01, 0x00, 0x7c, 0x7f, 0xd1, 0xd0, 0xd3, 0xd2, 0xdd, 0xdc, 0xdf, 0xde, 0xd9}, 25, ""},
{[]byte{0x01, 0x00, 0x7c, 0x7f, 0xd1, 0xd0, 0xd3, 0xd2, 0xdd, 0xdc, 0xdf, 0xde, 0x01, 0x00, 0x7c, 0x7f, 0xd1, 0xd0, 0xd3, 0xd2, 0xdd, 0xdc, 0xdf, 0xde, 0xd9}, 24, ""},
}},

{"1-2 Resampler stereo", 8000.0, 16000.0, 2, []struct {
Expand All @@ -96,7 +97,7 @@ var WriteTest = []struct {
}{
{[]byte{}, 0, ""},
{[]byte{0x01}, 0, "incomplete input frame data"},
{[]byte{0x01, 0x00, 0x7c, 0x7f}, 0, "not enough input to generate output"},
{[]byte{0x01, 0x00, 0x7c, 0x7f}, 4, ""},
}},
}

Expand Down
Loading