From 448425fcc757c359e158f5b9c460edda1486424c Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 02:50:33 +0100 Subject: [PATCH 01/32] Add emscripten_sleep implementation of Pa_Sleep --- src/os/unix/pa_unix_util.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/os/unix/pa_unix_util.c b/src/os/unix/pa_unix_util.c index b5898af41..dca50c5af 100644 --- a/src/os/unix/pa_unix_util.c +++ b/src/os/unix/pa_unix_util.c @@ -51,6 +51,10 @@ #include #include +#ifdef __EMSCRIPTEN__ +#include +#endif + #if defined(__APPLE__) && !defined(HAVE_MACH_ABSOLUTE_TIME) #define HAVE_MACH_ABSOLUTE_TIME #endif @@ -111,7 +115,9 @@ int PaUtil_CountCurrentlyAllocatedBlocks( void ) void Pa_Sleep( long msec ) { -#ifdef HAVE_NANOSLEEP +#ifdef __EMSCRIPTEN__ + emscripten_sleep(msec); +#elif defined(HAVE_NANOSLEEP) struct timespec req = {0}, rem = {0}; PaTime time = msec / 1.e3; req.tv_sec = (time_t)time; From 01f8f8d5ac0614a18bed683950256f2a939383c3 Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 02:57:51 +0100 Subject: [PATCH 02/32] Stub out Web Audio hostapi based on skeleton --- CMakeLists.txt | 18 + src/hostapi/webaudio/README.txt | 1 + src/hostapi/webaudio/pa_webaudio.c | 806 +++++++++++++++++++++++++++++ src/os/unix/pa_unix_hostapis.c | 5 + 4 files changed, 830 insertions(+) create mode 100644 src/hostapi/webaudio/README.txt create mode 100644 src/hostapi/webaudio/pa_webaudio.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 77e5388b5..aef8c1a68 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -399,6 +399,24 @@ elseif(UNIX) set(PKGCONFIG_CFLAGS "${PKGCONFIG_CFLAGS} -DPA_USE_SNDIO=1") set(PKGCONFIG_REQUIRES_PRIVATE "${PKGCONFIG_REQUIRES_PRIVATE} sndio") endif() + + if(EMSCRIPTEN) + option(PA_USE_WEBAUDIO "Enable support for Web Audio" ON) + + if(PA_USE_WEBAUDIO) + target_sources(PortAudio PRIVATE + src/hostapi/webaudio/pa_webaudio.c + ) + target_compile_definitions(PortAudio PUBLIC PA_USE_WEBAUDIO=1) + target_link_options(PortAudio PUBLIC -sASYNCIFY) + set(PKGCONFIG_CFLAGS "${PKGCONFIG_CFLAGS} -DPA_USE_WEBAUDIO=1") + endif() + + # This makes it easy to run the examples. The default output format is .js + # and is set in the Emscripten toolchain file, so it cannot be overriden + # from the command-line with -DCMAKE_EXECUTABLE_SUFFIX. + set(CMAKE_EXECUTABLE_SUFFIX ".html") + endif() endif() endif() diff --git a/src/hostapi/webaudio/README.txt b/src/hostapi/webaudio/README.txt new file mode 100644 index 000000000..39d4c8d2b --- /dev/null +++ b/src/hostapi/webaudio/README.txt @@ -0,0 +1 @@ +pa_hostapi_skeleton.c provides a starting point for implementing support for a new host API with PortAudio. The idea is that you copy it to a new directory inside /hostapi and start editing. \ No newline at end of file diff --git a/src/hostapi/webaudio/pa_webaudio.c b/src/hostapi/webaudio/pa_webaudio.c new file mode 100644 index 000000000..85af3322c --- /dev/null +++ b/src/hostapi/webaudio/pa_webaudio.c @@ -0,0 +1,806 @@ +/* + * Portable Audio I/O Library Web Audio implementation + * Copyright (c) 2024 fwcd + * + * Based on the Open Source API proposed by Ross Bencina + * Copyright (c) 1999-2002 Ross Bencina, Phil Burk + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* + * The text above constitutes the entire PortAudio license; however, + * the PortAudio community also makes the following non-binding requests: + * + * Any person wishing to distribute modifications to the Software is + * requested to send the modifications to the original developer so that + * they can be incorporated into the canonical version. It is also + * requested that these non-binding requests be included along with the + * license above. + */ + +/** @file + @ingroup common_src + + @brief Web Audio implementation of support for a host API. +*/ + + +#include /* strlen() */ + +#include "pa_util.h" +#include "pa_allocation.h" +#include "pa_hostapi.h" +#include "pa_stream.h" +#include "pa_cpuload.h" +#include "pa_process.h" + + +/* prototypes for functions declared in this file */ + +#ifdef __cplusplus +extern "C" +{ +#endif /* __cplusplus */ + +PaError PaWebAudio_Initialize( PaUtilHostApiRepresentation **hostApi, PaHostApiIndex index ); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + + +static void Terminate( struct PaUtilHostApiRepresentation *hostApi ); +static PaError IsFormatSupported( struct PaUtilHostApiRepresentation *hostApi, + const PaStreamParameters *inputParameters, + const PaStreamParameters *outputParameters, + double sampleRate ); +static PaError OpenStream( struct PaUtilHostApiRepresentation *hostApi, + PaStream** s, + const PaStreamParameters *inputParameters, + const PaStreamParameters *outputParameters, + double sampleRate, + unsigned long framesPerBuffer, + PaStreamFlags streamFlags, + PaStreamCallback *streamCallback, + void *userData ); +static PaError CloseStream( PaStream* stream ); +static PaError StartStream( PaStream *stream ); +static PaError StopStream( PaStream *stream ); +static PaError AbortStream( PaStream *stream ); +static PaError IsStreamStopped( PaStream *s ); +static PaError IsStreamActive( PaStream *stream ); +static PaTime GetStreamTime( PaStream *stream ); +static double GetStreamCpuLoad( PaStream* stream ); +static PaError ReadStream( PaStream* stream, void *buffer, unsigned long frames ); +static PaError WriteStream( PaStream* stream, const void *buffer, unsigned long frames ); +static signed long GetStreamReadAvailable( PaStream* stream ); +static signed long GetStreamWriteAvailable( PaStream* stream ); + + +/* IMPLEMENT ME: a macro like the following one should be used for reporting + host errors */ +#define PA_WEBAUDIO_SET_LAST_HOST_ERROR( errorCode, errorText ) \ + PaUtil_SetLastHostErrorInfo( paInDevelopment, errorCode, errorText ) + +/* PaWebAudioHostApiRepresentation - host api datastructure specific to this implementation */ + +typedef struct +{ + PaUtilHostApiRepresentation inheritedHostApiRep; + PaUtilStreamInterface callbackStreamInterface; + PaUtilStreamInterface blockingStreamInterface; + + PaUtilAllocationGroup *allocations; + + /* implementation specific data goes here */ +} +PaWebAudioHostApiRepresentation; + + +PaError PaWebAudio_Initialize( PaUtilHostApiRepresentation **hostApi, PaHostApiIndex hostApiIndex ) +{ + PaError result = paNoError; + int i, deviceCount; + PaWebAudioHostApiRepresentation *webAudioHostApi; + PaDeviceInfo *deviceInfoArray; + + webAudioHostApi = (PaWebAudioHostApiRepresentation*)PaUtil_AllocateZeroInitializedMemory( sizeof(PaWebAudioHostApiRepresentation) ); + if( !webAudioHostApi ) + { + result = paInsufficientMemory; + goto error; + } + + webAudioHostApi->allocations = PaUtil_CreateAllocationGroup(); + if( !webAudioHostApi->allocations ) + { + result = paInsufficientMemory; + goto error; + } + + *hostApi = &webAudioHostApi->inheritedHostApiRep; + (*hostApi)->info.structVersion = 1; + (*hostApi)->info.type = paInDevelopment; /* IMPLEMENT ME: change to correct type id */ + (*hostApi)->info.name = "Web Audio"; + + (*hostApi)->info.defaultInputDevice = paNoDevice; /* IMPLEMENT ME */ + (*hostApi)->info.defaultOutputDevice = paNoDevice; /* IMPLEMENT ME */ + + (*hostApi)->info.deviceCount = 0; + + deviceCount = 0; /* IMPLEMENT ME */ + + if( deviceCount > 0 ) + { + (*hostApi)->deviceInfos = (PaDeviceInfo**)PaUtil_GroupAllocateZeroInitializedMemory( + webAudioHostApi->allocations, sizeof(PaDeviceInfo*) * deviceCount ); + if( !(*hostApi)->deviceInfos ) + { + result = paInsufficientMemory; + goto error; + } + + /* allocate all device info structs in a contiguous block */ + deviceInfoArray = (PaDeviceInfo*)PaUtil_GroupAllocateZeroInitializedMemory( + webAudioHostApi->allocations, sizeof(PaDeviceInfo) * deviceCount ); + if( !deviceInfoArray ) + { + result = paInsufficientMemory; + goto error; + } + + for( i=0; i < deviceCount; ++i ) + { + PaDeviceInfo *deviceInfo = &deviceInfoArray[i]; + deviceInfo->structVersion = 2; + deviceInfo->hostApi = hostApiIndex; + deviceInfo->name = 0; /* IMPLEMENT ME: allocate block and copy name eg: + deviceName = (char*)PaUtil_GroupAllocateZeroInitializedMemory( webAudioHostApi->allocations, strlen(srcName) + 1 ); + if( !deviceName ) + { + result = paInsufficientMemory; + goto error; + } + strcpy( deviceName, srcName ); + deviceInfo->name = deviceName; + */ + + deviceInfo->maxInputChannels = 0; /* IMPLEMENT ME */ + deviceInfo->maxOutputChannels = 0; /* IMPLEMENT ME */ + + deviceInfo->defaultLowInputLatency = 0.; /* IMPLEMENT ME */ + deviceInfo->defaultLowOutputLatency = 0.; /* IMPLEMENT ME */ + deviceInfo->defaultHighInputLatency = 0.; /* IMPLEMENT ME */ + deviceInfo->defaultHighOutputLatency = 0.; /* IMPLEMENT ME */ + + deviceInfo->defaultSampleRate = 0.; /* IMPLEMENT ME */ + + (*hostApi)->deviceInfos[i] = deviceInfo; + ++(*hostApi)->info.deviceCount; + } + } + + (*hostApi)->Terminate = Terminate; + (*hostApi)->OpenStream = OpenStream; + (*hostApi)->IsFormatSupported = IsFormatSupported; + + PaUtil_InitializeStreamInterface( &webAudioHostApi->callbackStreamInterface, CloseStream, StartStream, + StopStream, AbortStream, IsStreamStopped, IsStreamActive, + GetStreamTime, GetStreamCpuLoad, + PaUtil_DummyRead, PaUtil_DummyWrite, + PaUtil_DummyGetReadAvailable, PaUtil_DummyGetWriteAvailable ); + + PaUtil_InitializeStreamInterface( &webAudioHostApi->blockingStreamInterface, CloseStream, StartStream, + StopStream, AbortStream, IsStreamStopped, IsStreamActive, + GetStreamTime, PaUtil_DummyGetCpuLoad, + ReadStream, WriteStream, GetStreamReadAvailable, GetStreamWriteAvailable ); + + return result; + +error: + if( webAudioHostApi ) + { + if( webAudioHostApi->allocations ) + { + PaUtil_FreeAllAllocations( webAudioHostApi->allocations ); + PaUtil_DestroyAllocationGroup( webAudioHostApi->allocations ); + } + + PaUtil_FreeMemory( webAudioHostApi ); + } + return result; +} + + +static void Terminate( struct PaUtilHostApiRepresentation *hostApi ) +{ + PaWebAudioHostApiRepresentation *webAudioHostApi = (PaWebAudioHostApiRepresentation*)hostApi; + + /* + IMPLEMENT ME: + - clean up any resources not handled by the allocation group + */ + + if( webAudioHostApi->allocations ) + { + PaUtil_FreeAllAllocations( webAudioHostApi->allocations ); + PaUtil_DestroyAllocationGroup( webAudioHostApi->allocations ); + } + + PaUtil_FreeMemory( webAudioHostApi ); +} + + +static PaError IsFormatSupported( struct PaUtilHostApiRepresentation *hostApi, + const PaStreamParameters *inputParameters, + const PaStreamParameters *outputParameters, + double sampleRate ) +{ + int inputChannelCount, outputChannelCount; + PaSampleFormat inputSampleFormat, outputSampleFormat; + + if( inputParameters ) + { + inputChannelCount = inputParameters->channelCount; + inputSampleFormat = inputParameters->sampleFormat; + + /* all standard sample formats are supported by the buffer adapter, + this implementation doesn't support any custom sample formats */ + if( inputSampleFormat & paCustomFormat ) + return paSampleFormatNotSupported; + + /* unless alternate device specification is supported, reject the use of + paUseHostApiSpecificDeviceSpecification */ + + if( inputParameters->device == paUseHostApiSpecificDeviceSpecification ) + return paInvalidDevice; + + /* check that input device can support inputChannelCount */ + if( inputChannelCount > hostApi->deviceInfos[ inputParameters->device ]->maxInputChannels ) + return paInvalidChannelCount; + + /* validate inputStreamInfo */ + if( inputParameters->hostApiSpecificStreamInfo ) + return paIncompatibleHostApiSpecificStreamInfo; /* this implementation doesn't use custom stream info */ + } + else + { + inputChannelCount = 0; + } + + if( outputParameters ) + { + outputChannelCount = outputParameters->channelCount; + outputSampleFormat = outputParameters->sampleFormat; + + /* all standard sample formats are supported by the buffer adapter, + this implementation doesn't support any custom sample formats */ + if( outputSampleFormat & paCustomFormat ) + return paSampleFormatNotSupported; + + /* unless alternate device specification is supported, reject the use of + paUseHostApiSpecificDeviceSpecification */ + + if( outputParameters->device == paUseHostApiSpecificDeviceSpecification ) + return paInvalidDevice; + + /* check that output device can support outputChannelCount */ + if( outputChannelCount > hostApi->deviceInfos[ outputParameters->device ]->maxOutputChannels ) + return paInvalidChannelCount; + + /* validate outputStreamInfo */ + if( outputParameters->hostApiSpecificStreamInfo ) + return paIncompatibleHostApiSpecificStreamInfo; /* this implementation doesn't use custom stream info */ + } + else + { + outputChannelCount = 0; + } + + /* + IMPLEMENT ME: + + - if a full duplex stream is requested, check that the combination + of input and output parameters is supported if necessary + + - check that the device supports sampleRate + + Because the buffer adapter handles conversion between all standard + sample formats, the following checks are only required if paCustomFormat + is implemented, or under some other unusual conditions. + + - check that input device can support inputSampleFormat, or that + we have the capability to convert from inputSampleFormat to + a native format + + - check that output device can support outputSampleFormat, or that + we have the capability to convert from outputSampleFormat to + a native format + */ + + + /* suppress unused variable warnings */ + (void) sampleRate; + + return paFormatIsSupported; +} + +/* PaWebAudioStream - a stream data structure specifically for this implementation */ + +typedef struct PaWebAudioStream +{ + PaUtilStreamRepresentation streamRepresentation; + PaUtilCpuLoadMeasurer cpuLoadMeasurer; + PaUtilBufferProcessor bufferProcessor; + + /* IMPLEMENT ME: + - implementation specific data goes here + */ + unsigned long framesPerHostCallback; /* just an example */ +} +PaWebAudioStream; + +/* see pa_hostapi.h for a list of validity guarantees made about OpenStream parameters */ + +static PaError OpenStream( struct PaUtilHostApiRepresentation *hostApi, + PaStream** s, + const PaStreamParameters *inputParameters, + const PaStreamParameters *outputParameters, + double sampleRate, + unsigned long framesPerBuffer, + PaStreamFlags streamFlags, + PaStreamCallback *streamCallback, + void *userData ) +{ + PaError result = paNoError; + PaWebAudioHostApiRepresentation *webAudioHostApi = (PaWebAudioHostApiRepresentation*)hostApi; + PaWebAudioStream *stream = 0; + unsigned long framesPerHostBuffer = framesPerBuffer; /* these may not be equivalent for all implementations */ + int inputChannelCount, outputChannelCount; + PaSampleFormat inputSampleFormat, outputSampleFormat; + PaSampleFormat hostInputSampleFormat, hostOutputSampleFormat; + + + if( inputParameters ) + { + inputChannelCount = inputParameters->channelCount; + inputSampleFormat = inputParameters->sampleFormat; + + /* unless alternate device specification is supported, reject the use of + paUseHostApiSpecificDeviceSpecification */ + + if( inputParameters->device == paUseHostApiSpecificDeviceSpecification ) + return paInvalidDevice; + + /* check that input device can support inputChannelCount */ + if( inputChannelCount > hostApi->deviceInfos[ inputParameters->device ]->maxInputChannels ) + return paInvalidChannelCount; + + /* validate inputStreamInfo */ + if( inputParameters->hostApiSpecificStreamInfo ) + return paIncompatibleHostApiSpecificStreamInfo; /* this implementation doesn't use custom stream info */ + + /* IMPLEMENT ME - establish which host formats are available */ + hostInputSampleFormat = + PaUtil_SelectClosestAvailableFormat( paInt16 /* native formats */, inputSampleFormat ); + } + else + { + inputChannelCount = 0; + inputSampleFormat = hostInputSampleFormat = paInt16; /* Suppress 'uninitialised var' warnings. */ + } + + if( outputParameters ) + { + outputChannelCount = outputParameters->channelCount; + outputSampleFormat = outputParameters->sampleFormat; + + /* unless alternate device specification is supported, reject the use of + paUseHostApiSpecificDeviceSpecification */ + + if( outputParameters->device == paUseHostApiSpecificDeviceSpecification ) + return paInvalidDevice; + + /* check that output device can support inputChannelCount */ + if( outputChannelCount > hostApi->deviceInfos[ outputParameters->device ]->maxOutputChannels ) + return paInvalidChannelCount; + + /* validate outputStreamInfo */ + if( outputParameters->hostApiSpecificStreamInfo ) + return paIncompatibleHostApiSpecificStreamInfo; /* this implementation doesn't use custom stream info */ + + /* IMPLEMENT ME - establish which host formats are available */ + hostOutputSampleFormat = + PaUtil_SelectClosestAvailableFormat( paInt16 /* native formats */, outputSampleFormat ); + } + else + { + outputChannelCount = 0; + outputSampleFormat = hostOutputSampleFormat = paInt16; /* Suppress 'uninitialized var' warnings. */ + } + + /* + IMPLEMENT ME: + + ( the following two checks are taken care of by PaUtil_InitializeBufferProcessor() FIXME - checks needed? ) + + - check that input device can support inputSampleFormat, or that + we have the capability to convert from outputSampleFormat to + a native format + + - check that output device can support outputSampleFormat, or that + we have the capability to convert from outputSampleFormat to + a native format + + - if a full duplex stream is requested, check that the combination + of input and output parameters is supported + + - check that the device supports sampleRate + + - alter sampleRate to a close allowable rate if possible / necessary + + - validate suggestedInputLatency and suggestedOutputLatency parameters, + use default values where necessary + */ + + + + + /* validate platform specific flags */ + if( (streamFlags & paPlatformSpecificFlags) != 0 ) + return paInvalidFlag; /* unexpected platform specific flag */ + + + stream = (PaWebAudioStream*)PaUtil_AllocateZeroInitializedMemory( sizeof(PaWebAudioStream) ); + if( !stream ) + { + result = paInsufficientMemory; + goto error; + } + + if( streamCallback ) + { + PaUtil_InitializeStreamRepresentation( &stream->streamRepresentation, + &webAudioHostApi->callbackStreamInterface, streamCallback, userData ); + } + else + { + PaUtil_InitializeStreamRepresentation( &stream->streamRepresentation, + &webAudioHostApi->blockingStreamInterface, streamCallback, userData ); + } + + PaUtil_InitializeCpuLoadMeasurer( &stream->cpuLoadMeasurer, sampleRate ); + + + /* we assume a fixed host buffer size in this example, but the buffer processor + can also support bounded and unknown host buffer sizes by passing + paUtilBoundedHostBufferSize or paUtilUnknownHostBufferSize instead of + paUtilFixedHostBufferSize below. */ + + result = PaUtil_InitializeBufferProcessor( &stream->bufferProcessor, + inputChannelCount, inputSampleFormat, hostInputSampleFormat, + outputChannelCount, outputSampleFormat, hostOutputSampleFormat, + sampleRate, streamFlags, framesPerBuffer, + framesPerHostBuffer, paUtilFixedHostBufferSize, + streamCallback, userData ); + if( result != paNoError ) + goto error; + + + /* + IMPLEMENT ME: initialise the following fields with estimated or actual + values. + */ + stream->streamRepresentation.streamInfo.inputLatency = + (PaTime)PaUtil_GetBufferProcessorInputLatencyFrames(&stream->bufferProcessor) / sampleRate; /* inputLatency is specified in _seconds_ */ + stream->streamRepresentation.streamInfo.outputLatency = + (PaTime)PaUtil_GetBufferProcessorOutputLatencyFrames(&stream->bufferProcessor) / sampleRate; /* outputLatency is specified in _seconds_ */ + stream->streamRepresentation.streamInfo.sampleRate = sampleRate; + + + /* + IMPLEMENT ME: + - additional stream setup + opening + */ + + stream->framesPerHostCallback = framesPerHostBuffer; + + *s = (PaStream*)stream; + + return result; + +error: + if( stream ) + PaUtil_FreeMemory( stream ); + + return result; +} + +/* + ExampleHostProcessingLoop() illustrates the kind of processing which may + occur in a host implementation. + +*/ +static void ExampleHostProcessingLoop( void *inputBuffer, void *outputBuffer, void *userData ) +{ + PaWebAudioStream *stream = (PaWebAudioStream*)userData; + PaStreamCallbackTimeInfo timeInfo = {0,0,0}; /* IMPLEMENT ME */ + int callbackResult; + unsigned long framesProcessed; + + PaUtil_BeginCpuLoadMeasurement( &stream->cpuLoadMeasurer ); + + /* + IMPLEMENT ME: + - generate timing information + - handle buffer slips + */ + + /* + If you need to byte swap or shift inputBuffer to convert it into a + portaudio format, do it here. + */ + + + + PaUtil_BeginBufferProcessing( &stream->bufferProcessor, &timeInfo, 0 /* IMPLEMENT ME: pass underflow/overflow flags when necessary */ ); + + /* + depending on whether the host buffers are interleaved, non-interleaved + or a mixture, you will want to call PaUtil_SetInterleaved*Channels(), + PaUtil_SetNonInterleaved*Channel() or PaUtil_Set*Channel() here. + */ + + PaUtil_SetInputFrameCount( &stream->bufferProcessor, 0 /* default to host buffer size */ ); + PaUtil_SetInterleavedInputChannels( &stream->bufferProcessor, + 0, /* first channel of inputBuffer is channel 0 */ + inputBuffer, + 0 ); /* 0 - use inputChannelCount passed to init buffer processor */ + + PaUtil_SetOutputFrameCount( &stream->bufferProcessor, 0 /* default to host buffer size */ ); + PaUtil_SetInterleavedOutputChannels( &stream->bufferProcessor, + 0, /* first channel of outputBuffer is channel 0 */ + outputBuffer, + 0 ); /* 0 - use outputChannelCount passed to init buffer processor */ + + /* you must pass a valid value of callback result to PaUtil_EndBufferProcessing() + in general you would pass paContinue for normal operation, and + paComplete to drain the buffer processor's internal output buffer. + You can check whether the buffer processor's output buffer is empty + using PaUtil_IsBufferProcessorOuputEmpty( bufferProcessor ) + */ + callbackResult = paContinue; + framesProcessed = PaUtil_EndBufferProcessing( &stream->bufferProcessor, &callbackResult ); + + + /* + If you need to byte swap or shift outputBuffer to convert it to + host format, do it here. + */ + + PaUtil_EndCpuLoadMeasurement( &stream->cpuLoadMeasurer, framesProcessed ); + + + if( callbackResult == paContinue ) + { + /* nothing special to do */ + } + else if( callbackResult == paAbort ) + { + /* IMPLEMENT ME - finish playback immediately */ + + /* once finished, call the finished callback */ + if( stream->streamRepresentation.streamFinishedCallback != 0 ) + stream->streamRepresentation.streamFinishedCallback( stream->streamRepresentation.userData ); + } + else + { + /* User callback has asked us to stop with paComplete or other non-zero value */ + + /* IMPLEMENT ME - finish playback once currently queued audio has completed */ + + /* once finished, call the finished callback */ + if( stream->streamRepresentation.streamFinishedCallback != 0 ) + stream->streamRepresentation.streamFinishedCallback( stream->streamRepresentation.userData ); + } +} + + +/* + When CloseStream() is called, the multi-api layer ensures that + the stream has already been stopped or aborted. +*/ +static PaError CloseStream( PaStream* s ) +{ + PaError result = paNoError; + PaWebAudioStream *stream = (PaWebAudioStream*)s; + + /* + IMPLEMENT ME: + - additional stream closing + cleanup + */ + + PaUtil_TerminateBufferProcessor( &stream->bufferProcessor ); + PaUtil_TerminateStreamRepresentation( &stream->streamRepresentation ); + PaUtil_FreeMemory( stream ); + + return result; +} + + +static PaError StartStream( PaStream *s ) +{ + PaError result = paNoError; + PaWebAudioStream *stream = (PaWebAudioStream*)s; + + PaUtil_ResetBufferProcessor( &stream->bufferProcessor ); + + /* IMPLEMENT ME, see portaudio.h for required behavior */ + + /* suppress unused function warning. the code in ExampleHostProcessingLoop or + something similar should be implemented to feed samples to and from the + host after StartStream() is called. + */ + (void) ExampleHostProcessingLoop; + + return result; +} + + +static PaError StopStream( PaStream *s ) +{ + PaError result = paNoError; + PaWebAudioStream *stream = (PaWebAudioStream*)s; + + /* suppress unused variable warnings */ + (void) stream; + + /* IMPLEMENT ME, see portaudio.h for required behavior */ + + return result; +} + + +static PaError AbortStream( PaStream *s ) +{ + PaError result = paNoError; + PaWebAudioStream *stream = (PaWebAudioStream*)s; + + /* suppress unused variable warnings */ + (void) stream; + + /* IMPLEMENT ME, see portaudio.h for required behavior */ + + return result; +} + + +static PaError IsStreamStopped( PaStream *s ) +{ + PaWebAudioStream *stream = (PaWebAudioStream*)s; + + /* suppress unused variable warnings */ + (void) stream; + + /* IMPLEMENT ME, see portaudio.h for required behavior */ + + return 0; +} + + +static PaError IsStreamActive( PaStream *s ) +{ + PaWebAudioStream *stream = (PaWebAudioStream*)s; + + /* suppress unused variable warnings */ + (void) stream; + + /* IMPLEMENT ME, see portaudio.h for required behavior */ + + return 0; +} + + +static PaTime GetStreamTime( PaStream *s ) +{ + PaWebAudioStream *stream = (PaWebAudioStream*)s; + + /* suppress unused variable warnings */ + (void) stream; + + /* IMPLEMENT ME, see portaudio.h for required behavior*/ + + return 0; +} + + +static double GetStreamCpuLoad( PaStream* s ) +{ + PaWebAudioStream *stream = (PaWebAudioStream*)s; + + return PaUtil_GetCpuLoad( &stream->cpuLoadMeasurer ); +} + + +/* + As separate stream interfaces are used for blocking and callback + streams, the following functions can be guaranteed to only be called + for blocking streams. +*/ + +static PaError ReadStream( PaStream* s, + void *buffer, + unsigned long frames ) +{ + PaWebAudioStream *stream = (PaWebAudioStream*)s; + + /* suppress unused variable warnings */ + (void) buffer; + (void) frames; + (void) stream; + + /* IMPLEMENT ME, see portaudio.h for required behavior*/ + + return paNoError; +} + + +static PaError WriteStream( PaStream* s, + const void *buffer, + unsigned long frames ) +{ + PaWebAudioStream *stream = (PaWebAudioStream*)s; + + /* suppress unused variable warnings */ + (void) buffer; + (void) frames; + (void) stream; + + /* IMPLEMENT ME, see portaudio.h for required behavior*/ + + return paNoError; +} + + +static signed long GetStreamReadAvailable( PaStream* s ) +{ + PaWebAudioStream *stream = (PaWebAudioStream*)s; + + /* suppress unused variable warnings */ + (void) stream; + + /* IMPLEMENT ME, see portaudio.h for required behavior*/ + + return 0; +} + + +static signed long GetStreamWriteAvailable( PaStream* s ) +{ + PaWebAudioStream *stream = (PaWebAudioStream*)s; + + /* suppress unused variable warnings */ + (void) stream; + + /* IMPLEMENT ME, see portaudio.h for required behavior*/ + + return 0; +} diff --git a/src/os/unix/pa_unix_hostapis.c b/src/os/unix/pa_unix_hostapis.c index 1fc022f30..fc90aa873 100644 --- a/src/os/unix/pa_unix_hostapis.c +++ b/src/os/unix/pa_unix_hostapis.c @@ -53,6 +53,7 @@ PaError PaSGI_Initialize( PaUtilHostApiRepresentation **hostApi, PaHostApiIndex /* Linux AudioScience HPI */ PaError PaAsiHpi_Initialize( PaUtilHostApiRepresentation **hostApi, PaHostApiIndex index ); PaError PaMacCore_Initialize( PaUtilHostApiRepresentation **hostApi, PaHostApiIndex index ); +PaError PaWebAudio_Initialize( PaUtilHostApiRepresentation **hostApi, PaHostApiIndex index ); PaError PaSkeleton_Initialize( PaUtilHostApiRepresentation **hostApi, PaHostApiIndex index ); /** Note that on Linux, ALSA is placed before OSS so that the former is preferred over the latter. @@ -114,6 +115,10 @@ PaUtilHostApiInitializer *paHostApiInitializers[] = PaPulseAudio_Initialize, #endif +#if PA_USE_WEBAUDIO + PaWebAudio_Initialize, +#endif + #if PA_USE_SKELETON PaSkeleton_Initialize, #endif From a8aee40df3d268ebbbfaff018d958807867968e0 Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 03:16:48 +0100 Subject: [PATCH 03/32] Create an audio context and enable audio worklets --- CMakeLists.txt | 2 +- src/hostapi/webaudio/pa_webaudio.c | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index aef8c1a68..fab3773bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -408,7 +408,7 @@ elseif(UNIX) src/hostapi/webaudio/pa_webaudio.c ) target_compile_definitions(PortAudio PUBLIC PA_USE_WEBAUDIO=1) - target_link_options(PortAudio PUBLIC -sASYNCIFY) + target_link_options(PortAudio PUBLIC -sASYNCIFY -sAUDIO_WORKLET -sWASM_WORKERS) set(PKGCONFIG_CFLAGS "${PKGCONFIG_CFLAGS} -DPA_USE_WEBAUDIO=1") endif() diff --git a/src/hostapi/webaudio/pa_webaudio.c b/src/hostapi/webaudio/pa_webaudio.c index 85af3322c..77f0867b8 100644 --- a/src/hostapi/webaudio/pa_webaudio.c +++ b/src/hostapi/webaudio/pa_webaudio.c @@ -43,6 +43,7 @@ */ +#include #include /* strlen() */ #include "pa_util.h" @@ -110,7 +111,7 @@ typedef struct PaUtilAllocationGroup *allocations; - /* implementation specific data goes here */ + EMSCRIPTEN_WEBAUDIO_T context; } PaWebAudioHostApiRepresentation; @@ -136,6 +137,8 @@ PaError PaWebAudio_Initialize( PaUtilHostApiRepresentation **hostApi, PaHostApiI goto error; } + webAudioHostApi->context = emscripten_create_audio_context(0); + *hostApi = &webAudioHostApi->inheritedHostApiRep; (*hostApi)->info.structVersion = 1; (*hostApi)->info.type = paInDevelopment; /* IMPLEMENT ME: change to correct type id */ From ac876e6c8ce2624f26771f2302c1fc0e7c97af1f Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 03:25:24 +0100 Subject: [PATCH 04/32] Make sure that PortAudio and consumers are compiled with -pthread We need pthreads and since WebAudio does not support linking non-pthread and pthread-enabled object files, we have to make this public. --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index fab3773bb..732453546 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -408,6 +408,7 @@ elseif(UNIX) src/hostapi/webaudio/pa_webaudio.c ) target_compile_definitions(PortAudio PUBLIC PA_USE_WEBAUDIO=1) + target_compile_options(PortAudio PUBLIC -pthread) target_link_options(PortAudio PUBLIC -sASYNCIFY -sAUDIO_WORKLET -sWASM_WORKERS) set(PKGCONFIG_CFLAGS "${PKGCONFIG_CFLAGS} -DPA_USE_WEBAUDIO=1") endif() From 0770e36092594e81da587ec2ce1a7e9cc0881fa5 Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 18:33:55 +0100 Subject: [PATCH 05/32] Add a README.md for building/testing the Web Audio hostapi --- src/hostapi/webaudio/README.md | 21 +++++++++++++++++++++ src/hostapi/webaudio/README.txt | 1 - 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 src/hostapi/webaudio/README.md delete mode 100644 src/hostapi/webaudio/README.txt diff --git a/src/hostapi/webaudio/README.md b/src/hostapi/webaudio/README.md new file mode 100644 index 000000000..e439862c6 --- /dev/null +++ b/src/hostapi/webaudio/README.md @@ -0,0 +1,21 @@ +# Web Audio API + +To build PortAudio for the web, make sure to have [the Emscripten SDK](https://emscripten.org/docs/getting_started/downloads.html) installed and on your `PATH`, then run from the repository's top-level directory: + +```sh +emcmake cmake -B build +cmake --build build +``` + +To build the examples, use + +```sh +emcmake cmake -B build -DPA_BUILD_EXAMPLES=ON +cmake --build build +``` + +You can now run the examples in a local browser using `emrun`, for example + +```sh +emrun build/examples/paex_sine.html +``` diff --git a/src/hostapi/webaudio/README.txt b/src/hostapi/webaudio/README.txt deleted file mode 100644 index 39d4c8d2b..000000000 --- a/src/hostapi/webaudio/README.txt +++ /dev/null @@ -1 +0,0 @@ -pa_hostapi_skeleton.c provides a starting point for implementing support for a new host API with PortAudio. The idea is that you copy it to a new directory inside /hostapi and start editing. \ No newline at end of file From 8da374538a84fa7889862967200bdc8940c28159 Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 18:52:36 +0100 Subject: [PATCH 06/32] Include pa_debugprint --- src/hostapi/webaudio/pa_webaudio.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/hostapi/webaudio/pa_webaudio.c b/src/hostapi/webaudio/pa_webaudio.c index 77f0867b8..3e0c16c30 100644 --- a/src/hostapi/webaudio/pa_webaudio.c +++ b/src/hostapi/webaudio/pa_webaudio.c @@ -48,6 +48,7 @@ #include "pa_util.h" #include "pa_allocation.h" +#include "pa_debugprint.h" #include "pa_hostapi.h" #include "pa_stream.h" #include "pa_cpuload.h" From 848fbdd93198d8d988f2d04de327d9eb32a5507b Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 18:53:56 +0100 Subject: [PATCH 07/32] Set up a basic 2-channel default output device --- CMakeLists.txt | 7 ++++++- src/hostapi/webaudio/pa_webaudio.c | 19 ++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 732453546..1cf8b288b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -409,7 +409,12 @@ elseif(UNIX) ) target_compile_definitions(PortAudio PUBLIC PA_USE_WEBAUDIO=1) target_compile_options(PortAudio PUBLIC -pthread) - target_link_options(PortAudio PUBLIC -sASYNCIFY -sAUDIO_WORKLET -sWASM_WORKERS) + target_link_options(PortAudio PUBLIC + -sASYNCIFY + -sAUDIO_WORKLET + -sWASM_WORKERS + -sEXPORTED_RUNTIME_METHODS=emscriptenGetAudioObject + ) set(PKGCONFIG_CFLAGS "${PKGCONFIG_CFLAGS} -DPA_USE_WEBAUDIO=1") endif() diff --git a/src/hostapi/webaudio/pa_webaudio.c b/src/hostapi/webaudio/pa_webaudio.c index 3e0c16c30..8b1a762c8 100644 --- a/src/hostapi/webaudio/pa_webaudio.c +++ b/src/hostapi/webaudio/pa_webaudio.c @@ -146,11 +146,20 @@ PaError PaWebAudio_Initialize( PaUtilHostApiRepresentation **hostApi, PaHostApiI (*hostApi)->info.name = "Web Audio"; (*hostApi)->info.defaultInputDevice = paNoDevice; /* IMPLEMENT ME */ - (*hostApi)->info.defaultOutputDevice = paNoDevice; /* IMPLEMENT ME */ + (*hostApi)->info.defaultOutputDevice = 0; (*hostApi)->info.deviceCount = 0; - deviceCount = 0; /* IMPLEMENT ME */ + // TODO: Add proper support for multiple devices + // https://developer.chrome.com/blog/audiocontext-setsinkid + // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/setSinkId + + int defaultSampleRate = EM_ASM_INT({ + const context = Module.emscriptenGetAudioObject($0); + return context.sampleRate; + }, webAudioHostApi->context); + + deviceCount = 1; /* IMPLEMENT ME */ if( deviceCount > 0 ) { @@ -176,7 +185,7 @@ PaError PaWebAudio_Initialize( PaUtilHostApiRepresentation **hostApi, PaHostApiI PaDeviceInfo *deviceInfo = &deviceInfoArray[i]; deviceInfo->structVersion = 2; deviceInfo->hostApi = hostApiIndex; - deviceInfo->name = 0; /* IMPLEMENT ME: allocate block and copy name eg: + deviceInfo->name = "Default"; /* IMPLEMENT ME: allocate block and copy name eg: deviceName = (char*)PaUtil_GroupAllocateZeroInitializedMemory( webAudioHostApi->allocations, strlen(srcName) + 1 ); if( !deviceName ) { @@ -188,14 +197,14 @@ PaError PaWebAudio_Initialize( PaUtilHostApiRepresentation **hostApi, PaHostApiI */ deviceInfo->maxInputChannels = 0; /* IMPLEMENT ME */ - deviceInfo->maxOutputChannels = 0; /* IMPLEMENT ME */ + deviceInfo->maxOutputChannels = 2; /* IMPLEMENT ME */ deviceInfo->defaultLowInputLatency = 0.; /* IMPLEMENT ME */ deviceInfo->defaultLowOutputLatency = 0.; /* IMPLEMENT ME */ deviceInfo->defaultHighInputLatency = 0.; /* IMPLEMENT ME */ deviceInfo->defaultHighOutputLatency = 0.; /* IMPLEMENT ME */ - deviceInfo->defaultSampleRate = 0.; /* IMPLEMENT ME */ + deviceInfo->defaultSampleRate = defaultSampleRate; (*hostApi)->deviceInfos[i] = deviceInfo; ++(*hostApi)->info.deviceCount; From da02a65dceec04f9c101feba4b578a5a201e164a Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 19:04:10 +0100 Subject: [PATCH 08/32] Add experimental Emscripten/Wasm CI --- .github/workflows/cmake.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 1d654c3cd..31829f707 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -25,6 +25,10 @@ jobs: -DPA_USE_ASIO=ON -DPA_USE_JACK=OFF -DASIO_SDK_ZIP_PATH=asiosdk.zip + - name: Ubuntu Emscripten + os: ubuntu-latest + vcpkg_triplet: wasm32-emscripten + cmake_generator: "Unix Makefiles" - name: Windows MSVC os: windows-latest vcpkg_triplet: x64-windows @@ -72,6 +76,11 @@ jobs: sudo apt-get update sudo apt-get install libasound2-dev libpulse-dev libsndio-dev ${{ matrix.dependencies_extras }} if: matrix.os == 'ubuntu-latest' + - name: Set up Emscripten SDK + uses: mymindstorm/setup-emsdk@v14 + with: + version: '3.1.55' + if: startsWith(matrix.vcpkg_triplet, 'wasm32-') - name: "Set up ASIO SDK cache [Windows/MinGW]" uses: actions/cache@v2 if: matrix.asio_sdk_cache_path != null From c1a686518d584f465b664107dfe9c2fa641534df Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 19:13:00 +0100 Subject: [PATCH 09/32] Disable jack feature when targeting emscripten --- vcpkg.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/vcpkg.json b/vcpkg.json index 4c9fa7bfd..7a6942188 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -5,7 +5,12 @@ "homepage": "http://portaudio.com/", "license": "MIT", "supports": "!uwp", - "default-features": [ "jack" ], + "default-features": [ + { + "name": "jack", + "platform": "!emscripten" + } + ], "features": { "jack": { "description": "Build with support for the JACK Audio Connection Kit host API.", From 1706a8808089d6471446fb6b2a9eef06d03c3193 Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 19:15:50 +0100 Subject: [PATCH 10/32] Set VCPKG_CHAINLOAD_TOOLCHAIN_FILE in CI This should hopefully ensure that Emscripten is actually used --- .github/workflows/cmake.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 31829f707..93d108a83 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -29,6 +29,8 @@ jobs: os: ubuntu-latest vcpkg_triplet: wasm32-emscripten cmake_generator: "Unix Makefiles" + cmake_options: + -DVCPKG_CHAINLOAD_TOOLCHAIN_FILE="$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake" - name: Windows MSVC os: windows-latest vcpkg_triplet: x64-windows From be3f169ad7014275abf318e9245ba44b21dd98b8 Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 19:26:38 +0100 Subject: [PATCH 11/32] Disable unavailable -Wno-error=stringop-overflow --- CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1cf8b288b..dce129eb9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,8 +32,10 @@ if(PA_WARNINGS_ARE_ERRORS) # Do *NOT* add warnings to this list. Instead, fix your code so that it doesn't produce the warning. # TODO: fix the offending code so that we don't have to exclude specific warnings anymore. -Wno-error=deprecated-declarations # https://github.com/PortAudio/portaudio/issues/213 https://github.com/PortAudio/portaudio/issues/641 - -Wno-error=stringop-overflow ) + if (NOT EMSCRIPTEN) + add_compile_options(-Wno-error=stringop-overflow) + endif() if (CMAKE_C_COMPILER_ID MATCHES "Clang") # Don't fail on older clang versions that don't recognize the latest warnings in the list above. # Note that unrecognized warning options are not a fatal error on GCC, and in fact, GCC will choke on this option. Hence the conditional. From 692d91590dda613cca378f489329d5e60e26aae0 Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 19:42:26 +0100 Subject: [PATCH 12/32] Rename to WebAudioHostProcessingLoop --- src/hostapi/webaudio/pa_webaudio.c | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/hostapi/webaudio/pa_webaudio.c b/src/hostapi/webaudio/pa_webaudio.c index 8b1a762c8..e266cf551 100644 --- a/src/hostapi/webaudio/pa_webaudio.c +++ b/src/hostapi/webaudio/pa_webaudio.c @@ -547,12 +547,7 @@ static PaError OpenStream( struct PaUtilHostApiRepresentation *hostApi, return result; } -/* - ExampleHostProcessingLoop() illustrates the kind of processing which may - occur in a host implementation. - -*/ -static void ExampleHostProcessingLoop( void *inputBuffer, void *outputBuffer, void *userData ) +static void WebAudioHostProcessingLoop( void *inputBuffer, void *outputBuffer, void *userData ) { PaWebAudioStream *stream = (PaWebAudioStream*)userData; PaStreamCallbackTimeInfo timeInfo = {0,0,0}; /* IMPLEMENT ME */ @@ -668,11 +663,11 @@ static PaError StartStream( PaStream *s ) /* IMPLEMENT ME, see portaudio.h for required behavior */ - /* suppress unused function warning. the code in ExampleHostProcessingLoop or + /* suppress unused function warning. the code in WebAudioHostProcessingLoop or something similar should be implemented to feed samples to and from the host after StartStream() is called. */ - (void) ExampleHostProcessingLoop; + (void) WebAudioHostProcessingLoop; return result; } From 83eb5a58550152d7756d0540fb2cc91dc880937b Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 20:09:24 +0100 Subject: [PATCH 13/32] Experimentally set up Wasm Audio Worklet --- src/hostapi/webaudio/pa_webaudio.c | 72 +++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/src/hostapi/webaudio/pa_webaudio.c b/src/hostapi/webaudio/pa_webaudio.c index e266cf551..43d014b80 100644 --- a/src/hostapi/webaudio/pa_webaudio.c +++ b/src/hostapi/webaudio/pa_webaudio.c @@ -45,6 +45,7 @@ #include #include /* strlen() */ +#include #include "pa_util.h" #include "pa_allocation.h" @@ -96,6 +97,16 @@ static PaError WriteStream( PaStream* stream, const void *buffer, unsigned long static signed long GetStreamReadAvailable( PaStream* stream ); static signed long GetStreamWriteAvailable( PaStream* stream ); +static void WasmAudioWorkletThreadInitialized( EMSCRIPTEN_WEBAUDIO_T context, + EM_BOOL success, + void *userData ); +static void WasmAudioWorkletProcessorCreated( EMSCRIPTEN_WEBAUDIO_T context, + EM_BOOL success, + void *userData ); +static EM_BOOL WebAudioHostProcessingLoop( int numInputs, const AudioSampleFrame *inputBuffer, + int numOutputs, AudioSampleFrame *outputBuffer, + int numParams, const AudioParamFrame *params, + void *userData ); /* IMPLEMENT ME: a macro like the following one should be used for reporting host errors */ @@ -371,6 +382,11 @@ typedef struct PaWebAudioStream } PaWebAudioStream; +/* Must be a multiple of (and aligned to) 16 bytes. See + - https://emscripten.org/docs/api_reference/wasm_audio_worklets.html#programming-example + - https://github.com/emscripten-core/emscripten/blob/2ba2078b/system/include/emscripten/webaudio.h#L70 */ +uint8_t WASM_AUDIO_WORKLET_THREAD_STACK[4096]; + /* see pa_hostapi.h for a list of validity guarantees made about OpenStream parameters */ static PaError OpenStream( struct PaUtilHostApiRepresentation *hostApi, @@ -529,10 +545,17 @@ static PaError OpenStream( struct PaUtilHostApiRepresentation *hostApi, stream->streamRepresentation.streamInfo.sampleRate = sampleRate; - /* - IMPLEMENT ME: - - additional stream setup + opening - */ + /* additional stream setup + opening */ + + EMSCRIPTEN_WEBAUDIO_T context = webAudioHostApi->context; + + PA_DEBUG(("Starting Wasm Audio Worklet thread...")); + emscripten_start_wasm_audio_worklet_thread_async( + context, + WASM_AUDIO_WORKLET_THREAD_STACK, + sizeof(WASM_AUDIO_WORKLET_THREAD_STACK), + &WasmAudioWorkletThreadInitialized, + stream); stream->framesPerHostCallback = framesPerHostBuffer; @@ -547,7 +570,42 @@ static PaError OpenStream( struct PaUtilHostApiRepresentation *hostApi, return result; } -static void WebAudioHostProcessingLoop( void *inputBuffer, void *outputBuffer, void *userData ) +static void WasmAudioWorkletThreadInitialized( EMSCRIPTEN_WEBAUDIO_T context, + EM_BOOL success, + void *userData ) +{ + if (!success) return; // Check browser console for detailed errors + + WebAudioWorkletProcessorCreateOptions opts = { + .name = "portaudio-stream", + }; + + PA_DEBUG(("Creating Wasm Audio Worklet processor...")); + emscripten_create_wasm_audio_worklet_processor_async( + context, &opts, &WasmAudioWorkletProcessorCreated, userData); +} + +static void WasmAudioWorkletProcessorCreated( EMSCRIPTEN_WEBAUDIO_T context, + EM_BOOL success, + void *userData ) +{ + if (!success) return; // Check browser console for detailed errors + + int outputChannelCounts[1] = { 2 }; + EmscriptenAudioWorkletNodeCreateOptions opts = { + .numberOfInputs = 0, + .numberOfOutputs = 1, + }; + + PA_DEBUG(("Creating Wasm Audio Worklet node...")); + EMSCRIPTEN_AUDIO_WORKLET_NODE_T node = emscripten_create_wasm_audio_worklet_node( + context, "portaudio-stream", &opts, &WebAudioHostProcessingLoop, userData); +} + +static EM_BOOL WebAudioHostProcessingLoop( int numInputs, const AudioSampleFrame *inputBuffer, + int numOutputs, AudioSampleFrame *outputBuffer, + int numParams, const AudioParamFrame *params, + void *userData ) { PaWebAudioStream *stream = (PaWebAudioStream*)userData; PaStreamCallbackTimeInfo timeInfo = {0,0,0}; /* IMPLEMENT ME */ @@ -580,7 +638,7 @@ static void WebAudioHostProcessingLoop( void *inputBuffer, void *outputBuffer, v PaUtil_SetInputFrameCount( &stream->bufferProcessor, 0 /* default to host buffer size */ ); PaUtil_SetInterleavedInputChannels( &stream->bufferProcessor, 0, /* first channel of inputBuffer is channel 0 */ - inputBuffer, + (void *)inputBuffer, 0 ); /* 0 - use inputChannelCount passed to init buffer processor */ PaUtil_SetOutputFrameCount( &stream->bufferProcessor, 0 /* default to host buffer size */ ); @@ -629,6 +687,8 @@ static void WebAudioHostProcessingLoop( void *inputBuffer, void *outputBuffer, v if( stream->streamRepresentation.streamFinishedCallback != 0 ) stream->streamRepresentation.streamFinishedCallback( stream->streamRepresentation.userData ); } + + return callbackResult == paContinue; } From 1431da185cdd7dd782d13bb572482385fc89d991 Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 20:34:53 +0100 Subject: [PATCH 14/32] Create a separate audio context per stream While we could try multiplexing streams into a single audio context, we can only set one sink per context. Both therefore, and because the start/stop semantics seem to map nicely to AudioContext's resume/suspend, we will create a context per stream. --- src/hostapi/webaudio/pa_webaudio.c | 53 +++++++++++++++++------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/src/hostapi/webaudio/pa_webaudio.c b/src/hostapi/webaudio/pa_webaudio.c index 43d014b80..ee132ec3c 100644 --- a/src/hostapi/webaudio/pa_webaudio.c +++ b/src/hostapi/webaudio/pa_webaudio.c @@ -122,8 +122,6 @@ typedef struct PaUtilStreamInterface blockingStreamInterface; PaUtilAllocationGroup *allocations; - - EMSCRIPTEN_WEBAUDIO_T context; } PaWebAudioHostApiRepresentation; @@ -149,8 +147,6 @@ PaError PaWebAudio_Initialize( PaUtilHostApiRepresentation **hostApi, PaHostApiI goto error; } - webAudioHostApi->context = emscripten_create_audio_context(0); - *hostApi = &webAudioHostApi->inheritedHostApiRep; (*hostApi)->info.structVersion = 1; (*hostApi)->info.type = paInDevelopment; /* IMPLEMENT ME: change to correct type id */ @@ -165,10 +161,7 @@ PaError PaWebAudio_Initialize( PaUtilHostApiRepresentation **hostApi, PaHostApiI // https://developer.chrome.com/blog/audiocontext-setsinkid // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/setSinkId - int defaultSampleRate = EM_ASM_INT({ - const context = Module.emscriptenGetAudioObject($0); - return context.sampleRate; - }, webAudioHostApi->context); + int defaultSampleRate = 44100; deviceCount = 1; /* IMPLEMENT ME */ @@ -375,9 +368,8 @@ typedef struct PaWebAudioStream PaUtilCpuLoadMeasurer cpuLoadMeasurer; PaUtilBufferProcessor bufferProcessor; - /* IMPLEMENT ME: - - implementation specific data goes here - */ + EMSCRIPTEN_WEBAUDIO_T context; + unsigned long framesPerHostCallback; /* just an example */ } PaWebAudioStream; @@ -547,11 +539,12 @@ static PaError OpenStream( struct PaUtilHostApiRepresentation *hostApi, /* additional stream setup + opening */ - EMSCRIPTEN_WEBAUDIO_T context = webAudioHostApi->context; + PA_DEBUG(("Creating audio context...")); + stream->context = emscripten_create_audio_context(0); PA_DEBUG(("Starting Wasm Audio Worklet thread...")); emscripten_start_wasm_audio_worklet_thread_async( - context, + stream->context, WASM_AUDIO_WORKLET_THREAD_STACK, sizeof(WASM_AUDIO_WORKLET_THREAD_STACK), &WasmAudioWorkletThreadInitialized, @@ -701,10 +694,12 @@ static PaError CloseStream( PaStream* s ) PaError result = paNoError; PaWebAudioStream *stream = (PaWebAudioStream*)s; - /* - IMPLEMENT ME: - - additional stream closing + cleanup - */ + /* additional stream closing + cleanup */ + + EM_ASM({ + const context = emscriptenGetAudioObject($0); + context.close(); + }, stream->context); PaUtil_TerminateBufferProcessor( &stream->bufferProcessor ); PaUtil_TerminateStreamRepresentation( &stream->streamRepresentation ); @@ -723,6 +718,12 @@ static PaError StartStream( PaStream *s ) /* IMPLEMENT ME, see portaudio.h for required behavior */ + PA_DEBUG(("Resuming audio context...")); + EM_ASM({ + const context = emscriptenGetAudioObject($0); + context.resume(); + }, stream->context); + /* suppress unused function warning. the code in WebAudioHostProcessingLoop or something similar should be implemented to feed samples to and from the host after StartStream() is called. @@ -738,10 +739,13 @@ static PaError StopStream( PaStream *s ) PaError result = paNoError; PaWebAudioStream *stream = (PaWebAudioStream*)s; - /* suppress unused variable warnings */ - (void) stream; + /* TODO: Check if this is right, see portaudio.h for required behavior */ - /* IMPLEMENT ME, see portaudio.h for required behavior */ + PA_DEBUG(("Suspending audio context upon stop...")); + EM_ASM({ + const context = emscriptenGetAudioObject($0); + context.suspend(); + }, stream->context); return result; } @@ -752,10 +756,13 @@ static PaError AbortStream( PaStream *s ) PaError result = paNoError; PaWebAudioStream *stream = (PaWebAudioStream*)s; - /* suppress unused variable warnings */ - (void) stream; + /* TODO: Check if this is right, see portaudio.h for required behavior */ - /* IMPLEMENT ME, see portaudio.h for required behavior */ + PA_DEBUG(("Suspending audio context upon abort...")); + EM_ASM({ + const context = emscriptenGetAudioObject($0); + context.suspend(); + }, stream->context); return result; } From a02a4e2c0234813c97591915a67364775b72a7e8 Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 20:42:19 +0100 Subject: [PATCH 15/32] Implement IsStreamStopped and IsStreamActive --- src/hostapi/webaudio/pa_webaudio.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/hostapi/webaudio/pa_webaudio.c b/src/hostapi/webaudio/pa_webaudio.c index ee132ec3c..2de3ccdd7 100644 --- a/src/hostapi/webaudio/pa_webaudio.c +++ b/src/hostapi/webaudio/pa_webaudio.c @@ -772,12 +772,12 @@ static PaError IsStreamStopped( PaStream *s ) { PaWebAudioStream *stream = (PaWebAudioStream*)s; - /* suppress unused variable warnings */ - (void) stream; - - /* IMPLEMENT ME, see portaudio.h for required behavior */ + /* TODO: Check if this is right, see portaudio.h for required behavior */ - return 0; + return EM_ASM_INT({ + const context = emscriptenGetAudioObject($0); + return context.state === 'suspended' ? 1 : 0; + }, stream->context); } @@ -785,12 +785,12 @@ static PaError IsStreamActive( PaStream *s ) { PaWebAudioStream *stream = (PaWebAudioStream*)s; - /* suppress unused variable warnings */ - (void) stream; - - /* IMPLEMENT ME, see portaudio.h for required behavior */ + /* TODO: Check if this is right, see portaudio.h for required behavior */ - return 0; + return EM_ASM_INT({ + const context = emscriptenGetAudioObject($0); + return context.state === 'running' ? 1 : 0; + }, stream->context); } From 42bdb7c3d3ef80f3e887a528bbec408cd22c6f4f Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 20:46:01 +0100 Subject: [PATCH 16/32] Update comments --- src/hostapi/webaudio/pa_webaudio.c | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/hostapi/webaudio/pa_webaudio.c b/src/hostapi/webaudio/pa_webaudio.c index 2de3ccdd7..2b980292c 100644 --- a/src/hostapi/webaudio/pa_webaudio.c +++ b/src/hostapi/webaudio/pa_webaudio.c @@ -716,7 +716,7 @@ static PaError StartStream( PaStream *s ) PaUtil_ResetBufferProcessor( &stream->bufferProcessor ); - /* IMPLEMENT ME, see portaudio.h for required behavior */ + /* TODO: Check if this is right, see portaudio.h for required behavior */ PA_DEBUG(("Resuming audio context...")); EM_ASM({ @@ -724,12 +724,6 @@ static PaError StartStream( PaStream *s ) context.resume(); }, stream->context); - /* suppress unused function warning. the code in WebAudioHostProcessingLoop or - something similar should be implemented to feed samples to and from the - host after StartStream() is called. - */ - (void) WebAudioHostProcessingLoop; - return result; } From d27dbc53c63560cb4623301bb02b88b7000e8f69 Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 20:51:32 +0100 Subject: [PATCH 17/32] Add missing newlines to debug statements --- src/hostapi/webaudio/pa_webaudio.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/hostapi/webaudio/pa_webaudio.c b/src/hostapi/webaudio/pa_webaudio.c index 2b980292c..330070258 100644 --- a/src/hostapi/webaudio/pa_webaudio.c +++ b/src/hostapi/webaudio/pa_webaudio.c @@ -539,10 +539,10 @@ static PaError OpenStream( struct PaUtilHostApiRepresentation *hostApi, /* additional stream setup + opening */ - PA_DEBUG(("Creating audio context...")); + PA_DEBUG(("Creating audio context...\n")); stream->context = emscripten_create_audio_context(0); - PA_DEBUG(("Starting Wasm Audio Worklet thread...")); + PA_DEBUG(("Starting Wasm Audio Worklet thread...\n")); emscripten_start_wasm_audio_worklet_thread_async( stream->context, WASM_AUDIO_WORKLET_THREAD_STACK, @@ -573,7 +573,7 @@ static void WasmAudioWorkletThreadInitialized( EMSCRIPTEN_WEBAUDIO_T context, .name = "portaudio-stream", }; - PA_DEBUG(("Creating Wasm Audio Worklet processor...")); + PA_DEBUG(("Creating Wasm Audio Worklet processor...\n")); emscripten_create_wasm_audio_worklet_processor_async( context, &opts, &WasmAudioWorkletProcessorCreated, userData); } @@ -590,7 +590,7 @@ static void WasmAudioWorkletProcessorCreated( EMSCRIPTEN_WEBAUDIO_T context, .numberOfOutputs = 1, }; - PA_DEBUG(("Creating Wasm Audio Worklet node...")); + PA_DEBUG(("Creating Wasm Audio Worklet node...\n")); EMSCRIPTEN_AUDIO_WORKLET_NODE_T node = emscripten_create_wasm_audio_worklet_node( context, "portaudio-stream", &opts, &WebAudioHostProcessingLoop, userData); } @@ -718,7 +718,7 @@ static PaError StartStream( PaStream *s ) /* TODO: Check if this is right, see portaudio.h for required behavior */ - PA_DEBUG(("Resuming audio context...")); + PA_DEBUG(("Resuming audio context...\n")); EM_ASM({ const context = emscriptenGetAudioObject($0); context.resume(); @@ -735,7 +735,7 @@ static PaError StopStream( PaStream *s ) /* TODO: Check if this is right, see portaudio.h for required behavior */ - PA_DEBUG(("Suspending audio context upon stop...")); + PA_DEBUG(("Suspending audio context upon stop...\n")); EM_ASM({ const context = emscriptenGetAudioObject($0); context.suspend(); @@ -752,7 +752,7 @@ static PaError AbortStream( PaStream *s ) /* TODO: Check if this is right, see portaudio.h for required behavior */ - PA_DEBUG(("Suspending audio context upon abort...")); + PA_DEBUG(("Suspending audio context upon abort...\n")); EM_ASM({ const context = emscriptenGetAudioObject($0); context.suspend(); From d2639d6813cb2be7058642121cd907c6ca07865d Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 20:58:17 +0100 Subject: [PATCH 18/32] Add some tips --- src/hostapi/webaudio/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/hostapi/webaudio/README.md b/src/hostapi/webaudio/README.md index e439862c6..594e1d1bf 100644 --- a/src/hostapi/webaudio/README.md +++ b/src/hostapi/webaudio/README.md @@ -14,8 +14,14 @@ emcmake cmake -B build -DPA_BUILD_EXAMPLES=ON cmake --build build ``` +> [!TIP] +> For debug logging, set `-DPA_ENABLE_DEBUG_OUTPUT=ON` + You can now run the examples in a local browser using `emrun`, for example ```sh emrun build/examples/paex_sine.html ``` + +> [!TIP] +> You can customize the browser e.g. by setting `--browser=firefox` and also pass arguments to the browser with `--browser-args` From 33c679afe16629f34cce13aafc0dbb837f97545a Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 21:06:25 +0100 Subject: [PATCH 19/32] Await context suspend/resume --- CMakeLists.txt | 1 + src/hostapi/webaudio/pa_webaudio.c | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index dce129eb9..107942920 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -413,6 +413,7 @@ elseif(UNIX) target_compile_options(PortAudio PUBLIC -pthread) target_link_options(PortAudio PUBLIC -sASYNCIFY + -sASYNCIFY_IMPORTS=emscripten_asm_const_int -sAUDIO_WORKLET -sWASM_WORKERS -sEXPORTED_RUNTIME_METHODS=emscriptenGetAudioObject diff --git a/src/hostapi/webaudio/pa_webaudio.c b/src/hostapi/webaudio/pa_webaudio.c index 330070258..bc0948796 100644 --- a/src/hostapi/webaudio/pa_webaudio.c +++ b/src/hostapi/webaudio/pa_webaudio.c @@ -721,7 +721,9 @@ static PaError StartStream( PaStream *s ) PA_DEBUG(("Resuming audio context...\n")); EM_ASM({ const context = emscriptenGetAudioObject($0); - context.resume(); + Asyncify.handleAsync(async () => { + await context.resume(); + }); }, stream->context); return result; @@ -738,7 +740,9 @@ static PaError StopStream( PaStream *s ) PA_DEBUG(("Suspending audio context upon stop...\n")); EM_ASM({ const context = emscriptenGetAudioObject($0); - context.suspend(); + Asyncify.handleAsync(async () => { + await context.suspend(); + }); }, stream->context); return result; @@ -755,7 +759,9 @@ static PaError AbortStream( PaStream *s ) PA_DEBUG(("Suspending audio context upon abort...\n")); EM_ASM({ const context = emscriptenGetAudioObject($0); - context.suspend(); + Asyncify.handleAsync(async () => { + await context.suspend(); + }); }, stream->context); return result; From 6e356cdd87bcbcb0b9576505f25d405efade9666 Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 21:19:21 +0100 Subject: [PATCH 20/32] Wait for user interaction when attempting to resume context --- src/hostapi/webaudio/pa_webaudio.c | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/hostapi/webaudio/pa_webaudio.c b/src/hostapi/webaudio/pa_webaudio.c index bc0948796..499c8b6cc 100644 --- a/src/hostapi/webaudio/pa_webaudio.c +++ b/src/hostapi/webaudio/pa_webaudio.c @@ -719,12 +719,22 @@ static PaError StartStream( PaStream *s ) /* TODO: Check if this is right, see portaudio.h for required behavior */ PA_DEBUG(("Resuming audio context...\n")); - EM_ASM({ - const context = emscriptenGetAudioObject($0); - Asyncify.handleAsync(async () => { - await context.resume(); - }); - }, stream->context); + emscripten_resume_audio_context_sync(stream->context); + + /* + Resuming the context is only allowed after the user has interacted with + the page e.g. by clicking somewhere. To make sure that the context is + actually running, we use a sleep-wait loop that periodically reattempts + to resume the context. + + TODO: Find a more elegant solution + */ + + while (!IsStreamActive(stream)) { + PA_DEBUG(("Audio context is not running yet, waiting for user interaction...\n")); + emscripten_sleep(500); + emscripten_resume_audio_context_sync(stream->context); + } return result; } From c8d1abf77d115a571bee46d0b43faf6fd0259b98 Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 21:23:44 +0100 Subject: [PATCH 21/32] Add note on user interaction --- src/hostapi/webaudio/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/hostapi/webaudio/README.md b/src/hostapi/webaudio/README.md index 594e1d1bf..8a3c9ab9d 100644 --- a/src/hostapi/webaudio/README.md +++ b/src/hostapi/webaudio/README.md @@ -23,5 +23,8 @@ You can now run the examples in a local browser using `emrun`, for example emrun build/examples/paex_sine.html ``` +> [!IMPORTANT] +> Due to browser policies you have to interact with the site at least once (e.g. by clicking anywhere) before audio contexts can be started. `Pa_StartStream` will (from the C/C++ perspective) block until this happens. Under the hood this is handled asynchronously using Asyncify. + > [!TIP] > You can customize the browser e.g. by setting `--browser=firefox` and also pass arguments to the browser with `--browser-args` From edffb124dac1b47b9bcff97daa3e56c31e350ce0 Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 21:42:34 +0100 Subject: [PATCH 22/32] Handle the buffers in host loop properly --- src/hostapi/webaudio/pa_webaudio.c | 35 ++++++++++++++++++------------ 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/hostapi/webaudio/pa_webaudio.c b/src/hostapi/webaudio/pa_webaudio.c index 499c8b6cc..c14834d7a 100644 --- a/src/hostapi/webaudio/pa_webaudio.c +++ b/src/hostapi/webaudio/pa_webaudio.c @@ -43,6 +43,7 @@ */ +#include #include #include /* strlen() */ #include @@ -394,7 +395,7 @@ static PaError OpenStream( struct PaUtilHostApiRepresentation *hostApi, PaError result = paNoError; PaWebAudioHostApiRepresentation *webAudioHostApi = (PaWebAudioHostApiRepresentation*)hostApi; PaWebAudioStream *stream = 0; - unsigned long framesPerHostBuffer = framesPerBuffer; /* these may not be equivalent for all implementations */ + unsigned long framesPerHostBuffer = 128; // This buffer size is fixed for AudioWorkletProcessor int inputChannelCount, outputChannelCount; PaSampleFormat inputSampleFormat, outputSampleFormat; PaSampleFormat hostInputSampleFormat, hostOutputSampleFormat; @@ -595,8 +596,8 @@ static void WasmAudioWorkletProcessorCreated( EMSCRIPTEN_WEBAUDIO_T context, context, "portaudio-stream", &opts, &WebAudioHostProcessingLoop, userData); } -static EM_BOOL WebAudioHostProcessingLoop( int numInputs, const AudioSampleFrame *inputBuffer, - int numOutputs, AudioSampleFrame *outputBuffer, +static EM_BOOL WebAudioHostProcessingLoop( int numInputs, const AudioSampleFrame *inputs, + int numOutputs, AudioSampleFrame *outputs, int numParams, const AudioParamFrame *params, void *userData ) { @@ -628,17 +629,23 @@ static EM_BOOL WebAudioHostProcessingLoop( int numInputs, const AudioSampleFrame PaUtil_SetNonInterleaved*Channel() or PaUtil_Set*Channel() here. */ - PaUtil_SetInputFrameCount( &stream->bufferProcessor, 0 /* default to host buffer size */ ); - PaUtil_SetInterleavedInputChannels( &stream->bufferProcessor, - 0, /* first channel of inputBuffer is channel 0 */ - (void *)inputBuffer, - 0 ); /* 0 - use inputChannelCount passed to init buffer processor */ - - PaUtil_SetOutputFrameCount( &stream->bufferProcessor, 0 /* default to host buffer size */ ); - PaUtil_SetInterleavedOutputChannels( &stream->bufferProcessor, - 0, /* first channel of outputBuffer is channel 0 */ - outputBuffer, - 0 ); /* 0 - use outputChannelCount passed to init buffer processor */ + if (numInputs > 0) { + assert(numInputs == 1); + PaUtil_SetInputFrameCount( &stream->bufferProcessor, 0 /* default to host buffer size */ ); + PaUtil_SetInterleavedInputChannels( &stream->bufferProcessor, + 0, /* first channel of inputBuffer is channel 0 */ + inputs[0].data, + inputs[0].numberOfChannels ); + } + + if (numOutputs > 0) { + assert(numOutputs == 1); + PaUtil_SetOutputFrameCount( &stream->bufferProcessor, 0 /* default to host buffer size */ ); + PaUtil_SetInterleavedOutputChannels( &stream->bufferProcessor, + 0, /* first channel of outputBuffer is channel 0 */ + outputs[0].data, + outputs[0].numberOfChannels ); + } /* you must pass a valid value of callback result to PaUtil_EndBufferProcessing() in general you would pass paContinue for normal operation, and From deb8a570149a8819d3f4c7a772eadbc7364a7c79 Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 21:47:17 +0100 Subject: [PATCH 23/32] Use C wrapper for querying audio context state --- src/hostapi/webaudio/pa_webaudio.c | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/hostapi/webaudio/pa_webaudio.c b/src/hostapi/webaudio/pa_webaudio.c index c14834d7a..c50f81acf 100644 --- a/src/hostapi/webaudio/pa_webaudio.c +++ b/src/hostapi/webaudio/pa_webaudio.c @@ -791,10 +791,7 @@ static PaError IsStreamStopped( PaStream *s ) /* TODO: Check if this is right, see portaudio.h for required behavior */ - return EM_ASM_INT({ - const context = emscriptenGetAudioObject($0); - return context.state === 'suspended' ? 1 : 0; - }, stream->context); + return emscripten_audio_context_state(stream->context) != AUDIO_CONTEXT_STATE_RUNNING; } @@ -804,10 +801,7 @@ static PaError IsStreamActive( PaStream *s ) /* TODO: Check if this is right, see portaudio.h for required behavior */ - return EM_ASM_INT({ - const context = emscriptenGetAudioObject($0); - return context.state === 'running' ? 1 : 0; - }, stream->context); + return emscripten_audio_context_state(stream->context) == AUDIO_CONTEXT_STATE_RUNNING; } From 405759868e2cd73b5844e666a2af769db114f863 Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 21:52:56 +0100 Subject: [PATCH 24/32] Connect node to context destination --- src/hostapi/webaudio/pa_webaudio.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/hostapi/webaudio/pa_webaudio.c b/src/hostapi/webaudio/pa_webaudio.c index c50f81acf..675f0a0b8 100644 --- a/src/hostapi/webaudio/pa_webaudio.c +++ b/src/hostapi/webaudio/pa_webaudio.c @@ -594,6 +594,13 @@ static void WasmAudioWorkletProcessorCreated( EMSCRIPTEN_WEBAUDIO_T context, PA_DEBUG(("Creating Wasm Audio Worklet node...\n")); EMSCRIPTEN_AUDIO_WORKLET_NODE_T node = emscripten_create_wasm_audio_worklet_node( context, "portaudio-stream", &opts, &WebAudioHostProcessingLoop, userData); + + PA_DEBUG(("Connecting node to audio context destination...\n")); + EM_ASM({ + const node = emscriptenGetAudioObject($0); + const context = emscriptenGetAudioObject($1); + node.connect(context.destination); + }, node, context); } static EM_BOOL WebAudioHostProcessingLoop( int numInputs, const AudioSampleFrame *inputs, From 6d2ff90a739578959724bbbc0cbfdead5c90605d Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 22:26:27 +0100 Subject: [PATCH 25/32] Set sample rate during audio context creation --- src/hostapi/webaudio/pa_webaudio.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/hostapi/webaudio/pa_webaudio.c b/src/hostapi/webaudio/pa_webaudio.c index 675f0a0b8..2314e2119 100644 --- a/src/hostapi/webaudio/pa_webaudio.c +++ b/src/hostapi/webaudio/pa_webaudio.c @@ -540,8 +540,13 @@ static PaError OpenStream( struct PaUtilHostApiRepresentation *hostApi, /* additional stream setup + opening */ + EmscriptenWebAudioCreateAttributes opts = { + .latencyHint = "playback", // One of "balanced", "interactive" or "playback" + .sampleRate = sampleRate, + }; + PA_DEBUG(("Creating audio context...\n")); - stream->context = emscripten_create_audio_context(0); + stream->context = emscripten_create_audio_context(&opts); PA_DEBUG(("Starting Wasm Audio Worklet thread...\n")); emscripten_start_wasm_audio_worklet_thread_async( From a2102c5659b4252020b2105749e4fd0e057938e8 Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 22:49:44 +0100 Subject: [PATCH 26/32] Use proper host format (non-interleaved 32-bit float) --- src/hostapi/webaudio/pa_webaudio.c | 34 ++++++++++++++++++------------ 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/hostapi/webaudio/pa_webaudio.c b/src/hostapi/webaudio/pa_webaudio.c index 2314e2119..55d79e37b 100644 --- a/src/hostapi/webaudio/pa_webaudio.c +++ b/src/hostapi/webaudio/pa_webaudio.c @@ -55,6 +55,7 @@ #include "pa_stream.h" #include "pa_cpuload.h" #include "pa_process.h" +#include "portaudio.h" /* prototypes for functions declared in this file */ @@ -114,6 +115,9 @@ static EM_BOOL WebAudioHostProcessingLoop( int numInputs, const AudioSampleFrame #define PA_WEBAUDIO_SET_LAST_HOST_ERROR( errorCode, errorText ) \ PaUtil_SetLastHostErrorInfo( paInDevelopment, errorCode, errorText ) +/* This buffer size is fixed for AudioWorkletProcessor */ +#define PA_WEBAUDIO_FRAME_COUNT 128 + /* PaWebAudioHostApiRepresentation - host api datastructure specific to this implementation */ typedef struct @@ -395,7 +399,7 @@ static PaError OpenStream( struct PaUtilHostApiRepresentation *hostApi, PaError result = paNoError; PaWebAudioHostApiRepresentation *webAudioHostApi = (PaWebAudioHostApiRepresentation*)hostApi; PaWebAudioStream *stream = 0; - unsigned long framesPerHostBuffer = 128; // This buffer size is fixed for AudioWorkletProcessor + unsigned long framesPerHostBuffer = PA_WEBAUDIO_FRAME_COUNT; int inputChannelCount, outputChannelCount; PaSampleFormat inputSampleFormat, outputSampleFormat; PaSampleFormat hostInputSampleFormat, hostOutputSampleFormat; @@ -430,6 +434,10 @@ static PaError OpenStream( struct PaUtilHostApiRepresentation *hostApi, inputSampleFormat = hostInputSampleFormat = paInt16; /* Suppress 'uninitialised var' warnings. */ } + // Web Audio always uses non-interleaved, 32-bit float buffers + // See https://github.com/emscripten-core/emscripten/blob/2ba2078b/system/include/emscripten/webaudio.h#L100-L105 + hostOutputSampleFormat = paFloat32 | paNonInterleaved; + if( outputParameters ) { outputChannelCount = outputParameters->channelCount; @@ -448,15 +456,11 @@ static PaError OpenStream( struct PaUtilHostApiRepresentation *hostApi, /* validate outputStreamInfo */ if( outputParameters->hostApiSpecificStreamInfo ) return paIncompatibleHostApiSpecificStreamInfo; /* this implementation doesn't use custom stream info */ - - /* IMPLEMENT ME - establish which host formats are available */ - hostOutputSampleFormat = - PaUtil_SelectClosestAvailableFormat( paInt16 /* native formats */, outputSampleFormat ); } else { outputChannelCount = 0; - outputSampleFormat = hostOutputSampleFormat = paInt16; /* Suppress 'uninitialized var' warnings. */ + outputSampleFormat = hostOutputSampleFormat; } /* @@ -643,20 +647,22 @@ static EM_BOOL WebAudioHostProcessingLoop( int numInputs, const AudioSampleFrame if (numInputs > 0) { assert(numInputs == 1); + const AudioSampleFrame input = inputs[0]; PaUtil_SetInputFrameCount( &stream->bufferProcessor, 0 /* default to host buffer size */ ); - PaUtil_SetInterleavedInputChannels( &stream->bufferProcessor, - 0, /* first channel of inputBuffer is channel 0 */ - inputs[0].data, - inputs[0].numberOfChannels ); + for (int i = 0; i < input.numberOfChannels; i++) + { + PaUtil_SetNonInterleavedInputChannel( &stream->bufferProcessor, i, input.data + PA_WEBAUDIO_FRAME_COUNT * i ); + } } if (numOutputs > 0) { assert(numOutputs == 1); + AudioSampleFrame output = outputs[0]; PaUtil_SetOutputFrameCount( &stream->bufferProcessor, 0 /* default to host buffer size */ ); - PaUtil_SetInterleavedOutputChannels( &stream->bufferProcessor, - 0, /* first channel of outputBuffer is channel 0 */ - outputs[0].data, - outputs[0].numberOfChannels ); + for (int i = 0; i < output.numberOfChannels; i++) + { + PaUtil_SetNonInterleavedOutputChannel( &stream->bufferProcessor, i, output.data + PA_WEBAUDIO_FRAME_COUNT * i ); + } } /* you must pass a valid value of callback result to PaUtil_EndBufferProcessing() From 47d42027154ce400f57fa1c4ff08fa2c1de2ce04 Mon Sep 17 00:00:00 2001 From: fwcd Date: Sun, 10 Mar 2024 22:57:32 +0100 Subject: [PATCH 27/32] Format code towards the prevalent style --- src/hostapi/webaudio/pa_webaudio.c | 37 +++++++++++++++++------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/hostapi/webaudio/pa_webaudio.c b/src/hostapi/webaudio/pa_webaudio.c index 55d79e37b..494b9925d 100644 --- a/src/hostapi/webaudio/pa_webaudio.c +++ b/src/hostapi/webaudio/pa_webaudio.c @@ -550,7 +550,7 @@ static PaError OpenStream( struct PaUtilHostApiRepresentation *hostApi, }; PA_DEBUG(("Creating audio context...\n")); - stream->context = emscripten_create_audio_context(&opts); + stream->context = emscripten_create_audio_context( &opts ); PA_DEBUG(("Starting Wasm Audio Worklet thread...\n")); emscripten_start_wasm_audio_worklet_thread_async( @@ -558,7 +558,7 @@ static PaError OpenStream( struct PaUtilHostApiRepresentation *hostApi, WASM_AUDIO_WORKLET_THREAD_STACK, sizeof(WASM_AUDIO_WORKLET_THREAD_STACK), &WasmAudioWorkletThreadInitialized, - stream); + stream ); stream->framesPerHostCallback = framesPerHostBuffer; @@ -577,7 +577,7 @@ static void WasmAudioWorkletThreadInitialized( EMSCRIPTEN_WEBAUDIO_T context, EM_BOOL success, void *userData ) { - if (!success) return; // Check browser console for detailed errors + if ( !success ) return; // Check browser console for detailed errors WebAudioWorkletProcessorCreateOptions opts = { .name = "portaudio-stream", @@ -585,14 +585,14 @@ static void WasmAudioWorkletThreadInitialized( EMSCRIPTEN_WEBAUDIO_T context, PA_DEBUG(("Creating Wasm Audio Worklet processor...\n")); emscripten_create_wasm_audio_worklet_processor_async( - context, &opts, &WasmAudioWorkletProcessorCreated, userData); + context, &opts, &WasmAudioWorkletProcessorCreated, userData ); } static void WasmAudioWorkletProcessorCreated( EMSCRIPTEN_WEBAUDIO_T context, EM_BOOL success, void *userData ) { - if (!success) return; // Check browser console for detailed errors + if ( !success ) return; // Check browser console for detailed errors int outputChannelCounts[1] = { 2 }; EmscriptenAudioWorkletNodeCreateOptions opts = { @@ -602,7 +602,7 @@ static void WasmAudioWorkletProcessorCreated( EMSCRIPTEN_WEBAUDIO_T context, PA_DEBUG(("Creating Wasm Audio Worklet node...\n")); EMSCRIPTEN_AUDIO_WORKLET_NODE_T node = emscripten_create_wasm_audio_worklet_node( - context, "portaudio-stream", &opts, &WebAudioHostProcessingLoop, userData); + context, "portaudio-stream", &opts, &WebAudioHostProcessingLoop, userData ); PA_DEBUG(("Connecting node to audio context destination...\n")); EM_ASM({ @@ -645,23 +645,27 @@ static EM_BOOL WebAudioHostProcessingLoop( int numInputs, const AudioSampleFrame PaUtil_SetNonInterleaved*Channel() or PaUtil_Set*Channel() here. */ - if (numInputs > 0) { + if ( numInputs > 0 ) + { assert(numInputs == 1); const AudioSampleFrame input = inputs[0]; PaUtil_SetInputFrameCount( &stream->bufferProcessor, 0 /* default to host buffer size */ ); for (int i = 0; i < input.numberOfChannels; i++) { - PaUtil_SetNonInterleavedInputChannel( &stream->bufferProcessor, i, input.data + PA_WEBAUDIO_FRAME_COUNT * i ); + PaUtil_SetNonInterleavedInputChannel( + &stream->bufferProcessor, i, input.data + PA_WEBAUDIO_FRAME_COUNT * i ); } } - if (numOutputs > 0) { + if ( numOutputs > 0 ) + { assert(numOutputs == 1); AudioSampleFrame output = outputs[0]; PaUtil_SetOutputFrameCount( &stream->bufferProcessor, 0 /* default to host buffer size */ ); for (int i = 0; i < output.numberOfChannels; i++) { - PaUtil_SetNonInterleavedOutputChannel( &stream->bufferProcessor, i, output.data + PA_WEBAUDIO_FRAME_COUNT * i ); + PaUtil_SetNonInterleavedOutputChannel( + &stream->bufferProcessor, i, output.data + PA_WEBAUDIO_FRAME_COUNT * i ); } } @@ -744,7 +748,7 @@ static PaError StartStream( PaStream *s ) /* TODO: Check if this is right, see portaudio.h for required behavior */ PA_DEBUG(("Resuming audio context...\n")); - emscripten_resume_audio_context_sync(stream->context); + emscripten_resume_audio_context_sync( stream->context ); /* Resuming the context is only allowed after the user has interacted with @@ -755,10 +759,11 @@ static PaError StartStream( PaStream *s ) TODO: Find a more elegant solution */ - while (!IsStreamActive(stream)) { + while ( !IsStreamActive(stream) ) + { PA_DEBUG(("Audio context is not running yet, waiting for user interaction...\n")); - emscripten_sleep(500); - emscripten_resume_audio_context_sync(stream->context); + emscripten_sleep( 500 ); + emscripten_resume_audio_context_sync( stream->context ); } return result; @@ -809,7 +814,7 @@ static PaError IsStreamStopped( PaStream *s ) /* TODO: Check if this is right, see portaudio.h for required behavior */ - return emscripten_audio_context_state(stream->context) != AUDIO_CONTEXT_STATE_RUNNING; + return emscripten_audio_context_state( stream->context ) != AUDIO_CONTEXT_STATE_RUNNING; } @@ -819,7 +824,7 @@ static PaError IsStreamActive( PaStream *s ) /* TODO: Check if this is right, see portaudio.h for required behavior */ - return emscripten_audio_context_state(stream->context) == AUDIO_CONTEXT_STATE_RUNNING; + return emscripten_audio_context_state( stream->context ) == AUDIO_CONTEXT_STATE_RUNNING; } From 6375db9fcfbf7e2f31286df8d43a4531795caf01 Mon Sep 17 00:00:00 2001 From: fwcd Date: Mon, 11 Mar 2024 00:30:59 +0100 Subject: [PATCH 28/32] Add utility function for suspension and suspend initially --- src/hostapi/webaudio/pa_webaudio.c | 32 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/hostapi/webaudio/pa_webaudio.c b/src/hostapi/webaudio/pa_webaudio.c index 494b9925d..ff343201c 100644 --- a/src/hostapi/webaudio/pa_webaudio.c +++ b/src/hostapi/webaudio/pa_webaudio.c @@ -109,6 +109,7 @@ static EM_BOOL WebAudioHostProcessingLoop( int numInputs, const AudioSampleFrame int numOutputs, AudioSampleFrame *outputBuffer, int numParams, const AudioParamFrame *params, void *userData ); +void WebAudioSuspendContextSync( EMSCRIPTEN_WEBAUDIO_T context ); /* IMPLEMENT ME: a macro like the following one should be used for reporting host errors */ @@ -551,6 +552,7 @@ static PaError OpenStream( struct PaUtilHostApiRepresentation *hostApi, PA_DEBUG(("Creating audio context...\n")); stream->context = emscripten_create_audio_context( &opts ); + WebAudioSuspendContextSync( stream->context ); PA_DEBUG(("Starting Wasm Audio Worklet thread...\n")); emscripten_start_wasm_audio_worklet_thread_async( @@ -714,6 +716,17 @@ static EM_BOOL WebAudioHostProcessingLoop( int numInputs, const AudioSampleFrame } +void WebAudioSuspendContextSync( EMSCRIPTEN_WEBAUDIO_T context ) +{ + EM_ASM({ + const context = emscriptenGetAudioObject($0); + Asyncify.handleAsync(async () => { + await context.suspend(); + }); + }, context); +} + + /* When CloseStream() is called, the multi-api layer ensures that the stream has already been stopped or aborted. @@ -725,10 +738,7 @@ static PaError CloseStream( PaStream* s ) /* additional stream closing + cleanup */ - EM_ASM({ - const context = emscriptenGetAudioObject($0); - context.close(); - }, stream->context); + emscripten_destroy_audio_context( stream->context ); PaUtil_TerminateBufferProcessor( &stream->bufferProcessor ); PaUtil_TerminateStreamRepresentation( &stream->streamRepresentation ); @@ -778,12 +788,7 @@ static PaError StopStream( PaStream *s ) /* TODO: Check if this is right, see portaudio.h for required behavior */ PA_DEBUG(("Suspending audio context upon stop...\n")); - EM_ASM({ - const context = emscriptenGetAudioObject($0); - Asyncify.handleAsync(async () => { - await context.suspend(); - }); - }, stream->context); + WebAudioSuspendContextSync( stream->context ); return result; } @@ -797,12 +802,7 @@ static PaError AbortStream( PaStream *s ) /* TODO: Check if this is right, see portaudio.h for required behavior */ PA_DEBUG(("Suspending audio context upon abort...\n")); - EM_ASM({ - const context = emscriptenGetAudioObject($0); - Asyncify.handleAsync(async () => { - await context.suspend(); - }); - }, stream->context); + WebAudioSuspendContextSync( stream->context ); return result; } From 831c6360e0dec868859fcef45a832c206d90bf86 Mon Sep 17 00:00:00 2001 From: fwcd Date: Mon, 11 Mar 2024 19:05:13 +0100 Subject: [PATCH 29/32] Add PA_WEBAUDIO_ASYNCIFY and experimental synchronous mode This synchronous mode uses `emscripten_thread_sleep` instead of `emscripten_sleep` and is intended to run from a separate thread where blocking is allowed. In its current state, this seems to depend on https://github.com/WebAudio/web-audio-api/issues/2423 The alternative would be to proxy audio context-related calls to the main thread. --- CMakeLists.txt | 11 +++++++++-- src/hostapi/webaudio/README.md | 3 +++ src/hostapi/webaudio/pa_webaudio.c | 14 +++++++++++++- src/os/unix/pa_unix_util.c | 5 +++++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 107942920..53bf1764f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -412,12 +412,19 @@ elseif(UNIX) target_compile_definitions(PortAudio PUBLIC PA_USE_WEBAUDIO=1) target_compile_options(PortAudio PUBLIC -pthread) target_link_options(PortAudio PUBLIC - -sASYNCIFY - -sASYNCIFY_IMPORTS=emscripten_asm_const_int -sAUDIO_WORKLET -sWASM_WORKERS -sEXPORTED_RUNTIME_METHODS=emscriptenGetAudioObject ) + + option(PA_WEBAUDIO_ASYNCIFY "Build with -sASYNCIFY and use async operations. Recommended when intending to call PortAudio from the main thread to avoid blocking." ON) + if(PA_WEBAUDIO_ASYNCIFY) + target_compile_definitions(PortAudio PUBLIC PA_WEBAUDIO_ASYNCIFY=1) + target_link_options(PortAudio PUBLIC + -sASYNCIFY + -sASYNCIFY_IMPORTS=emscripten_asm_const_int + ) + endif() set(PKGCONFIG_CFLAGS "${PKGCONFIG_CFLAGS} -DPA_USE_WEBAUDIO=1") endif() diff --git a/src/hostapi/webaudio/README.md b/src/hostapi/webaudio/README.md index 8a3c9ab9d..035a72d9e 100644 --- a/src/hostapi/webaudio/README.md +++ b/src/hostapi/webaudio/README.md @@ -14,6 +14,9 @@ emcmake cmake -B build -DPA_BUILD_EXAMPLES=ON cmake --build build ``` +> [!TIP] +> By default PortAudio will be built with `-sASYNCIFY`. This makes it safe to be called from the main thread, but also introduces overhead and potentially other issues when the application already uses other asynchronous operations. PortAudio can also be built without Asyncify (i.e. using synchronous/blocking operations) by setting `-DPA_WEBAUDIO_ASYNCIFY=OFF`, in that case it is recommended to call PortAudio from a worker thread to avoid blocking the main thread (which may result in locking up the UI or in worse cases deadlock the application, e.g. if PortAudio waits for user interaction, as it does during `Pa_StartStream`). This can be done automatically using `-sPROXY_TO_PTHREAD`. + > [!TIP] > For debug logging, set `-DPA_ENABLE_DEBUG_OUTPUT=ON` diff --git a/src/hostapi/webaudio/pa_webaudio.c b/src/hostapi/webaudio/pa_webaudio.c index ff343201c..58b59f1d1 100644 --- a/src/hostapi/webaudio/pa_webaudio.c +++ b/src/hostapi/webaudio/pa_webaudio.c @@ -718,12 +718,24 @@ static EM_BOOL WebAudioHostProcessingLoop( int numInputs, const AudioSampleFrame void WebAudioSuspendContextSync( EMSCRIPTEN_WEBAUDIO_T context ) { +#ifdef PA_WEBAUDIO_ASYNCIFY EM_ASM({ const context = emscriptenGetAudioObject($0); Asyncify.handleAsync(async () => { await context.suspend(); }); }, context); +#else + EM_ASM({ + const context = emscriptenGetAudioObject($0); + context.suspend(); + }, context); + + while ( emscripten_audio_context_state( context ) != AUDIO_CONTEXT_STATE_SUSPENDED ) + { + Pa_Sleep ( 100 ); + } +#endif } @@ -772,7 +784,7 @@ static PaError StartStream( PaStream *s ) while ( !IsStreamActive(stream) ) { PA_DEBUG(("Audio context is not running yet, waiting for user interaction...\n")); - emscripten_sleep( 500 ); + Pa_Sleep( 500 ); emscripten_resume_audio_context_sync( stream->context ); } diff --git a/src/os/unix/pa_unix_util.c b/src/os/unix/pa_unix_util.c index dca50c5af..295fcd702 100644 --- a/src/os/unix/pa_unix_util.c +++ b/src/os/unix/pa_unix_util.c @@ -53,6 +53,7 @@ #ifdef __EMSCRIPTEN__ #include +#include #endif #if defined(__APPLE__) && !defined(HAVE_MACH_ABSOLUTE_TIME) @@ -116,7 +117,11 @@ int PaUtil_CountCurrentlyAllocatedBlocks( void ) void Pa_Sleep( long msec ) { #ifdef __EMSCRIPTEN__ +#ifdef PA_WEBAUDIO_ASYNCIFY emscripten_sleep(msec); +#else + emscripten_thread_sleep(msec); +#endif #elif defined(HAVE_NANOSLEEP) struct timespec req = {0}, rem = {0}; PaTime time = msec / 1.e3; From c80758a49c12b1eb0b0f0b54c694576b0db05d5a Mon Sep 17 00:00:00 2001 From: fwcd Date: Mon, 11 Mar 2024 20:03:18 +0100 Subject: [PATCH 30/32] Experimentally build examples with PROXY_TO_PTHREAD --- examples/CMakeLists.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index eca4beb33..bbbb11fd9 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -10,6 +10,10 @@ macro(add_example appl_name) if(WIN32) set_property(TARGET ${appl_name} APPEND_STRING PROPERTY COMPILE_DEFINITIONS _CRT_SECURE_NO_WARNINGS) endif() + if(EMSCRIPTEN AND NOT PA_WEBAUDIO_ASYNCIFY) + target_compile_options(${appl_name} PUBLIC -pthread) + target_link_options(${appl_name} PUBLIC -pthread -sPROXY_TO_PTHREAD) + endif() endmacro() macro(add_example_cpp appl_name) @@ -19,6 +23,10 @@ macro(add_example_cpp appl_name) if(WIN32) set_property(TARGET ${appl_name} APPEND_STRING PROPERTY COMPILE_DEFINITIONS _CRT_SECURE_NO_WARNINGS) endif() + if(EMSCRIPTEN AND NOT PA_WEBAUDIO_ASYNCIFY) + target_compile_options(${appl_name} PUBLIC -pthread) + target_link_options(${appl_name} PUBLIC -pthread -sPROXY_TO_PTHREAD) + endif() endmacro() add_example(pa_devs) From 3a6a5b7b532192d0d180d8991e99045e6f7cdacd Mon Sep 17 00:00:00 2001 From: fwcd Date: Mon, 11 Mar 2024 20:33:58 +0100 Subject: [PATCH 31/32] Add checks to not block the main thread --- src/hostapi/webaudio/pa_webaudio.c | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/hostapi/webaudio/pa_webaudio.c b/src/hostapi/webaudio/pa_webaudio.c index 58b59f1d1..a707f1dc9 100644 --- a/src/hostapi/webaudio/pa_webaudio.c +++ b/src/hostapi/webaudio/pa_webaudio.c @@ -731,9 +731,16 @@ void WebAudioSuspendContextSync( EMSCRIPTEN_WEBAUDIO_T context ) context.suspend(); }, context); - while ( emscripten_audio_context_state( context ) != AUDIO_CONTEXT_STATE_SUSPENDED ) + if ( emscripten_is_main_browser_thread() ) { - Pa_Sleep ( 100 ); + PA_DEBUG(("Warning: Not waiting for the context to suspend since we're on the main browser thread, this may result in inconsistent behavior!")); + } + else + { + while ( emscripten_audio_context_state( context ) != AUDIO_CONTEXT_STATE_SUSPENDED ) + { + Pa_Sleep ( 100 ); + } } #endif } @@ -781,12 +788,23 @@ static PaError StartStream( PaStream *s ) TODO: Find a more elegant solution */ +#ifndef PA_WEBAUDIO_ASYNCIFY + if ( emscripten_is_main_browser_thread() ) + { + PA_DEBUG(("Warning: Not waiting for the context to initialize in StartStream since we're on the main browser thread, this may result in inconsistent behavior!")); + } + else + { +#endif while ( !IsStreamActive(stream) ) { PA_DEBUG(("Audio context is not running yet, waiting for user interaction...\n")); Pa_Sleep( 500 ); emscripten_resume_audio_context_sync( stream->context ); } +#ifndef PA_WEBAUDIO_ASYNCIFY + } +#endif return result; } From c9015e5d3fdd57634def7aad88f00ac53cefa44d Mon Sep 17 00:00:00 2001 From: fwcd Date: Mon, 11 Mar 2024 20:35:11 +0100 Subject: [PATCH 32/32] Add missing import --- src/hostapi/webaudio/pa_webaudio.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/hostapi/webaudio/pa_webaudio.c b/src/hostapi/webaudio/pa_webaudio.c index a707f1dc9..d316d3854 100644 --- a/src/hostapi/webaudio/pa_webaudio.c +++ b/src/hostapi/webaudio/pa_webaudio.c @@ -44,6 +44,7 @@ #include +#include #include #include /* strlen() */ #include