Vuo 2.4.4
Loading...
Searching...
No Matches
VuoAudio.cc
Go to the documentation of this file.
1
10#include "VuoAudio.h"
11#include "VuoPool.hh"
12#include "VuoTriggerSet.hh"
13#include "VuoApp.h"
14#include "VuoEventLoop.h"
15#include "VuoOsStatus.h"
16
17#pragma clang diagnostic push
18#pragma clang diagnostic ignored "-Wdocumentation"
19#include <RtAudio/RtAudio.h>
20#pragma clang diagnostic pop
21
22#include <queue>
23#include <CoreAudio/CoreAudio.h>
24#include <objc/objc-runtime.h>
25
26
27extern "C"
28{
29
30#ifdef VUO_COMPILER
32 "title" : "VuoAudio",
33 "dependencies" : [
34 "VuoAudioSamples",
35 "VuoAudioInputDevice",
36 "VuoAudioOutputDevice",
37 "VuoList_VuoAudioSamples",
38 "VuoList_VuoAudioInputDevice",
39 "VuoList_VuoAudioOutputDevice",
40 "VuoOsStatus",
41 "rtaudio",
42 "CoreAudio.framework"
43 ]
44 });
45#endif
46}
47
49
52unsigned int VuoAudio_useCount = 0;
53
59static void __attribute__((constructor)) VuoAudio_init()
60{
61 // Some audio drivers (such as Jack) assume that they're being initialized on the main thread,
62 // so our first audio-related call (which initializes the drivers) should be on the main thread.
63 // https://b33p.net/kosada/node/12798
64 dispatch_async(dispatch_get_main_queue(), ^{
65 CFRunLoopRef theRunLoop = NULL;
66 AudioObjectPropertyAddress theAddress = { kAudioHardwarePropertyRunLoop, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster };
67 AudioObjectSetPropertyData(kAudioObjectSystemObject, &theAddress, 0, NULL, sizeof(CFRunLoopRef), &theRunLoop);
68 });
69}
70
75{
76 RtAudio rta;
77 unsigned int deviceCount = rta.getDeviceCount();
79 for (unsigned int i = 0; i < deviceCount; ++i)
80 {
81 RtAudio::DeviceInfo info = rta.getDeviceInfo(i);
82 if (info.inputChannels)
83 VuoListAppendValue_VuoAudioInputDevice(inputDevices, VuoAudioInputDevice_make(i, VuoText_make(info.modelUid.c_str()), VuoText_make(info.name.c_str()), info.inputChannels));
84 }
85 return inputDevices;
86}
87
92{
93 RtAudio rta;
94 unsigned int deviceCount = rta.getDeviceCount();
96 for (unsigned int i = 0; i < deviceCount; ++i)
97 {
98 RtAudio::DeviceInfo info = rta.getDeviceInfo(i);
99 if (info.outputChannels)
100 VuoListAppendValue_VuoAudioOutputDevice(outputDevices, VuoAudioOutputDevice_make(i, VuoText_make(info.modelUid.c_str()), VuoText_make(info.name.c_str()), info.outputChannels));
101 }
102 return outputDevices;
103}
104
108static OSStatus VuoAudio_reconfigurationCallback(AudioObjectID inObjectID, UInt32 inNumberAddresses, const AudioObjectPropertyAddress *inAddresses, void *inClientData)
109{
112 return noErr;
113}
114
121void VuoAudio_use(void)
122{
123 if (__sync_add_and_fetch(&VuoAudio_useCount, 1) == 1)
124 {
125 AudioObjectPropertyAddress address;
126 address.mSelector = kAudioHardwarePropertyDevices;
127 address.mScope = kAudioObjectPropertyScopeGlobal;
128 address.mElement = kAudioObjectPropertyElementMaster;
129 OSStatus ret = AudioObjectAddPropertyListener(kAudioObjectSystemObject, &address, &VuoAudio_reconfigurationCallback, NULL);
130 if (ret)
131 {
132 char *description = VuoOsStatus_getText(ret);
133 VUserLog("Error: Couldn't register device change listener: %s", description);
134 free(description);
135 }
136 }
137}
138
146{
147 if (VuoAudio_useCount <= 0)
148 {
149 VUserLog("Error: Unbalanced VuoAudio_use() / _disuse() calls.");
150 return;
151 }
152
153 if (__sync_sub_and_fetch(&VuoAudio_useCount, 1) == 0)
154 {
155 AudioObjectPropertyAddress address;
156 address.mSelector = kAudioHardwarePropertyDevices;
157 address.mScope = kAudioObjectPropertyScopeGlobal;
158 address.mElement = kAudioObjectPropertyElementMaster;
159 OSStatus ret = AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &address, &VuoAudio_reconfigurationCallback, NULL);
160 if (ret)
161 {
162 char *description = VuoOsStatus_getText(ret);
163 VUserLog("Error: Couldn't unregister device change listener: %s", description);
164 free(description);
165 }
166 }
167}
168
178{
179 VuoAudio_inputDeviceCallbacks.addTrigger(inputDevices);
180 VuoAudio_outputDeviceCallbacks.addTrigger(outputDevices);
181 inputDevices(VuoAudio_getInputDevices());
182 outputDevices(VuoAudio_getOutputDevices());
183}
184
196
200typedef std::map<void *, std::queue<VuoList_VuoAudioSamples> > pendingOutputType;
201
205typedef std::map<void *, std::map<VuoInteger, VuoReal> > lastOutputSampleType;
206
210typedef struct _VuoAudio_internal
211{
212 RtAudio *rta;
213 VuoAudioInputDevice inputDevice;
214 VuoAudioOutputDevice outputDevice;
215
217 VuoTriggerSet<VuoReal> outputTriggers;
218
219 dispatch_queue_t pendingOutputQueue;
220 pendingOutputType pendingOutput;
221
222 lastOutputSampleType lastOutputSample;
223
224 VuoReal priorStreamTime;
226
230int VuoAudio_receivedEvent(void *outputBuffer, void *inputBuffer, unsigned int nBufferFrames, double streamTime, RtAudioStreamStatus status, void *userData)
231{
233
234// if (streamTime - ai->priorStreamTime > ((float)VuoAudioSamples_bufferSize/VuoAudioSamples_sampleRate)*1.02)
235// VLog("called too late: %g (should have been < %g)", streamTime-ai->priorStreamTime, ((float)VuoAudioSamples_bufferSize/VuoAudioSamples_sampleRate)*1.02);
236// ai->priorStreamTime=streamTime;
237
238 if (status)
239 VUserLog("Stream %s (%d) on %s.",
240 status == RTAUDIO_INPUT_OVERFLOW ? "overflow"
241 : (status == RTAUDIO_OUTPUT_UNDERFLOW ? "underflow" : "error"),
242 status,
243 ai->inputDevice.name);
244
245 // Fire triggers requesting audio output buffers.
246 ai->outputTriggers.fire(streamTime);
247
248 // Fire triggers providing audio input buffers.
249 if (ai->inputTriggers.size())
250 {
252 VuoRetain(channels);
253
254 unsigned int samplesPerSecond = ai->rta->getStreamSampleRate();
255
256 // When creating the stream, we requested that inputBuffer be non-interleaved, so the samples should appear consecutively.
257 for (VuoInteger i = 0; i < ai->inputDevice.channelCount; ++i)
258 {
259 VuoAudioSamples samples = VuoAudioSamples_alloc(nBufferFrames);
260 samples.samplesPerSecond = samplesPerSecond;
261 memcpy(samples.samples, (VuoReal *)inputBuffer + i*nBufferFrames, sizeof(VuoReal)*nBufferFrames);
262 VuoListAppendValue_VuoAudioSamples(channels, samples);
263 }
264
265 ai->inputTriggers.fire(channels);
266 VuoRelease(channels);
267 }
268
269 // Zero the final output buffer.
270 double *outputBufferDouble = (double *)outputBuffer;
271 unsigned int outputChannelCount = ai->outputDevice.channelCount;
272 if (outputBuffer)
273 memset(outputBufferDouble, 0, nBufferFrames*sizeof(VuoReal)*outputChannelCount);
274
275 // Process the pending output buffers.
276 dispatch_sync(ai->pendingOutputQueue, ^{
277 if (!outputBuffer)
278 {
279 // If there are any pending buffers, squander them.
280 bool pendingOutput = false;
281 for (pendingOutputType::iterator it = ai->pendingOutput.begin(); it != ai->pendingOutput.end(); ++it)
282 while (!it->second.empty())
283 {
284 VuoRelease(it->second.front());
285 it->second.pop();
286 pendingOutput = true;
287 }
288
289 if (pendingOutput)
290 VUserLog("This audio device (%s) doesn't support output.", ai->outputDevice.name);
291 return;
292 }
293
294 // Mix the next buffer from each source into a single output buffer.
296 for (pendingOutputType::iterator it = ai->pendingOutput.begin(); it != ai->pendingOutput.end(); ++it)
297 {
298 void *id = it->first;
299 if (it->second.empty()) // No pending buffers for this source.
300 {
301 if (ai->lastOutputSample.find(id) == ai->lastOutputSample.end())
302 continue;
303
304 for (unsigned int channel = 0; channel < outputChannelCount; ++channel)
305 {
306 if (ai->lastOutputSample[id].find(channel) == ai->lastOutputSample[id].end())
307 continue;
308
309 // Since this channel was previously playing audio, smoothly fade the last amplitude to zero...
310 VuoReal lastOutputSample = ai->lastOutputSample[id][channel];
311 for (VuoInteger i=0; i<nBufferFrames; ++i)
312 outputBufferDouble[nBufferFrames*(channel) + i] = VuoReal_lerp(lastOutputSample, 0, (float)i/nBufferFrames);
313
314 // ...and indicate that we already faded out.
315 ai->lastOutputSample[id].erase(ai->lastOutputSample[id].find(channel));
316 }
317 }
318 else // Have pending buffers for this source.
319 {
320 VuoList_VuoAudioSamples channels = it->second.front();
321
322 for (unsigned int channel = 0; channel < outputChannelCount; ++channel)
323 {
324 VuoAudioSamples as = VuoListGetValue_VuoAudioSamples(channels, channel+1);
325 if (!as.samples || as.sampleCount == 0)
326 continue;
327
328 if (ai->lastOutputSample[id].find(channel) == ai->lastOutputSample[id].end())
329 {
330 // This is the first sample buffer ever, or the first after a dropout.
331 // Make sure the queue is primed with a few buffers before we start draining it.
332 if (it->second.size() < VuoAudio_queueSize)
333 goto skipPop;
334
335 // Smoothly fade from zero to the sample buffer.
336 for (VuoInteger i=0; i<nBufferFrames; ++i)
337 outputBufferDouble[nBufferFrames*channel + i] = (float)i/nBufferFrames * as.samples[i];
338 }
339 else
340 {
341 // We were previously playing audio, so just copy the samples intact.
342 for (VuoInteger i=0; i<nBufferFrames; ++i)
344 outputBufferDouble[nBufferFrames*(channel) + i] += as.samples[i];
345 }
346
347 ai->lastOutputSample[id][channel] = as.samples[as.sampleCount-1];
348 }
349
350 it->second.pop();
351 VuoRelease(channels);
352
353 skipPop:;
354 }
355 }
356 });
357 return 0;
358}
359
363void VuoAudio_rtAudioError(RtAudioError::Type type, const std::string &errorText)
364{
365 VUserLog("%s: %s",
366 (type == RtAudioError::WARNING || type == RtAudioError::DEBUG_WARNING) ? "Warning" : "Error",
367 errorText.c_str());
368}
369
372static void VuoAudio_destroy(VuoAudio_internal ai);
373VuoAudio_internal VuoAudio_make(unsigned int deviceId)
374{
375 VuoAudio_internal ai = NULL;
376 try
377 {
378 Class avCaptureDeviceClass = objc_getClass("AVCaptureDevice");
379 if (class_getClassMethod(avCaptureDeviceClass, sel_getUid("authorizationStatusForMediaType:")))
380 {
381 CFStringRef mediaType = CFStringCreateWithCString(NULL, "soun", kCFStringEncodingUTF8);
382 long status = ((long (*)(id, SEL, CFStringRef))objc_msgSend)((id)avCaptureDeviceClass, sel_getUid("authorizationStatusForMediaType:"), mediaType);
383 CFRelease(mediaType);
384
385 if (status == 0 /* AVAuthorizationStatusNotDetermined */)
386 VUserLog("Warning: Audio input may be unavailable due to system restrictions. Check System Settings > Privacy & Security > Microphone.");
387 else if (status == 1 /* AVAuthorizationStatusRestricted */
388 || status == 2 /* AVAuthorizationStatusDenied */)
389 VUserLog("Error: Audio input is unavailable due to system restrictions. Check System Settings > Privacy & Security > Microphone.");
390 }
391
392 ai = new _VuoAudio_internal;
393 ai->inputDevice.id = deviceId;
394 ai->outputDevice.id = deviceId;
395
396 ai->pendingOutputQueue = dispatch_queue_create("VuoAudio pending output", VuoEventLoop_getDispatchInteractiveAttribute());
397
398 // Though neither RtAudio's documentation nor Apple's documentation
399 // specify that audio must be initialized on the main thread,
400 // some audio drivers (such as Jack) make that assumption.
401 // https://b33p.net/kosada/node/12068
403 ai->rta = new RtAudio();
404 });
405
406 RtAudio::StreamParameters inputParameters;
407 inputParameters.deviceId = deviceId;
408 inputParameters.nChannels = ai->rta->getDeviceInfo(deviceId).inputChannels;
409 ai->inputDevice.name = VuoText_make(ai->rta->getDeviceInfo(deviceId).name.c_str());
410 VuoRetain(ai->inputDevice.name);
411 ai->inputDevice.channelCount = inputParameters.nChannels;
412
413 RtAudio::StreamParameters outputParameters;
414 outputParameters.deviceId = deviceId;
415 outputParameters.nChannels = ai->rta->getDeviceInfo(deviceId).outputChannels;
416 ai->outputDevice.name = VuoText_make(ai->rta->getDeviceInfo(deviceId).name.c_str());
417 VuoRetain(ai->outputDevice.name);
418 ai->outputDevice.channelCount = outputParameters.nChannels;
419
420 RtAudio::StreamOptions options;
421 options.flags = RTAUDIO_NONINTERLEAVED;
422
423 unsigned int bufferFrames = VuoAudioSamples_bufferSize;
424
425 ai->rta->openStream(
426 outputParameters.nChannels ? &outputParameters : NULL,
427 inputParameters.nChannels ? &inputParameters : NULL,
428 RTAUDIO_FLOAT64,
430 &bufferFrames,
432 ai,
433 &options,
435 ai->rta->startStream();
436 }
437 catch (RtAudioError &error)
438 {
440 VUserLog("Failed to open the audio device (%d): %s", deviceId, error.what());
441
442 if (ai)
443 {
444 delete ai->rta;
445 delete ai;
446 ai = NULL;
447 }
448 }
449
450 if (ai)
452
453 return ai;
454}
456{
457 try
458 {
459 if (ai->rta->isStreamOpen())
460 {
461 ai->rta->stopStream();
462 ai->rta->closeStream();
463 }
464 }
465 catch (RtAudioError &error)
466 {
467 VUserLog("Failed to close the audio device (%s): %s", ai->inputDevice.name, error.what());
468 }
469
470 // Now that the audio stream is stopped (and the last callback has returned), it's safe to delete the queue.
471 dispatch_release(ai->pendingOutputQueue);
472
473 // Release any leftover buffers.
474 for (pendingOutputType::iterator it = ai->pendingOutput.begin(); it != ai->pendingOutput.end(); ++it)
475 while (!it->second.empty())
476 {
477 VuoRelease(it->second.front());
478 it->second.pop();
479 }
480
481 delete ai->rta;
482 VuoRelease(ai->inputDevice.name);
483 VuoRelease(ai->outputDevice.name);
484 delete ai;
485}
488
493{
494 VuoAudioInputDevice realizedDevice;
495 if (!VuoAudioInputDevice_realize(aid, &realizedDevice))
496 return nullptr;
497
498 VuoAudioInputDevice_retain(realizedDevice);
499 VuoAudioIn ai = static_cast<VuoAudioIn>(VuoAudio_internalPool->useSharedInstance(realizedDevice.id));
500 VuoAudioInputDevice_release(realizedDevice);
501
502 return ai;
503}
504
509{
510 VuoAudio_internalPool->disuseSharedInstance(static_cast<VuoAudio_internal>(ai));
511}
512
517{
518 VuoAudioOutputDevice realizedDevice;
519 if (!VuoAudioOutputDevice_realize(aod, &realizedDevice))
520 return nullptr;
521
522 VuoAudioOutputDevice_retain(realizedDevice);
523 VuoAudioOut ao = static_cast<VuoAudioOut>(VuoAudio_internalPool->useSharedInstance(realizedDevice.id));
524 VuoAudioOutputDevice_release(realizedDevice);
525
526 return ao;
527}
528
533{
534 VuoAudio_internalPool->disuseSharedInstance(static_cast<VuoAudio_internal>(ao));
535}
536
543(
544 VuoAudioIn ai,
546)
547{
548 if (!ai)
549 return;
550
552 aii->inputTriggers.addTrigger(receivedChannels);
553}
554
561(
562 VuoAudioOut ao,
563 VuoOutputTrigger(requestedChannels, VuoReal)
564)
565{
566 if (!ao)
567 return;
568
570 aii->outputTriggers.addTrigger(requestedChannels);
571}
572
579(
580 VuoAudioIn ai,
582)
583{
584 if (!ai)
585 return;
586
588 aii->inputTriggers.removeTrigger(receivedChannels);
589}
590
597(
598 VuoAudioOut ao,
599 VuoOutputTrigger(requestedChannels, VuoReal)
600)
601{
602 if (!ao)
603 return;
604
606 aii->outputTriggers.removeTrigger(requestedChannels);
607}
608
620{
621 if (!ao)
622 return;
623
624 VuoRetain(channels);
626 dispatch_async(aii->pendingOutputQueue, ^{
627 aii->pendingOutput[id].push(channels);
628 });
629}
630
632#define setRealizedDevice(newDevice) \
633 realizedDevice->id = newDevice.id; \
634 realizedDevice->modelUid = VuoText_make(newDevice.modelUid); \
635 realizedDevice->name = VuoText_make(newDevice.name); \
636 realizedDevice->channelCount = newDevice.channelCount;
637
651{
652 // Already have channel count; nothing to do.
653 if (device.channelCount > 0)
654 {
655 setRealizedDevice(device);
656 return true;
657 }
658
659 // Otherwise, try to find a matching device.
660
661 VUserLog("Requested device: %s", json_object_to_json_string(VuoAudioInputDevice_getJson(device)));
663 VuoLocal(devices);
664 __block bool found = false;
665
666 // First pass: try to find an exact match by ID.
668 if (device.id != -1 && device.id == item.id)
669 {
670 VUserLog("Matched by ID: %s",json_object_to_json_string(VuoAudioInputDevice_getJson(item)));
671 setRealizedDevice(item);
672 found = true;
673 return false;
674 }
675 return true;
676 });
677
678 // Second pass: try to find a match by model AND name.
679 // (Try AND first since, for example, Soundflower creates multiple devices with different names but the same model.)
680 if (!found)
682 if (device.id == -1 && !VuoText_isEmpty(device.modelUid) && !VuoText_isEmpty(device.name)
683 && VuoText_compare(item.modelUid, (VuoTextComparison){VuoTextComparison_Contains, true}, device.modelUid)
684 && VuoText_compare(item.name, (VuoTextComparison){VuoTextComparison_Contains, true}, device.name))
685 {
686 VUserLog("Matched by model and name: %s",json_object_to_json_string(VuoAudioInputDevice_getJson(item)));
687 setRealizedDevice(item);
688 found = true;
689 return false;
690 }
691 return true;
692 });
693
694 // Third pass: try to find a loose match by model OR name.
695 if (!found)
697 if ((!VuoText_isEmpty(device.modelUid) && VuoText_compare(item.modelUid, (VuoTextComparison){VuoTextComparison_Contains, true}, device.modelUid))
698 || (!VuoText_isEmpty(device.name) && VuoText_compare(item.name, (VuoTextComparison){VuoTextComparison_Contains, true}, device.name)))
699 {
700 VUserLog("Matched by model or name: %s",json_object_to_json_string(VuoAudioInputDevice_getJson(item)));
701 setRealizedDevice(item);
702 found = true;
703 return false;
704 }
705 return true;
706 });
707
708 // Fourth pass: if the user hasn't specified a device, use the default device.
709 if (!found && device.id == -1 && VuoText_isEmpty(device.modelUid) && VuoText_isEmpty(device.name))
710 {
711 __block RtAudio *temporaryRTA; // Just for getting device info prior to opening a shared device.
712
713 try
714 {
715 // https://b33p.net/kosada/node/12068
717 temporaryRTA = new RtAudio();
718 });
719
720 int defaultID = temporaryRTA->getDefaultInputDevice();
722 if (item.id == defaultID)
723 {
724 VUserLog("Using default device: %s",json_object_to_json_string(VuoAudioInputDevice_getJson(item)));
725 setRealizedDevice(item);
726 found = true;
727 return false;
728 }
729 return true;
730 });
731
732 delete temporaryRTA;
733 }
734 catch (RtAudioError &error)
735 {
736 VUserLog("Error: Couldn't enumerate audio devices: %s", error.what());
737 delete temporaryRTA;
738 }
739 }
740
741 if (!found)
742 VUserLog("No matching device found.");
743
744 return found;
745}
746
760{
761 // Already have channel count; nothing to do.
762 if (device.channelCount > 0)
763 {
764 setRealizedDevice(device);
765 return true;
766 }
767
768 // Otherwise, try to find a matching device.
769
770 VUserLog("Requested device: %s", json_object_to_json_string(VuoAudioOutputDevice_getJson(device)));
772 VuoLocal(devices);
773 __block bool found = false;
774
775 // First pass: try to find an exact match by ID.
777 if (device.id != -1 && device.id == item.id)
778 {
779 VUserLog("Matched by ID: %s",json_object_to_json_string(VuoAudioOutputDevice_getJson(item)));
780 setRealizedDevice(item);
781 found = true;
782 return false;
783 }
784 return true;
785 });
786
787 // Second pass: try to find a match by model AND name.
788 // (Try AND first since, for example, Soundflower creates multiple devices with different names but the same model.)
789 if (!found)
791 if (device.id == -1 && !VuoText_isEmpty(device.modelUid) && !VuoText_isEmpty(device.name)
792 && VuoText_compare(item.modelUid, (VuoTextComparison){VuoTextComparison_Contains, true}, device.modelUid)
793 && VuoText_compare(item.name, (VuoTextComparison){VuoTextComparison_Contains, true}, device.name))
794 {
795 VUserLog("Matched by model and name: %s",json_object_to_json_string(VuoAudioOutputDevice_getJson(item)));
796 setRealizedDevice(item);
797 found = true;
798 return false;
799 }
800 return true;
801 });
802
803 // Third pass: try to find a loose match by model OR name.
804 if (!found)
806 if ((!VuoText_isEmpty(device.modelUid) && VuoText_compare(item.modelUid, (VuoTextComparison){VuoTextComparison_Contains, true}, device.modelUid))
807 || (!VuoText_isEmpty(device.name) && VuoText_compare(item.name, (VuoTextComparison){VuoTextComparison_Contains, true}, device.name)))
808 {
809 VUserLog("Matched by model or name: %s",json_object_to_json_string(VuoAudioOutputDevice_getJson(item)));
810 setRealizedDevice(item);
811 found = true;
812 return false;
813 }
814 return true;
815 });
816
817 // Fourth pass: if the user hasn't specified a device, use the default device.
818 if (!found && device.id == -1 && VuoText_isEmpty(device.modelUid) && VuoText_isEmpty(device.name))
819 {
820 __block RtAudio *temporaryRTA; // Just for getting device info prior to opening a shared device.
821
822 try
823 {
824 // https://b33p.net/kosada/node/12068
826 temporaryRTA = new RtAudio();
827 });
828
829 int defaultID = temporaryRTA->getDefaultOutputDevice();
831 if (item.id == defaultID)
832 {
833 VUserLog("Using default device: %s",json_object_to_json_string(VuoAudioOutputDevice_getJson(item)));
834 setRealizedDevice(item);
835 found = true;
836 return false;
837 }
838 return true;
839 });
840
841 delete temporaryRTA;
842 }
843 catch (RtAudioError &error)
844 {
845 VUserLog("Error: Couldn't enumerate audio devices: %s", error.what());
846 delete temporaryRTA;
847 }
848 }
849
850 if (!found)
851 VUserLog("No matching device found.");
852
853 return found;
854}