nativeaudio

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

commit b3e9ecc819164f8243e012563dedbe6368801801
parent 2c4a24aed38e2aa94732cde2cb10905f600a18be
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Thu,  4 Jan 2024 06:25:03 +0800

audio: [Windows] move some code into Go space

- structured error type that contains the error code
- call MFStartup and MFShutdown directly
- use modern unsafe facilities

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

Diffstat:
Maudio_windows.go | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
1 file changed, 74 insertions(+), 18 deletions(-)

diff --git a/audio_windows.go b/audio_windows.go @@ -9,26 +9,45 @@ package nativeaudio #cgo CFLAGS: -Werror -g -O3 #cgo LDFLAGS: -lwinmm -lmf -lmfplat -lmfuuid -loleaut32 -limm32 -lversion -lwindowsapp -lmfreadwrite -lshlwapi #include "audio_windows.h" +#include <crtdbg.h> +#include <stdlib.h> +#include <stdio.h> +#include <windows.h> +#include <winbase.h> +#include <combaseapi.h> +#include <mfapi.h> +#include <mfidl.h> +#include <mferror.h> +#include <initguid.h> +#include <wmcodecdsp.h> +#include <mmdeviceapi.h> +#include <mfreadwrite.h> +#include <shlwapi.h> +#include <assert.h> +#include <stdint.h> */ import "C" import ( "errors" "fmt" + "runtime" "strings" "unsafe" + + "golang.org/x/sys/windows" ) func start() error { - if err := C.StartMediaFramework(); err != nil { - return fmt.Errorf("initializing Windows Media Framework: %w", collectErrors(err)) + if hr := C.MFStartup(C.MF_VERSION, C.MFSTARTUP_LITE); hr != C.S_OK { + return fmt.Errorf("initializing Media Framework: %w", MFErr{Code: hr}) } return nil } func end() error { - if err := C.EndMediaFramework(); err != nil { - return fmt.Errorf("shutting down Windows Media Framework: %w", collectErrors(err)) + if hr := C.MFShutdown(); hr != C.S_OK { + return fmt.Errorf("shutting down Media Framework: %w", MFErr{Code: hr}) } return nil } @@ -46,40 +65,39 @@ func play(path string) error { } // load raw pcm data from the Windows Media Foundation. -// -// uncompressed is a read-only slice backed by a C buffer. Do not mutate. -// -// PERF(jfm): we can optimize this by allocating the buffer from Go, -// 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) (uncompressed []byte, format Format, err error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) + r := C.Load(cPath) + if r.Err != 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), C.int(r.Uncompressed.Len)) + + uncompressed = GoSlice((*byte)(r.Uncompressed.Data), int64(r.Uncompressed.Len)) + format = Format{ SampleRate: int(r.Format.SampleRate), BitDepth: int(r.Format.BitDepth), Channels: int(r.Format.Channels), } + + runtime.KeepAlive(path) + return uncompressed, format, nil } // decode compressed data, returning the uncompressed data as PCM data // (s16le) and details about the PCM required to playback correctly. -// -// uncompressed is a read-only slice backed by a C buffer. Do not mutate. func decode(compressed []byte) (uncompressed []byte, format Format, err error) { - data := C.CBytes(compressed) - defer C.free(data) + data := compressed + + r := C.Decode((*C.uchar)(unsafe.SliceData(data)), C.uint(len(compressed))) - r := C.Decode((*C.uchar)(data), C.uint(len(compressed))) if r.Err != nil && r.Err.Str != nil { defer C.ErrorFree(r.Err) return nil, format, collectErrors(r.Err) @@ -87,13 +105,16 @@ func decode(compressed []byte) (uncompressed []byte, format Format, err error) { defer C.BufferFree(r.Uncompressed) - uncompressed = C.GoBytes(unsafe.Pointer(r.Uncompressed.Data), C.int(r.Uncompressed.Len)) + uncompressed = GoSlice((*byte)(r.Uncompressed.Data), int64(r.Uncompressed.Len)) + format = Format{ Channels: int(r.Format.Channels), BitDepth: int(r.Format.BitDepth), SampleRate: int(r.Format.SampleRate), } + runtime.KeepAlive(compressed) + return uncompressed, format, nil } @@ -118,3 +139,38 @@ func collectErrors(err *C.Error) error { } return errors.New(str) } + +// GoSlice takes a native array and returns a Go managed slice via a memory copy. +// The caller is responsible for freeing the native memory. +func GoSlice[T any](t *T, size int64) []T { + src := unsafe.Slice(t, size) + dst := make([]T, size) + copy(dst, src) + return dst +} + +// MFErr is a Media Foundation error that can render a formatted message. +type MFErr struct { + Code C.HRESULT +} + +func (err MFErr) Error() string { + outBuf := new(C.ushort) + + size := C.FormatMessageW( + C.FORMAT_MESSAGE_ALLOCATE_BUFFER|C.FORMAT_MESSAGE_FROM_SYSTEM, + nil, + C.ulong(err.Code), + 0, + outBuf, + 0, + nil, + ) + if outBuf == nil || size == 0 { + return fmt.Sprintf("<cannot render string for HRESULT=%x>", err.Code) + } + + defer C.LocalFree((C.HANDLE)(unsafe.Pointer(outBuf))) + + return windows.UTF16ToString(GoSlice((*uint16)(unsafe.Pointer(outBuf)), int64(size))) +}