From caeaf858044e6e058105822f473ba3a9edf785bd Mon Sep 17 00:00:00 2001 From: Jacob Peters Date: Tue, 21 Jun 2016 22:37:10 -0400 Subject: [PATCH 01/21] Revert "Merge with main fork" From 3556ef8fef29d0864c2ba801e782f17fd35627e5 Mon Sep 17 00:00:00 2001 From: Jacob Peters Date: Wed, 22 Jun 2016 01:59:31 -0400 Subject: [PATCH 02/21] Initial implementation of JsPaStreamCallback class Note that this is not a direct implementation of the PaStreamCallback type. Instead it is a class to facilitate the cross thread communication between the portaudio thread and the node event thread. --- binding.gyp | 8 +++++++- src/callback.cc | 32 ++++++++++++++++++++++++++++++++ src/callback.h | 28 ++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 src/callback.cc create mode 100644 src/callback.h 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 = this; + + uv_mutex_init(&async_lock); +} + +JsPaStreamCallback::~JsPaStreamCallback() { + uv_mutex_destroy(&async_lock); +} + +int JsPaStreamCallback::sendToCallback(unsigned long frameCount) { + uv_async_send(async); + return 0; +} + +void JsPaStreamCallback::dispatchJSCallback() { + HandleScope scope; + callback->Call(0, NULL); +} + +void* JsPaStreamCallback::consumeAudioData(unsigned long frameCount) { + //stub +} \ No newline at end of file diff --git a/src/callback.h b/src/callback.h new file mode 100644 index 0000000..85a24b7 --- /dev/null +++ b/src/callback.h @@ -0,0 +1,28 @@ +#ifndef CALLBACK_H +#define CALLBack_H + +#include "jsaudio.h" + +class JsPaStreamCallback : public AsyncWorker { +public: + explicit JsPaStreamCallback(Callback *callback_); + ~JsPaStreamCallback(); + + int sendToCallback(unsigned long frameCount); + void dispatchJSCallback(); + + void* consumeAudioData(unsigned long frameCount); + +private: + NAN_INLINE static NAUV_WORK_CB(UVCallback) { + JsPaStreamCallback *callback = + static_cast(async->data); + callback->dispatchJSCallback(); + } + void Execute() {} + + uv_async_t *async; + uv_mutex_t async_lock; +}; + +#endif \ No newline at end of file From e6bc97794e652148ee2bb4a0a8710200f7c3aa36 Mon Sep 17 00:00:00 2001 From: Jacob Peters Date: Wed, 22 Jun 2016 05:00:15 -0400 Subject: [PATCH 03/21] Added callback code to PaOpenDefaultStream --- src/helpers.cc | 4 ++++ src/helpers.h | 1 + src/jsaudio.cc | 27 ++++++++++++++++++++++++--- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/helpers.cc b/src/helpers.cc index 88bcda8..2db5433 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); diff --git a/src/helpers.h b/src/helpers.h index f9497a4..55fe1e4 100644 --- a/src/helpers.h +++ b/src/helpers.h @@ -13,6 +13,7 @@ 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); diff --git a/src/jsaudio.cc b/src/jsaudio.cc index 652df21..f6533ce 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,19 @@ 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 +) { + JsPaStreamCallback* callback = static_cast(userData); + return callback->sendToCallback(frameCount); +} + // http://portaudio.com/docs/v19-doxydocs/portaudio_8h.html#a443ad16338191af364e3be988014cbbe NAN_METHOD(openStream) { HandleScope scope; @@ -210,6 +223,13 @@ NAN_METHOD(openDefaultStream) { LocalObject obj = info[0]->ToObject(); JsPaStream* stream = ObjectWrap::Unwrap( ToLocObject(Get(obj, ToLocString("stream")))); + // Get callback Function + JsPaStreamCallback* callback = NULL; + bool hasCallback = HasOwnProperty(obj, ToLocString("callback")).FromMaybe(false); + if (hasCallback) { + Callback* function = new Callback(ToLocFunction(Get(obj, ToLocString("callback")))); + callback = new JsPaStreamCallback(function); + } // Get stream options int inputChannels = LocalizeInt(Get(obj, ToLocString("numInputChannels"))); int outputChannels = LocalizeInt(Get(obj, ToLocString("numOutputChannels"))); @@ -226,9 +246,10 @@ NAN_METHOD(openDefaultStream) { sampleFormat, sampleRate, framesPerBuffer, - NULL, - NULL + hasCallback ? StreamCallbackDispatcher : NULL, + static_cast(callback) ); + callback->dispatchJSCallback(); ThrowIfPaError(err); info.GetReturnValue().Set(true); } From e7cf0df797556ace64a5ec8e7d7750f153ff4499 Mon Sep 17 00:00:00 2001 From: Jacob Peters Date: Wed, 22 Jun 2016 05:00:55 -0400 Subject: [PATCH 04/21] Created a test for the callback --- test/callback_test.js | 53 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 test/callback_test.js diff --git a/test/callback_test.js b/test/callback_test.js new file mode 100644 index 0000000..8a73400 --- /dev/null +++ b/test/callback_test.js @@ -0,0 +1,53 @@ +'use strict' + +// Requires +const JsAudioExports = require('./../lib/jsaudio') +const JsAudio = JsAudioExports.JsAudioNative +const JsPaStream = JsAudioExports.JsPaStream + +/* BLOCKING SINE EXAMPLE +http://portaudio.com/docs/v19-doxydocs/paex__write__sine_8c_source.html +*/ + +// 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 +} + +function blockingSine () { + // 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: ()=> {console.log('Callback')} + } + // open stream with options + console.log(JsAudio.openDefaultStream(streamOpts)); + + JsAudio.startStream(stream); +} + +// Run it +blockingSine() From 75104cb6e349357ec1170e1abdc15d0861a100e4 Mon Sep 17 00:00:00 2001 From: Jacob Peters Date: Wed, 22 Jun 2016 05:05:32 -0400 Subject: [PATCH 05/21] Removed debugging code --- src/jsaudio.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/src/jsaudio.cc b/src/jsaudio.cc index f6533ce..dd2bfed 100644 --- a/src/jsaudio.cc +++ b/src/jsaudio.cc @@ -249,7 +249,6 @@ NAN_METHOD(openDefaultStream) { hasCallback ? StreamCallbackDispatcher : NULL, static_cast(callback) ); - callback->dispatchJSCallback(); ThrowIfPaError(err); info.GetReturnValue().Set(true); } From b78eedb2b02775788cfa34c5a0d8039326ff5472 Mon Sep 17 00:00:00 2001 From: Jacob Peters Date: Wed, 22 Jun 2016 17:17:33 -0400 Subject: [PATCH 06/21] Added callback code to PaOpenStream --- src/jsaudio.cc | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/jsaudio.cc b/src/jsaudio.cc index dd2bfed..362fefd 100644 --- a/src/jsaudio.cc +++ b/src/jsaudio.cc @@ -190,6 +190,13 @@ NAN_METHOD(openStream) { LocalObject obj = info[0]->ToObject(); JsPaStream* stream = ObjectWrap::Unwrap( ToLocObject(Get(obj, ToLocString("stream")))); + // Get callback Function + JsPaStreamCallback* callback = NULL; + bool hasCallback = HasOwnProperty(obj, ToLocString("callback")).FromMaybe(false); + if (hasCallback) { + Callback* function = new Callback(ToLocFunction(Get(obj, ToLocString("callback")))); + callback = new JsPaStreamCallback(function); + } // Prepare in / out params LocalObject objInput = ToLocObject(Get(obj, ToLocString("input"))); LocalObject objOutput = ToLocObject(Get(obj, ToLocString("output"))); @@ -209,8 +216,8 @@ NAN_METHOD(openStream) { sampleRate, framesPerBuffer, streamFlags, - NULL, - NULL + hasCallback ? StreamCallbackDispatcher : NULL, + static_cast(callback) ); ThrowIfPaError(err); info.GetReturnValue().Set(true); From b4663297ddfc002c28afd54f32a772a82e531e21 Mon Sep 17 00:00:00 2001 From: Jacob Peters Date: Wed, 22 Jun 2016 22:30:31 -0400 Subject: [PATCH 07/21] Renamed JsPaStreamCallback to JsPaStreamCallbackBridge This will prevent anyone from misunderstanding what the class is really doing. --- src/callback.cc | 10 +++++----- src/callback.h | 10 +++++----- src/jsaudio.cc | 15 +++++++++------ 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/callback.cc b/src/callback.cc index aaf5527..6064691 100644 --- a/src/callback.cc +++ b/src/callback.cc @@ -1,6 +1,6 @@ #include "callback.h" -JsPaStreamCallback::JsPaStreamCallback(Callback *callback_) +JsPaStreamCallbackBridge::JsPaStreamCallbackBridge(Callback *callback_) : AsyncWorker(callback_) { async = new uv_async_t; uv_async_init( @@ -13,20 +13,20 @@ JsPaStreamCallback::JsPaStreamCallback(Callback *callback_) uv_mutex_init(&async_lock); } -JsPaStreamCallback::~JsPaStreamCallback() { +JsPaStreamCallbackBridge::~JsPaStreamCallbackBridge() { uv_mutex_destroy(&async_lock); } -int JsPaStreamCallback::sendToCallback(unsigned long frameCount) { +int JsPaStreamCallbackBridge::sendToCallback(unsigned long frameCount) { uv_async_send(async); return 0; } -void JsPaStreamCallback::dispatchJSCallback() { +void JsPaStreamCallbackBridge::dispatchJSCallback() { HandleScope scope; callback->Call(0, NULL); } -void* JsPaStreamCallback::consumeAudioData(unsigned long frameCount) { +void* JsPaStreamCallbackBridge::consumeAudioData(unsigned long frameCount) { //stub } \ No newline at end of file diff --git a/src/callback.h b/src/callback.h index 85a24b7..7a85128 100644 --- a/src/callback.h +++ b/src/callback.h @@ -3,10 +3,10 @@ #include "jsaudio.h" -class JsPaStreamCallback : public AsyncWorker { +class JsPaStreamCallbackBridge : public AsyncWorker { public: - explicit JsPaStreamCallback(Callback *callback_); - ~JsPaStreamCallback(); + explicit JsPaStreamCallbackBridge(Callback *callback_); + ~JsPaStreamCallbackBridge(); int sendToCallback(unsigned long frameCount); void dispatchJSCallback(); @@ -15,8 +15,8 @@ class JsPaStreamCallback : public AsyncWorker { private: NAN_INLINE static NAUV_WORK_CB(UVCallback) { - JsPaStreamCallback *callback = - static_cast(async->data); + JsPaStreamCallbackBridge *callback = + static_cast(async->data); callback->dispatchJSCallback(); } void Execute() {} diff --git a/src/jsaudio.cc b/src/jsaudio.cc index 362fefd..1ad7d64 100644 --- a/src/jsaudio.cc +++ b/src/jsaudio.cc @@ -179,8 +179,11 @@ static int StreamCallbackDispatcher ( PaStreamCallbackFlags statusFlags, void *userData ) { - JsPaStreamCallback* callback = static_cast(userData); - return callback->sendToCallback(frameCount); + JsPaStreamCallbackBridge* callback = static_cast(userData); + + callback->sendToCallback(frameCount); + + return 0; } // http://portaudio.com/docs/v19-doxydocs/portaudio_8h.html#a443ad16338191af364e3be988014cbbe @@ -191,11 +194,11 @@ NAN_METHOD(openStream) { JsPaStream* stream = ObjectWrap::Unwrap( ToLocObject(Get(obj, ToLocString("stream")))); // Get callback Function - JsPaStreamCallback* callback = NULL; + JsPaStreamCallbackBridge* callback = NULL; bool hasCallback = HasOwnProperty(obj, ToLocString("callback")).FromMaybe(false); if (hasCallback) { Callback* function = new Callback(ToLocFunction(Get(obj, ToLocString("callback")))); - callback = new JsPaStreamCallback(function); + callback = new JsPaStreamCallbackBridge(function); } // Prepare in / out params LocalObject objInput = ToLocObject(Get(obj, ToLocString("input"))); @@ -231,11 +234,11 @@ NAN_METHOD(openDefaultStream) { JsPaStream* stream = ObjectWrap::Unwrap( ToLocObject(Get(obj, ToLocString("stream")))); // Get callback Function - JsPaStreamCallback* callback = NULL; + JsPaStreamCallbackBridge* callback = NULL; bool hasCallback = HasOwnProperty(obj, ToLocString("callback")).FromMaybe(false); if (hasCallback) { Callback* function = new Callback(ToLocFunction(Get(obj, ToLocString("callback")))); - callback = new JsPaStreamCallback(function); + callback = new JsPaStreamCallbackBridge(function); } // Get stream options int inputChannels = LocalizeInt(Get(obj, ToLocString("numInputChannels"))); From 8788ebca9960820cfa569d328b371a473d5a7b01 Mon Sep 17 00:00:00 2001 From: Jacob Peters Date: Thu, 23 Jun 2016 03:57:04 -0400 Subject: [PATCH 08/21] Further Implement JsPaStreamCallbackBridge --- src/callback.cc | 81 ++++++++++++++++++++++++++++++++++++++++++++++--- src/callback.h | 7 +++-- 2 files changed, 81 insertions(+), 7 deletions(-) diff --git a/src/callback.cc b/src/callback.cc index 6064691..59a3977 100644 --- a/src/callback.cc +++ b/src/callback.cc @@ -1,7 +1,7 @@ #include "callback.h" JsPaStreamCallbackBridge::JsPaStreamCallbackBridge(Callback *callback_) - : AsyncWorker(callback_) { + : AsyncWorker(callback_), m_frameCount(0), m_inputBuffer(nullptr), m_outputBuffer(nullptr) { async = new uv_async_t; uv_async_init( uv_default_loop() @@ -17,16 +17,87 @@ JsPaStreamCallbackBridge::~JsPaStreamCallbackBridge() { uv_mutex_destroy(&async_lock); } -int JsPaStreamCallbackBridge::sendToCallback(unsigned long frameCount) { +int JsPaStreamCallbackBridge::sendToCallback(const void* input, unsigned long frameCount) { + uv_mutex_lock(&async_lock); + m_frameCount = frameCount; + + if(m_inputBuffer != nullptr) + free(m_inputBuffer); + m_inputBuffer = malloc(sizeof(float) * frameCount * 2); + + memmove( + m_inputBuffer, + input, + sizeof(float) * frameCount * 2 + ); + uv_mutex_unlock(&async_lock); + uv_async_send(async); return 0; } void JsPaStreamCallbackBridge::dispatchJSCallback() { HandleScope scope; - callback->Call(0, NULL); + unsigned long frameCount; + v8::Local input; + v8::Local output; + v8::Local callbackReturn; + + uv_mutex_lock(&async_lock); + + frameCount = m_frameCount; + + // Setup ArrayBuffer for input audio data + input = v8::ArrayBuffer::New( + v8::Isolate::GetCurrent(), + sizeof(float) * frameCount * 2 + ); + // Copy input audio data from bridge buffer to ArrayBuffer + memmove( + input->GetContents().Data(), + m_inputBuffer, + input->ByteLength() + ); + + // Setup ArrayBuffer for output audio data + output = v8::ArrayBuffer::New( + v8::Isolate::GetCurrent(), + sizeof(float) * frameCount * 2 + ); + + LocalValue argv[] = { + input, + output, + New(frameCount) + }; + callbackReturn = callback->Call(3, argv); + + if(m_outputBuffer != nullptr) + free(m_outputBuffer); + m_outputBuffer = malloc(output->ByteLength()); + // Copy output audio data from bridge buffer to ArrayBuffer + memmove( + m_outputBuffer, + output->GetContents().Data(), + output->ByteLength() + ); + + + uv_mutex_unlock(&async_lock); + } -void* JsPaStreamCallbackBridge::consumeAudioData(unsigned long frameCount) { - //stub +void JsPaStreamCallbackBridge::consumeAudioData(void* output, unsigned long frameCount) { + + if(m_outputBuffer != nullptr) { + memmove( + output, + m_outputBuffer, + sizeof(float) * frameCount * 2 + ); + + // Free the output buffer and set it to nullptr to prevent it from sending the same output data twice + free(m_outputBuffer); + m_outputBuffer = nullptr; + } } \ No newline at end of file diff --git a/src/callback.h b/src/callback.h index 7a85128..39f7352 100644 --- a/src/callback.h +++ b/src/callback.h @@ -8,10 +8,10 @@ class JsPaStreamCallbackBridge : public AsyncWorker { explicit JsPaStreamCallbackBridge(Callback *callback_); ~JsPaStreamCallbackBridge(); - int sendToCallback(unsigned long frameCount); + int sendToCallback(const void* input, unsigned long frameCount); void dispatchJSCallback(); - void* consumeAudioData(unsigned long frameCount); + void consumeAudioData(void* output, unsigned long frameCount); private: NAN_INLINE static NAUV_WORK_CB(UVCallback) { @@ -23,6 +23,9 @@ class JsPaStreamCallbackBridge : public AsyncWorker { uv_async_t *async; uv_mutex_t async_lock; + unsigned long m_frameCount; + void* m_inputBuffer; + void* m_outputBuffer; }; #endif \ No newline at end of file From 13dcc8d81134a5d6cacaa1d4aae6642daab6c635 Mon Sep 17 00:00:00 2001 From: Jacob Peters Date: Thu, 23 Jun 2016 04:00:04 -0400 Subject: [PATCH 09/21] Update intermediary PortAudio callback --- src/jsaudio.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/jsaudio.cc b/src/jsaudio.cc index 1ad7d64..5612d7c 100644 --- a/src/jsaudio.cc +++ b/src/jsaudio.cc @@ -179,9 +179,10 @@ static int StreamCallbackDispatcher ( PaStreamCallbackFlags statusFlags, void *userData ) { - JsPaStreamCallbackBridge* callback = static_cast(userData); + JsPaStreamCallbackBridge* bridge = static_cast(userData); - callback->sendToCallback(frameCount); + bridge->sendToCallback(input, frameCount); + bridge->consumeAudioData(output, frameCount); return 0; } From 86a9fa8d682b1eec020d160114d5629d9cc463ae Mon Sep 17 00:00:00 2001 From: Jacob Peters Date: Thu, 23 Jun 2016 04:00:44 -0400 Subject: [PATCH 10/21] Add sine output to test script --- test/callback_test.js | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/test/callback_test.js b/test/callback_test.js index 8a73400..8664a86 100644 --- a/test/callback_test.js +++ b/test/callback_test.js @@ -12,9 +12,9 @@ http://portaudio.com/docs/v19-doxydocs/paex__write__sine_8c_source.html // Setup const sampleRate = 48000 const channels = 2 -const framesPerBuffer = 8192 +const framesPerBuffer = 2048 const streamFlags = 0 -const tableSize = 200 +const tableSize = 400 const formats = { paFloat32: 1, paInt32: 2, @@ -26,6 +26,34 @@ const formats = { paNonInterleaved: 2147483648 } +var left_phase = 0 +var right_phase = 0 +var sine = [] + +// initialise sinusoidal wavetable +for(var i=0; i= tableSize ) left_phase -= tableSize; + right_phase += 3 /* higher pitch so we can distinguish left and right. */ + if( right_phase >= tableSize ) right_phase -= tableSize; + } +} + + + + +//Main function function blockingSine () { // initialize stream instance let stream = new JsPaStream() @@ -41,10 +69,10 @@ function blockingSine () { sampleFormat: formats.paFloat32, numInputChannels: channels, numOutputChannels: channels, - callback: ()=> {console.log('Callback')} + callback: SineCallback } // open stream with options - console.log(JsAudio.openDefaultStream(streamOpts)); + console.log(JsAudio.openDefaultStream(streamOpts)) JsAudio.startStream(stream); } From 0f43c39198be5d8440b086053026c37544008dd4 Mon Sep 17 00:00:00 2001 From: Jacob Peters Date: Thu, 23 Jun 2016 07:59:58 -0400 Subject: [PATCH 11/21] Refactoring of callback test into example --- examples/pa_api_callback_sine.js | 90 ++++++++++++++++++++++++++++++++ test/callback_test.js | 81 ---------------------------- 2 files changed, 90 insertions(+), 81 deletions(-) create mode 100644 examples/pa_api_callback_sine.js delete mode 100644 test/callback_test.js diff --git a/examples/pa_api_callback_sine.js b/examples/pa_api_callback_sine.js new file mode 100644 index 0000000..230757b --- /dev/null +++ b/examples/pa_api_callback_sine.js @@ -0,0 +1,90 @@ +'use strict' + +// Requires +const JsAudioExports = require('./../lib/jsaudio') +const JsAudio = JsAudioExports.JsAudioNative +const JsPaStream = JsAudioExports.JsPaStream + +/* Callback SINE EXAMPLE +http://portaudio.com/docs/v19-doxydocs/paex__sine_8c_source.html +*/ + +// 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 class that outputs a sinewave +class SineCallback { + + constructor(tableSize) { + this.left_phase = 0 + this.right_phase = 0 + this.tableSize = tableSize + this.sine = []; + + // initialise sinusoidal wavetable + for(var i=0; i= this.tableSize ) this.left_phase -= this.tableSize; + this.right_phase += 3 /* higher pitch so we can distinguish left and right. */ + if( this.right_phase >= this.tableSize ) this.right_phase -= this.tableSize; + } + } +} + +//Main function +function callbackSine () { + // initialize stream instance + let stream = new JsPaStream(), + callback = new SineCallback(200), + test = 12 + + // initialize PortAudio + JsAudio.initialize() + // setup stream options + let streamOpts = { + stream, + sampleRate, + framesPerBuffer, + streamFlags, + sampleFormat: formats.paFloat32, + numInputChannels: channels, + numOutputChannels: channels, + callback: (input, output, frameCount)=>{ + return callback.callback(input, output, frameCount) + } + } + // open stream with options + JsAudio.openDefaultStream(streamOpts) + + JsAudio.startStream(stream); +} + +// Run it +callbackSine() diff --git a/test/callback_test.js b/test/callback_test.js deleted file mode 100644 index 8664a86..0000000 --- a/test/callback_test.js +++ /dev/null @@ -1,81 +0,0 @@ -'use strict' - -// Requires -const JsAudioExports = require('./../lib/jsaudio') -const JsAudio = JsAudioExports.JsAudioNative -const JsPaStream = JsAudioExports.JsPaStream - -/* BLOCKING SINE EXAMPLE -http://portaudio.com/docs/v19-doxydocs/paex__write__sine_8c_source.html -*/ - -// Setup -const sampleRate = 48000 -const channels = 2 -const framesPerBuffer = 2048 -const streamFlags = 0 -const tableSize = 400 -const formats = { - paFloat32: 1, - paInt32: 2, - paInt24: 4, - paInt16: 8, - paInt8: 16, - paUInt8: 32, - paCustomFormat: 65536, - paNonInterleaved: 2147483648 -} - -var left_phase = 0 -var right_phase = 0 -var sine = [] - -// initialise sinusoidal wavetable -for(var i=0; i= tableSize ) left_phase -= tableSize; - right_phase += 3 /* higher pitch so we can distinguish left and right. */ - if( right_phase >= tableSize ) right_phase -= tableSize; - } -} - - - - -//Main function -function blockingSine () { - // 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: SineCallback - } - // open stream with options - console.log(JsAudio.openDefaultStream(streamOpts)) - - JsAudio.startStream(stream); -} - -// Run it -blockingSine() From 099679014817bafa78de5900c74fb990b2a6b19b Mon Sep 17 00:00:00 2001 From: Jacob Peters Date: Fri, 24 Jun 2016 02:48:08 -0400 Subject: [PATCH 12/21] Cleaned up example code --- examples/pa_api_callback_sine.js | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/examples/pa_api_callback_sine.js b/examples/pa_api_callback_sine.js index 230757b..49b206f 100644 --- a/examples/pa_api_callback_sine.js +++ b/examples/pa_api_callback_sine.js @@ -5,7 +5,7 @@ const JsAudioExports = require('./../lib/jsaudio') const JsAudio = JsAudioExports.JsAudioNative const JsPaStream = JsAudioExports.JsPaStream -/* Callback SINE EXAMPLE +/* CALLBACK SINE EXAMPLE http://portaudio.com/docs/v19-doxydocs/paex__sine_8c_source.html */ @@ -29,12 +29,11 @@ const formats = { //Create callback class that outputs a sinewave class SineCallback { - - constructor(tableSize) { + constructor (tableSize) { this.left_phase = 0 this.right_phase = 0 this.tableSize = tableSize - this.sine = []; + this.sine = [] // initialise sinusoidal wavetable for(var i=0; i= this.tableSize ) this.left_phase -= this.tableSize; - this.right_phase += 3 /* higher pitch so we can distinguish left and right. */ - if( this.right_phase >= this.tableSize ) this.right_phase -= this.tableSize; + if( this.left_phase >= this.tableSize ) this.left_phase -= this.tableSize + this.right_phase += 3 // higher pitch so we can distinguish left and right. + if( this.right_phase >= this.tableSize ) this.right_phase -= this.tableSize } } } @@ -76,14 +75,12 @@ function callbackSine () { sampleFormat: formats.paFloat32, numInputChannels: channels, numOutputChannels: channels, - callback: (input, output, frameCount)=>{ - return callback.callback(input, output, frameCount) - } + callback: callback.callback.bind(callback) } // open stream with options JsAudio.openDefaultStream(streamOpts) - - JsAudio.startStream(stream); + // start stream + JsAudio.startStream(stream) } // Run it From 80249ef39f05aa59be9f2bbfac8e3a224cbe0256 Mon Sep 17 00:00:00 2001 From: Jacob Peters Date: Fri, 24 Jun 2016 17:19:47 -0400 Subject: [PATCH 13/21] Refactored CallbackBridge to work with more sample formats --- src/callback.cc | 38 ++++++++++++++++++++++++++++++++------ src/callback.h | 8 +++++++- src/helpers.cc | 30 ++++++++++++++++++++++++++++++ src/helpers.h | 1 + src/jsaudio.cc | 35 +++++++++++++++++++---------------- 5 files changed, 89 insertions(+), 23 deletions(-) diff --git a/src/callback.cc b/src/callback.cc index 59a3977..1a0c122 100644 --- a/src/callback.cc +++ b/src/callback.cc @@ -1,7 +1,27 @@ #include "callback.h" -JsPaStreamCallbackBridge::JsPaStreamCallbackBridge(Callback *callback_) - : AsyncWorker(callback_), m_frameCount(0), m_inputBuffer(nullptr), m_outputBuffer(nullptr) { +JsPaStreamCallbackBridge::JsPaStreamCallbackBridge(Callback *callback_, + size_t bytesPerFrame) + : AsyncWorker(callback_), m_frameCount(0), + m_bytesPerFrameIn(bytesPerFrame), m_bytesPerFrameOut(bytesPerFrame), + m_inputBuffer(nullptr), m_outputBuffer(nullptr) { + async = new uv_async_t; + uv_async_init( + uv_default_loop() + , async + , UVCallback + ); + async->data = this; + + uv_mutex_init(&async_lock); +} + +JsPaStreamCallbackBridge::JsPaStreamCallbackBridge(Callback *callback_, + size_t bytesPerFrameIn, + size_t bytesPerFrameOut) + : AsyncWorker(callback_), m_frameCount(0), + m_bytesPerFrameIn(bytesPerFrameIn), m_bytesPerFrameOut(bytesPerFrameOut), + m_inputBuffer(nullptr), m_outputBuffer(nullptr) { async = new uv_async_t; uv_async_init( uv_default_loop() @@ -15,6 +35,12 @@ JsPaStreamCallbackBridge::JsPaStreamCallbackBridge(Callback *callback_) JsPaStreamCallbackBridge::~JsPaStreamCallbackBridge() { uv_mutex_destroy(&async_lock); + + //free buffer memory + if(m_inputBuffer != nullptr) + free(m_inputBuffer); + if(m_outputBuffer != nullptr) + free(m_outputBuffer); } int JsPaStreamCallbackBridge::sendToCallback(const void* input, unsigned long frameCount) { @@ -28,7 +54,7 @@ int JsPaStreamCallbackBridge::sendToCallback(const void* input, unsigned long fr memmove( m_inputBuffer, input, - sizeof(float) * frameCount * 2 + m_bytesPerFrameIn * frameCount ); uv_mutex_unlock(&async_lock); @@ -50,7 +76,7 @@ void JsPaStreamCallbackBridge::dispatchJSCallback() { // Setup ArrayBuffer for input audio data input = v8::ArrayBuffer::New( v8::Isolate::GetCurrent(), - sizeof(float) * frameCount * 2 + m_bytesPerFrameIn * frameCount ); // Copy input audio data from bridge buffer to ArrayBuffer memmove( @@ -62,7 +88,7 @@ void JsPaStreamCallbackBridge::dispatchJSCallback() { // Setup ArrayBuffer for output audio data output = v8::ArrayBuffer::New( v8::Isolate::GetCurrent(), - sizeof(float) * frameCount * 2 + m_bytesPerFrameOut * frameCount ); LocalValue argv[] = { @@ -93,7 +119,7 @@ void JsPaStreamCallbackBridge::consumeAudioData(void* output, unsigned long fram memmove( output, m_outputBuffer, - sizeof(float) * frameCount * 2 + m_bytesPerFrameOut * frameCount ); // Free the output buffer and set it to nullptr to prevent it from sending the same output data twice diff --git a/src/callback.h b/src/callback.h index 39f7352..879887d 100644 --- a/src/callback.h +++ b/src/callback.h @@ -5,7 +5,11 @@ class JsPaStreamCallbackBridge : public AsyncWorker { public: - explicit JsPaStreamCallbackBridge(Callback *callback_); + explicit JsPaStreamCallbackBridge(Callback *callback_, + size_t bytesPerFrame); + explicit JsPaStreamCallbackBridge(Callback *callback_, + size_t bytesPerFrameIn, + size_t bytesPerFrameOut); ~JsPaStreamCallbackBridge(); int sendToCallback(const void* input, unsigned long frameCount); @@ -24,6 +28,8 @@ class JsPaStreamCallbackBridge : public AsyncWorker { uv_async_t *async; uv_mutex_t async_lock; unsigned long m_frameCount; + size_t m_bytesPerFrameIn; + size_t m_bytesPerFrameOut; void* m_inputBuffer; void* m_outputBuffer; }; diff --git a/src/helpers.cc b/src/helpers.cc index 2db5433..1e01725 100644 --- a/src/helpers.cc +++ b/src/helpers.cc @@ -112,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 55fe1e4..5e00d45 100644 --- a/src/helpers.h +++ b/src/helpers.h @@ -18,5 +18,6 @@ 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 5612d7c..866f86e 100644 --- a/src/jsaudio.cc +++ b/src/jsaudio.cc @@ -194,13 +194,6 @@ NAN_METHOD(openStream) { LocalObject obj = info[0]->ToObject(); JsPaStream* stream = ObjectWrap::Unwrap( ToLocObject(Get(obj, ToLocString("stream")))); - // Get callback Function - JsPaStreamCallbackBridge* callback = NULL; - bool hasCallback = HasOwnProperty(obj, ToLocString("callback")).FromMaybe(false); - if (hasCallback) { - Callback* function = new Callback(ToLocFunction(Get(obj, ToLocString("callback")))); - callback = new JsPaStreamCallbackBridge(function); - } // Prepare in / out params LocalObject objInput = ToLocObject(Get(obj, ToLocString("input"))); LocalObject objOutput = ToLocObject(Get(obj, ToLocString("output"))); @@ -212,6 +205,15 @@ 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) + ); + } // Start stream PaError err = Pa_OpenStream( stream->streamPtrRef(), @@ -220,7 +222,7 @@ NAN_METHOD(openStream) { sampleRate, framesPerBuffer, streamFlags, - hasCallback ? StreamCallbackDispatcher : NULL, + callback != nullptr ? StreamCallbackDispatcher : NULL, static_cast(callback) ); ThrowIfPaError(err); @@ -234,13 +236,6 @@ NAN_METHOD(openDefaultStream) { LocalObject obj = info[0]->ToObject(); JsPaStream* stream = ObjectWrap::Unwrap( ToLocObject(Get(obj, ToLocString("stream")))); - // Get callback Function - JsPaStreamCallbackBridge* callback = NULL; - bool hasCallback = HasOwnProperty(obj, ToLocString("callback")).FromMaybe(false); - if (hasCallback) { - Callback* function = new Callback(ToLocFunction(Get(obj, ToLocString("callback")))); - callback = new JsPaStreamCallbackBridge(function); - } // Get stream options int inputChannels = LocalizeInt(Get(obj, ToLocString("numInputChannels"))); int outputChannels = LocalizeInt(Get(obj, ToLocString("numOutputChannels"))); @@ -249,6 +244,14 @@ 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) + ); + } // Start stream PaError err = Pa_OpenDefaultStream( stream->streamPtrRef(), @@ -257,7 +260,7 @@ NAN_METHOD(openDefaultStream) { sampleFormat, sampleRate, framesPerBuffer, - hasCallback ? StreamCallbackDispatcher : NULL, + callback != nullptr ? StreamCallbackDispatcher : NULL, static_cast(callback) ); ThrowIfPaError(err); From a0f3d5d8c24fd8ebdbe52d018730d15aa05609c8 Mon Sep 17 00:00:00 2001 From: Jacob Peters Date: Fri, 24 Jun 2016 17:45:02 -0400 Subject: [PATCH 14/21] Added comments to ReadStream and WriteStream to clarify code The only thing that Nan::TypedArrayContents uses the template types for are the length attribute and the pointer type. Both are irrelevant in the current use case. The length attribute isn't used, and the PAReadStream and PaWriteStream functions both use void pointers as parameters. --- src/jsaudio.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/jsaudio.cc b/src/jsaudio.cc index 866f86e..d6dfc1a 100644 --- a/src/jsaudio.cc +++ b/src/jsaudio.cc @@ -380,6 +380,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(); @@ -394,6 +395,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(); From b6ae676825e15712bfd880fe8b1ee1b0eef4f8f1 Mon Sep 17 00:00:00 2001 From: Jacob Peters Date: Fri, 24 Jun 2016 18:08:29 -0400 Subject: [PATCH 15/21] Removed unused variable --- examples/pa_api_callback_sine.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/pa_api_callback_sine.js b/examples/pa_api_callback_sine.js index 49b206f..a51682a 100644 --- a/examples/pa_api_callback_sine.js +++ b/examples/pa_api_callback_sine.js @@ -61,8 +61,7 @@ class SineCallback { function callbackSine () { // initialize stream instance let stream = new JsPaStream(), - callback = new SineCallback(200), - test = 12 + callback = new SineCallback(200) // initialize PortAudio JsAudio.initialize() From 4fbb10dcc1a49bbcd99efa753dc490fd2c99a715 Mon Sep 17 00:00:00 2001 From: Jacob Peters Date: Fri, 24 Jun 2016 18:36:33 -0400 Subject: [PATCH 16/21] Added a callback wire example --- examples/pa_api_callback_wire.js | 63 ++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 examples/pa_api_callback_wire.js 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() From 08e50458cbe5ca77751da36de5c12d2e6665b4e5 Mon Sep 17 00:00:00 2001 From: Jacob Peters Date: Fri, 24 Jun 2016 19:45:58 -0400 Subject: [PATCH 17/21] Refactor JsPaStreamCallbackBridge to accept a userData Object --- src/callback.cc | 28 +++++++++------------------- src/callback.h | 11 ++++++++--- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/src/callback.cc b/src/callback.cc index 1a0c122..be960c3 100644 --- a/src/callback.cc +++ b/src/callback.cc @@ -1,24 +1,10 @@ #include "callback.h" - -JsPaStreamCallbackBridge::JsPaStreamCallbackBridge(Callback *callback_, - size_t bytesPerFrame) - : AsyncWorker(callback_), m_frameCount(0), - m_bytesPerFrameIn(bytesPerFrame), m_bytesPerFrameOut(bytesPerFrame), - m_inputBuffer(nullptr), m_outputBuffer(nullptr) { - async = new uv_async_t; - uv_async_init( - uv_default_loop() - , async - , UVCallback - ); - async->data = this; - - uv_mutex_init(&async_lock); -} +#include "helpers.h" JsPaStreamCallbackBridge::JsPaStreamCallbackBridge(Callback *callback_, size_t bytesPerFrameIn, - size_t bytesPerFrameOut) + size_t bytesPerFrameOut, + const LocalValue &userData) : AsyncWorker(callback_), m_frameCount(0), m_bytesPerFrameIn(bytesPerFrameIn), m_bytesPerFrameOut(bytesPerFrameOut), m_inputBuffer(nullptr), m_outputBuffer(nullptr) { @@ -30,7 +16,10 @@ JsPaStreamCallbackBridge::JsPaStreamCallbackBridge(Callback *callback_, ); async->data = this; - uv_mutex_init(&async_lock); + uv_mutex_init(&async_lock); + + // Save userData to persistent object + SaveToPersistent(ToLocString("userData"), userData); } JsPaStreamCallbackBridge::~JsPaStreamCallbackBridge() { @@ -94,7 +83,8 @@ void JsPaStreamCallbackBridge::dispatchJSCallback() { LocalValue argv[] = { input, output, - New(frameCount) + New(frameCount), + GetFromPersistent(ToLocString("userData")) }; callbackReturn = callback->Call(3, argv); diff --git a/src/callback.h b/src/callback.h index 879887d..723ba0a 100644 --- a/src/callback.h +++ b/src/callback.h @@ -5,11 +5,14 @@ class JsPaStreamCallbackBridge : public AsyncWorker { public: - explicit JsPaStreamCallbackBridge(Callback *callback_, - size_t bytesPerFrame); explicit JsPaStreamCallbackBridge(Callback *callback_, size_t bytesPerFrameIn, - size_t bytesPerFrameOut); + size_t bytesPerFrameOut, + const LocalValue &userData); + explicit JsPaStreamCallbackBridge(Callback *callback_, size_t bytesPerFrame, + const LocalValue &userData) + : JsPaStreamCallbackBridge(callback_, bytesPerFrame, bytesPerFrame, userData) {} + ~JsPaStreamCallbackBridge(); int sendToCallback(const void* input, unsigned long frameCount); @@ -32,6 +35,8 @@ class JsPaStreamCallbackBridge : public AsyncWorker { size_t m_bytesPerFrameOut; void* m_inputBuffer; void* m_outputBuffer; + + }; #endif \ No newline at end of file From d017f5778e2c36da0aa28638ae238a0b77a3d9a5 Mon Sep 17 00:00:00 2001 From: Jacob Peters Date: Sat, 25 Jun 2016 23:12:33 -0400 Subject: [PATCH 18/21] Call deconstructors when stream is stopped, and add support for userData Object --- src/callback.cc | 17 +++++++++++++---- src/callback.h | 8 +++++--- src/jsaudio.cc | 27 ++++++++++++++++++++++++--- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/callback.cc b/src/callback.cc index be960c3..c1fa790 100644 --- a/src/callback.cc +++ b/src/callback.cc @@ -5,7 +5,7 @@ JsPaStreamCallbackBridge::JsPaStreamCallbackBridge(Callback *callback_, size_t bytesPerFrameIn, size_t bytesPerFrameOut, const LocalValue &userData) - : AsyncWorker(callback_), m_frameCount(0), + : 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; @@ -15,7 +15,6 @@ JsPaStreamCallbackBridge::JsPaStreamCallbackBridge(Callback *callback_, , UVCallback ); async->data = this; - uv_mutex_init(&async_lock); // Save userData to persistent object @@ -24,6 +23,7 @@ JsPaStreamCallbackBridge::JsPaStreamCallbackBridge(Callback *callback_, JsPaStreamCallbackBridge::~JsPaStreamCallbackBridge() { uv_mutex_destroy(&async_lock); + uv_close((uv_handle_t*)async, NULL); //free buffer memory if(m_inputBuffer != nullptr) @@ -86,7 +86,7 @@ void JsPaStreamCallbackBridge::dispatchJSCallback() { New(frameCount), GetFromPersistent(ToLocString("userData")) }; - callbackReturn = callback->Call(3, argv); + callbackReturn = callback->Call(4, argv); if(m_outputBuffer != nullptr) free(m_outputBuffer); @@ -98,8 +98,9 @@ void JsPaStreamCallbackBridge::dispatchJSCallback() { output->ByteLength() ); + m_callbackResult = LocalizeInt(callbackReturn); - uv_mutex_unlock(&async_lock); + uv_mutex_unlock(&async_lock); } @@ -116,4 +117,12 @@ void JsPaStreamCallbackBridge::consumeAudioData(void* output, unsigned long fram free(m_outputBuffer); m_outputBuffer = nullptr; } +} + +int JsPaStreamCallbackBridge::getCallbackResult() { + int ret; + uv_mutex_lock(&async_lock); + ret = m_callbackResult; + uv_mutex_unlock(&async_lock); + return ret; } \ No newline at end of file diff --git a/src/callback.h b/src/callback.h index 723ba0a..0ed43f5 100644 --- a/src/callback.h +++ b/src/callback.h @@ -16,9 +16,10 @@ class JsPaStreamCallbackBridge : public AsyncWorker { ~JsPaStreamCallbackBridge(); int sendToCallback(const void* input, unsigned long frameCount); - void dispatchJSCallback(); - + void dispatchJSCallback(); void consumeAudioData(void* output, unsigned long frameCount); + int getCallbackResult(); + private: NAN_INLINE static NAUV_WORK_CB(UVCallback) { @@ -26,6 +27,7 @@ class JsPaStreamCallbackBridge : public AsyncWorker { static_cast(async->data); callback->dispatchJSCallback(); } + void Execute() {} uv_async_t *async; @@ -35,7 +37,7 @@ class JsPaStreamCallbackBridge : public AsyncWorker { size_t m_bytesPerFrameOut; void* m_inputBuffer; void* m_outputBuffer; - + int m_callbackResult; }; diff --git a/src/jsaudio.cc b/src/jsaudio.cc index d6dfc1a..8678cfa 100644 --- a/src/jsaudio.cc +++ b/src/jsaudio.cc @@ -184,7 +184,14 @@ static int StreamCallbackDispatcher ( bridge->sendToCallback(input, frameCount); bridge->consumeAudioData(output, frameCount); - return 0; + return bridge->getCallbackResult(); +} + +void StreamFinishedCallback (void* userData) { + JsPaStreamCallbackBridge* bridge = static_cast(userData); + + // call JsPaStreamCallbackBridge deconstructor + delete bridge; } // http://portaudio.com/docs/v19-doxydocs/portaudio_8h.html#a443ad16338191af364e3be988014cbbe @@ -211,7 +218,8 @@ NAN_METHOD(openStream) { callback = new JsPaStreamCallbackBridge( new Callback(ToLocFunction(Get(obj, ToLocString("callback")))), bytesPerFrame(paramsIn.sampleFormat), - bytesPerFrame(paramsOut.sampleFormat) + bytesPerFrame(paramsOut.sampleFormat), + Get(obj, ToLocString("sampleFormat")).FromMaybe(New()) ); } // Start stream @@ -226,6 +234,12 @@ NAN_METHOD(openStream) { 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); } @@ -249,7 +263,8 @@ NAN_METHOD(openDefaultStream) { if (HasOwnProperty(obj, ToLocString("callback")).FromMaybe(false)) { callback = new JsPaStreamCallbackBridge( new Callback(ToLocFunction(Get(obj, ToLocString("callback")))), - bytesPerFrame(sampleFormat) + bytesPerFrame(sampleFormat), + Get(obj, ToLocString("userData")).FromMaybe(New()) ); } // Start stream @@ -264,6 +279,12 @@ NAN_METHOD(openDefaultStream) { 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); } From 15c2426f4d7e704359017ba72079fad05ce7931c Mon Sep 17 00:00:00 2001 From: Jacob Peters Date: Sat, 25 Jun 2016 23:13:25 -0400 Subject: [PATCH 19/21] Refactor example to show how to use a userData Object --- examples/pa_api_callback_sine.js | 70 ++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/examples/pa_api_callback_sine.js b/examples/pa_api_callback_sine.js index a51682a..4102a78 100644 --- a/examples/pa_api_callback_sine.js +++ b/examples/pa_api_callback_sine.js @@ -10,6 +10,7 @@ http://portaudio.com/docs/v19-doxydocs/paex__sine_8c_source.html */ // Setup +const numSeconds = process.argv[2] || 5 const sampleRate = 48000 const channels = 2 const framesPerBuffer = 8192 @@ -27,41 +28,43 @@ const formats = { } -//Create callback class that outputs a sinewave -class SineCallback { - constructor (tableSize) { - this.left_phase = 0 - this.right_phase = 0 - this.tableSize = tableSize - this.sine = [] - - // initialise sinusoidal wavetable - for(var i=0; i= this.tableSize ) this.left_phase -= this.tableSize - this.right_phase += 3 // higher pitch so we can distinguish left and right. - if( this.right_phase >= this.tableSize ) this.right_phase -= this.tableSize - } +function callback (input, output, frameCount, data) { + var outputBufferView = new Float32Array(output) + + for(var i=0; i= 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(), - callback = new SineCallback(200) + 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 From 1afc64adf2414a9d5b6af0987205c44610923bf6 Mon Sep 17 00:00:00 2001 From: Jacob Peters Date: Tue, 28 Jun 2016 01:01:16 -0400 Subject: [PATCH 20/21] Added a few comments --- src/callback.cc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/callback.cc b/src/callback.cc index c1fa790..3568257 100644 --- a/src/callback.cc +++ b/src/callback.cc @@ -79,7 +79,8 @@ void JsPaStreamCallbackBridge::dispatchJSCallback() { v8::Isolate::GetCurrent(), m_bytesPerFrameOut * frameCount ); - + + // Create array of arguments and call the javascript callback LocalValue argv[] = { input, output, @@ -98,6 +99,8 @@ void JsPaStreamCallbackBridge::dispatchJSCallback() { output->ByteLength() ); + // Store the return result of the javascript callback + // so it be sent to the PaStreamCallback function m_callbackResult = LocalizeInt(callbackReturn); uv_mutex_unlock(&async_lock); From ba38b2c6f36be9a470351f3e83b697431034e341 Mon Sep 17 00:00:00 2001 From: Jacob Peters Date: Thu, 30 Jun 2016 08:24:40 -0400 Subject: [PATCH 21/21] Make Js callback synchronous with PortAudio callback This greatly reduces memory operations, and negates the need for a mutex. It will eliminate the need for extra, complicated, code to implement paFramesPerBufferUnspecified. --- examples/pa_api_callback_sine.js | 6 +- src/callback.cc | 129 ++++++++++--------------------- src/callback.h | 11 +-- src/jsaudio.cc | 6 +- 4 files changed, 48 insertions(+), 104 deletions(-) diff --git a/examples/pa_api_callback_sine.js b/examples/pa_api_callback_sine.js index 4102a78..1c5932a 100644 --- a/examples/pa_api_callback_sine.js +++ b/examples/pa_api_callback_sine.js @@ -87,9 +87,9 @@ function callbackSine () { // log what we're doing console.log(`Play for ${numSeconds} seconds.\n`) // stop stream when timeout fires - //setTimeout(() => { - //JsAudio.closeStream(stream) - //}, numSeconds * 1000) + setTimeout(() => { + JsAudio.closeStream(stream) + }, numSeconds * 1000) } // Run it diff --git a/src/callback.cc b/src/callback.cc index 3568257..5ff652c 100644 --- a/src/callback.cc +++ b/src/callback.cc @@ -15,40 +15,16 @@ JsPaStreamCallbackBridge::JsPaStreamCallbackBridge(Callback *callback_, , UVCallback ); async->data = this; - uv_mutex_init(&async_lock); + uv_barrier_init(&async_barrier, 2); // Save userData to persistent object SaveToPersistent(ToLocString("userData"), userData); } JsPaStreamCallbackBridge::~JsPaStreamCallbackBridge() { - uv_mutex_destroy(&async_lock); + uv_barrier_destroy(&async_barrier); uv_close((uv_handle_t*)async, NULL); - //free buffer memory - if(m_inputBuffer != nullptr) - free(m_inputBuffer); - if(m_outputBuffer != nullptr) - free(m_outputBuffer); -} - -int JsPaStreamCallbackBridge::sendToCallback(const void* input, unsigned long frameCount) { - uv_mutex_lock(&async_lock); - m_frameCount = frameCount; - - if(m_inputBuffer != nullptr) - free(m_inputBuffer); - m_inputBuffer = malloc(sizeof(float) * frameCount * 2); - - memmove( - m_inputBuffer, - input, - m_bytesPerFrameIn * frameCount - ); - uv_mutex_unlock(&async_lock); - - uv_async_send(async); - return 0; } void JsPaStreamCallbackBridge::dispatchJSCallback() { @@ -58,74 +34,47 @@ void JsPaStreamCallbackBridge::dispatchJSCallback() { v8::Local output; v8::Local callbackReturn; - uv_mutex_lock(&async_lock); - - frameCount = m_frameCount; - - // Setup ArrayBuffer for input audio data - input = v8::ArrayBuffer::New( - v8::Isolate::GetCurrent(), - m_bytesPerFrameIn * frameCount - ); - // Copy input audio data from bridge buffer to ArrayBuffer - memmove( - input->GetContents().Data(), - m_inputBuffer, - input->ByteLength() - ); - // Setup ArrayBuffer for output audio data - output = v8::ArrayBuffer::New( - v8::Isolate::GetCurrent(), - m_bytesPerFrameOut * frameCount - ); - - // Create array of arguments and call the javascript callback - LocalValue argv[] = { - input, - output, - New(frameCount), - GetFromPersistent(ToLocString("userData")) - }; - callbackReturn = callback->Call(4, argv); - - if(m_outputBuffer != nullptr) - free(m_outputBuffer); - m_outputBuffer = malloc(output->ByteLength()); - // Copy output audio data from bridge buffer to ArrayBuffer - memmove( - m_outputBuffer, - output->GetContents().Data(), - output->ByteLength() - ); - - // Store the return result of the javascript callback - // so it be sent to the PaStreamCallback function - m_callbackResult = LocalizeInt(callbackReturn); - - uv_mutex_unlock(&async_lock); + 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); -void JsPaStreamCallbackBridge::consumeAudioData(void* output, unsigned long frameCount) { + // Wait for the asyncronous callback + uv_barrier_wait(&async_barrier); - if(m_outputBuffer != nullptr) { - memmove( - output, - m_outputBuffer, - m_bytesPerFrameOut * frameCount - ); - - // Free the output buffer and set it to nullptr to prevent it from sending the same output data twice - free(m_outputBuffer); - m_outputBuffer = nullptr; - } + return m_callbackResult; } -int JsPaStreamCallbackBridge::getCallbackResult() { - int ret; - uv_mutex_lock(&async_lock); - ret = m_callbackResult; - uv_mutex_unlock(&async_lock); - return ret; -} \ No newline at end of file diff --git a/src/callback.h b/src/callback.h index 0ed43f5..d9a6291 100644 --- a/src/callback.h +++ b/src/callback.h @@ -15,11 +15,8 @@ class JsPaStreamCallbackBridge : public AsyncWorker { ~JsPaStreamCallbackBridge(); - int sendToCallback(const void* input, unsigned long frameCount); - void dispatchJSCallback(); - void consumeAudioData(void* output, unsigned long frameCount); - int getCallbackResult(); - + void dispatchJSCallback(); + int Execute(const void* input, void* output, unsigned long frameCount); private: NAN_INLINE static NAUV_WORK_CB(UVCallback) { @@ -31,11 +28,11 @@ class JsPaStreamCallbackBridge : public AsyncWorker { void Execute() {} uv_async_t *async; - uv_mutex_t async_lock; + uv_barrier_t async_barrier; unsigned long m_frameCount; size_t m_bytesPerFrameIn; size_t m_bytesPerFrameOut; - void* m_inputBuffer; + const void* m_inputBuffer; void* m_outputBuffer; int m_callbackResult; diff --git a/src/jsaudio.cc b/src/jsaudio.cc index 8678cfa..2fd505d 100644 --- a/src/jsaudio.cc +++ b/src/jsaudio.cc @@ -181,10 +181,8 @@ static int StreamCallbackDispatcher ( ) { JsPaStreamCallbackBridge* bridge = static_cast(userData); - bridge->sendToCallback(input, frameCount); - bridge->consumeAudioData(output, frameCount); - - return bridge->getCallbackResult(); + // Call Js callback + return bridge->Execute(input, output, frameCount); } void StreamFinishedCallback (void* userData) {