nativeaudio

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

commit 6579c4ac6c3d5dfeab328ec59f36d4e9c13e9a69
parent b3d9551a31372069f565f83b35239a60662e66e8
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Tue, 26 Oct 2021 14:35:22 +0800

nativeaudio: report pcm format from load function

Introduce a function for getting metadata from a stream which we can
use to populate a Format struct.

Furthermore Load relies on decode minor function, which means the
incorrect output of Decode major is specific to how it initializes
the stream reader.

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

Diffstat:
Maudio.go | 2+-
Maudio_windows.c | 199+++++++++++++++++++++++++++++++++++++++----------------------------------------
Maudio_windows.go | 19++++++++++++-------
Maudio_windows.h | 18++++++++++++++----
Minternal/test/audio_test.go | 14++++++++++++--
Ainternal/test/uncompressed.f32le.pcm | 0
Ainternal/test/uncompressed.s16le.pcm | 0
7 files changed, 138 insertions(+), 114 deletions(-)

diff --git a/audio.go b/audio.go @@ -16,7 +16,7 @@ func Play(path string) error { // 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, err error) { +func Load(path string) (uncompressed []byte, format Format, err error) { return load(path) } diff --git a/audio_windows.c b/audio_windows.c @@ -998,129 +998,130 @@ cleanup: return r; } - -Result -Load(char* path) +// GetFormat reads format meta data from a source reader. +FormatResult +GetFormat(IMFSourceReader * reader) { - Result r; - Error *err = NULL; // Dyanmic error. - HRESULT hr = S_OK; // Windows return code. - IMFSourceReader *reader = NULL; // Object to stream bytes from. - DWORD cbBuffer = 0; // size of chunk. - BYTE *chunk = NULL; // pointer to start of chunk. - IMFSample *pSample = NULL; // sample object containing on or more streams. - IMFMediaBuffer *bufferReader = NULL; // buffer object containing the raw buffer. - Buffer *buffer = NULL; // Buffer to accumulate decoded PCM and return to Go. - - LONGLONG prev_time_stamp = -1; - LONGLONG time_stamp = 0; + IMFMediaType * m_type = NULL; + HRESULT hr = S_OK; + FormatResult r = { + .Format = {}, + .Err = NULL, + }; - hr = MFStartup(MF_VERSION, MFSTARTUP_FULL); + hr = reader->lpVtbl->GetCurrentMediaType(reader, (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, &m_type); if (FAILED(hr)) { - err = ErrorWithCode(ErrorStr("starting media platform"), hr); + r.Err = ErrorWithCode(ErrorStr("getting media type"), hr); goto cleanup; } - - r = NewSourceReaderForFile(path); - if (r.Err != NULL) + UINT32 num_channels = 0; + + hr = m_type->lpVtbl->GetUINT32(m_type, &MF_MT_AUDIO_NUM_CHANNELS, &num_channels); + + if (FAILED(hr)) { - err = ErrorWrap(r.Err, "setting up source reader for audio file"); + r.Err = ErrorWithCode(ErrorStr("getting num channels"), hr); goto cleanup; } - reader = (IMFSourceReader*)(r.Value); + UINT32 sample_rate = 0; - // Heap allocated buffer to accumulate the audio data. - // NOTE(jfm): Free from cgo side with BufferFree(). - buffer = BufferNew(); + hr = m_type->lpVtbl->GetUINT32(m_type, &MF_MT_AUDIO_SAMPLES_PER_SECOND, &sample_rate); - // Stream all the data into a byte buffer. + if (FAILED(hr)) + { + r.Err = ErrorWithCode(ErrorStr("getting num channels"), hr); + goto cleanup; + } + - // NOTE(jfm): we can create a streaming api by extracting this loop - // to the Go side, and implement something like an io.Reader. - // However this api currently reads the entire thing and passes - // it all back to Go at once. - while (1) { - DWORD dwFlags = 0; + UINT32 bits_per_sample = 0; - // Read the next sample. - hr = reader->lpVtbl->ReadSample( - reader, - (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, - 0, - NULL, - &dwFlags, - &time_stamp, - &pSample - ); + hr = m_type->lpVtbl->GetUINT32(m_type, &MF_MT_AUDIO_BITS_PER_SAMPLE, &bits_per_sample); - // NOTE(jfm): Avoid chunks that we have already seen. - // - // For some reason, ReadSample can produce more than - // one sample at time stamp "0". - // - // Emitting all of them produces both larger files and - // audio artefacts. - if (time_stamp == prev_time_stamp) - { - continue; - } + if (FAILED(hr)) + { + r.Err = ErrorWithCode(ErrorStr("getting num channels"), hr); + goto cleanup; + } + + printf("sample rate: %d\n", sample_rate); + printf("num channels: %d\n", num_channels); + printf("bits per sample: %d\n", bits_per_sample); - prev_time_stamp = time_stamp; +cleanup: - if (FAILED(hr)) - { - err = ErrorWithCode(ErrorStr("reading sample"), hr); - goto cleanup; - } + if (m_type != NULL) + { + m_type->lpVtbl->Release(m_type); + } - if (dwFlags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED) - { - break; - } - if (dwFlags & MF_SOURCE_READERF_ENDOFSTREAM) - { - break; - } + r.Format = (Format){ + .SampleRate = sample_rate, + .Channels = num_channels, + .BitDepth = bits_per_sample / 8, + }; - if (pSample == NULL) - { - continue; - } + printf(".SampleRate: %d\n", r.Format.SampleRate); + printf(".Channels: %d\n", r.Format.Channels); + printf(".BitDepth: %d\n", r.Format.BitDepth); - // Get a pointer to the buffer object. - hr = pSample->lpVtbl->ConvertToContiguousBuffer(pSample, &bufferReader); + return r; +} - if (FAILED(hr)) - { - err = ErrorWithCode(ErrorStr("converting to contiguous buffer"), hr); - goto cleanup; - } +// Load decodes the file at path and returns raw PCM s16le with the +// given format required for correct playback. +DecodeResult +Load(char* path) +{ + IMFSourceReader *reader = NULL; // Object to stream bytes from. + Buffer *buffer = NULL; // Buffer to accumulate decoded PCM and return to Go. + Error *err = NULL; // Dyanmic error. + HRESULT hr = S_OK; // Windows return code. + DecodeResult dr = { + .Err = NULL, + .Uncompressed = NULL, + .Format = {} + }; - // Get read/write access to the next chunk of audio data. - hr = bufferReader->lpVtbl->Lock(bufferReader, &chunk, NULL, &cbBuffer); + hr = MFStartup(MF_VERSION, MFSTARTUP_FULL); - if (FAILED(hr)) - { - err = ErrorWithCode(ErrorStr("locking buffer"), hr); - goto cleanup; - } + if (FAILED(hr)) + { + err = ErrorWithCode(ErrorStr("starting media platform"), hr); + goto cleanup; + } + + Result r = NewSourceReaderForFile(path); - BufferWrite(buffer, cbBuffer, chunk); + if (r.Err != NULL) + { + dr.Err = ErrorWrap(r.Err, "setting up source reader for audio file"); + goto cleanup; + } - // Unlock the reader that we just copied from. - hr = bufferReader->lpVtbl->Unlock(bufferReader); + reader = (IMFSourceReader*)(r.Value); - if (FAILED(hr)) - { - err = ErrorWithCode(ErrorStr("unlocking buffer"), hr); - goto cleanup; - } + FormatResult fr = GetFormat(reader); + if (fr.Err != NULL) + { + dr.Err = ErrorWrap(fr.Err, "getting format"); + goto cleanup; + } - chunk = NULL; + // Heap allocated buffer to accumulate the audio data. + // NOTE(jfm): Free from cgo side with BufferFree(). + buffer = BufferNew(); + + err = decode(reader, &buffer); + + if (err != NULL) + { + dr.Err = ErrorWrap(err, "decode minor"); + goto cleanup; } cleanup: @@ -1131,10 +1132,8 @@ cleanup: reader->lpVtbl->Release(reader); } - if (bufferReader != NULL) - { - bufferReader->lpVtbl->Release(bufferReader); - } - - return NewResult(buffer, err); + dr.Uncompressed = buffer; + dr.Format = fr.Format; + + return dr; } \ No newline at end of file diff --git a/audio_windows.go b/audio_windows.go @@ -37,17 +37,22 @@ 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, error) { +func load(path string) ([]byte, Format, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) - result := C.Load(cPath) - if result.Err != nil { + r := C.Load(cPath) + if r.Err != nil { // defer C.ErrorFree(result.Err) - return nil, collectErrors(result.Err) + return nil, Format{}, collectErrors(r.Err) } - buffer := (*C.Buffer)(result.Value) - defer C.BufferFree(buffer) - return C.GoBytes(unsafe.Pointer(buffer.Data), buffer.Len), nil + defer C.BufferFree(r.Uncompressed) + uncompressed := C.GoBytes(unsafe.Pointer(r.Uncompressed.Data), r.Uncompressed.Len) + format := Format{ + SampleRate: int(r.Format.SampleRate), + BitDepth: int(r.Format.BitDepth), + Channels: int(r.Format.Channels), + } + return uncompressed, format, nil } // decode compressed data, returning the uncompressed data as PCM data diff --git a/audio_windows.h b/audio_windows.h @@ -13,6 +13,7 @@ #include <mfreadwrite.h> #include <shlwapi.h> #include <assert.h> +#include <stdint.h> // Error declares an error return containing a message and possibly // wrapping another error. @@ -41,9 +42,9 @@ typedef struct Result // Format describes uncompressed PCM necessary for correct playback. typedef struct Format { - int SampleRate; - int Channels; - int BitDepth; + uint32_t SampleRate; + uint32_t Channels; + uint32_t BitDepth; } Format; @@ -57,6 +58,15 @@ typedef struct Buffer } Buffer; + +// FormatResult captures the result of decoding an audio buffer. +typedef struct FormatResult +{ + Format Format; + Error* Err; +} FormatResult; + + // DecodeResult captures the result of decoding an audio buffer. typedef struct DecodeResult { @@ -78,7 +88,7 @@ void ErrorFree(Error*); // what a wild ride that is. // // https://docs.microsoft.com/en-us/windows/win32/medfound/about-the-media-foundation-sdk -Result Load(char* path); +DecodeResult Load(char* path); // Play an audio file at the given file path. // diff --git a/internal/test/audio_test.go b/internal/test/audio_test.go @@ -13,17 +13,27 @@ import ( var ( //go:embed compressed.m4a compressed []byte - //go:embed uncompressed.pcm + //go:embed uncompressed.s16le.pcm uncompressed []byte ) // TestLoad ensures that output from the native decoders are close to // the output of ffmpeg. func TestLoad(t *testing.T) { - by, err := nativeaudio.Load("compressed.m4a") + by, f, err := nativeaudio.Load("compressed.m4a") if err != nil { t.Fatalf("unexpected error: %v", err) } + // Check for known meta data values (ffprobe -i compressed.m4a). + if f.BitDepth != 2 { + t.Fatalf("unexpected bit depth: want 2, got %d", f.BitDepth) + } + if f.SampleRate != 44100 { + t.Fatalf("unexpected sample rate: want 44100, got %d", f.SampleRate) + } + if f.Channels != 2 { + t.Fatalf("unexpected channel count: want 2, got %d", f.Channels) + } // Test passes on exact match, otherwise do a tolerance test. if bytes.Equal(by, uncompressed) { return diff --git a/internal/test/uncompressed.f32le.pcm b/internal/test/uncompressed.f32le.pcm Binary files differ. diff --git a/internal/test/uncompressed.s16le.pcm b/internal/test/uncompressed.s16le.pcm Binary files differ.