commit 7981a03eda14412c644a71b4cd9820fb5d097584
parent e99fa2291ea8951e4b5524ebabf95504533c44b5
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date: Tue, 26 Oct 2021 21:46:06 +0800
nativeaudio: [perf] init media framework exactly once
Turns out, initializing the media framework every call was VERY
expensive. By introducing startup/shutdown semantics we reduce ns/op
by 2 orders of magnitude.
goos: windows
goarch: amd64
pkg: git.sr.ht/~jackmordaunt/nativeaudio/internal/test
cpu: AMD Ryzen 5 3600 6-Core Processor
BenchmarkDecode/native-load-12 165 7272938 ns/op 88 B/op 3 allocs/op
BenchmarkDecode/native-decode-12 164 7075614 ns/op 72 B/op 2 allocs/op
BenchmarkDecode/ffmpeg-load-12 19 61365137 ns/op 2262458 B/op 1423 allocs/op
Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Diffstat:
4 files changed, 157 insertions(+), 64 deletions(-)
diff --git a/audio.go b/audio.go
@@ -9,20 +9,66 @@
//
package nativeaudio
+import (
+ "sync"
+)
+
+var (
+ mu sync.Mutex
+ initialized bool = false
+)
+
+// Start initializes any platform code required.
+func Start() error {
+ mu.Lock()
+ defer mu.Unlock()
+ if initialized {
+ return nil
+ }
+ if err := start(); err != nil {
+ return err
+ }
+ initialized = true
+ return nil
+}
+
+// End cleans up platform code, if any.
+func End() error {
+ mu.Lock()
+ defer mu.Unlock()
+ if !initialized {
+ return nil
+ }
+ if err := end(); err != nil {
+ return err
+ }
+ initialized = false
+ return nil
+}
+
// Play an audio file exactly once, synchronously.
func Play(path string) error {
+ if err := Start(); err != nil {
+ return err
+ }
return play(path)
}
// Load compressed data, returning the uncompressed data as PCM data
// (s16le) and details about the PCM required to playback correctly.
func Load(path string) (uncompressed []byte, format Format, err error) {
+ if err := Start(); err != nil {
+ return nil, format, err
+ }
return load(path)
}
// Decode compressed data, returning the uncompressed data as PCM data
// (s16le) and details about the PCM required to playback correctly.
func Decode(compressed []byte) (uncompressed []byte, format Format, err error) {
+ if err := Start(); err != nil {
+ return nil, format, err
+ }
return decode(compressed)
}
diff --git a/audio_windows.c b/audio_windows.c
@@ -70,14 +70,16 @@ CharWiden(char* str)
return NewResult(target, NULL);
}
+#define BUFFER_DEFAULT_SIZE 1024*1024
+
// BufferNew allocates a new buffer ready to use.
Buffer*
BufferNew()
{
Buffer *buffer = calloc(1, sizeof(Buffer));
- buffer->Data = NULL;
+ buffer->Data = calloc(BUFFER_DEFAULT_SIZE, 1);
buffer->Len = 0;
- buffer->Cap = 0;
+ buffer->Cap = BUFFER_DEFAULT_SIZE;
return buffer;
}
@@ -87,8 +89,8 @@ void
BufferGrow(Buffer *buffer, int amount)
{
BYTE* tmp = NULL;
- int target = 0;
- if (buffer->Cap > buffer->Len + amount)
+ int target = buffer->Cap;
+ if (buffer->Cap > buffer->Len + amount)
{
return;
}
@@ -107,14 +109,7 @@ BufferGrow(Buffer *buffer, int amount)
// BufferWrite the data to the buffer, growing if necessary.
void
BufferWrite(Buffer* buffer, int size, BYTE* data)
-{
- // TMP sum counting.
- int sum = 0;
- for (int ii = 0; ii < size; ii++)
- {
- sum += data[ii];
- }
-
+{
if (buffer->Cap < buffer->Len + size)
{
BufferGrow(buffer, size);
@@ -483,7 +478,11 @@ GetFormat(IMFMediaType * m_type)
{
HRESULT hr = S_OK;
FormatResult r = {
- .Format = {},
+ .Format = {
+ .SampleRate = 0,
+ .BitDepth = 0,
+ .Channels = 0
+ },
.Err = NULL,
};
@@ -682,14 +681,6 @@ Decode(BYTE* compressed, UINT size)
.Err = NULL,
};
- hr = MFStartup(MF_VERSION, MFSTARTUP_LITE);
-
- if (FAILED(hr))
- {
- r.Err = ErrorWithCode(ErrorStr("initializing media foundation"), hr);
- goto done;
- }
-
// stream is the type required by Media Foundation.
// We can get one of these by wrapping a COM IStream.
IMFByteStream * stream = NULL;
@@ -746,17 +737,6 @@ Decode(BYTE* compressed, UINT size)
done:
- hr = MFShutdown();
-
- if (FAILED(hr))
- {
- // Capture the shutdown error only if we didn't already encounter one.
- if (r.Err == NULL)
- {
- r.Err = ErrorWithCode(ErrorStr("shutting down media foundation"), hr);
- }
- }
-
if (m_type != NULL)
{
m_type->lpVtbl->Release(m_type);
@@ -791,20 +771,24 @@ Load(char* path)
Buffer *buffer = NULL; // Buffer to accumulate decoded PCM and return to Go.
Error *err = NULL; // Dyanmic error.
HRESULT hr = S_OK; // Windows return code.
+ FormatResult fr = {
+ .Err = NULL,
+ .Format = {
+ .BitDepth = 0,
+ .SampleRate = 0,
+ .Channels = 0,
+ }
+ };
DecodeResult dr = {
.Err = NULL,
.Uncompressed = NULL,
- .Format = {}
+ .Format = {
+ .BitDepth = 0,
+ .SampleRate = 0,
+ .Channels = 0,
+ }
};
- hr = MFStartup(MF_VERSION, MFSTARTUP_FULL);
-
- if (FAILED(hr))
- {
- err = ErrorWithCode(ErrorStr("starting media platform"), hr);
- goto done;
- }
-
Result r = NewSourceReaderForFile(path);
if (r.Err != NULL)
@@ -825,7 +809,7 @@ Load(char* path)
goto done;
}
- FormatResult fr = GetFormat(m_type);
+ fr = GetFormat(m_type);
if (fr.Err != NULL)
{
@@ -833,6 +817,12 @@ Load(char* path)
goto done;
}
+ assert(fr.Format.BitDepth != 0);
+ assert(fr.Format.BitDepth <= 2);
+ assert(fr.Format.SampleRate != 0);
+ assert(fr.Format.Channels != 0);
+ assert(fr.Format.Channels <= 2);
+
// Heap allocated buffer to accumulate the audio data.
// NOTE(jfm): Free from cgo side with BufferFree().
buffer = BufferNew();
@@ -847,17 +837,6 @@ Load(char* path)
done:
- hr = MFShutdown();
-
- if (FAILED(hr))
- {
- // Capture the shutdown error only if we didn't already encounter one.
- if (r.Err == NULL)
- {
- r.Err = ErrorWithCode(ErrorStr("shutting down media foundation"), hr);
- }
- }
-
if (m_type != NULL)
{
m_type->lpVtbl->Release(m_type);
@@ -933,13 +912,6 @@ Play(char* path)
audio_file_path = (WCHAR*)r.Value;
- // Start the platform.
- if ((hr = MFStartup(MF_VERSION, MFSTARTUP_LITE)) != S_OK)
- {
- err = ErrorWithCode(ErrorStr("starting media platform"), hr);
- goto done;
- }
-
// Create a media session which orchestrates the media processing
// graph.
if ((hr = MFCreateMediaSession(NULL, &session)) != S_OK)
@@ -1104,3 +1076,43 @@ done:
}
return err;
}
+
+Error*
+StartMediaFramework()
+{
+
+ Error * err = NULL;
+ HRESULT hr = S_OK;
+
+ hr = MFStartup(MF_VERSION, MFSTARTUP_LITE);
+
+ if (FAILED(hr))
+ {
+ err = ErrorWithCode(ErrorStr("initializing media foundation"), hr);
+ goto done;
+ }
+
+done:
+ return err;
+}
+
+Error*
+EndMediaFramework()
+{
+ Error * err = NULL;
+ HRESULT hr = S_OK;
+
+ hr = MFShutdown();
+
+ if (FAILED(hr))
+ {
+ // Capture the shutdown error only if we didn't already encounter one.
+ if (err == NULL)
+ {
+ err = ErrorWithCode(ErrorStr("shutting down media foundation"), hr);
+ }
+ }
+
+done:
+ return err;
+}
+\ No newline at end of file
diff --git a/audio_windows.go b/audio_windows.go
@@ -3,9 +3,10 @@
package nativeaudio
// -g: add to CFLAGS to include dwarf debug data
+// -O: optimization level 0, 1, 2, 3, s
/*
-#cgo CFLAGS: -Wall -Werror
+#cgo CFLAGS: -Werror -g -O3
#cgo LDFLAGS: -lWinmm -lMf -lMfplat -lMfuuid -loleaut32 -limm32 -lversion -lWindowsApp -lMfreadwrite -lShlwapi
#include "audio_windows.h"
*/
@@ -19,6 +20,20 @@ import (
"unsafe"
)
+func start() error {
+ if err := C.StartMediaFramework(); err != nil {
+ return fmt.Errorf("initializing Windows Media Framework: %w", collectErrors(err))
+ }
+ return nil
+}
+
+func end() error {
+ if err := C.EndMediaFramework(); err != nil {
+ return fmt.Errorf("shutting down Windows Media Framework: %w", collectErrors(err))
+ }
+ return nil
+}
+
// play the audio file using Windows Media Foundation.
func play(path string) error {
cPath := C.CString(path)
@@ -33,6 +48,8 @@ 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
@@ -59,6 +76,8 @@ func load(path string) (uncompressed []byte, format Format, err error) {
// 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) {
defer runtime.KeepAlive(compressed)
r := C.Decode(cBytes(compressed))
@@ -80,9 +99,14 @@ func decode(compressed []byte) (uncompressed []byte, format Format, err error) {
// goBytes returns a slice backed by a C byte array.
//
-// [1 << 30] means assume backing array is huge, and then slice into it
+// [1 << 30] means assume backing array is 1GB, and then slice into it
// with length.
+//
+// If the data is larger than 1GB, allocate more memory.
func goBytes(ptr unsafe.Pointer, length int) []byte {
+ if length > 1<<30 {
+ return C.GoBytes(ptr, C.int(length))
+ }
return (*[1 << 30]byte)(ptr)[:length:length]
}
diff --git a/audio_windows.h b/audio_windows.h
@@ -108,4 +108,14 @@ DecodeResult Decode(BYTE* compressed, UINT size);
HRESULT MFCreateMFByteStreamOnStream(
IStream *pStream,
IMFByteStream **ppByteStream
-);
-\ No newline at end of file
+);
+
+// StartMediaFramewok initializes the media framework ready to decode
+// and playback audio.
+Error*
+StartMediaFramework();
+
+// EndMediaFramewok shuts down the media framework. Decoding and playback
+// will not work hence forth.
+Error*
+EndMediaFramework();
+\ No newline at end of file