nativeaudio

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

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:
Maudio.go | 8++++----
Maudio_windows.c | 923++++++++++++++++++++++++++++++++++++++-----------------------------------------
Maudio_windows.h | 8++++----
Minternal/test/audio_test.go | 43+++++++++++++++++++++++++++----------------
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.