commit b94efcdfe4f3304c6ea9de1da665ef3e3ff3c29c
parent 6579c4ac6c3d5dfeab328ec59f36d4e9c13e9a69
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date: Tue, 26 Oct 2021 15:40:14 +0800
nativeaudio: correctly specify output format
This patch does a big tidy alongside correctly specifying the output
format as PCM.
Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Diffstat:
4 files changed, 482 insertions(+), 500 deletions(-)
diff --git a/audio.go b/audio.go
@@ -26,10 +26,10 @@ func Decode(compressed []byte) (uncompressed []byte, format Format, err error) {
return decode(compressed)
}
-// Format desribes the features of the associated PCM data necessary
+// Format describes the features of the associated PCM data necessary
// for correct playback.
type Format struct {
- SampleRate int
- Channels int
- BitDepth int
+ SampleRate int // samples per second.
+ Channels int // number channels.
+ BitDepth int // bytes per sample.
}
diff --git a/audio_windows.c b/audio_windows.c
@@ -1,5 +1,8 @@
#include "audio_windows.h"
+FormatResult
+GetFormat(IMFMediaType * m_type);
+
// ErrorStr creates an error with the provided message as a char*.
Error*
ErrorStr(char *s)
@@ -45,7 +48,6 @@ ErrorFree(Error* err)
}
}
-
// NewResult constructs a Result with the provided value and error.
// Usually one of the hose pointers will be NULL.
Result
@@ -73,10 +75,73 @@ CharWiden(char* str)
return NewResult(target, NULL);
}
+// BufferNew allocates a new buffer ready to use.
+Buffer*
+BufferNew()
+{
+ Buffer *buffer = calloc(1, sizeof(Buffer));
+ buffer->Data = NULL;
+ buffer->Len = 0;
+ buffer->Cap = 0;
+ return buffer;
+}
+
+// BufferGrow grows the capacity by at least the provided amount.
+// Growth algorithm uses the double+1 strategy.
+void
+BufferGrow(Buffer *buffer, int amount)
+{
+ BYTE* tmp = NULL;
+ int target = 0;
+ if (buffer->Cap > buffer->Len + amount)
+ {
+ return;
+ }
+ // Expand the target capacity until we can fit the amount.
+ while (target < buffer->Len + amount)
+ {
+ target = target*2+1;
+ }
+ tmp = calloc(target, 1);
+ memcpy(tmp, buffer->Data, buffer->Len);
+ free(buffer->Data);
+ buffer->Data = tmp;
+ buffer->Cap = target;
+}
+
+// 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);
+ }
+ memcpy(buffer->Data+buffer->Len, data, size);
+ buffer->Len += size;
+}
+
+// BufferFree frees the memory for the buffer.
+void
+BufferFree(Buffer* buffer)
+{
+ free(buffer->Data);
+ free(buffer);
+}
+
+
// RunMediaSession executes the media session until complete. This
// should output the sound.
+// This is used in Play.
HRESULT
-RunMediaSession(IMFMediaSession* pSession){
+RunMediaSession(IMFMediaSession* pSession) {
HRESULT hr = S_OK;
@@ -105,7 +170,6 @@ RunMediaSession(IMFMediaSession* pSession){
switch(meType){
case MESessionTopologySet:
- wprintf(L"MESessionTopologySet\n");
break;
case MESessionTopologyStatus:
@@ -118,7 +182,6 @@ RunMediaSession(IMFMediaSession* pSession){
case MF_TOPOSTATUS_READY:
{
- wprintf(L"MESessionTopologyStatus: MF_TOPOSTATUS_READY\n");
PROPVARIANT varStartPosition;
PropVariantInit(&varStartPosition);
hr = pSession->lpVtbl->Start(pSession, NULL, &varStartPosition);
@@ -127,53 +190,42 @@ RunMediaSession(IMFMediaSession* pSession){
break;
case MF_TOPOSTATUS_STARTED_SOURCE:
- wprintf(L"MESessionTopologyStatus: MF_TOPOSTATUS_STARTED_SOURCE\n");
break;
case MF_TOPOSTATUS_ENDED:
- wprintf(L"MESessionTopologyStatus: MF_TOPOSTATUS_ENDED\n");
break;
default:
- wprintf(L"MESessionTopologyStatus: %d\n", TopoStatus);
break;
}
}
break;
case MESessionStarted:
- wprintf(L"MESessionStarted\n");
break;
case MESessionEnded:
- wprintf(L"MESessionEnded\n");
hr = pSession->lpVtbl->Stop(pSession);
break;
case MESessionStopped:
- wprintf(L"MESessionStopped\n");
hr = pSession->lpVtbl->Close(pSession);
break;
case MESessionClosed:
- wprintf(L"MESessionClosed\n");
bSessionEvent = FALSE;
break;
case MESessionNotifyPresentationTime:
- wprintf(L"MESessionNotifyPresentationTime\n");
break;
case MESessionCapabilitiesChanged:
- wprintf(L"MESessionCapabilitiesChanged\n");
break;
case MEEndOfPresentation:
- wprintf(L"MEEndOfPresentation\n");
break;
default:
- wprintf(L"Media session event: %d\n", meType);
break;
}
@@ -187,10 +239,13 @@ RunMediaSession(IMFMediaSession* pSession){
}
// AddOutputNode to the topology.
-HRESULT AddOutputNode(
+// This is used in Play.
+HRESULT
+AddOutputNode(
IMFTopology *pTopology, // Topology.
IMFStreamSink *pStreamSink, // Stream sink.
- IMFTopologyNode **ppNode) // Receives the node pointer.
+ IMFTopologyNode **ppNode // Receives the node pointer.
+)
{
IMFTopologyNode *pNode = NULL;
HRESULT hr = S_OK;
@@ -246,12 +301,15 @@ HRESULT AddOutputNode(
}
// Add a source node to a topology.
-HRESULT AddSourceNode(
+// This is used in Play.
+HRESULT
+AddSourceNode(
IMFTopology *pTopology, // Topology.
IMFMediaSource *pSource, // Media source.
IMFPresentationDescriptor *pPD, // Presentation descriptor.
IMFStreamDescriptor *pSD, // Stream descriptor.
- IMFTopologyNode **ppNode) // Receives the node pointer.
+ IMFTopologyNode **ppNode // Receives the node pointer.
+)
{
IMFTopologyNode *pNode = NULL;
@@ -306,10 +364,11 @@ done:
//
// hr = ConfigureAudioStream(pReader, &pAudioType);
//-------------------------------------------------------------------
-HRESULT ConfigureAudioStream(
+HRESULT
+ConfigureAudioStream(
IMFSourceReader *pReader, // Pointer to the source reader.
IMFMediaType **ppPCMAudio // Receives the audio format.
- )
+)
{
IMFMediaType *pUncompressedAudioType = NULL;
IMFMediaType *pPartialType = NULL;
@@ -372,366 +431,121 @@ HRESULT ConfigureAudioStream(
return hr;
}
-Error*
-Play(char* path)
+// Setup the source reader for the audio file at the given path.
+// IMFSourceReader* is returned as result value.
+Result
+NewSourceReaderForFile(char* path)
{
- Error *err = NULL;
- Result r;
- HRESULT hr;
-
- // We have to build a wide string from the C string for the path.
- // NOTE(jfm): size cannot exceed 256 without dynamic allocation.
- WCHAR *audio_file_path = NULL;
-
- // session is the top-level object wherein all stream
- // processing occurs.
- IMFMediaSession *session;
- // resolver can resolve an abitrary byte source.
- // In our case this will be a plain audio file.
- IMFSourceResolver *resolver;
-
- // obj_type describes the source: media source or byte source.
- MF_OBJECT_TYPE obj_type;
- // obj is the true source object.
- IUnknown *obj;
- // src is the object casted to it's concrete type.
- IMFMediaSource *src = NULL;
- // desc provides meta data about the playback.
- IMFPresentationDescriptor *desc;
-
- // Stream selection: we assume exactly 1 stream of AAC audio,
- // however this code may in fact be too brittle.
- IMFStreamDescriptor *stream_desc; // info about the stream.
- BOOL fSelected = FALSE; // if a stream exists for the index.
- DWORD stream_count = 0; // number of streams found.
+ Error *err = NULL; // Dyanmic error.
+ HRESULT hr = S_OK; // Windows return code.
- // topology configures a graph of stream processing. The only
- // processing we want is to decode AAC LC audio.
- IMFTopology *topology;
+ WCHAR *audio_file_path = NULL; // Path to audio file.
- // activate is an object that can initialize itself.
- // This wraps the audio decoder.
- IMFActivate *activate;
+ // reader is returned to caller.
+ IMFSourceReader *reader = NULL; // Object to stream bytes from.
- // source and output nodes: the only two nodes in our topology
- // graph.
- IMFTopologyNode *pSourceNode = NULL;
- IMFTopologyNode *pOutputNode = NULL;
-
// Since Windows uses wide strings we need to convert the char*
// to such a format.
- r = CharWiden(path);
+ Result r = CharWiden(path);
if (r.Err != NULL)
{
- err = ErrorWrap(r.Err, "converting path string");
- goto cleanup;
+ err = ErrorWithCode(ErrorWrap(r.Err, "converting path string"), hr);
+ goto done;
}
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 cleanup;
- }
-
- // Create a media session which orchestrates the media processing
- // graph.
- if ((hr = MFCreateMediaSession(NULL, &session)) != S_OK)
- {
- err = ErrorWithCode(ErrorStr("creating media session"), hr);
- goto cleanup;
- }
+ hr = MFCreateSourceReaderFromURL(audio_file_path, NULL, &reader);
- // Create a source resolver. This object can open files and urls.
- if ((hr = MFCreateSourceResolver(&resolver)) != S_OK)
+ if (FAILED(hr))
{
- err = ErrorWithCode(ErrorStr("creating source resolver"), hr);
- goto cleanup;
+ err = ErrorWithCode(ErrorStr("creating source reader"), hr);
+ goto done;
}
- // Create a "media source" from the sound file.
- // Perhaps a bytestream would also work?
- if ((hr = resolver->lpVtbl->CreateObjectFromURL(
- resolver,
- audio_file_path,
- MF_RESOLUTION_MEDIASOURCE|MF_RESOLUTION_READ,
- NULL,
- &obj_type,
- &obj
- )) != S_OK)
- {
- err = ErrorWithCode(ErrorStr("creating object from url"), hr);
- goto cleanup;
- }
-
- if (obj_type != MF_OBJECT_MEDIASOURCE)
- {
- err = ErrorWithCode(ErrorStr("not a media source"), hr);
- goto cleanup;
- }
+done:
- // We know it's a media source so we can do the cast safely.
- src = (IMFMediaSource*)obj;
+ free(audio_file_path);
- // Create a presentation descriptor.
- // This object describes the media source, e.g. what it's audio
- // and video streams are.
- if ((hr = src->lpVtbl->CreatePresentationDescriptor(src, &desc)) != S_OK)
+ if (err != NULL)
{
- err = ErrorWithCode(ErrorStr("creating presentation descriptor"), hr);
- goto cleanup;
+ r.Err = err;
}
-
- // Get information about the audio stream. We pull the count
- // and validate it, but we assume there's only one stream:
- // AAC-LC audio.
- if ((hr = desc->lpVtbl->GetStreamDescriptorCount(desc, &stream_count)) != S_OK)
+
+ if (reader != NULL)
{
- err = ErrorWithCode(ErrorStr("getting stream descriptor count"), hr);
- goto cleanup;
+ r.Value = reader;
}
- if (stream_count != 1)
- {
- err = ErrorWithCode(ErrorStr("expected exactly one stream"), hr);
- goto cleanup;
+ return r;
+}
- }
- if ((hr = desc->lpVtbl->GetStreamDescriptorByIndex(desc, 0, &fSelected, &stream_desc)) != S_OK)
- {
- err = ErrorWithCode(ErrorStr("getting stream descriptor by index"), hr);
- goto cleanup;
- }
+// GetFormat reads format meta data from a source reader.
+FormatResult
+GetFormat(IMFMediaType * m_type)
+{
+ HRESULT hr = S_OK;
+ FormatResult r = {
+ .Format = {},
+ .Err = NULL,
+ };
- if (!fSelected)
- {
- err = ErrorWithCode(ErrorStr("stream was not selected"), hr);
- goto cleanup;
- }
+ UINT32 num_channels = 0;
- if ((hr = MFCreateAudioRendererActivate(&activate)) != S_OK)
- {
- err = ErrorWithCode(ErrorStr("creating audio renderer activate"), hr);
- goto cleanup;
- }
+ hr = m_type->lpVtbl->GetUINT32(m_type, &MF_MT_AUDIO_NUM_CHANNELS, &num_channels);
- if ((hr = MFCreateTopology(&topology)) != S_OK)
+ if (FAILED(hr))
{
- err = ErrorWithCode(ErrorStr("creating topology"), hr);
- goto cleanup;
+ r.Err = ErrorWithCode(ErrorStr("getting num channels"), hr);
+ goto done;
}
- if ((hr = AddSourceNode(topology, src, desc, stream_desc, &pSourceNode)) != S_OK)
+ UINT32 sample_rate = 0;
+
+ hr = m_type->lpVtbl->GetUINT32(m_type, &MF_MT_AUDIO_SAMPLES_PER_SECOND, &sample_rate);
+
+ if (FAILED(hr))
{
- err = ErrorWithCode(ErrorStr("adding source node"), hr);
- goto cleanup;
+ r.Err = ErrorWithCode(ErrorStr("getting num channels"), hr);
+ goto done;
}
- if ((hr = AddOutputNode(topology, (IMFStreamSink *)activate, &pOutputNode)) != S_OK)
- {
- err = ErrorWithCode(ErrorStr("adding output node"), hr);
- goto cleanup;
- }
- if ((hr = pSourceNode->lpVtbl->ConnectOutput(pSourceNode, 0, pOutputNode, 0)) != S_OK)
+ UINT32 bits_per_sample = 0;
+
+ hr = m_type->lpVtbl->GetUINT32(m_type, &MF_MT_AUDIO_BITS_PER_SAMPLE, &bits_per_sample);
+
+ if (FAILED(hr))
{
- err = ErrorWithCode(ErrorStr("connect output"), hr);
- goto cleanup;
+ r.Err = ErrorWithCode(ErrorStr("getting num channels"), hr);
+ goto done;
}
- if ((hr = session->lpVtbl->SetTopology(session, MFSESSION_SETTOPOLOGY_IMMEDIATE, topology)) != S_OK)
+ GUID sub_type;
+
+ hr = m_type->lpVtbl->GetGUID(m_type, &MF_MT_SUBTYPE, &sub_type);
+
+ if (FAILED(hr))
{
- err = ErrorWithCode(ErrorStr("setting topology on session: %ld\n"), hr);
- goto cleanup;
+ r.Err = ErrorWithCode(ErrorStr("getting sub type"), hr);
+ goto done;
}
- hr = RunMediaSession(session);
-
- if (hr != S_OK)
- {
- err = ErrorWithCode(ErrorStr("running media session"), hr);
- goto cleanup;
- }
+done:
-cleanup:
- free(audio_file_path);
- if (pSourceNode != NULL)
- {
- pSourceNode->lpVtbl->Release(pSourceNode);
- }
- if (pOutputNode != NULL)
- {
- pOutputNode->lpVtbl->Release(pOutputNode);
- }
- if (activate != NULL)
- {
- activate->lpVtbl->Release(activate);
- }
- if (topology != NULL)
- {
- topology->lpVtbl->Release(topology);
- }
- if (stream_desc != NULL)
- {
- stream_desc->lpVtbl->Release(stream_desc);
- }
- if (desc != NULL)
- {
- desc->lpVtbl->Release(desc);
- }
- if (src != NULL)
- {
- src->lpVtbl->Release(src);
- }
- if (obj != NULL)
- {
- obj->lpVtbl->Release(obj);
- }
- if (resolver != NULL)
- {
- resolver->lpVtbl->Release(resolver);
- }
- if (session != NULL)
- {
- session->lpVtbl->Release(session);
- }
- return err;
-}
-
-
-// BufferNew allocates a new buffer ready to use.
-Buffer*
-BufferNew()
-{
- Buffer *buffer = calloc(1, sizeof(Buffer));
- buffer->Data = NULL;
- buffer->Len = 0;
- buffer->Cap = 0;
- return buffer;
-}
-
-// BufferGrow grows the capacity by at least the provided amount.
-// Growth algorithm uses the double+1 strategy.
-void
-BufferGrow(Buffer *buffer, int amount)
-{
- BYTE* tmp = NULL;
- int target = 0;
- if (buffer->Cap > buffer->Len + amount)
- {
- return;
- }
- // Expand the target capacity until we can fit the amount.
- while (target < buffer->Len + amount)
- {
- target = target*2+1;
- }
- tmp = calloc(target, 1);
- memcpy(tmp, buffer->Data, buffer->Len);
- free(buffer->Data);
- buffer->Data = tmp;
- buffer->Cap = target;
-}
-
-// 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);
- }
- memcpy(buffer->Data+buffer->Len, data, size);
- buffer->Len += size;
-}
-
-// BufferFree frees the memory for the buffer.
-void
-BufferFree(Buffer* buffer)
-{
- free(buffer->Data);
- free(buffer);
-}
-
-// Setup the source reader for the audio file at the given path.
-// IMFSourceReader* is returned as result value.
-Result
-NewSourceReaderForFile(char* path)
-{
- Error *err = NULL; // Dyanmic error.
- HRESULT hr = S_OK; // Windows return code.
-
- WCHAR *audio_file_path = NULL; // Path to audio file.
- IMFMediaType *pAudioType = NULL; // Represents the PCM audio format.
-
- // reader is returned to caller.
- IMFSourceReader *reader = NULL; // Object to stream bytes from.
-
- // Since Windows uses wide strings we need to convert the char*
- // to such a format.
- Result r = CharWiden(path);
-
- if (r.Err != NULL)
- {
- err = ErrorWithCode(ErrorWrap(r.Err, "converting path string"), hr);
- goto cleanup;
- }
-
- audio_file_path = (WCHAR*)r.Value;
-
- hr = MFCreateSourceReaderFromURL(audio_file_path, NULL, &reader);
-
- if (FAILED(hr))
- {
- err = ErrorWithCode(ErrorStr("creating source reader"), hr);
- goto cleanup;
- }
-
- hr = ConfigureAudioStream(reader, &pAudioType);
-
- if (FAILED(hr))
- {
- err = ErrorWithCode(ErrorStr("configuring audio stream"), hr);
- goto cleanup;
- }
-
-cleanup:
-
- if (pAudioType != NULL)
- {
- pAudioType->lpVtbl->Release(pAudioType);
- }
-
- free(audio_file_path);
-
- if (err != NULL)
- {
- r.Err = err;
- }
+ r.Format = (Format){
+ .SampleRate = sample_rate,
+ .Channels = num_channels,
+ .BitDepth = bits_per_sample / 8,
+ };
- if (reader != NULL)
- {
- r.Value = reader;
- }
-
return r;
}
-// decode buffers the decoded PCM data and returns it via out.
-//
-// By default an unconfigured AAC decoder will output PCM 16le which is
-// luckily what we need.
+
+// decode buffers the decoded PCM s16le data and returns it via out.
Error*
decode(IMFSourceReader * reader, Buffer ** out)
{
@@ -788,7 +602,7 @@ decode(IMFSourceReader * reader, Buffer ** out)
if (FAILED(hr))
{
err = ErrorWithCode(ErrorStr("reading sample"), hr);
- goto cleanup;
+ goto done;
}
if (dwFlags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED)
@@ -811,7 +625,7 @@ decode(IMFSourceReader * reader, Buffer ** out)
if (FAILED(hr))
{
err = ErrorWithCode(ErrorStr("converting to contiguous buffer"), hr);
- goto cleanup;
+ goto done;
}
// Get read/write access to the next chunk of audio data.
@@ -820,7 +634,7 @@ decode(IMFSourceReader * reader, Buffer ** out)
if (FAILED(hr))
{
err = ErrorWithCode(ErrorStr("locking buffer"), hr);
- goto cleanup;
+ goto done;
}
BufferWrite(buffer, cbBuffer, chunk);
@@ -831,7 +645,7 @@ decode(IMFSourceReader * reader, Buffer ** out)
if (FAILED(hr))
{
err = ErrorWithCode(ErrorStr("unlocking buffer"), hr);
- goto cleanup;
+ goto done;
}
chunk = NULL;
@@ -839,7 +653,7 @@ decode(IMFSourceReader * reader, Buffer ** out)
*out = buffer;
-cleanup:
+done:
if (pSample != NULL)
{
@@ -878,80 +692,52 @@ Decode(BYTE* compressed, UINT size)
if (FAILED(hr))
{
r.Err = ErrorWithCode(ErrorStr("initializing media foundation"), hr);
- goto cleanup;
+ goto done;
}
- IMFByteStream *stream = NULL;
- IStream *mem_stream = SHCreateMemStream(compressed, size);
+ // stream is the type required by Media Foundation.
+ // We can get one of these by wrapping a COM IStream.
+ IMFByteStream * stream = NULL;
+ // mem_stream is a plain COM IStream that streams from an in-memory
+ // buffer.
+ IStream * mem_stream = SHCreateMemStream(compressed, size);
hr = MFCreateMFByteStreamOnStream(mem_stream, &stream);
if (FAILED(hr))
{
r.Err = ErrorWithCode(ErrorStr("creating byte stream"), hr);
- goto cleanup;
+ goto done;
}
IMFSourceReader *reader = NULL;
+
hr = MFCreateSourceReaderFromByteStream(stream, NULL, &reader);
if (FAILED(hr))
{
r.Err = ErrorWithCode(ErrorStr("creating source reader from byte stream"), hr);
- goto cleanup;
- }
-
- // Deselect all streams and then select the first audio stream.
-
- hr = reader->lpVtbl->SetStreamSelection(reader, MF_SOURCE_READER_ALL_STREAMS, FALSE);
-
- if (FAILED(hr))
- {
- r.Err = ErrorWithCode(ErrorStr("deslecting all streams"), hr);
- goto cleanup;
- }
-
- hr = reader->lpVtbl->SetStreamSelection(reader, MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE);
-
- if (FAILED(hr))
- {
- r.Err = ErrorWithCode(ErrorStr("selecting first audio stream"), hr);
- goto cleanup;
+ goto done;
}
IMFMediaType * m_type = NULL;
- hr = reader->lpVtbl->GetCurrentMediaType(
- reader,
- (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM,
- &m_type
- );
+ hr = ConfigureAudioStream(reader, &m_type);
if (FAILED(hr))
{
- r.Err = ErrorWithCode(ErrorStr("getting media type"), hr);
- goto cleanup;
+ r.Err = ErrorWithCode(ErrorStr("configuring audio stream"), hr);
+ goto done;
}
- // WAVE format gives us the right meta data, so we'll use it.
- //
- // Format tag == 5648 (MPEG_HEAAC)
- WAVEFORMATEX * format = NULL;
- hr = MFCreateWaveFormatExFromMFMediaType(
- m_type,
- &format,
- NULL,
- MFWaveFormatExConvertFlag_Normal
- );
+ FormatResult fr = GetFormat(m_type);
- if (FAILED(hr))
+ if (fr.Err != NULL)
{
- r.Err = ErrorWithCode(ErrorStr("getting meta data"), hr);
- goto cleanup;
+ r.Err = ErrorWrap(fr.Err, "getting format");
+ goto done;
}
- assert(format);
-
Buffer * buffer = NULL;
r.Err = decode(reader, &buffer);
@@ -963,17 +749,7 @@ Decode(BYTE* compressed, UINT size)
assert(buffer);
-cleanup:
-
- if (stream != NULL)
- {
- stream->lpVtbl->Release(stream);
- }
-
- if (reader != NULL)
- {
- reader->lpVtbl->Release(reader);
- }
+done:
hr = MFShutdown();
@@ -986,88 +762,27 @@ cleanup:
}
}
- if (buffer != NULL)
- {
- r.Uncompressed = buffer;
- }
-
- r.Format.SampleRate = format->nSamplesPerSec;
- r.Format.BitDepth = format->wBitsPerSample/8;
- r.Format.Channels = format->nChannels;
-
- return r;
-}
-
-// GetFormat reads format meta data from a source reader.
-FormatResult
-GetFormat(IMFSourceReader * reader)
-{
- IMFMediaType * m_type = NULL;
- HRESULT hr = S_OK;
- FormatResult r = {
- .Format = {},
- .Err = NULL,
- };
-
- hr = reader->lpVtbl->GetCurrentMediaType(reader, (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, &m_type);
-
- if (FAILED(hr))
- {
- r.Err = ErrorWithCode(ErrorStr("getting media type"), hr);
- goto cleanup;
- }
-
- UINT32 num_channels = 0;
-
- hr = m_type->lpVtbl->GetUINT32(m_type, &MF_MT_AUDIO_NUM_CHANNELS, &num_channels);
-
- if (FAILED(hr))
+ if (m_type != NULL)
{
- r.Err = ErrorWithCode(ErrorStr("getting num channels"), hr);
- goto cleanup;
+ m_type->lpVtbl->Release(m_type);
}
- UINT32 sample_rate = 0;
-
- hr = m_type->lpVtbl->GetUINT32(m_type, &MF_MT_AUDIO_SAMPLES_PER_SECOND, &sample_rate);
-
- if (FAILED(hr))
+ if (stream != NULL)
{
- r.Err = ErrorWithCode(ErrorStr("getting num channels"), hr);
- goto cleanup;
+ stream->lpVtbl->Release(stream);
}
-
- UINT32 bits_per_sample = 0;
-
- hr = m_type->lpVtbl->GetUINT32(m_type, &MF_MT_AUDIO_BITS_PER_SAMPLE, &bits_per_sample);
-
- if (FAILED(hr))
+ if (reader != NULL)
{
- r.Err = ErrorWithCode(ErrorStr("getting num channels"), hr);
- goto cleanup;
+ reader->lpVtbl->Release(reader);
}
-
- printf("sample rate: %d\n", sample_rate);
- printf("num channels: %d\n", num_channels);
- printf("bits per sample: %d\n", bits_per_sample);
-
-cleanup:
- if (m_type != NULL)
+ if (buffer != NULL)
{
- m_type->lpVtbl->Release(m_type);
+ r.Uncompressed = buffer;
}
- r.Format = (Format){
- .SampleRate = sample_rate,
- .Channels = num_channels,
- .BitDepth = bits_per_sample / 8,
- };
-
- printf(".SampleRate: %d\n", r.Format.SampleRate);
- printf(".Channels: %d\n", r.Format.Channels);
- printf(".BitDepth: %d\n", r.Format.BitDepth);
+ r.Format = fr.Format;
return r;
}
@@ -1092,7 +807,7 @@ Load(char* path)
if (FAILED(hr))
{
err = ErrorWithCode(ErrorStr("starting media platform"), hr);
- goto cleanup;
+ goto done;
}
Result r = NewSourceReaderForFile(path);
@@ -1100,16 +815,27 @@ Load(char* path)
if (r.Err != NULL)
{
dr.Err = ErrorWrap(r.Err, "setting up source reader for audio file");
- goto cleanup;
+ goto done;
}
reader = (IMFSourceReader*)(r.Value);
- FormatResult fr = GetFormat(reader);
- if (fr.Err != NULL)
- {
+ IMFMediaType * m_type = NULL;
+
+ hr = ConfigureAudioStream(reader, &m_type);
+
+ if (FAILED(hr))
+ {
+ err = ErrorWithCode(ErrorStr("configuring audio stream"), hr);
+ goto done;
+ }
+
+ FormatResult fr = GetFormat(m_type);
+
+ if (fr.Err != NULL)
+ {
dr.Err = ErrorWrap(fr.Err, "getting format");
- goto cleanup;
+ goto done;
}
// Heap allocated buffer to accumulate the audio data.
@@ -1121,11 +847,26 @@ Load(char* path)
if (err != NULL)
{
dr.Err = ErrorWrap(err, "decode minor");
- goto cleanup;
+ goto done;
}
-cleanup:
- MFShutdown();
+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);
+ }
if (reader != NULL)
{
@@ -1136,4 +877,235 @@ cleanup:
dr.Format = fr.Format;
return dr;
-}
-\ No newline at end of file
+}
+
+// Play the audio file at the given path directly to the speakers.
+Error*
+Play(char* path)
+{
+ Error *err = NULL;
+ Result r;
+ HRESULT hr;
+
+ // We have to build a wide string from the C string for the path.
+ // NOTE(jfm): size cannot exceed 256 without dynamic allocation.
+ WCHAR *audio_file_path = NULL;
+
+ // session is the top-level object wherein all stream
+ // processing occurs.
+ IMFMediaSession *session;
+ // resolver can resolve an abitrary byte source.
+ // In our case this will be a plain audio file.
+ IMFSourceResolver *resolver;
+
+ // obj_type describes the source: media source or byte source.
+ MF_OBJECT_TYPE obj_type;
+ // obj is the true source object.
+ IUnknown *obj;
+ // src is the object casted to it's concrete type.
+ IMFMediaSource *src = NULL;
+ // desc provides meta data about the playback.
+ IMFPresentationDescriptor *desc;
+
+ // Stream selection: we assume exactly 1 stream of AAC audio,
+ // however this code may in fact be too brittle.
+ IMFStreamDescriptor *stream_desc; // info about the stream.
+ BOOL fSelected = FALSE; // if a stream exists for the index.
+ DWORD stream_count = 0; // number of streams found.
+
+ // topology configures a graph of stream processing. The only
+ // processing we want is to decode AAC LC audio.
+ IMFTopology *topology;
+
+ // activate is an object that can initialize itself.
+ // This wraps the audio decoder.
+ IMFActivate *activate;
+
+ // source and output nodes: the only two nodes in our topology
+ // graph.
+ IMFTopologyNode *pSourceNode = NULL;
+ IMFTopologyNode *pOutputNode = NULL;
+
+ // Since Windows uses wide strings we need to convert the char*
+ // to such a format.
+ r = CharWiden(path);
+
+ if (r.Err != NULL)
+ {
+ err = ErrorWrap(r.Err, "converting path string");
+ goto done;
+ }
+
+ 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)
+ {
+ err = ErrorWithCode(ErrorStr("creating media session"), hr);
+ goto done;
+ }
+
+ // Create a source resolver. This object can open files and urls.
+ if ((hr = MFCreateSourceResolver(&resolver)) != S_OK)
+ {
+ err = ErrorWithCode(ErrorStr("creating source resolver"), hr);
+ goto done;
+ }
+
+ // Create a "media source" from the sound file.
+ // Perhaps a bytestream would also work?
+ if ((hr = resolver->lpVtbl->CreateObjectFromURL(
+ resolver,
+ audio_file_path,
+ MF_RESOLUTION_MEDIASOURCE|MF_RESOLUTION_READ,
+ NULL,
+ &obj_type,
+ &obj
+ )) != S_OK)
+ {
+ err = ErrorWithCode(ErrorStr("creating object from url"), hr);
+ goto done;
+ }
+
+ if (obj_type != MF_OBJECT_MEDIASOURCE)
+ {
+ err = ErrorWithCode(ErrorStr("not a media source"), hr);
+ goto done;
+ }
+
+ // We know it's a media source so we can do the cast safely.
+ src = (IMFMediaSource*)obj;
+
+ // Create a presentation descriptor.
+ // This object describes the media source, e.g. what it's audio
+ // and video streams are.
+ if ((hr = src->lpVtbl->CreatePresentationDescriptor(src, &desc)) != S_OK)
+ {
+ err = ErrorWithCode(ErrorStr("creating presentation descriptor"), hr);
+ goto done;
+ }
+
+ // Get information about the audio stream. We pull the count
+ // and validate it, but we assume there's only one stream:
+ // AAC-LC audio.
+ if ((hr = desc->lpVtbl->GetStreamDescriptorCount(desc, &stream_count)) != S_OK)
+ {
+ err = ErrorWithCode(ErrorStr("getting stream descriptor count"), hr);
+ goto done;
+ }
+
+ if (stream_count != 1)
+ {
+ err = ErrorWithCode(ErrorStr("expected exactly one stream"), hr);
+ goto done;
+
+ }
+
+ if ((hr = desc->lpVtbl->GetStreamDescriptorByIndex(desc, 0, &fSelected, &stream_desc)) != S_OK)
+ {
+ err = ErrorWithCode(ErrorStr("getting stream descriptor by index"), hr);
+ goto done;
+ }
+
+ if (!fSelected)
+ {
+ err = ErrorWithCode(ErrorStr("stream was not selected"), hr);
+ goto done;
+ }
+
+ if ((hr = MFCreateAudioRendererActivate(&activate)) != S_OK)
+ {
+ err = ErrorWithCode(ErrorStr("creating audio renderer activate"), hr);
+ goto done;
+ }
+
+ if ((hr = MFCreateTopology(&topology)) != S_OK)
+ {
+ err = ErrorWithCode(ErrorStr("creating topology"), hr);
+ goto done;
+ }
+
+ if ((hr = AddSourceNode(topology, src, desc, stream_desc, &pSourceNode)) != S_OK)
+ {
+ err = ErrorWithCode(ErrorStr("adding source node"), hr);
+ goto done;
+ }
+
+ if ((hr = AddOutputNode(topology, (IMFStreamSink *)activate, &pOutputNode)) != S_OK)
+ {
+ err = ErrorWithCode(ErrorStr("adding output node"), hr);
+ goto done;
+ }
+
+ if ((hr = pSourceNode->lpVtbl->ConnectOutput(pSourceNode, 0, pOutputNode, 0)) != S_OK)
+ {
+ err = ErrorWithCode(ErrorStr("connect output"), hr);
+ goto done;
+ }
+
+ if ((hr = session->lpVtbl->SetTopology(session, MFSESSION_SETTOPOLOGY_IMMEDIATE, topology)) != S_OK)
+ {
+ err = ErrorWithCode(ErrorStr("setting topology on session: %ld\n"), hr);
+ goto done;
+ }
+
+ hr = RunMediaSession(session);
+
+ if (hr != S_OK)
+ {
+ err = ErrorWithCode(ErrorStr("running media session"), hr);
+ goto done;
+ }
+
+done:
+ free(audio_file_path);
+ if (pSourceNode != NULL)
+ {
+ pSourceNode->lpVtbl->Release(pSourceNode);
+ }
+ if (pOutputNode != NULL)
+ {
+ pOutputNode->lpVtbl->Release(pOutputNode);
+ }
+ if (activate != NULL)
+ {
+ activate->lpVtbl->Release(activate);
+ }
+ if (topology != NULL)
+ {
+ topology->lpVtbl->Release(topology);
+ }
+ if (stream_desc != NULL)
+ {
+ stream_desc->lpVtbl->Release(stream_desc);
+ }
+ if (desc != NULL)
+ {
+ desc->lpVtbl->Release(desc);
+ }
+ if (src != NULL)
+ {
+ src->lpVtbl->Release(src);
+ }
+ if (obj != NULL)
+ {
+ obj->lpVtbl->Release(obj);
+ }
+ if (resolver != NULL)
+ {
+ resolver->lpVtbl->Release(resolver);
+ }
+ if (session != NULL)
+ {
+ session->lpVtbl->Release(session);
+ }
+ return err;
+}
diff --git a/audio_windows.h b/audio_windows.h
@@ -15,6 +15,8 @@
#include <assert.h>
#include <stdint.h>
+// TODO: native volume (https://docs.microsoft.com/en-us/windows/win32/api/mfidl/nn-mfidl-imfaudiostreamvolume)
+
// Error declares an error return containing a message and possibly
// wrapping another error.
//
@@ -101,10 +103,8 @@ Error* Play(char *path);
// Decode a buffer of compressed audio using Windows Media Foundation.
DecodeResult Decode(BYTE* compressed, UINT size);
-// TODO: native volume (https://docs.microsoft.com/en-us/windows/win32/api/mfidl/nn-mfidl-imfaudiostreamvolume)
-
-
-// Stub.
+// Stub to compile against mingw64 which apparently does not include
+// this function in it's header file.
HRESULT MFCreateMFByteStreamOnStream(
IStream *pStream,
IMFByteStream **ppByteStream
diff --git a/internal/test/audio_test.go b/internal/test/audio_test.go
@@ -24,6 +24,7 @@ func TestLoad(t *testing.T) {
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
+ t.Logf("format: %+v", f)
// 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)
@@ -43,22 +44,32 @@ func TestLoad(t *testing.T) {
}
}
-// // TestDecode ensures that output from the native decoders are similar to
-// // the output of ffmpeg.
-// func TestDecode(t *testing.T) {
-// by, f, err := nativeaudio.Decode(compressed)
-// if err != nil {
-// t.Fatalf("unexpected error: %v", err)
-// }
-// t.Logf("format: %+v", f)
-// // Test passes on exact match, otherwise do a tolerance test.
-// if bytes.Equal(by, uncompressed) {
-// return
-// }
-// if !equal(t, by, uncompressed) {
-// t.Fatalf("native output does not match ffmpeg output")
-// }
-// }
+// TestDecode ensures that output from the native decoders are similar to
+// the output of ffmpeg.
+func TestDecode(t *testing.T) {
+ by, f, err := nativeaudio.Decode(compressed)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ t.Logf("format: %+v", f)
+ // 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
+ }
+ if !equal(t, by, uncompressed) {
+ t.Fatalf("native output does not match ffmpeg output")
+ }
+}
// equal decodes the PCM samples and tests if they are "close enough"
// using a heuristic tolerance.