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:
| M | audio_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)))
+}