nativeaudio

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

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:
Maudio.go | 46++++++++++++++++++++++++++++++++++++++++++++++
Maudio_windows.c | 133+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Maudio_windows.go | 28++++++++++++++++++++++++++--
Maudio_windows.h | 14++++++++++++--
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