nativeaudio

audio playback for Go
Log | Files | Refs | README | LICENSE

commit e99fa2291ea8951e4b5524ebabf95504533c44b5
parent e68bad3f1f3680f47dbc02c63f7d0242787a16a9
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Tue, 26 Oct 2021 17:50:00 +0800

nativeaudio: [windows] avoid copying buffer out from C

We can still free the memory inside a finalizer (I think) when the GC
decides to free it.

That way we can use the C memory, without copying anything and the
caller can treat it like a standard slice in the meantime.

This lets us avoid allocating memory back out of C, without needing to
go all the way and manage the buffer from Go (which is still on the
cards because it gives Go more control - but not now!).

In the benchmark, you'll notice that Go side only allocates 72b total;
I think the finalizer closure escapes to the heap.

The other 16b are for the CString allocation containing the file path
in nativeaudio.Load.

Unfortunately this optimization is, again, negiligible in terms of ns/op
because the heavy lifting is the audio decoding.

I don't like that native is 6x more expensive than shelling out to
ffmpeg. Could simply be my amateur C code, OR Windows Media Foundation
is just that bad.

goos: windows
goarch: amd64
pkg: git.sr.ht/~jackmordaunt/nativeaudio/internal/test
cpu: AMD Ryzen 5 3600 6-Core Processor
BenchmarkDecode/native-load-12         	       4	 317390725 ns/op	      88 B/op	       3 allocs/op
BenchmarkDecode/native-decode-12       	       4	 269792350 ns/op	      72 B/op	       2 allocs/op
BenchmarkDecode/ffmpeg-load-12         	      19	  61750084 ns/op	 2279866 B/op	    1498 allocs/op

Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>

Diffstat:
Maudio_windows.go | 31++++++++++++++++++++++++-------
1 file changed, 24 insertions(+), 7 deletions(-)

diff --git a/audio_windows.go b/audio_windows.go @@ -37,7 +37,7 @@ func play(path string) error { // and passing it in for C to fill up. It would require more // orchestration, but would save the copy. At the moment, C allocates // its own buffer, we then copy the data and free the C buffer. -func load(path string) ([]byte, Format, error) { +func load(path string) (uncompressed []byte, format Format, err error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) r := C.Load(cPath) @@ -45,9 +45,11 @@ func load(path string) ([]byte, Format, error) { defer C.ErrorFree(r.Err) return nil, Format{}, collectErrors(r.Err) } - defer C.BufferFree(r.Uncompressed) - uncompressed := C.GoBytes(unsafe.Pointer(r.Uncompressed.Data), r.Uncompressed.Len) - format := Format{ + uncompressed = goBytes(unsafe.Pointer(r.Uncompressed.Data), int(r.Uncompressed.Len)) + runtime.SetFinalizer(&uncompressed, func(_ *[]byte) { + C.BufferFree(r.Uncompressed) + }) + format = Format{ SampleRate: int(r.Format.SampleRate), BitDepth: int(r.Format.BitDepth), Channels: int(r.Format.Channels), @@ -59,13 +61,15 @@ func load(path string) ([]byte, Format, error) { // (s16le) and details about the PCM required to playback correctly. func decode(compressed []byte) (uncompressed []byte, format Format, err error) { defer runtime.KeepAlive(compressed) - r := C.Decode((*C.uchar)(unsafe.Pointer(&compressed[0])), C.uint(len(compressed))) + r := C.Decode(cBytes(compressed)) if r.Err != nil && r.Err.Str != nil { defer C.ErrorFree(r.Err) return nil, format, collectErrors(r.Err) } - defer C.BufferFree(r.Uncompressed) - uncompressed = C.GoBytes(unsafe.Pointer(r.Uncompressed.Data), r.Uncompressed.Len) + uncompressed = goBytes(unsafe.Pointer(r.Uncompressed.Data), int(r.Uncompressed.Len)) + runtime.SetFinalizer(&uncompressed, func(_ *[]byte) { + C.BufferFree(r.Uncompressed) + }) format = Format{ Channels: int(r.Format.Channels), BitDepth: int(r.Format.BitDepth), @@ -74,6 +78,19 @@ func decode(compressed []byte) (uncompressed []byte, format Format, err error) { return uncompressed, format, nil } +// goBytes returns a slice backed by a C byte array. +// +// [1 << 30] means assume backing array is huge, and then slice into it +// with length. +func goBytes(ptr unsafe.Pointer, length int) []byte { + return (*[1 << 30]byte)(ptr)[:length:length] +} + +// cBytes returns a dynamic C byte array backed by a Go slice. +func cBytes(by []byte) (*C.uchar, C.uint) { + return (*C.uchar)(unsafe.Pointer(&by[0])), C.uint(len(by)) +} + // collectErrors unwraps all the errors in the chain and coalesces them // into a single Go error. func collectErrors(err *C.Error) error {