nativeaudio

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

commit 7cc149941bfb12c0c82dddc7b63d1bbaae515ce4
parent 54d36e7969249bef53db07591ffd595cc19bcd05
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Fri,  5 Jan 2024 18:25:01 +0800

nativeaudio: [Windows] pure Go implementation

This commits orchestrates Windows Media Foundation using COM directly in
Go - no CGO needed.

This means Windows is now an easy cross compile target from any other
platform.

This version is more efficient because it avoids copying the data into
the Media Foundation and has less bugs. It also enables hardware
acceleration.

Playback has been changed to use oto instead of a different code tree
dedicated to direct playback: it's less code to manage.

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

Diffstat:
Daudio_windows.c | 1148-------------------------------------------------------------------------------
Maudio_windows.go | 960+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Daudio_windows.h | 128-------------------------------------------------------------------------------
Mgo.mod | 7++++++-
Ago.sum | 6++++++
5 files changed, 856 insertions(+), 1393 deletions(-)

diff --git a/audio_windows.c b/audio_windows.c @@ -1,1147 +0,0 @@ -#include "audio_windows.h" - -FormatResult -GetFormat(IMFMediaType * m_type); - -// ErrorStr creates an error with the provided message as a char*. -Error* -ErrorStr(char *s) -{ - Error *err = calloc(1, sizeof(Error)); - err->Str = s; - return err; -} - -// ErrorWrap wraps an error with more context. -Error* -ErrorWrap(Error *err, char* s) -{ - Error *new = calloc(1, sizeof(Error)); - new->Str = s; - new->Err = err; - return new; -} - -// ErrorWithCode attaches an error code to the error. -Error* -ErrorWithCode(Error *err, int code) -{ - err->Code = code; - return err; -} - -// ErrorFree deallocates the error and any wrapped errors. -void -ErrorFree(Error* err) -{ - Error *tmp = NULL; - while (err != NULL) - { - tmp = err; - err = err->Err; - free(tmp); - } -} - -// NewResult constructs a Result with the provided value and error. -// Usually one of the hose pointers will be NULL. -Result -NewResult(void* value, Error* err) -{ - return (Result){value, err}; -} - -// CharWiden converts a raw C string to a wide string used by Windows. -Result -CharWiden(char* str) -{ - int hr = 0; - size_t length = 0; - WCHAR *target = NULL; - - length = strlen(str); - target = calloc(sizeof(WCHAR), length); - - if ((hr = MultiByteToWideChar(CP_ACP, 0, str, -1, target, length)) != 0) - { - return NewResult(NULL, ErrorWithCode(ErrorStr("converting C string to Windows wide string (WCHAR)"), hr)); - } - - return NewResult(target, NULL); -} - -// BufferNew allocates a new buffer ready to use. -Buffer* -BufferNew() -{ - Buffer *buffer = calloc(1, sizeof(Buffer)); - buffer->Data = calloc(BUFFER_DEFAULT_SIZE, 1); - buffer->Len = 0; - buffer->Cap = BUFFER_DEFAULT_SIZE; - 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 = buffer->Cap; - 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) -{ - 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) -{ - if (buffer == NULL) { - return; - } - 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) { - - HRESULT hr = S_OK; - - BOOL bSessionEvent = TRUE; - - while(bSessionEvent){ - - HRESULT hrStatus = S_OK; - IMFMediaEvent* pEvent = NULL; - MediaEventType meType = MEUnknown; - - MF_TOPOSTATUS TopoStatus = MF_TOPOSTATUS_INVALID; - - hr = pSession->lpVtbl->GetEvent(pSession, 0, &pEvent); - - if(SUCCEEDED(hr)){ - hr = pEvent->lpVtbl->GetStatus(pEvent, &hrStatus); - } - - if(SUCCEEDED(hr)){ - hr = pEvent->lpVtbl->GetType(pEvent, &meType); - } - - if(SUCCEEDED(hr) && SUCCEEDED(hrStatus)){ - - switch(meType){ - - case MESessionTopologySet: - break; - - case MESessionTopologyStatus: - - hr = pEvent->lpVtbl->GetUINT32(pEvent, &MF_EVENT_TOPOLOGY_STATUS, (UINT32*)&TopoStatus); - - if(SUCCEEDED(hr)){ - - switch(TopoStatus){ - - case MF_TOPOSTATUS_READY: - { - PROPVARIANT varStartPosition; - PropVariantInit(&varStartPosition); - hr = pSession->lpVtbl->Start(pSession, NULL, &varStartPosition); - PropVariantClear(&varStartPosition); - } - break; - - case MF_TOPOSTATUS_STARTED_SOURCE: - break; - - case MF_TOPOSTATUS_ENDED: - break; - - default: - break; - } - } - break; - - case MESessionStarted: - break; - - case MESessionEnded: - hr = pSession->lpVtbl->Stop(pSession); - break; - - case MESessionStopped: - hr = pSession->lpVtbl->Close(pSession); - break; - - case MESessionClosed: - bSessionEvent = FALSE; - break; - - case MESessionNotifyPresentationTime: - break; - - case MESessionCapabilitiesChanged: - break; - - case MEEndOfPresentation: - break; - - default: - break; - } - - if(FAILED(hr) || FAILED(hrStatus)){ - bSessionEvent = FALSE; - } - } - } - - return hr; -} - -// AddOutputNode to the topology. -// This is used in Play. -HRESULT -AddOutputNode( - IMFTopology *pTopology, // Topology. - IMFStreamSink *pStreamSink, // Stream sink. - IMFTopologyNode **ppNode // Receives the node pointer. -) -{ - IMFTopologyNode *pNode = NULL; - HRESULT hr = S_OK; - - // Create the node. - hr = MFCreateTopologyNode(MF_TOPOLOGY_OUTPUT_NODE, &pNode); - - if (hr != S_OK) - { - return hr; - } - - // Set the object pointer. - if (SUCCEEDED(hr)) - { - hr = pNode->lpVtbl->SetObject(pNode, (IUnknown *)pStreamSink); - } - - if (hr != S_OK) - { - return hr; - } - - // Add the node to the topology. - if (SUCCEEDED(hr)) - { - hr = pTopology->lpVtbl->AddNode(pTopology, pNode); - } - - if (hr != S_OK) - { - return hr; - } - - if (SUCCEEDED(hr)) - { - hr = pNode->lpVtbl->SetUINT32(pNode, &MF_TOPONODE_NOSHUTDOWN_ON_REMOVE, TRUE); - } - - if (hr != S_OK) - { - return hr; - } - - // Return the pointer to the caller. - if (SUCCEEDED(hr)) - { - *ppNode = pNode; - (*ppNode)->lpVtbl->AddRef(*ppNode); - } - - return hr; -} - -// Add a source node to a topology. -// 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 *pNode = NULL; - - // Create the node. - HRESULT hr = MFCreateTopologyNode(MF_TOPOLOGY_SOURCESTREAM_NODE, &pNode); - if (FAILED(hr)) - { - goto done; - } - - // Set the attributes. - hr = pNode->lpVtbl->SetUnknown(pNode, &MF_TOPONODE_SOURCE, (IUnknown *)pSource); - if (FAILED(hr)) - { - goto done; - } - - hr = pNode->lpVtbl->SetUnknown(pNode, &MF_TOPONODE_PRESENTATION_DESCRIPTOR, (IUnknown *)pPD); - if (FAILED(hr)) - { - goto done; - } - - hr = pNode->lpVtbl->SetUnknown(pNode, &MF_TOPONODE_STREAM_DESCRIPTOR, (IUnknown *)pSD); - if (FAILED(hr)) - { - goto done; - } - - // Add the node to the topology. - hr = pTopology->lpVtbl->AddNode(pTopology, pNode); - if (FAILED(hr)) - { - goto done; - } - - // Return the pointer to the caller. - *ppNode = pNode; - (*ppNode)->lpVtbl->AddRef(*ppNode); - -done: - return hr; -} - -//------------------------------------------------------------------- -// ConfigureAudioStream -// -// Selects an audio stream from the source file, and configures the -// stream to deliver decoded PCM audio. -// -// We can use the source reader to load all PCM samples in a loop. -// -// hr = ConfigureAudioStream(pReader, &pAudioType); -//------------------------------------------------------------------- -HRESULT -ConfigureAudioStream( - IMFSourceReader *pReader, // Pointer to the source reader. - IMFMediaType **pUncompressedAudioType, - IMFMediaType **pPartialType, - IMFMediaType **ppPCMAudio // Receives the audio format. -) -{ - // Select the first audio stream, and deselect all other streams. - HRESULT hr = pReader->lpVtbl->SetStreamSelection(pReader, - (DWORD)MF_SOURCE_READER_ALL_STREAMS, FALSE); - - if (SUCCEEDED(hr)) - { - hr = pReader->lpVtbl->SetStreamSelection(pReader, - (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE); - } - - // Create a partial media type that specifies uncompressed PCM audio. - hr = MFCreateMediaType(pPartialType); - - if (SUCCEEDED(hr)) - { - hr = (*pPartialType)->lpVtbl->SetGUID((*pPartialType), &MF_MT_MAJOR_TYPE, &MFMediaType_Audio); - } - - if (SUCCEEDED(hr)) - { - hr = (*pPartialType)->lpVtbl->SetGUID((*pPartialType), &MF_MT_SUBTYPE, &MFAudioFormat_PCM); - } - - // Set this type on the source reader. The source reader will - // load the necessary decoder. - if (SUCCEEDED(hr)) - { - hr = pReader->lpVtbl->SetCurrentMediaType(pReader, - (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, - NULL, (*pPartialType)); - } - - // Get the complete uncompressed format. - if (SUCCEEDED(hr)) - { - hr = pReader->lpVtbl->GetCurrentMediaType(pReader, - (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, - pUncompressedAudioType); - } - - // Ensure the stream is selected. - if (SUCCEEDED(hr)) - { - hr = pReader->lpVtbl->SetStreamSelection(pReader, - (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, - TRUE); - } - - // Return the PCM format to the caller. - if (SUCCEEDED(hr)) - { - *ppPCMAudio = (*pUncompressedAudioType); - (*ppPCMAudio)->lpVtbl->AddRef(*ppPCMAudio); - } - - return hr; -} - -// 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. - - // 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 done; - } - - audio_file_path = (WCHAR*)r.Value; - - hr = MFCreateSourceReaderFromURL(audio_file_path, NULL, &reader); - - if (FAILED(hr)) - { - err = ErrorWithCode(ErrorStr("creating source reader"), hr); - goto done; - } - -done: - - free(audio_file_path); - - if (err != NULL) - { - r.Err = err; - } - - if (reader != NULL) - { - r.Value = reader; - } - - return r; -} - - -// GetFormat reads format meta data from a source reader. -FormatResult -GetFormat(IMFMediaType * m_type) -{ - HRESULT hr = S_OK; - FormatResult r = { - .Format = { - .SampleRate = 0, - .BitDepth = 0, - .Channels = 0 - }, - .Err = NULL, - }; - - UINT32 num_channels = 0; - - hr = m_type->lpVtbl->GetUINT32(m_type, &MF_MT_AUDIO_NUM_CHANNELS, &num_channels); - - if (FAILED(hr)) - { - r.Err = ErrorWithCode(ErrorStr("getting num channels"), hr); - goto done; - } - - UINT32 sample_rate = 0; - - hr = m_type->lpVtbl->GetUINT32(m_type, &MF_MT_AUDIO_SAMPLES_PER_SECOND, &sample_rate); - - if (FAILED(hr)) - { - r.Err = ErrorWithCode(ErrorStr("getting num channels"), hr); - goto done; - } - - - UINT32 bits_per_sample = 0; - - hr = m_type->lpVtbl->GetUINT32(m_type, &MF_MT_AUDIO_BITS_PER_SAMPLE, &bits_per_sample); - - if (FAILED(hr)) - { - r.Err = ErrorWithCode(ErrorStr("getting num channels"), hr); - goto done; - } - - GUID sub_type; - - hr = m_type->lpVtbl->GetGUID(m_type, &MF_MT_SUBTYPE, &sub_type); - - if (FAILED(hr)) - { - r.Err = ErrorWithCode(ErrorStr("getting sub type"), hr); - goto done; - } - -done: - - r.Format = (Format){ - .SampleRate = sample_rate, - .Channels = num_channels, - .BitDepth = bits_per_sample / 8, - }; - - return r; -} - - -// decode buffers the decoded PCM s16le data and returns it via out. -Error* -decode(IMFSourceReader * reader, Buffer * out) -{ - assert(reader); - - IMFMediaBuffer *bufferReader = NULL; // buffer object containing the raw buffer. - IMFSample *pSample = NULL; // sample object containing on or more streams. - BYTE *chunk = NULL; // pointer to start of chunk. - - LONGLONG prev_time_stamp = -1; - LONGLONG time_stamp = 0; - DWORD cbBuffer = 0; // size of chunk. - HRESULT hr = S_OK; - Error *err = NULL; - - // Stream all the data into a byte buffer. - - // 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; - - if (pSample != NULL) - { - pSample->lpVtbl->RemoveAllBuffers(pSample); - pSample->lpVtbl->Release(pSample); - } - - // Read the next sample. - hr = reader->lpVtbl->ReadSample( - reader, - (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, - 0, - NULL, - &dwFlags, - &time_stamp, - &pSample - ); - - // 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; - } - - prev_time_stamp = time_stamp; - - if (FAILED(hr)) - { - err = ErrorWithCode(ErrorStr("reading sample"), hr); - goto done; - } - - if (dwFlags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED) - { - break; - } - if (dwFlags & MF_SOURCE_READERF_ENDOFSTREAM) - { - break; - } - - if (pSample == NULL) - { - continue; - } - - if (bufferReader != NULL) { - bufferReader->lpVtbl->Release(bufferReader); - } - - // Get a pointer to the buffer object. - hr = pSample->lpVtbl->ConvertToContiguousBuffer(pSample, &bufferReader); - - if (FAILED(hr)) - { - err = ErrorWithCode(ErrorStr("converting to contiguous buffer"), hr); - goto done; - } - - // Get read/write access to the next chunk of audio data. - hr = bufferReader->lpVtbl->Lock(bufferReader, &chunk, NULL, &cbBuffer); - - if (FAILED(hr)) - { - err = ErrorWithCode(ErrorStr("locking buffer"), hr); - goto done; - } - - BufferWrite(out, cbBuffer, chunk); - - // Unlock the reader that we just copied from. - hr = bufferReader->lpVtbl->Unlock(bufferReader); - - if (FAILED(hr)) - { - err = ErrorWithCode(ErrorStr("unlocking buffer"), hr); - goto done; - } - - chunk = NULL; - } - -done: - - if (pSample != NULL) - { - pSample->lpVtbl->RemoveAllBuffers(pSample); - pSample->lpVtbl->Release(pSample); - } - - if (bufferReader != NULL) - { - bufferReader->lpVtbl->Release(bufferReader); - } - - return err; -} - -// Decode the compresed audio data into uncompressed s16le PCM. -// -// We get a bit lucky here because Media Foundation defaults to that -// PCM format when it auto-inits the AAC decoder. -// -// For different input formats (other than AAC) the PCM format may not -// be guaranteed. -DecodeResult -Decode(BYTE* compressed, UINT size) -{ - assert(compressed); - - HRESULT hr = S_OK; - DecodeResult r = { - .Uncompressed = NULL, - .Format = { .Channels = 0, .SampleRate = 0, .BitDepth = 0}, - .Err = NULL, - }; - - // 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); - - IMFMediaType * m_type = NULL; - IMFMediaType * pUncompressedAudioType = NULL; - IMFMediaType * pPartialType = NULL; - Buffer * buffer = BufferNew(); - - hr = MFCreateMFByteStreamOnStream(mem_stream, &stream); - - if (FAILED(hr)) - { - r.Err = ErrorWithCode(ErrorStr("creating byte stream"), hr); - 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 done; - } - - - hr = ConfigureAudioStream(reader, &pUncompressedAudioType, &pPartialType, &m_type); - - if (FAILED(hr)) - { - r.Err = ErrorWithCode(ErrorStr("configuring audio stream"), hr); - goto done; - } - - FormatResult fr = GetFormat(m_type); - - if (fr.Err != NULL) - { - r.Err = ErrorWrap(fr.Err, "getting format"); - goto done; - } - - r.Err = decode(reader, buffer); - - if (r.Err != NULL) - { - r.Err = ErrorWrap(r.Err, "decode minor"); - } - - assert(buffer); - -done: - if (pUncompressedAudioType != NULL) { - pUncompressedAudioType->lpVtbl->Release(pUncompressedAudioType); - } - - if (pPartialType != NULL) { - pPartialType->lpVtbl->Release(pPartialType); - } - - if (m_type != NULL) - { - m_type->lpVtbl->Release(m_type); - } - - if (stream != NULL) - { - stream->lpVtbl->Release(stream); - } - - if (mem_stream != NULL) { - mem_stream->lpVtbl->Release(mem_stream); - } - - if (reader != NULL) - { - reader->lpVtbl->Release(reader); - } - - if (buffer != NULL) - { - r.Uncompressed = buffer; - } - - r.Format = fr.Format; - - return r; -} - -// 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. - FormatResult fr = { - .Err = NULL, - .Format = { - .BitDepth = 0, - .SampleRate = 0, - .Channels = 0, - } - }; - DecodeResult dr = { - .Err = NULL, - .Uncompressed = NULL, - .Format = { - .BitDepth = 0, - .SampleRate = 0, - .Channels = 0, - } - }; - - IMFMediaType * m_type = NULL; - IMFMediaType * pUncompressedAudioType = NULL; - IMFMediaType * pPartialType = NULL; - - Result r = NewSourceReaderForFile(path); - - if (r.Err != NULL) - { - dr.Err = ErrorWrap(r.Err, "setting up source reader for audio file"); - goto done; - } - - reader = (IMFSourceReader*)(r.Value); - - - hr = ConfigureAudioStream(reader, &pUncompressedAudioType, &pPartialType, &m_type); - - if (FAILED(hr)) - { - err = ErrorWithCode(ErrorStr("configuring audio stream"), hr); - goto done; - } - - fr = GetFormat(m_type); - - if (fr.Err != NULL) - { - dr.Err = ErrorWrap(fr.Err, "getting format"); - 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(); - - err = decode(reader, buffer); - - if (err != NULL) - { - dr.Err = ErrorWrap(err, "decode minor"); - goto done; - } - -done: - - if (pUncompressedAudioType != NULL) { - pUncompressedAudioType->lpVtbl->Release(pUncompressedAudioType); - } - - if (pPartialType != NULL) { - pPartialType->lpVtbl->Release(pPartialType); - } - - if (m_type != NULL) - { - m_type->lpVtbl->Release(m_type); - } - - if (reader != NULL) - { - reader->lpVtbl->Release(reader); - } - - dr.Uncompressed = buffer; - dr.Format = fr.Format; - - return dr; -} - -// 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; - - // 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; -} - -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 @@ -1,176 +1,904 @@ -//go:build windows && cgo - package nativeaudio -// -g: add to CFLAGS to include dwarf debug data -// -O: optimization level 0, 1, 2, 3, s - -/* -#cgo CFLAGS: -Werror -g -O3 -#cgo LDFLAGS: -lwinmm -lmf -lmfplat -lmfuuid -loleaut32 -limm32 -lversion -lwindowsapp -lmfreadwrite -lshlwapi -#include "audio_windows.h" -#include <crtdbg.h> -#include <stdlib.h> -#include <stdio.h> -#include <windows.h> -#include <winbase.h> -#include <combaseapi.h> -#include <mfapi.h> -#include <mfidl.h> -#include <mferror.h> -#include <initguid.h> -#include <wmcodecdsp.h> -#include <mmdeviceapi.h> -#include <mfreadwrite.h> -#include <shlwapi.h> -#include <assert.h> -#include <stdint.h> -*/ -import "C" - import ( - "errors" + "bytes" "fmt" + "io" + "os" "runtime" - "strings" + "syscall" + "unicode/utf16" "unsafe" + "git.sr.ht/~jackmordaunt/nativeaudio/internal" + "github.com/ebitengine/oto/v3" "golang.org/x/sys/windows" ) func start() error { - if hr := C.MFStartup(C.MF_VERSION, C.MFSTARTUP_LITE); hr != C.S_OK { - return fmt.Errorf("initializing Media Framework: %w", MFErr{Code: hr}) - } - return nil + return MFStartup(MF_VERSION, MFSTARTUP_LITE) } func end() error { - if hr := C.MFShutdown(); hr != C.S_OK { - return fmt.Errorf("shutting down Media Framework: %w", MFErr{Code: hr}) - } - return nil + return MFShutdown() } // play the audio file using Windows Media Foundation. func play(path string) error { - cPath := C.CString(path) - defer C.free(unsafe.Pointer(cPath)) - err := C.Play(cPath) + data, format, err := load(path) if err != nil { - defer C.ErrorFree(err) - return collectErrors(err) + return fmt.Errorf("decoding: %w", err) } - return nil + ctx, ready, err := oto.NewContext(&oto.NewContextOptions{ + SampleRate: format.SampleRate, + ChannelCount: format.Channels, + Format: oto.FormatSignedInt16LE, + }) + if err != nil { + return fmt.Errorf("starting playback context: %w", err) + } + <-ready + done := make(chan any) + player := ctx.NewPlayer(internal.NewTriggerReader(bytes.NewReader(data), func() { close(done) })) + player.Play() + <-done + ctx.Suspend() + return player.Close() } // load raw pcm data from the Windows Media Foundation. func load(path string) (uncompressed []byte, format Format, err error) { - cPath := C.CString(path) - defer C.free(unsafe.Pointer(cPath)) + f, err := os.Open(path) + if err != nil { + return nil, format, fmt.Errorf("opening input file: %w", err) + } + defer f.Close() + by, err := io.ReadAll(f) + if err != nil { + return nil, format, fmt.Errorf("buffering input file: %w", err) + } + return decode(by) +} - r := C.Load(cPath) +// 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) { + stream := SHCreateMemStream(unsafe.SliceData(compressed), len(compressed)) + if stream == nil { + return nil, format, fmt.Errorf("could not allocate IStream") + } + defer stream.Release() - if r.Err != nil { - defer C.ErrorFree(r.Err) - return nil, Format{}, collectErrors(r.Err) + // We need to adapt the generic IStream to a Media Foundation stream type. + var mfByteStream *IMFByteStream + defer mfByteStream.Release() + + if err := MFCreateMFByteStreamOnStream(stream, &mfByteStream); err != nil { + return nil, format, fmt.Errorf("creating MFByteStream from IStream: %w", err) } - defer C.BufferFree(r.Uncompressed) + // Attributes to configure the source reader with; specifically, enable hardware codecs. + var attributes *IMFAttributes + defer attributes.Release() - uncompressed = GoSlice((*byte)(r.Uncompressed.Data), int64(r.Uncompressed.Len)) + if err := MFCreateAttributes(&attributes, 1); err != nil { + return nil, format, fmt.Errorf("creating attributes to apply to source reader: %w", err) + } - format = Format{ - SampleRate: int(r.Format.SampleRate), - BitDepth: int(r.Format.BitDepth), - Channels: int(r.Format.Channels), + if err := attributes.SetUINT32(&MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, 1); err != nil { + return nil, format, fmt.Errorf("enabling hardware transforms: %w", err) } - runtime.KeepAlive(path) + // Create the source reader using the byte stream. + var mfSourceReader *IMFSourceReader + defer mfSourceReader.Release() + + if err := MFCreateSourceReaderFromByteStream(mfByteStream, attributes, &mfSourceReader); err != nil { + return nil, format, fmt.Errorf("creating IMFSourceReader from IMFByteStream: %w", err) + } + + if err := configureAudioStream(mfSourceReader); err != nil { + return nil, format, fmt.Errorf("configuring audio stream: %w", err) + } + + format, err = getSourceReaderFormat(mfSourceReader) + if err != nil { + return nil, format, fmt.Errorf("getting format: %w", err) + } + + buf, err := io.ReadAll(NewSourceReader(mfSourceReader)) + if err != nil { + return nil, format, err + } + + runtime.KeepAlive(compressed) - return uncompressed, format, nil + return buf, format, nil } -// 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) { - data := compressed +// configureAudioStream selects the first audio stream and configures it output PCM. +func configureAudioStream(pReader *IMFSourceReader) error { + var pPartialType *IMFMediaType + defer pPartialType.Release() - r := C.Decode((*C.uchar)(unsafe.SliceData(data)), C.uint(len(compressed))) + // Create a partial media pUncompressedAutioTypee that specifies uncompressed PCM audio. + if err := MFCreateMediaType(&pPartialType); err != nil { + return fmt.Errorf("creating media type: %w", err) + } - if r.Err != nil && r.Err.Str != nil { - defer C.ErrorFree(r.Err) - return nil, format, collectErrors(r.Err) + if err := pPartialType.SetGUID(&MF_MT_MAJOR_TYPE, &MFMediaType_Audio); err != nil { + return fmt.Errorf("setting major type: %w", err) } - defer C.BufferFree(r.Uncompressed) + if err := pPartialType.SetGUID(&MF_MT_SUBTYPE, &MFAudioFormat_PCM); err != nil { + return fmt.Errorf("setting sub type: %w", err) + } - uncompressed = GoSlice((*byte)(r.Uncompressed.Data), int64(r.Uncompressed.Len)) + // Select the first audio stream, and deselect all other streams. + if err := pReader.SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, false); err != nil { + return fmt.Errorf("deselecting audio streams: %w", err) + } + if err := pReader.SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, true); err != nil { + return fmt.Errorf("selecting first audio stream: %w", err) + } - format = Format{ - Channels: int(r.Format.Channels), - BitDepth: int(r.Format.BitDepth), - SampleRate: int(r.Format.SampleRate), + // Set this type on the source reader. The source reader will load the necessary decoder. + if err := pReader.SetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, pPartialType); err != nil { + return fmt.Errorf("setting media type on source reader: %w", err) } - runtime.KeepAlive(compressed) + return nil +} + +// getSourceReaderFormat returns the audio format configured for the source reader. +func getSourceReaderFormat(sr *IMFSourceReader) (f Format, _ error) { + var mfMediaType *IMFMediaType + defer mfMediaType.Release() + + // Get the complete uncompressed format. + if err := sr.GetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, &mfMediaType); err != nil { + return f, fmt.Errorf("getting the current media type: %w", err) + } + + return getFormat(mfMediaType) +} + +// getFormat extracts the audio format from a media type. +func getFormat(mt *IMFMediaType) (f Format, _ error) { + var ( + numChannels uint32 + sampleRate uint32 + bitsPerSample uint32 + ) + + if err := mt.GetUINT32(&MF_MT_AUDIO_NUM_CHANNELS, &numChannels); err != nil { + return f, fmt.Errorf("getting numChannels for the media type: %w", err) + } + if err := mt.GetUINT32(&MF_MT_AUDIO_SAMPLES_PER_SECOND, &sampleRate); err != nil { + return f, fmt.Errorf("getting sampleRate for the media type: %w", err) + } + if err := mt.GetUINT32(&MF_MT_AUDIO_BITS_PER_SAMPLE, &bitsPerSample); err != nil { + return f, fmt.Errorf("getting bitsPerSample for the media type: %w", err) + } + + f = Format{ + SampleRate: int(sampleRate), + Channels: int(numChannels), + BitDepth: int(bitsPerSample / 8), + } + + return f, nil +} + +// SourceReader wraps an IMFSourceReader and implements [io.Reader]. +type SourceReader struct { + source *IMFSourceReader + + sample *IMFSample // sample object containing one or more streams + buffer *IMFMediaBuffer // buffer object containing the raw buffer + chunk *byte // start of chunk of audio data + chunkSz int64 // size of chunk + + prevTimestamp int64 + currentTimestamp int64 + + data []byte // Go view of the audio data, backed by native memory. +} + +// NewSourceReader allocates a [SourceReader]. +// The caller is responsible for releasing the underlying [IMFSourceReader]. +func NewSourceReader(r *IMFSourceReader) *SourceReader { + return &SourceReader{source: r} +} + +func (s *SourceReader) Read(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } - return uncompressed, format, nil -} - -// collectErrors unwraps all the errors in the chain and coalesces them -// into a single Go error. -func collectErrors(err *C.Error) error { - var buf strings.Builder - for first := err; err != nil; err = err.Err { - if err.Str != nil { - if err != first { - buf.WriteString(": ") - } - buf.WriteString(strings.TrimSpace(C.GoString(err.Str))) - if err.Code != 0 { - buf.WriteString(fmt.Sprintf(" (%d)", err.Code)) - } + // Write residual data into p before requesting more. + if len(s.data) > 0 { + return s.copyInto(p), nil + } + + if !s.next() { + return 0, io.EOF + } + + return s.copyInto(p), nil +} + +// next reads the next audio sample, returning true if found, or false if EOF. +func (s *SourceReader) next() bool { + for { + var flags int64 + + // Read the next sample; skipping samples with matching time stamps. + // For some reason ReadSample can produce more than one sample at time 0. + // Emitting all of them produces largers files and audio artifacts. + s.source.ReadSample(MF_SOURCE_READER_FIRST_AUDIO_STREAM, 0, nil, &flags, &s.currentTimestamp, &s.sample) + + if flags&MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED != 0 { + return false } + + if flags&MF_SOURCE_READERF_ENDOFSTREAM != 0 { + return false + } + + if s.sample == nil { + continue + } + + if s.currentTimestamp != s.prevTimestamp-1 { + break + } + } + + s.prevTimestamp = s.currentTimestamp + 1 + + s.sample.ConvertToContiguousBuffer(&s.buffer) + s.buffer.Lock(&s.chunk, nil, &s.chunkSz) + + // Make a Go slice view of the data for easy consumption. + s.data = unsafe.Slice((*byte)(s.chunk), s.chunkSz) + + return true +} + +// copyInto copies audio data into p, unlocking the memory once fully copied. +func (s *SourceReader) copyInto(p []byte) int { + n := copy(p, s.data) + + s.data = s.data[n:] + + if len(s.data) == 0 { + s.buffer.Unlock() + s.buffer.Release() + s.sample.Release() + s.data = nil + } + + return n +} + +/* + The following contains the minimal set of definitions we need to decode + audio using Media Framework. + + Definitions are derived from appropriate SDK headers. +*/ + +type GUID struct { + Data1 uint32 + Data2 uint16 + Data3 uint16 + Data4 [8]uint8 +} + +type HRESULT = uintptr + +const ( + S_OK = 0x0 + MFSTARTUP_LITE = 0x1 + MF_VERSION = 0x20070 + MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED = 0x20 + MF_SOURCE_READERF_ENDOFSTREAM = 0x2 + MF_SOURCE_READER_ALL_STREAMS = 0xfffffffe + MF_SOURCE_READER_FIRST_AUDIO_STREAM = 0xfffffffd +) + +var ( + MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS = GUID{0xa634a91c, 0x822b, 0x41b9, [8]uint8{0xa4, 0x94, 0x4d, 0xe4, 0x64, 0x36, 0x12, 0xb0}} + + MF_MT_AUDIO_NUM_CHANNELS = GUID{0x37e48bf5, 0x645e, 0x4c5b, [8]uint8{0x89, 0xde, 0xad, 0xa9, 0xe2, 0x9b, 0x69, 0x6a}} + MF_MT_AUDIO_SAMPLES_PER_SECOND = GUID{0x5faeeae7, 0x0290, 0x4c31, [8]uint8{0x9e, 0x8a, 0xc5, 0x34, 0xf6, 0x8d, 0x9d, 0xba}} + MF_MT_AUDIO_FLOAT_SAMPLES_PER_SECOND = GUID{0xfb3b724a, 0xcfb5, 0x4319, [8]uint8{0xae, 0xfe, 0x6e, 0x42, 0xb2, 0x40, 0x61, 0x32}} + MF_MT_AUDIO_AVG_BYTES_PER_SECOND = GUID{0x1aab75c8, 0xcfef, 0x451c, [8]uint8{0xab, 0x95, 0xac, 0x03, 0x4b, 0x8e, 0x17, 0x31}} + MF_MT_AUDIO_BLOCK_ALIGNMENT = GUID{0x322de230, 0x9eeb, 0x43bd, [8]uint8{0xab, 0x7a, 0xff, 0x41, 0x22, 0x51, 0x54, 0x1d}} + MF_MT_AUDIO_BITS_PER_SAMPLE = GUID{0xf2deb57f, 0x40fa, 0x4764, [8]uint8{0xaa, 0x33, 0xed, 0x4f, 0x2d, 0x1f, 0xf6, 0x69}} + MF_MT_AUDIO_VALID_BITS_PER_SAMPLE = GUID{0xd9bf8d6a, 0x9530, 0x4b7c, [8]uint8{0x9d, 0xdf, 0xff, 0x6f, 0xd5, 0x8b, 0xbd, 0x06}} + MF_MT_AUDIO_SAMPLES_PER_BLOCK = GUID{0xaab15aac, 0xe13a, 0x4995, [8]uint8{0x92, 0x22, 0x50, 0x1e, 0xa1, 0x5c, 0x68, 0x77}} + MF_MT_AUDIO_CHANNEL_MASK = GUID{0x55fb5765, 0x644a, 0x4caf, [8]uint8{0x84, 0x79, 0x93, 0x89, 0x83, 0xbb, 0x15, 0x88}} + + MF_MT_MAJOR_TYPE = GUID{0x48eba18e, 0xf8c9, 0x4687, [8]uint8{0xbf, 0x11, 0x0a, 0x74, 0xc9, 0xf9, 0x6a, 0x8f}} + MF_MT_SUBTYPE = GUID{0xf7e34c9a, 0x42e8, 0x4714, [8]uint8{0xb7, 0x4b, 0xcb, 0x29, 0xd7, 0x2c, 0x35, 0xe5}} + + MFMediaType_Audio = GUID{0x73647561, 0x0000, 0x0010, [8]uint8{0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71}} + MFAudioFormat_PCM = GUID{0x00000001, 0x0000, 0x0010, [8]uint8{0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}} +) + +type IMFByteStream struct { + VTable *IMFByteStreamVTable +} + +type IMFByteStreamVTable struct { + QueryInterface uintptr + AddRef uintptr + Release uintptr + GetCapabilities uintptr + GetLength uintptr + SetLength uintptr + GetCurrentPosition uintptr + SetCurrentPosition uintptr + IsEndOfStream uintptr + Read uintptr + BeginRead uintptr + EndRead uintptr + Write uintptr + BeginWrite uintptr + EndWrite uintptr + Seek uintptr + Flush uintptr + Close uintptr +} + +func (v *IMFByteStream) Release() error { + if v == nil { + return nil + } + r, _, _ := syscall.SyscallN( + v.VTable.Release, + uintptr(unsafe.Pointer(v)), + ) + if r != S_OK { + return MFErr{Code: r} + } + return nil +} + +type IStream struct { + VTable *IStreamVTable +} + +type IStreamVTable struct { + QueryInterface uintptr + AddRef uintptr + Release uintptr + Read uintptr + Write uintptr + Seek uintptr + SetSize uintptr + CopyTo uintptr + Commit uintptr + Revert uintptr + LockRegion uintptr + UnlockRegion uintptr + Stat uintptr + Clone uintptr +} + +func (v *IStream) Release() error { + if v == nil { + return nil + } + r, _, _ := syscall.SyscallN( + v.VTable.Release, + uintptr(unsafe.Pointer(v)), + ) + if r != S_OK { + return MFErr{Code: r} + } + return nil +} + +type IMFAttributes struct { + VTable *IMFAttributesVTable +} + +type IMFAttributesVTable struct { + QueryInterface uintptr + AddRef uintptr + Release uintptr + GetItem uintptr + GetItemType uintptr + CompareItem uintptr + Compare uintptr + GetUINT32 uintptr + GetUINT64 uintptr + GetDouble uintptr + GetGUID uintptr + GetStringLength uintptr + GetString uintptr + GetAllocatedString uintptr + GetBlobSize uintptr + GetBlob uintptr + GetAllocatedBlob uintptr + GetUnknown uintptr + SetItem uintptr + DeleteItem uintptr + DeleteAllItems uintptr + SetUINT32 uintptr + SetUINT64 uintptr + SetDouble uintptr + SetGUID uintptr + SetString uintptr + SetBlob uintptr + SetUnknown uintptr + LockStore uintptr + UnlockStore uintptr + GetCount uintptr + GetItemByIndex uintptr + CopyAllItems uintptr +} + +func (v *IMFAttributes) Release() error { + if v == nil { + return nil + } + r, _, _ := syscall.SyscallN( + v.VTable.Release, + uintptr(unsafe.Pointer(v)), + ) + if r != S_OK { + return MFErr{Code: r} + } + return nil +} + +func (v *IMFAttributes) SetUINT32(guid *GUID, unValue uint32) error { + r, _, _ := syscall.SyscallN( + v.VTable.SetUINT32, + uintptr(unsafe.Pointer(v)), + uintptr(unsafe.Pointer(guid)), + uintptr(unValue), + ) + if r != S_OK { + return MFErr{Code: r} + } + return nil +} + +type IMFMediaType struct { + VTable *IMFMediaTypeVTable +} + +type IMFMediaTypeVTable struct { + QueryInterface uintptr + AddRef uintptr + Release uintptr + GetItem uintptr + GetItemType uintptr + CompareItem uintptr + Compare uintptr + GetUINT32 uintptr + GetUINT64 uintptr + GetDouble uintptr + GetGUID uintptr + GetStringLength uintptr + GetString uintptr + GetAllocatedString uintptr + GetBlobSize uintptr + GetBlob uintptr + GetAllocatedBlob uintptr + GetUnknown uintptr + SetItem uintptr + DeleteItem uintptr + DeleteAllItems uintptr + SetUINT32 uintptr + SetUINT64 uintptr + SetDouble uintptr + SetGUID uintptr + SetString uintptr + SetBlob uintptr + SetUnknown uintptr + LockStore uintptr + UnlockStore uintptr + GetCount uintptr + GetItemByIndex uintptr + CopyAllItems uintptr + GetMajorType uintptr + IsCompressedFormat uintptr + IsEqual uintptr + GetRepresentation uintptr + FreeRepresentation uintptr +} + +func (v *IMFMediaType) Release() error { + if v == nil { + return nil + } + r, _, _ := syscall.SyscallN( + v.VTable.Release, + uintptr(unsafe.Pointer(v)), + ) + if r != S_OK { + return MFErr{Code: r} } - str := strings.TrimSpace(buf.String()) - if str == "" { - panic("error message is empty") + return nil +} + +func (v *IMFMediaType) SetGUID(guid *GUID, value *GUID) error { + r, _, _ := syscall.SyscallN( + v.VTable.SetGUID, + uintptr(unsafe.Pointer(v)), + uintptr(unsafe.Pointer(guid)), + uintptr(unsafe.Pointer(value)), + ) + if r != S_OK { + return MFErr{Code: r} } - return errors.New(str) + return nil +} + +func (v *IMFMediaType) GetUINT32(guid *GUID, punValue *uint32) error { + r, _, _ := syscall.SyscallN( + v.VTable.GetUINT32, + uintptr(unsafe.Pointer(v)), + uintptr(unsafe.Pointer(guid)), + uintptr(unsafe.Pointer(punValue)), + ) + if r != S_OK { + return MFErr{Code: r} + } + return nil +} + +type IMFSourceReader struct { + VTable *IMFSourceReaderVtbl +} + +type IMFSourceReaderVtbl struct { + QueryInterface uintptr + AddRef uintptr + Release uintptr + GetStreamSelection uintptr + SetStreamSelection uintptr + GetNativeMediaType uintptr + GetCurrentMediaType uintptr + SetCurrentMediaType uintptr + SetCurrentPosition uintptr + ReadSample uintptr + Flush uintptr + GetServiceForStream uintptr + GetPresentationAttribute uintptr +} + +func (v *IMFSourceReader) Release() error { + if v == nil { + return nil + } + r, _, _ := syscall.SyscallN( + v.VTable.Release, + uintptr(unsafe.Pointer(v)), + ) + if r != S_OK { + return MFErr{Code: r} + } + return nil +} + +func (v *IMFSourceReader) SetStreamSelection(index int64, selected bool) error { + r, _, _ := syscall.SyscallN( + v.VTable.SetStreamSelection, + uintptr(unsafe.Pointer(v)), + uintptr(index), + uintptr(boolToInt(selected)), + ) + if r != S_OK { + return MFErr{Code: r} + } + return nil +} + +func (v *IMFSourceReader) SetCurrentMediaType(index int64, mt *IMFMediaType) error { + r, _, _ := syscall.SyscallN( + v.VTable.SetCurrentMediaType, + uintptr(unsafe.Pointer(v)), + uintptr(index), + uintptr(0), + uintptr(unsafe.Pointer(mt)), + ) + if r != S_OK { + return MFErr{Code: r} + } + return nil +} + +func (v *IMFSourceReader) GetCurrentMediaType(index int64, mt **IMFMediaType) error { + r, _, _ := syscall.SyscallN( + v.VTable.GetCurrentMediaType, + uintptr(unsafe.Pointer(v)), + uintptr(index), + uintptr(unsafe.Pointer(mt)), + ) + if r != S_OK { + return MFErr{Code: r} + } + return nil +} + +func (v *IMFSourceReader) ReadSample(index, controlFlags int64, actualIndex *int64, streamFlags *int64, timestamp *int64, sample **IMFSample) error { + r, _, _ := syscall.SyscallN( + v.VTable.ReadSample, + uintptr(unsafe.Pointer(v)), + uintptr(index), + uintptr(controlFlags), + uintptr(unsafe.Pointer(actualIndex)), + uintptr(unsafe.Pointer(streamFlags)), + uintptr(unsafe.Pointer(timestamp)), + uintptr(unsafe.Pointer(sample)), + ) + if r != S_OK { + return MFErr{Code: r} + } + return nil +} + +type IMFSample struct { + VTable *IMFSampleVTable +} + +type IMFSampleVTable struct { + QueryInterface uintptr + AddRef uintptr + Release uintptr + GetItem uintptr + GetItemType uintptr + CompareItem uintptr + Compare uintptr + GetUINT32 uintptr + GetUINT64 uintptr + GetDouble uintptr + GetGUID uintptr + GetStringLength uintptr + GetString uintptr + GetAllocatedString uintptr + GetBlobSize uintptr + GetBlob uintptr + GetAllocatedBlob uintptr + GetUnknown uintptr + SetItem uintptr + DeleteItem uintptr + DeleteAllItems uintptr + SetUINT32 uintptr + SetUINT64 uintptr + SetDouble uintptr + SetGUID uintptr + SetString uintptr + SetBlob uintptr + SetUnknown uintptr + LockStore uintptr + UnlockStore uintptr + GetCount uintptr + GetItemByIndex uintptr + CopyAllItems uintptr + GetSampleFlags uintptr + SetSampleFlags uintptr + GetSampleTime uintptr + SetSampleTime uintptr + GetSampleDuration uintptr + SetSampleDuration uintptr + GetBufferCount uintptr + GetBufferByIndex uintptr + ConvertToContiguousBuffer uintptr + AddBuffer uintptr + RemoveBufferByIndex uintptr + RemoveAllBuffers uintptr + GetTotalLength uintptr + CopyToBuffer uintptr +} + +func (v *IMFSample) Release() error { + if v == nil { + return nil + } + r, _, _ := syscall.SyscallN( + v.VTable.Release, + uintptr(unsafe.Pointer(v)), + ) + if r != S_OK { + return MFErr{Code: r} + } + return nil +} + +func (v *IMFSample) ConvertToContiguousBuffer(b **IMFMediaBuffer) error { + r, _, _ := syscall.SyscallN( + v.VTable.ConvertToContiguousBuffer, + uintptr(unsafe.Pointer(v)), + uintptr(unsafe.Pointer(b)), + ) + if r != S_OK { + return MFErr{Code: r} + } + return nil } -// GoSlice takes a native array and returns a Go managed slice via a memory copy. -// The caller is responsible for freeing the native memory. -func GoSlice[T any](t *T, size int64) []T { - src := unsafe.Slice(t, size) - dst := make([]T, size) - copy(dst, src) - return dst +type IMFMediaBuffer struct { + VTable *IMFMediaBufferVTable +} + +type IMFMediaBufferVTable struct { + QueryInterface uintptr + AddRef uintptr + Release uintptr + Lock uintptr + Unlock uintptr + GetCurrentLength uintptr + SetCurrentLength uintptr + GetMaxLength uintptr +} + +func (v *IMFMediaBuffer) Release() error { + if v == nil { + return nil + } + r, _, _ := syscall.SyscallN( + v.VTable.Release, + uintptr(unsafe.Pointer(v)), + ) + if r != S_OK { + return MFErr{Code: r} + } + return nil +} + +func (v *IMFMediaBuffer) Lock(buf **byte, length, capacity *int64) error { + r, _, _ := syscall.SyscallN( + v.VTable.Lock, + uintptr(unsafe.Pointer(v)), + uintptr(unsafe.Pointer(buf)), + uintptr(unsafe.Pointer(length)), + uintptr(unsafe.Pointer(capacity)), + ) + if r != S_OK { + return MFErr{Code: r} + } + return nil +} + +func (v *IMFMediaBuffer) Unlock() error { + r, _, _ := syscall.SyscallN( + v.VTable.Unlock, + uintptr(unsafe.Pointer(v)), + ) + if r != S_OK { + return MFErr{Code: r} + } + return nil +} + +/* + The following defines exactly the functions required. +*/ + +var ( + _mfplat = windows.NewLazySystemDLL("Mfplat.dll") + _shlwapi = windows.NewLazySystemDLL("Shlwapi.dll") + _mfreadwrite = windows.NewLazySystemDLL("Mfreadwrite.dll") + + _SHCreateMemStream = _shlwapi.NewProc("SHCreateMemStream") + + _MFStartup = _mfplat.NewProc("MFStartup") + _MFShutdown = _mfplat.NewProc("MFShutdown") + _MFCreateMediaType = _mfplat.NewProc("MFCreateMediaType") + _MFCreateAttributes = _mfplat.NewProc("MFCreateAttributes") + _MFCreateMFByteStreamOnStream = _mfplat.NewProc("MFCreateMFByteStreamOnStream") + + _MFCreateSourceReaderFromByteStream = _mfreadwrite.NewProc("MFCreateSourceReaderFromByteStream") +) + +func MFStartup(version, flags uintptr) error { + r, _, _ := _MFStartup.Call(version, flags) + if r != S_OK { + return MFErr{Code: r} + } + return nil +} + +func MFShutdown() error { + r, _, _ := _MFStartup.Call() + if r != S_OK { + return MFErr{Code: r} + } + return nil +} + +func SHCreateMemStream(pInit *byte, cbInit int) *IStream { + r, _, _ := _SHCreateMemStream.Call( + uintptr(unsafe.Pointer(pInit)), + uintptr(uint32(cbInit)), + ) + if r == 0 { + return nil + } + return (*IStream)(unsafe.Pointer(r)) +} + +func MFCreateAttributes(out **IMFAttributes, size uint64) error { + r, _, _ := _MFCreateAttributes.Call( + uintptr(unsafe.Pointer(out)), + uintptr(size), + ) + if r != S_OK { + return MFErr{Code: r} + } + return nil +} + +func MFCreateMFByteStreamOnStream(s *IStream, out **IMFByteStream) error { + r, _, _ := _MFCreateMFByteStreamOnStream.Call( + uintptr(unsafe.Pointer(s)), + uintptr(unsafe.Pointer(out)), + ) + if r != S_OK { + return MFErr{Code: r} + } + return nil +} + +func MFCreateSourceReaderFromByteStream(bs *IMFByteStream, attributes *IMFAttributes, out **IMFSourceReader) error { + r, _, _ := _MFCreateSourceReaderFromByteStream.Call( + uintptr(unsafe.Pointer(bs)), + uintptr(unsafe.Pointer(attributes)), + uintptr(unsafe.Pointer(out)), + ) + if r != S_OK { + return MFErr{Code: r} + } + return nil +} + +func MFCreateMediaType(out **IMFMediaType) error { + r, _, _ := _MFCreateMediaType.Call( + uintptr(unsafe.Pointer(out)), + ) + if r != S_OK { + return MFErr{Code: r} + } + return nil +} + +func boolToInt(b bool) int { + const False = 0 + const True = 1 + if b { + return True + } + return False } // MFErr is a Media Foundation error that can render a formatted message. type MFErr struct { - Code C.HRESULT + Code HRESULT } -func (err MFErr) Error() string { - outBuf := new(C.ushort) - - size := C.FormatMessageW( - C.FORMAT_MESSAGE_ALLOCATE_BUFFER|C.FORMAT_MESSAGE_FROM_SYSTEM, - nil, - C.ulong(err.Code), - 0, - outBuf, +func (e MFErr) Error() string { + out := make([]uint16, 300) + size, err := windows.FormatMessage( + windows.FORMAT_MESSAGE_FROM_SYSTEM|windows.FORMAT_MESSAGE_FROM_HMODULE|windows.FORMAT_MESSAGE_ARGUMENT_ARRAY, + _mfplat.Handle(), + uint32(e.Code), 0, + out, nil, ) - if outBuf == nil || size == 0 { - return fmt.Sprintf("<cannot render string for HRESULT=%x>", err.Code) + if err != nil { + return fmt.Sprintf("code %x (<format message: %v>)", e.Code, err.Error()) } - - defer C.LocalFree((C.HANDLE)(unsafe.Pointer(outBuf))) - - return windows.UTF16ToString(GoSlice((*uint16)(unsafe.Pointer(outBuf)), int64(size))) + // trim terminating \r and \n + for ; size > 0 && (out[size-1] == '\n' || out[size-1] == '\r'); size-- { + } + return fmt.Sprintf("%s (code %x)", string(utf16.Decode(out[:size])), e.Code) } diff --git a/audio_windows.h b/audio_windows.h @@ -1,128 +0,0 @@ -#include <crtdbg.h> -#include <stdlib.h> -#include <stdio.h> -#include <windows.h> -#include <winbase.h> -#include <combaseapi.h> -#include <mfapi.h> -#include <mfidl.h> -#include <mferror.h> -#include <initguid.h> -#include <wmcodecdsp.h> -#include <mmdeviceapi.h> -#include <mfreadwrite.h> -#include <shlwapi.h> -#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. -// -// Errors are dynamically allocated and are designed to provide immediate -// feedback with a user facing description. -// -// Use the wrapped error code to get the raw code for manual lookup. -typedef struct Error -{ - int Code; // Underlying error code from traditional C calls. - struct Error* Err; // Wrapped error, if any. - char* Str; // String description of error. -} Error; - -// Result captures a generic value return along side a possible error. -// Check error before accessing value. Caller must know what type the -// value can be. -typedef struct Result -{ - void* Value; - Error* Err; -} Result; - -// Format describes uncompressed PCM necessary for correct playback. -typedef struct Format -{ - uint32_t SampleRate; - uint32_t Channels; - uint32_t BitDepth; -} Format; - -// Buffer describes a dynamic byte buffer with a length, capacity and -// a pointer to the first element. -typedef struct Buffer -{ - int Len; // Len is the currently used region of the buffer. - int Cap; // Capacity is the total allocated memory. - BYTE* Data; // Data is the pointer to the first byte. -} 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 -{ - Buffer* Uncompressed; - Format Format; - Error* Err; -} DecodeResult; - -#define BUFFER_DEFAULT_SIZE 1024*1024 - -// BufferNew allocates a buffer object that can read and write data. -Buffer* -BufferNew(); - -// BufferWrites the data to the buffer. -void -BufferWrite(Buffer* buf, int size, BYTE* data); - -// BufferFree deallocates the memory for a buffer, including the pointer -// to it and it's pointer to the raw data. -void -BufferFree(Buffer*); - -// ErrorFree deallocates the memory for an error and all wrapped errors. -void ErrorFree(Error*); - -// Load the decoded PCM data from the given file. -// -// Load is implemented over the top of Windows Media Foundation and -// what a wild ride that is. -// -// https://docs.microsoft.com/en-us/windows/win32/medfound/about-the-media-foundation-sdk -DecodeResult Load(char* path); - -// Play an audio file at the given file path. -// -// Play is implemented over the top of Windows Media Foundation and -// what a wild ride that is. -// -// https://docs.microsoft.com/en-us/windows/win32/medfound/about-the-media-foundation-sdk -Error* Play(char *path); - -// Decode a buffer of compressed audio using Windows Media Foundation. -DecodeResult Decode(BYTE* compressed, UINT size); - -// Stub to compile against mingw64 which apparently does not include -// this function in it's header file. -HRESULT MFCreateMFByteStreamOnStream( - IStream *pStream, - IMFByteStream **ppByteStream -); - -// 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(); diff --git a/go.mod b/go.mod @@ -2,4 +2,9 @@ module git.sr.ht/~jackmordaunt/nativeaudio go 1.21 -require golang.org/x/sys v0.15.0 +require ( + github.com/ebitengine/oto/v3 v3.1.0 + golang.org/x/sys v0.15.0 +) + +require github.com/ebitengine/purego v0.5.0 // indirect diff --git a/go.sum b/go.sum @@ -0,0 +1,6 @@ +github.com/ebitengine/oto/v3 v3.1.0 h1:9tChG6rizyeR2w3vsygTTTVVJ9QMMyu00m2yBOCch6U= +github.com/ebitengine/oto/v3 v3.1.0/go.mod h1:IK1QTnlfZK2GIB6ziyECm433hAdTaPpOsGMLhEyEGTg= +github.com/ebitengine/purego v0.5.0 h1:JrMGKfRIAM4/QVKaesIIT7m/UVjTj5GYhRSQYwfVdpo= +github.com/ebitengine/purego v0.5.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=