diff --git a/binding.gyp b/binding.gyp index 2c07771..5d1f644 100644 --- a/binding.gyp +++ b/binding.gyp @@ -2,7 +2,13 @@ 'targets': [ { 'target_name': 'jsaudio', - 'sources': ['src/addon.cc', 'src/jsaudio.cc', 'src/helpers.cc', 'src/stream.cc'], + 'sources': [ + 'src/addon.cc', + 'src/jsaudio.cc', + 'src/helpers.cc', + 'src/stream.cc', + 'src/callback.cc' + ], 'include_dirs': [ '= data.tableSize ) data.left_phase -= data.tableSize + data.right_phase += 3 // higher pitch so we can distinguish left and right. + if( data.right_phase >= data.tableSize ) data.right_phase -= data.tableSize + } + return 0 +} + +//Main function +function callbackSine () { + // initialize stream instance + let stream = new JsPaStream() + // initialize data that is sent to callback + let data = { + left_phase: 0, + right_phase: 0, + tableSize: tableSize, + sine: [] + } + console.log( + 'PortAudio Test: output sine wave\n', + `SR = ${sampleRate}, BufSize = ${framesPerBuffer}\n` + ) + + // initialise sinusoidal wavetable + for(var i=0; i { + JsAudio.closeStream(stream) + }, numSeconds * 1000) +} + +// Run it +callbackSine() diff --git a/examples/pa_api_callback_wire.js b/examples/pa_api_callback_wire.js new file mode 100644 index 0000000..9cc947d --- /dev/null +++ b/examples/pa_api_callback_wire.js @@ -0,0 +1,63 @@ +'use strict' + +// Requires +const JsAudioExports = require('./../lib/jsaudio') +const JsAudio = JsAudioExports.JsAudioNative +const JsPaStream = JsAudioExports.JsPaStream + +/* CALLBACK WIRE EXAMPLE */ + +// Setup +const sampleRate = 48000 +const channels = 2 +const framesPerBuffer = 8192 +const streamFlags = 0 +const tableSize = 200 +const formats = { + paFloat32: 1, + paInt32: 2, + paInt24: 4, + paInt16: 8, + paInt8: 16, + paUInt8: 32, + paCustomFormat: 65536, + paNonInterleaved: 2147483648 +} + + +//Create callback function that copies input to output +function callback (input, output, frameCount) { + var outputBufferView = new Float32Array(output) + var inputBufferView = new Float32Array(input) + + for(var i=0; i < frameCount * 2; ++i) { + outputBufferView[i] = inputBufferView[i] + } +} + +//Main function +function callbackWire () { + // initialize stream instance + let stream = new JsPaStream() + + // initialize PortAudio + JsAudio.initialize() + // setup stream options + let streamOpts = { + stream, + sampleRate, + framesPerBuffer, + streamFlags, + sampleFormat: formats.paFloat32, + numInputChannels: channels, + numOutputChannels: channels, + callback: callback + } + // open stream with options + JsAudio.openDefaultStream(streamOpts) + // start stream + JsAudio.startStream(stream) +} + +// Run it +callbackWire() diff --git a/src/callback.cc b/src/callback.cc new file mode 100644 index 0000000..5ff652c --- /dev/null +++ b/src/callback.cc @@ -0,0 +1,80 @@ +#include "callback.h" +#include "helpers.h" + +JsPaStreamCallbackBridge::JsPaStreamCallbackBridge(Callback *callback_, + size_t bytesPerFrameIn, + size_t bytesPerFrameOut, + const LocalValue &userData) + : AsyncWorker(callback_), m_frameCount(0), m_callbackResult(0), + m_bytesPerFrameIn(bytesPerFrameIn), m_bytesPerFrameOut(bytesPerFrameOut), + m_inputBuffer(nullptr), m_outputBuffer(nullptr) { + async = new uv_async_t; + uv_async_init( + uv_default_loop() + , async + , UVCallback + ); + async->data = this; + uv_barrier_init(&async_barrier, 2); + + // Save userData to persistent object + SaveToPersistent(ToLocString("userData"), userData); +} + +JsPaStreamCallbackBridge::~JsPaStreamCallbackBridge() { + uv_barrier_destroy(&async_barrier); + uv_close((uv_handle_t*)async, NULL); + +} + +void JsPaStreamCallbackBridge::dispatchJSCallback() { + HandleScope scope; + unsigned long frameCount; + v8::Local input; + v8::Local output; + v8::Local callbackReturn; + + + frameCount = m_frameCount; + + // Setup ArrayBuffer for input audio data + input = v8::ArrayBuffer::New( + v8::Isolate::GetCurrent(), + const_cast(m_inputBuffer), + m_bytesPerFrameIn * frameCount + ); + + // Setup ArrayBuffer for output audio data + output = v8::ArrayBuffer::New( + v8::Isolate::GetCurrent(), + m_outputBuffer, + m_bytesPerFrameOut * frameCount + ); + + // Create array of arguments and call the javascript callback + LocalValue argv[] = { + input, + output, + New(frameCount), + GetFromPersistent(ToLocString("userData")) + }; + m_callbackResult = LocalizeInt(callback->Call(4, argv)); + + uv_barrier_wait(&async_barrier); +} + +int JsPaStreamCallbackBridge::Execute(const void* input, void* output, unsigned long frameCount) { + m_frameCount = frameCount; + + m_inputBuffer = input; + m_outputBuffer = output; + + // Dispatch the asyncronous callback + uv_async_send(async); + + // Wait for the asyncronous callback + uv_barrier_wait(&async_barrier); + + return m_callbackResult; +} + diff --git a/src/callback.h b/src/callback.h new file mode 100644 index 0000000..d9a6291 --- /dev/null +++ b/src/callback.h @@ -0,0 +1,41 @@ +#ifndef CALLBACK_H +#define CALLBack_H + +#include "jsaudio.h" + +class JsPaStreamCallbackBridge : public AsyncWorker { +public: + explicit JsPaStreamCallbackBridge(Callback *callback_, + size_t bytesPerFrameIn, + size_t bytesPerFrameOut, + const LocalValue &userData); + explicit JsPaStreamCallbackBridge(Callback *callback_, size_t bytesPerFrame, + const LocalValue &userData) + : JsPaStreamCallbackBridge(callback_, bytesPerFrame, bytesPerFrame, userData) {} + + ~JsPaStreamCallbackBridge(); + + void dispatchJSCallback(); + int Execute(const void* input, void* output, unsigned long frameCount); + +private: + NAN_INLINE static NAUV_WORK_CB(UVCallback) { + JsPaStreamCallbackBridge *callback = + static_cast(async->data); + callback->dispatchJSCallback(); + } + + void Execute() {} + + uv_async_t *async; + uv_barrier_t async_barrier; + unsigned long m_frameCount; + size_t m_bytesPerFrameIn; + size_t m_bytesPerFrameOut; + const void* m_inputBuffer; + void* m_outputBuffer; + int m_callbackResult; + +}; + +#endif \ No newline at end of file diff --git a/src/helpers.cc b/src/helpers.cc index 88bcda8..1e01725 100644 --- a/src/helpers.cc +++ b/src/helpers.cc @@ -43,6 +43,10 @@ LocalObject ToLocObject (MaybeLocalValue lvIn) { return lvIn.ToLocalChecked()->ToObject(); } +LocalFunction ToLocFunction (MaybeLocalValue lvIn) { + return lvIn.ToLocalChecked().As(); +} + LocalString ConstCharPointerToLocString (const char* constCharPointer) { if (constCharPointer == NULL) return New("").ToLocalChecked(); std::string str(constCharPointer); @@ -108,3 +112,33 @@ PaStreamParameters LocObjToPaStreamParameters (LocalObject obj) { }; return params; } + +size_t bytesPerFrame (PaSampleFormat sampleFormat) { + size_t retVal; + + switch (sampleFormat) { + case paFloat32: + retVal = sizeof(float) * 2; + break; + case paInt32: + retVal = sizeof(int32_t) * 2; + break; + case paInt24: + retVal = (sizeof(int16_t) + sizeof(int8_t)) * 2; + break; + case paInt16: + retVal = sizeof(int16_t) * 2; + break; + case paInt8: + retVal = sizeof(int8_t) * 2; + break; + case paUInt8: + retVal = sizeof(uint16_t) * 2; + break; + default: + retVal = 0; + break; + } + + return retVal; +} diff --git a/src/helpers.h b/src/helpers.h index f9497a4..5e00d45 100644 --- a/src/helpers.h +++ b/src/helpers.h @@ -13,9 +13,11 @@ double LocalizeDouble (MaybeLocalValue lvIn); unsigned long LocalizeULong (MaybeLocalValue lvIn); LocalString ToLocString (std::string str); LocalObject ToLocObject (MaybeLocalValue lvIn); +LocalFunction ToLocFunction (MaybeLocalValue lvIn); LocalString ConstCharPointerToLocString (const char* constCharPointer); void HostApiInfoToLocalObject (LocalObject obj, const PaHostApiInfo* hai); void DeviceInfoToLocalObject (LocalObject obj, const PaDeviceInfo* di); PaStreamParameters LocObjToPaStreamParameters (LocalObject obj); +size_t bytesPerFrame (PaSampleFormat sampleFormat); #endif diff --git a/src/jsaudio.cc b/src/jsaudio.cc index 652df21..2fd505d 100644 --- a/src/jsaudio.cc +++ b/src/jsaudio.cc @@ -7,6 +7,7 @@ #include "jsaudio.h" #include "helpers.h" #include "stream.h" +#include "callback.h" /* Initialize stream and jsStreamCb as global */ LocalFunction jsStreamCb; @@ -132,7 +133,6 @@ NAN_METHOD(getDeviceInfo) { info.GetReturnValue().Set(obj); } -/* BEGIN Stream APIs */ // http://portaudio.com/docs/v19-doxydocs/portaudio_8h.html#a443ad16338191af364e3be988014cbbe NAN_METHOD(isFormatSupported) { HandleScope scope; @@ -170,6 +170,28 @@ NAN_METHOD(whyIsFormatUnsupported) { info.GetReturnValue().Set(ConstCharPointerToLocString(errText)); } +/* BEGIN Stream APIs */ +static int StreamCallbackDispatcher ( + const void *input, + void *output, + unsigned long frameCount, + const PaStreamCallbackTimeInfo *timeInfo, + PaStreamCallbackFlags statusFlags, + void *userData +) { + JsPaStreamCallbackBridge* bridge = static_cast(userData); + + // Call Js callback + return bridge->Execute(input, output, frameCount); +} + +void StreamFinishedCallback (void* userData) { + JsPaStreamCallbackBridge* bridge = static_cast(userData); + + // call JsPaStreamCallbackBridge deconstructor + delete bridge; +} + // http://portaudio.com/docs/v19-doxydocs/portaudio_8h.html#a443ad16338191af364e3be988014cbbe NAN_METHOD(openStream) { HandleScope scope; @@ -188,6 +210,16 @@ NAN_METHOD(openStream) { Get(obj, ToLocString("framesPerBuffer"))); PaStreamFlags streamFlags = static_cast( Get(obj, ToLocString("streamFlags")).ToLocalChecked()->IntegerValue()); + // Get callback Function + JsPaStreamCallbackBridge* callback = nullptr; + if (HasOwnProperty(obj, ToLocString("callback")).FromMaybe(false)) { + callback = new JsPaStreamCallbackBridge( + new Callback(ToLocFunction(Get(obj, ToLocString("callback")))), + bytesPerFrame(paramsIn.sampleFormat), + bytesPerFrame(paramsOut.sampleFormat), + Get(obj, ToLocString("sampleFormat")).FromMaybe(New()) + ); + } // Start stream PaError err = Pa_OpenStream( stream->streamPtrRef(), @@ -196,10 +228,16 @@ NAN_METHOD(openStream) { sampleRate, framesPerBuffer, streamFlags, - NULL, - NULL + callback != nullptr ? StreamCallbackDispatcher : NULL, + static_cast(callback) ); ThrowIfPaError(err); + // set a stream finished callback to mark Persitent as GC ready + // only set if there is a JsPaStreamCallbackBridge registered + if (callback != nullptr) + err = Pa_SetStreamFinishedCallback(stream->streamPtr(), StreamFinishedCallback); + + // Set return Value info.GetReturnValue().Set(true); } @@ -218,6 +256,15 @@ NAN_METHOD(openDefaultStream) { Get(obj, ToLocString("sampleFormat")))); unsigned long framesPerBuffer = LocalizeULong( Get(obj, ToLocString("framesPerBuffer"))); + // Get callback Function + JsPaStreamCallbackBridge* callback = nullptr; + if (HasOwnProperty(obj, ToLocString("callback")).FromMaybe(false)) { + callback = new JsPaStreamCallbackBridge( + new Callback(ToLocFunction(Get(obj, ToLocString("callback")))), + bytesPerFrame(sampleFormat), + Get(obj, ToLocString("userData")).FromMaybe(New()) + ); + } // Start stream PaError err = Pa_OpenDefaultStream( stream->streamPtrRef(), @@ -226,10 +273,16 @@ NAN_METHOD(openDefaultStream) { sampleFormat, sampleRate, framesPerBuffer, - NULL, - NULL + callback != nullptr ? StreamCallbackDispatcher : NULL, + static_cast(callback) ); ThrowIfPaError(err); + // set a stream finished callback to mark Persitent as GC ready + // only set if there is a JsPaStreamCallbackBridge registered + if (callback != nullptr) + err = Pa_SetStreamFinishedCallback(stream->streamPtr(), StreamFinishedCallback); + ThrowIfPaError(err); + info.GetReturnValue().Set(true); } @@ -346,6 +399,7 @@ NAN_METHOD(readStream) { // Get stream object from info[0] JsPaStream* stream = ObjectWrap::Unwrap(info[0]->ToObject()); // Get the buffer data from info[1] + // Uses float as template type, but should work regardless of sample format TypedArrayContents buffer(info[1]); // Get frames from info[2] unsigned long frames = info[2]->Uint32Value(); @@ -360,6 +414,7 @@ NAN_METHOD(writeStream) { // Get stream object from info[0] JsPaStream* stream = ObjectWrap::Unwrap(info[0]->ToObject()); // Get the buffer data from info[1] + // Uses float as template type, but should work regardless of sample format TypedArrayContents buffer(info[1]); // Get frames from info[2] unsigned long frames = info[2]->Uint32Value();