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:
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.