Merge pull request #76 from lucannez64/fix/mic-input

Pass microphone input through for voice chat
This commit is contained in:
Kalah authored and GitHub committed 2026-10-19 00:56:58 +02:00
commit 7f544d063b
3 files changed
+791 -44

No files matched your search

+595 -36
View File
@@ -1642,10 +1642,40 @@ index 0425ae97f..fdf08d2dc 100644
#endif /* AUDIOHARDWARESTREAMPA_H */
-
diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.cpp b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.cpp
index d6825f1d5..739b70011 100644
index d6825f1d5..fd9efc988 100644
--- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.cpp
+++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.cpp
@@ -25,6 +25,7 @@ along with Darling. If not, see <http://www.gnu.org/licenses/>.
@@ -21,10 +21,37 @@ along with Darling. If not, see <http://www.gnu.org/licenses/>.
#include "AudioHardwareImplPA.h"
#include <iostream>
#include <vector>
+#include <algorithm>
+#include <cstring>
+#include <mach/mach_time.h>
+#include "../../AudioTrace.h"
+
+static const mach_timebase_info_data_t& timebase() {
+ static mach_timebase_info_data_t tb = [] {
+ mach_timebase_info_data_t t = {0, 0};
+ mach_timebase_info(&t);
+ if (!t.numer || !t.denom) t.numer = t.denom = 1;
+ return t;
+ }();
+ return tb;
+}
+static uint64_t hostToNs(uint64_t host) {
+ return uint64_t((unsigned __int128) host * timebase().numer / timebase().denom);
+}
+static uint64_t nsToHost(uint64_t ns) {
+ return uint64_t((unsigned __int128) ns * timebase().denom / timebase().numer);
+}
+static unsigned g_readEvents = 0;
+static bool readShouldTrace() {
+ if (!rclolAudioTraceEnabled()) return false;
+ unsigned n = g_readEvents++;
+ return n < 40 || n % 1000 == 0;
+}
AudioHardwareStreamPAInput::AudioHardwareStreamPAInput(AudioHardwareImplPA* hw, AudioDeviceIOProc callback, void* clientData)
: AudioHardwareStreamPA(hw, callback, clientData)
{
@@ -1653,7 +1683,7 @@ index d6825f1d5..739b70011 100644
// m_dump.open("/tmp/dump.raw", std::ios_base::out | std::ios_base::binary);
}
@@ -32,7 +33,6 @@ void AudioHardwareStreamPAInput::paStreamReadCB(pa_stream* s, size_t length, voi
@@ -32,25 +59,26 @@ void AudioHardwareStreamPAInput::paStreamReadCB(pa_stream* s, size_t length, voi
{
AudioHardwareStreamPAInput* This = static_cast<AudioHardwareStreamPAInput*>(self);
@@ -1661,14 +1691,166 @@ index d6825f1d5..739b70011 100644
if (!This->m_running)
return;
@@ -74,6 +74,7 @@ void AudioHardwareStreamPAInput::paStreamReadCB(pa_stream* s, size_t length, voi
// std::cout << "AudioHardwareStreamPAInput::paStreamReadCB(): bytes=" << nbytes << std::endl;
OSStatus status = This->m_callback(This->m_hw->id(), &fake, abl, &fake, nullptr, nullptr, This->m_clientData);
+ if (!This->m_running) return;
pa_stream_drop(This->m_stream);
- AudioTimeStamp fake = {0};
+ const pa_sample_spec* spec = pa_stream_get_sample_spec(This->m_stream);
+ const size_t frameBytes = spec ? pa_frame_size(spec) : 0;
+ const double rate = spec ? double(spec->rate) : 0;
+ if (!frameBytes || rate <= 0)
+ return;
+
AudioBufferList* abl = static_cast<AudioBufferList*>(alloca(sizeof(AudioBufferList) + sizeof(AudioBuffer)));
- std::vector<uint8_t> hole;
if (status != noErr)
// std::cout << "AudioHardwareStreamPAInput::paStreamReadCB()\n";
while (true)
{
- abl->mNumberBuffers = 1;
- abl->mBuffers[0].mNumberChannels = 2;
-
- size_t nbytes;
+ const void* data = nullptr;
+ size_t nbytes = 0;
- int rv = pa_stream_peek(This->m_stream, (const void**) &abl->mBuffers[0].mData, &nbytes);
+ int rv = pa_stream_peek(This->m_stream, &data, &nbytes);
if (rv < 0 || !nbytes)
{
@@ -58,29 +86,82 @@ void AudioHardwareStreamPAInput::paStreamReadCB(pa_stream* s, size_t length, voi
break;
}
- if (nbytes && !abl->mBuffers[0].mData)
+ // The fragment at the read index was captured `latency` ago: source latency plus
+ // everything still unread. Without timing info yet, count only the unread data.
+ pa_usec_t usec = 0;
+ int negative = 0;
+ uint64_t latencyNs;
+ if (pa_stream_get_latency(This->m_stream, &usec, &negative) == 0)
+ latencyNs = negative ? 0 : uint64_t(usec) * 1000;
+ else
{
- // "If there is a hole, data will be NULL and nbytes will contain the length of the hole."
- hole.resize(nbytes);
- abl->mBuffers[0].mData = hole.data();
+ size_t readable = pa_stream_readable_size(This->m_stream);
+ latencyNs = uint64_t(pa_bytes_to_usec(std::max(readable, nbytes), spec)) * 1000;
}
- abl->mBuffers[0].mDataByteSize = nbytes;
-
- // This->m_dump.write((char*) abl->mBuffers[0].mData, nbytes);
- // This->m_dump.flush();
- if (This->m_convertSignedUnsigned)
- This->transformSignedUnsigned(abl);
+ const uint64_t nowHost = mach_absolute_time();
- // std::cout << "AudioHardwareStreamPAInput::paStreamReadCB(): bytes=" << nbytes << std::endl;
- OSStatus status = This->m_callback(This->m_hw->id(), &fake, abl, &fake, nullptr, nullptr, This->m_clientData);
-
- pa_stream_drop(This->m_stream);
+ const bool hole = !data;
+ if (hole || This->m_convertSignedUnsigned)
+ {
+ // "If there is a hole, data will be NULL and nbytes will contain the length of the hole."
+ // Holes are delivered as silence so the sample clock keeps counting real time.
+ // The peeked memory belongs to PulseAudio, so convert a private copy.
+ This->m_scratch.resize(nbytes);
+ if (data)
+ memcpy(This->m_scratch.data(), data, nbytes);
+ else
+ memset(This->m_scratch.data(), spec->format == PA_SAMPLE_U8 ? 0x80 : 0, nbytes);
+ data = This->m_scratch.data();
+ }
- if (status != noErr)
+ // Like a real HAL, hand the IOProc at most one device buffer per call.
+ const size_t block = std::max(frameBytes, This->m_blockBytes);
+ for (size_t offset = 0; offset + frameBytes <= nbytes; )
{
- pa_stream_cork(This->m_stream, true, [](pa_stream*, int, void*) {}, nullptr);
- break;
+ size_t piece = std::min(block, nbytes - offset);
+ piece -= piece % frameBytes;
+
+ abl->mNumberBuffers = 1;
+ abl->mBuffers[0].mNumberChannels = spec->channels;
+ abl->mBuffers[0].mData = const_cast<uint8_t*>(static_cast<const uint8_t*>(data) + offset);
+ abl->mBuffers[0].mDataByteSize = piece;
+
+ if (This->m_convertSignedUnsigned)
+ This->transformSignedUnsigned(abl);
+
+ const uint64_t offsetNs = uint64_t(double(offset / frameBytes) * 1e9 / rate);
+ const uint64_t agoNs = latencyNs > offsetNs ? latencyNs - offsetNs : 0;
+ uint64_t inputHost = nowHost - std::min(nowHost, nsToHost(agoNs));
+ // Keep host time monotonic even when PA's latency estimate jumps.
+ if (inputHost <= This->m_lastInputHost)
+ inputHost = This->m_lastInputHost + 1;
+ This->m_lastInputHost = inputHost;
+
+ AudioTimeStamp input = {0};
+ input.mSampleTime = double(This->m_frames);
+ input.mHostTime = inputHost;
+ input.mRateScalar = 1.0;
+ input.mFlags = kAudioTimeStampHostTimeValid | kAudioTimeStampSampleTimeValid | kAudioTimeStampRateScalarValid;
+
+ AudioTimeStamp now = input;
+ now.mHostTime = std::max(nowHost, inputHost);
+ now.mSampleTime = input.mSampleTime + double(hostToNs(now.mHostTime - inputHost)) * rate / 1e9;
+
+ // This->m_dump.write((char*) abl->mBuffers[0].mData, nbytes);
+ // This->m_dump.flush();
+ // A real HAL ignores the IOProc's result; one failed callback must not
+ // silence the microphone for the rest of the session.
+ OSStatus status = This->m_callback(This->m_hw->id(), &now, abl, &input, nullptr, nullptr, This->m_clientData);
+ if (readShouldTrace())
+ rclolAudioTrace("PA_IN iop bytes=%zu frames=%llu latencyMs=%.2f hole=%d status=%d\n", piece,
+ (unsigned long long) This->m_frames, double(latencyNs) / 1e6, hole, (int) status);
+
+ if (!This->m_running) return;
+ This->m_frames += piece / frameBytes;
+ offset += piece;
}
+
+ pa_stream_drop(This->m_stream);
}
}
@@ -88,10 +169,19 @@ void AudioHardwareStreamPAInput::start()
{
struct pa_buffer_attr battr;
- battr.fragsize = uint32_t(-1);
- battr.maxlength = uint32_t(m_bufferSize);
+ // Record streams only use maxlength and fragsize. Ask for fragments of one device buffer
+ // (kAudioDevicePropertyBufferFrameSize) for voice latency, and bound the server-side
+ // backlog so a stalled client can't accumulate seconds of delay.
+ const uint32_t bpf = m_hw->asbd().mBytesPerFrame;
+ uint32_t block = bpf ? (m_hw->bufferSize() / bpf) * bpf : m_bufferSize;
+ if (!block)
+ block = m_bufferSize;
+ m_blockBytes = block;
+
+ battr.fragsize = block;
+ battr.maxlength = block * 16;
battr.minreq = uint32_t(-1);
- battr.prebuf = battr.maxlength;
+ battr.prebuf = uint32_t(-1);
battr.tlength = uint32_t(-1);
// std::cout << "AudioHardwareStreamPAInput::start()\n";
@@ -101,6 +191,9 @@ void AudioHardwareStreamPAInput::start()
int rv = pa_stream_connect_record(m_stream, nullptr, &battr,
pa_stream_flags_t(PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_ADJUST_LATENCY | PA_STREAM_AUTO_TIMING_UPDATE)
);
+ if (rclolAudioTraceEnabled())
+ rclolAudioTrace("PA_IN start block=%u rate=%g channels=%u rv=%d\n", block, m_hw->asbd().mSampleRate,
+ m_hw->asbd().mChannelsPerFrame, rv);
// std::cout << "AudioHardwareStreamPAInput::start() rv = " << rv << std::endl;
AudioHardwareStreamPA::start();
diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.cpp b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.cpp
index 278630f02..54b463c2f 100644
--- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.cpp
@@ -2390,7 +2572,7 @@ index ff83d1e15..1e86f9e99 100644
bool m_suspended = true;
};
diff --git a/src/CoreAudio/CoreAudioComponent/AUHAL.cpp b/src/CoreAudio/CoreAudioComponent/AUHAL.cpp
index fe4d1eaa1..9321edcdf 100644
index fe4d1eaa1..769b06586 100644
--- a/src/CoreAudio/CoreAudioComponent/AUHAL.cpp
+++ b/src/CoreAudio/CoreAudioComponent/AUHAL.cpp
@@ -1,3 +1,4 @@
@@ -2414,7 +2596,20 @@ index fe4d1eaa1..9321edcdf 100644
#pragma GCC visibility push(default)
AUDIOCOMPONENT_ENTRY(AUOutputBaseFactory, AUHAL);
@@ -42,6 +52,37 @@ AUHAL::~AUHAL()
@@ -29,8 +39,11 @@ enum {
kInputBus
};
+// Like macOS: element 0 is the output (app -> device) side, element 1 the input
+// (device -> app) side, on both scopes. Without output element 1 the app's
+// recording format and AudioUnitRender(bus 1) had nowhere to go.
AUHAL::AUHAL(AudioComponentInstance inInstance, bool supportRecording)
-: AUBase(inInstance, 1, supportRecording ? 1 : 0)
+: AUBase(inInstance, supportRecording ? 2 : 1, supportRecording ? 2 : 1)
{
UInt32 propSize = sizeof(AudioDeviceID);
AudioHardwareGetProperty(kAudioHardwarePropertyDefaultOutputDevice, &propSize, &m_outputDevice);
@@ -42,6 +55,37 @@ AUHAL::~AUHAL()
Stop();
}
@@ -2452,16 +2647,114 @@ index fe4d1eaa1..9321edcdf 100644
bool AUHAL::CanScheduleParameters() const
{
return true;
@@ -49,7 +90,7 @@ bool AUHAL::CanScheduleParameters() const
@@ -49,7 +93,36 @@ bool AUHAL::CanScheduleParameters() const
bool AUHAL::StreamFormatWritable(AudioUnitScope scope, AudioUnitElement element)
{
- return !m_running && IsInitialized();
+ return !m_running;
+}
+
+// AUBase only accepts the canonical layout (Float32, non-interleaved). A real AUHAL
+// converts any linear PCM, and Vivox asks for interleaved Float32 on both client sides
+// (recording: output scope element 1; playback: input scope element 0, at its own rate).
+// Those accept what PulseAudio converts and our copies handle: native-endian float32 or
+// signed 16/32-bit integers, either layout. Missing frames are padded with zeros, which is
+// only silence for float and signed samples, so unsigned (and 8-bit) formats are left out;
+// so is 24-bit (PulseAudio may use 4-byte samples while we copy 3). Device sides keep the
+// canonical rule.
+bool AUHAL::ValidFormat(AudioUnitScope scope, AudioUnitElement element, const CAStreamBasicDescription& format)
+{
+ const bool clientSide = (scope == kAudioUnitScope_Output && element == kInputBus)
+ || (scope == kAudioUnitScope_Input && element == kOutputBus);
+ if (!clientSide)
+ return AUBase::ValidFormat(scope, element, format);
+
+ const UInt32 flags = format.mFormatFlags;
+ const UInt32 bits = format.mBitsPerChannel;
+ const bool isFloat = flags & kAudioFormatFlagIsFloat;
+ if (format.mFormatID != kAudioFormatLinearPCM || format.mFramesPerPacket != 1
+ || format.mSampleRate <= 0 || format.mChannelsPerFrame < 1 || format.mChannelsPerFrame > 32
+ || (flags & kAudioFormatFlagIsBigEndian) != (kAudioFormatFlagsNativeEndian & kAudioFormatFlagIsBigEndian))
+ return false;
+ if (isFloat ? bits != 32 : (!(flags & kAudioFormatFlagIsSignedInteger) || (bits != 16 && bits != 32)))
+ return false;
+
+ const UInt32 frameBytes = (flags & kAudioFormatFlagIsNonInterleaved) ? bits / 8 : format.mChannelsPerFrame * bits / 8;
+ return format.mBytesPerFrame == frameBytes && format.mBytesPerPacket == frameBytes;
}
OSStatus AUHAL::Version()
@@ -76,19 +117,26 @@ OSStatus AUHAL::Render(AudioUnitRenderActionFlags& ioActionFlags, const AudioTim
@@ -57,45 +130,129 @@ OSStatus AUHAL::Version()
return 1;
}
-// Provide data from the microphone
-OSStatus AUHAL::Render(AudioUnitRenderActionFlags& ioActionFlags, const AudioTimeStamp& inTimeStamp, UInt32 inNumberFrames)
+// Provide data from the microphone: AudioUnitRender(bus 1), normally called from the input callback.
+// Never blocks; frames not recorded yet are silence. Bypasses NeedsToRender(), as a real AUHAL
+// renders whatever timestamp the client passes.
+OSStatus AUHAL::RenderBus(AudioUnitRenderActionFlags& ioActionFlags, const AudioTimeStamp& inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames)
{
- std::unique_lock<std::mutex> lk(m_dataAvailableMutex);
+ if (inBusNumber != kInputBus || !m_enableInput)
+ return kAudioUnitErr_NoConnection;
- m_dataAvailableCV.wait(lk, [=]{ return m_dataAvailable; });
+ const CAStreamBasicDescription& desc = GetStreamFormat(kAudioUnitScope_Output, kInputBus);
+ const UInt32 sampleBytes = desc.mBitsPerChannel / 8;
+ const UInt32 frameBytes = desc.mChannelsPerFrame * sampleBytes;
+ if (!sampleBytes || !frameBytes)
+ return kAudioUnitErr_FormatNotSupported;
+ const bool planar = desc.mFormatFlags & kAudioFormatFlagIsNonInterleaved;
AudioBufferList& abl = GetOutput(kInputBus)->GetBufferList();
+ std::lock_guard<std::mutex> lk(m_dataAvailableMutex);
+ const UInt32 frames = std::min<UInt32>(inNumberFrames, m_bufferUsed / frameBytes);
+ const uint8_t* source = m_buffer.get();
+
+ if (planar)
+ {
+ for (UInt32 c = 0; c < abl.mNumberBuffers && c < desc.mChannelsPerFrame; c++)
+ {
+ uint8_t* destination = static_cast<uint8_t*>(abl.mBuffers[c].mData);
+ for (UInt32 f = 0; f < frames; f++)
+ memcpy(destination + f * sampleBytes, source + f * frameBytes + c * sampleBytes, sampleBytes);
+ memset(destination + frames * sampleBytes, 0, (inNumberFrames - frames) * sampleBytes);
+ abl.mBuffers[c].mDataByteSize = inNumberFrames * sampleBytes;
+ }
+ }
+ else
+ {
+ uint8_t* destination = static_cast<uint8_t*>(abl.mBuffers[0].mData);
+ memcpy(destination, source, frames * frameBytes);
+ memset(destination + frames * frameBytes, 0, (inNumberFrames - frames) * frameBytes);
+ abl.mBuffers[0].mDataByteSize = inNumberFrames * frameBytes;
+ }
- // TODO: Prepare for non-interleaved audio
- UInt32 howMuch = std::min<UInt32>(abl.mBuffers[0].mDataByteSize, m_bufferUsed);
- memcpy(abl.mBuffers[0].mData, m_buffer.get(), howMuch);
- abl.mBuffers[0].mDataByteSize = howMuch;
+ // Level only (no samples): shows whether the client pulls real sound or silence.
+ static std::atomic<unsigned> renders{0};
+ const unsigned n = renders++;
+ if (rclolAudioTraceEnabled() && (n < 40 || n % 1000 == 0))
+ {
+ float peak = 0;
+ const UInt32 count = frames * desc.mChannelsPerFrame;
+ if (desc.IsFloat() && sampleBytes == 4)
+ for (UInt32 i = 0; i < count; i++)
+ peak = std::max(peak, std::abs(reinterpret_cast<const float*>(source)[i]));
+ else if (sampleBytes == 2)
+ for (UInt32 i = 0; i < count; i++)
+ peak = std::max(peak, std::abs(reinterpret_cast<const int16_t*>(source)[i] / 32768.0f));
+ rclolAudioTrace("AUDIO_INPUT_RENDER n=%u frames=%u available=%u planar=%d peak=%.4f\n",
+ n, inNumberFrames, frames, (int) planar, peak);
+ }
return noErr;
}
OSStatus AUHAL::Start()
{
@@ -2491,7 +2784,53 @@ index fe4d1eaa1..9321edcdf 100644
}
if (m_enableInput)
{
@@ -111,20 +159,42 @@ OSStatus AUHAL::Stop()
- AudioDeviceCreateIOProcID(m_inputDevice, recordCallback, this, &m_inputProcID);
- const CAStreamBasicDescription& desc = GetStreamFormat(kAudioUnitScope_Output, kInputBus);
- AudioDeviceSetProperty(m_inputDevice, nullptr, 0, true, kAudioDevicePropertyStreamFormat, sizeof(AudioStreamBasicDescription), &desc);
- AudioDeviceStart(m_inputDevice, m_inputProcID);
+ // The device records directly in the client's bus 1 format (PulseAudio converts rate,
+ // channels and sample type); only planar layouts are converted, in RenderBus().
+ OSStatus status = AudioDeviceCreateIOProcID(m_inputDevice, recordCallback, this, &m_inputProcID);
+ if (!status)
+ {
+ const CAStreamBasicDescription& desc = GetStreamFormat(kAudioUnitScope_Output, kInputBus);
+ AudioStreamBasicDescription hardware = desc;
+ hardware.mFormatFlags &= ~kAudioFormatFlagIsNonInterleaved;
+ hardware.mBytesPerFrame = hardware.mChannelsPerFrame * hardware.mBitsPerChannel / 8;
+ hardware.mBytesPerPacket = hardware.mBytesPerFrame;
+ // The device keeps its buffer size in bytes; keep the frame count across the format change.
+ AudioObjectPropertyAddress p = {kAudioDevicePropertyBufferFrameSize, kAudioObjectPropertyScopeInput, kAudioObjectPropertyElementMaster};
+ UInt32 frames = 0, size = sizeof(frames);
+ const bool haveFrames = AudioObjectGetPropertyData(m_inputDevice, &p, 0, nullptr, &size, &frames) == noErr;
+ status = AudioDeviceSetProperty(m_inputDevice, nullptr, 0, true, kAudioDevicePropertyStreamFormat, sizeof(hardware), &hardware);
+ if (!status && haveFrames)
+ AudioObjectSetPropertyData(m_inputDevice, &p, 0, nullptr, sizeof(frames), &frames);
+ {
+ std::lock_guard<std::mutex> lk(m_dataAvailableMutex);
+ m_bufferUsed = 0;
+ }
+ if (!status)
+ status = AudioDeviceStart(m_inputDevice, m_inputProcID);
+ if (status)
+ {
+ AudioDeviceDestroyIOProcID(m_inputDevice, m_inputProcID);
+ m_inputProcID = 0;
+ }
+ }
+ if (status)
+ {
+ if (m_outputProcID)
+ {
+ AudioDeviceStop(m_outputDevice, m_outputProcID);
+ AudioDeviceDestroyIOProcID(m_outputDevice, m_outputProcID);
+ m_outputProcID = 0;
+ }
+ return status;
+ }
}
m_running = m_enableOutput || m_enableInput;
@@ -111,20 +268,49 @@ OSStatus AUHAL::Stop()
{
AudioDeviceStop(m_outputDevice, m_outputProcID);
AudioDeviceDestroyIOProcID(m_outputDevice, m_outputProcID);
@@ -2529,13 +2868,36 @@ index fe4d1eaa1..9321edcdf 100644
switch (inID)
{
+ case kAudioDevicePropertyBufferFrameSize: {
+ // An input-only unit (EnableIO output=0) sizes its input device's IO cycle.
+ AudioObjectPropertyAddress p={inID,kAudioObjectPropertyScopeOutput,kAudioObjectPropertyElementMaster};
+ return AudioObjectSetPropertyData(m_outputDevice,&p,0,nullptr,inDataSize,inData);
+ OSStatus status=noErr;
+ if(m_enableOutput || !m_enableInput) status=AudioObjectSetPropertyData(m_outputDevice,&p,0,nullptr,inDataSize,inData);
+ if(!status && m_enableInput) {
+ p.mScope=kAudioObjectPropertyScopeInput;
+ status=AudioObjectSetPropertyData(m_inputDevice,&p,0,nullptr,inDataSize,inData);
+ }
+ return status;
+ }
case kAudioOutputUnitProperty_SetInputCallback:
{
ca_require(inDataSize == sizeof(AURenderCallbackStruct), InvalidPropertyValue);
@@ -176,15 +246,23 @@ OSStatus AUHAL::SetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, Au
@@ -161,7 +347,14 @@ OSStatus AUHAL::SetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, Au
ca_require(inDataSize == sizeof(AudioDeviceID), InvalidPropertyValue);
const AudioDeviceID* dev = static_cast<const AudioDeviceID*>(inData);
- if (inElement == kOutputBus)
+ // macOS AUHAL has one current device, normally set on element 0; for an
+ // input-only unit (EnableIO input=1, output=0) that is the capture device.
+ if (inElement == kOutputBus && m_enableInput && !m_enableOutput)
+ {
+ m_inputDevice = *dev;
+ PropertyChanged(inID, inScope, inElement);
+ }
+ else if (inElement == kOutputBus)
{
m_outputDevice = *dev;
PropertyChanged(inID, inScope, inElement);
@@ -176,20 +369,28 @@ OSStatus AUHAL::SetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, Au
return noErr;
}
}
@@ -2560,7 +2922,13 @@ index fe4d1eaa1..9321edcdf 100644
case kAudioOutputUnitProperty_SetInputCallback:
{
outDataSize = sizeof(AURenderCallbackStruct);
@@ -210,13 +288,22 @@ OSStatus AUHAL::GetPropertyInfo(AudioUnitPropertyID inID, AudioUnitScope inScope
outWritable = true;
- break;
+ return noErr;
}
case kAudioOutputUnitProperty_EnableIO:
{
@@ -210,13 +411,23 @@ OSStatus AUHAL::GetPropertyInfo(AudioUnitPropertyID inID, AudioUnitScope inScope
return noErr;
}
}
@@ -2577,14 +2945,15 @@ index fe4d1eaa1..9321edcdf 100644
switch (inID)
{
+ case kAudioDevicePropertyBufferFrameSize: {
+ AudioObjectPropertyAddress p={inID,kAudioObjectPropertyScopeOutput,kAudioObjectPropertyElementMaster};
+ const bool input=m_enableInput && !m_enableOutput;
+ AudioObjectPropertyAddress p={inID,input?kAudioObjectPropertyScopeInput:kAudioObjectPropertyScopeOutput,kAudioObjectPropertyElementMaster};
+ UInt32 size=sizeof(UInt32);
+ return AudioObjectGetPropertyData(m_outputDevice,&p,0,nullptr,&size,outData);
+ return AudioObjectGetPropertyData(input?m_inputDevice:m_outputDevice,&p,0,nullptr,&size,outData);
+ }
case kAudioOutputUnitProperty_SetInputCallback:
{
memcpy(outData, &m_outputAvailableCb, sizeof(m_outputAvailableCb));
@@ -228,12 +315,12 @@ OSStatus AUHAL::GetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, Au
@@ -228,12 +439,12 @@ OSStatus AUHAL::GetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, Au
if (inElement == kOutputBus)
{
UInt32 value = m_enableOutput;
@@ -2599,7 +2968,20 @@ index fe4d1eaa1..9321edcdf 100644
}
else
return kAudioUnitErr_InvalidElement;
@@ -277,9 +364,89 @@ OSStatus AUHAL::recordCallback(AudioObjectID inObjectID,
@@ -241,7 +452,11 @@ OSStatus AUHAL::GetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, Au
}
case kAudioOutputUnitProperty_CurrentDevice:
{
- if (inElement == kOutputBus)
+ if (inElement == kOutputBus && m_enableInput && !m_enableOutput)
+ {
+ memcpy(outData, &m_inputDevice, sizeof(m_inputDevice));
+ }
+ else if (inElement == kOutputBus)
{
memcpy(outData, &m_outputDevice, sizeof(m_outputDevice));
}
@@ -277,9 +492,89 @@ OSStatus AUHAL::recordCallback(AudioObjectID inObjectID,
return This->doRecord(inNow, inInputData, inInputTime);
}
@@ -2689,7 +3071,7 @@ index fe4d1eaa1..9321edcdf 100644
if (!HasInput(0))
{
// std::cerr << "No connection\n";
@@ -290,9 +457,75 @@ OSStatus AUHAL::doPlayback(const AudioTimeStamp* inNow, AudioBufferList* outOutp
@@ -290,9 +585,75 @@ OSStatus AUHAL::doPlayback(const AudioTimeStamp* inNow, AudioBufferList* outOutp
AudioUnitRenderActionFlags flags = kAudioUnitRenderAction_PreRender;
const CAStreamBasicDescription& desc = GetStreamFormat(kAudioUnitScope_Input, kOutputBus);
@@ -2768,8 +3150,75 @@ index fe4d1eaa1..9321edcdf 100644
// std::cout << "Pull result: " << result << std::endl;
// std::cout << "Bytes: " << outOutputData->mBuffers[0].mDataByteSize << std::endl;
// m_auhalData.write((char*) outOutputData->mBuffers[0].mData, outOutputData->mBuffers[0].mDataByteSize);
@@ -303,30 +664,46 @@ OSStatus AUHAL::doPlayback(const AudioTimeStamp* inNow, AudioBufferList* outOutp
OSStatus AUHAL::doRecord(const AudioTimeStamp* inNow, const AudioBufferList* inInputData, const AudioTimeStamp* inInputTime)
{
- // TODO: Prepare for non-interleaved audio
- std::unique_lock<std::mutex> lk(m_dataAvailableMutex);
-
- if (m_bufferSize < inInputData->mBuffers[0].mDataByteSize)
- {
- m_buffer.reset(new uint8_t[inInputData->mBuffers[0].mDataByteSize]);
- m_bufferSize = inInputData->mBuffers[0].mDataByteSize;
- }
-
- m_bufferUsed = inInputData->mBuffers[0].mDataByteSize;
- memcpy(m_buffer.get(), inInputData->mBuffers[0].mData, m_bufferUsed);
-
- m_dataAvailable = true;
+ // The device records interleaved data in the bus 1 output format (see Start()).
+ const CAStreamBasicDescription& desc = GetStreamFormat(kAudioUnitScope_Output, kInputBus);
+ const UInt32 frameBytes = desc.mChannelsPerFrame * (desc.mBitsPerChannel / 8);
+ if (!inInputData || !inInputData->mNumberBuffers || !frameBytes)
+ return noErr;
- lk.unlock();
- m_dataAvailableCV.notify_one();
+ const uint8_t* source = static_cast<const uint8_t*>(inInputData->mBuffers[0].mData);
+ const UInt32 totalFrames = inInputData->mBuffers[0].mDataByteSize / frameBytes;
+ // AudioUnitRender refuses more than kAudioUnitProperty_MaximumFramesPerSlice frames.
+ const UInt32 maxFrames = std::max<UInt32>(1, GetMaxFramesPerSlice());
+ static mach_timebase_info_data_t tb = {0, 0};
+ if (!tb.denom) mach_timebase_info(&tb);
- if (m_outputAvailableCb.inputProc)
+ for (UInt32 offset = 0; offset < totalFrames; )
{
- AudioUnitRenderActionFlags flags = kAudioUnitRenderAction_PostRender;
- const CAStreamBasicDescription& desc = GetStreamFormat(kAudioUnitScope_Output, kInputBus);
- UInt32 numFrames = m_bufferUsed / desc.mBytesPerFrame;
+ const UInt32 numFrames = std::min(maxFrames, totalFrames - offset);
+ const size_t bytes = size_t(numFrames) * frameBytes;
+ {
+ std::lock_guard<std::mutex> lk(m_dataAvailableMutex);
+ if (m_bufferSize < bytes)
+ {
+ m_buffer.reset(new uint8_t[bytes]);
+ m_bufferSize = bytes;
+ }
+ memcpy(m_buffer.get(), source + size_t(offset) * frameBytes, bytes);
+ m_bufferUsed = bytes;
+ }
- m_outputAvailableCb.inputProc(m_outputAvailableCb.inputProcRefCon, &flags, inInputTime, kInputBus, numFrames, nullptr);
+ AudioTimeStamp time = *inInputTime;
+ time.mSampleTime += offset;
+ if (offset && desc.mSampleRate > 0 && tb.numer)
+ time.mHostTime += uint64_t(double(offset) / desc.mSampleRate * 1e9 * tb.denom / tb.numer);
+
+ // The client pulls the slice with AudioUnitRender(bus 1, numFrames) from this callback.
+ if (m_outputAvailableCb.inputProc)
+ {
+ AudioUnitRenderActionFlags flags = 0;
+ m_outputAvailableCb.inputProc(m_outputAvailableCb.inputProcRefCon, &flags, &time, kInputBus, numFrames, nullptr);
+ }
+ offset += numFrames;
}
return noErr;
diff --git a/src/CoreAudio/CoreAudioComponent/AUHAL.h b/src/CoreAudio/CoreAudioComponent/AUHAL.h
index bc21bcbbb..e369f95d1 100644
index bc21bcbbb..fad727962 100644
--- a/src/CoreAudio/CoreAudioComponent/AUHAL.h
+++ b/src/CoreAudio/CoreAudioComponent/AUHAL.h
@@ -25,6 +25,7 @@ along with Darling. If not, see <http://www.gnu.org/licenses/>.
@@ -2780,7 +3229,7 @@ index bc21bcbbb..e369f95d1 100644
class AUHAL : public AUBase
{
@@ -32,6 +33,9 @@ public:
@@ -32,8 +33,12 @@ public:
AUHAL(AudioComponentInstance inInstance, bool supportRecording = true);
~AUHAL();
@@ -2789,8 +3238,20 @@ index bc21bcbbb..e369f95d1 100644
+ OSStatus SetAudioChannelLayout(AudioUnitScope scope,AudioUnitElement element,const AudioChannelLayout* layout) override;
bool CanScheduleParameters() const override;
bool StreamFormatWritable(AudioUnitScope scope, AudioUnitElement element) override;
+ bool ValidFormat(AudioUnitScope scope, AudioUnitElement element, const CAStreamBasicDescription& format) override;
OSStatus Version() override;
@@ -59,6 +63,7 @@ private:
OSStatus Start() override;
@@ -43,7 +48,7 @@ public:
OSStatus GetPropertyInfo(AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement, UInt32& outDataSize, Boolean& outWritable) override;
OSStatus GetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement, void* outData) override;
- OSStatus Render(AudioUnitRenderActionFlags& ioActionFlags, const AudioTimeStamp& inTimeStamp, UInt32 inNumberFrames) override;
+ OSStatus RenderBus(AudioUnitRenderActionFlags& ioActionFlags, const AudioTimeStamp& inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames) override;
static OSStatus ComponentEntryDispatch(ComponentParameters *params, AUHAL *This);
private:
@@ -59,6 +64,7 @@ private:
void* inClientData);
OSStatus doPlayback(const AudioTimeStamp* inNow, AudioBufferList* outOutputData, const AudioTimeStamp* inOutputTime);
@@ -2798,8 +3259,17 @@ index bc21bcbbb..e369f95d1 100644
OSStatus doRecord(const AudioTimeStamp* inNow, const AudioBufferList* inInputData, const AudioTimeStamp* inInputTime);
protected:
@@ -76,6 +81,24 @@ protected:
// TODO: Prepare for non-interleaved audio
@@ -69,13 +75,29 @@ protected:
AudioDeviceID m_outputDevice, m_inputDevice;
AudioDeviceIOProcID m_outputProcID = 0, m_inputProcID = 0;
- bool m_dataAvailable = false;
- std::condition_variable m_dataAvailableCV;
+ // The latest recorded slice (interleaved, in the bus 1 output format), handed to
+ // AudioUnitRender(bus 1) from inside the input callback.
std::mutex m_dataAvailableMutex;
-
- // TODO: Prepare for non-interleaved audio
std::unique_ptr<uint8_t[]> m_buffer;
size_t m_bufferSize = 0, m_bufferUsed = 0;
+
@@ -2984,10 +3454,16 @@ index cb2896957..b2c25fbd4 100644
+
@end
diff --git a/src/frameworks/AVFoundation/src/AVCaptureDevice.m b/src/frameworks/AVFoundation/src/AVCaptureDevice.m
index cab525b91..2fcab003d 100644
index cab525b91..4b3dd6541 100644
--- a/src/frameworks/AVFoundation/src/AVCaptureDevice.m
+++ b/src/frameworks/AVFoundation/src/AVCaptureDevice.m
@@ -21,6 +21,37 @@
@@ -18,9 +18,75 @@
*/
#import <AVFoundation/AVCaptureDevice.h>
+#include <dispatch/dispatch.h>
+#include <dlfcn.h>
+#include <CoreAudio/AudioHardware.h>
@implementation AVCaptureDevice
@@ -3006,20 +3482,52 @@ index cab525b91..2fcab003d 100644
+}
+
+
+// Darling has no TCC consent store. Microphone input works through CoreAudio
+// (PulseAudio), so audio is authorized whenever CoreAudio reports a default input
+// device. CoreAudio is looked up at run time: AVFoundation does not link it, and
+// this query only reads its device table (no PulseAudio connection is made).
+static BOOL hasDefaultInputDevice(void)
+{
+ static OSStatus (*getProperty)(AudioObjectID, const AudioObjectPropertyAddress*, UInt32, const void*, UInt32*, void*);
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ // Never closed: CoreAudio keeps process-wide state once loaded.
+ void* coreAudio = dlopen("/System/Library/Frameworks/CoreAudio.framework/CoreAudio", RTLD_LAZY | RTLD_LOCAL);
+ if (coreAudio)
+ getProperty = dlsym(coreAudio, "AudioObjectGetPropertyData");
+ });
+ if (!getProperty)
+ return NO;
+ AudioDeviceID device = kAudioObjectUnknown;
+ UInt32 size = sizeof(device);
+ AudioObjectPropertyAddress address = {
+ kAudioHardwarePropertyDefaultInputDevice, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster
+ };
+ return getProperty(kAudioObjectSystemObject, &address, 0, NULL, &size, &device) == noErr
+ && device != kAudioObjectUnknown;
+}
+
++ (AVAuthorizationStatus)authorizationStatusForMediaType:(AVMediaType)mediaType
+{
+ // Darling has no camera/microphone capture or TCC consent store;
+ // report Denied so callers take their no-access path instead of
+ // raising an unrecognized-selector exception.
+ (void)mediaType;
+ // There is no camera capture; report Denied so callers take their
+ // no-access path instead of raising an unrecognized-selector exception.
+ if ([mediaType isEqualToString: AVMediaTypeAudio] && hasDefaultInputDevice())
+ return AVAuthorizationStatusAuthorized;
+ return AVAuthorizationStatusDenied;
+}
+
++ (void)requestAccessForMediaType:(AVMediaType)mediaType completionHandler:(void (^)(BOOL))handler
+{
+ (void)mediaType;
+ if (handler)
+ handler(NO);
+ if (!handler)
+ return;
+ BOOL granted = [self authorizationStatusForMediaType: mediaType] == AVAuthorizationStatusAuthorized;
+ // macOS calls the handler later on an arbitrary queue, never inside this call;
+ // callers may hold locks here or wait on the main thread for the result.
+ void (^copied)(BOOL) = [handler copy];
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ copied(granted);
+ [copied release];
+ });
+}
+
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
@@ -10798,3 +11306,54 @@ index e789986..0d56b4e 100644
}
}
return t_server_socket;
diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPAInput.cpp b/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPAInput.cpp
index c1fd1d402..777ca7c38 100644
--- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPAInput.cpp
+++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPAInput.cpp
@@ -31,15 +31,18 @@ OSStatus AudioHardwareImplPAInput::getPropertyData(const AudioObjectPropertyAddr
{
if (inAddress->mSelector == kAudioDevicePropertyStreamConfiguration)
{
+ // A capture device has its channels on the input scope only. Reporting them on the
+ // output scope (copied from the output device) made capture enumerators such as
+ // Vivox and Chromium see a playback-only device with no input channels.
size_t size = sizeof(AudioBufferList);
- if (inAddress->mScope == kAudioDevicePropertyScopeOutput)
+ if (inAddress->mScope == kAudioDevicePropertyScopeInput)
size += 1* sizeof(AudioBuffer);
// Number of returned buffers is the number of channels
if (AudioBufferList* abl = static_cast<AudioBufferList*>(outData); abl && *ioDataSize >= size)
{
- if (inAddress->mScope == kAudioDevicePropertyScopeOutput)
+ if (inAddress->mScope == kAudioDevicePropertyScopeInput)
{
abl->mNumberBuffers = 1;
abl->mBuffers[0].mNumberChannels = 2;
diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.h b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.h
index 87db50d12..01a7b2a3e 100644
--- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.h
+++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.h
@@ -21,6 +21,8 @@ along with Darling. If not, see <http://www.gnu.org/licenses/>.
#define AUDIOHARDWARESTREAMPAINPUT_H
#include "AudioHardwareStreamPA.h"
#include <fstream>
+#include <cstdint>
+#include <vector>
class AudioHardwareStreamPAInput : public AudioHardwareStreamPA
{
@@ -30,6 +32,12 @@ private:
void start() override;
static void paStreamReadCB(pa_stream* s, size_t length, void* self);
+ // Only touched on the PA queue.
+ uint64_t m_frames = 0;
+ uint64_t m_lastInputHost = 0;
+ size_t m_blockBytes = 0;
+ std::vector<uint8_t> m_scratch;
+
// std::ofstream m_dump;
};
+17 -8
View File
@@ -43,14 +43,16 @@ if matrix_obj in link:
link[link.index(matrix_obj)]=str(obj)
else:
link.append(str(obj))
# The default-output unit must be recompiled too: its SetProperty refuses kAudioOutputUnitProperty_CurrentDevice unless patched.
obj=project/'build/riot-DefaultOutputAU.o'
subprocess.run(['clang++',*component_flags['CXX_DEFINES'],*component_flags['CXX_INCLUDES'],
*component_flags['CXX_FLAGS'],'-O2','-c',str(project/'build/darling-src/src/CoreAudio/CoreAudioComponent/DefaultOutputAU.cpp'),
'-o',str(obj)],check=True)
default_obj='CMakeFiles/CoreAudioComponent.dir/DefaultOutputAU.cpp.o'
assert default_obj in link, 'DefaultOutputAU object missing from the link line'
link[link.index(default_obj)]=str(obj)
# The AUHAL subclasses must be recompiled too: they share AUHAL.h's layout and vtable, and the default-output
# unit's SetProperty refuses kAudioOutputUnitProperty_CurrentDevice unless patched.
for name in ['DefaultOutputAU','SystemOutputAU']:
obj=project/f'build/riot-{name}.o'
subprocess.run(['clang++',*component_flags['CXX_DEFINES'],*component_flags['CXX_INCLUDES'],
*component_flags['CXX_FLAGS'],'-O2','-c',str(project/f'build/darling-src/src/CoreAudio/CoreAudioComponent/{name}.cpp'),
'-o',str(obj)],check=True)
default_obj=f'CMakeFiles/CoreAudioComponent.dir/{name}.cpp.o'
assert default_obj in link, f'{name} object missing from the link line'
link[link.index(default_obj)]=str(obj)
link[link.index('-o')+1]=str(project/'build/riot-CoreAudioComponent')
subprocess.run(link,cwd=component,check=True)
toolbox=build/'src/CoreAudio/AudioToolbox'
@@ -90,3 +92,10 @@ subprocess.run(['clang++',*cpp_flags['CXX_DEFINES'],*cpp_flags['CXX_INCLUDES'],
subprocess.run(['clang++','-target','x86_64-apple-macosx11.0','-fuse-ld=lld','-nostdlib',
'-isysroot','@DARLING_SDK@',str(obj),'-lSystem','-lc++',
str(project/'build/riot-AudioToolbox'),'-o',str(project/'build/riot-audio-graph-check')],check=True)
obj=project/'build/riot-audio-input-check.o'
subprocess.run(['clang++',*cpp_flags['CXX_DEFINES'],*cpp_flags['CXX_INCLUDES'],
*cpp_flags['CXX_FLAGS'],'-O2','-c',str(project/'tools/riot-audio-input-check.cpp'),'-o',str(obj)],check=True)
subprocess.run(['clang++','-target','x86_64-apple-macosx11.0','-fuse-ld=lld','-nostdlib',
'-isysroot','@DARLING_SDK@',str(obj),'-lSystem','-lc++','-lobjc','-framework','CoreAudio',
'-framework','AudioToolbox','-framework','AVFoundation','-o',str(project/'build/riot-audio-input-check')],check=True)
+179
View File
@@ -0,0 +1,179 @@
// Microphone path used by voice chat (Vivox/Chromium): AVFoundation audio authorization, the
// input device's channels, and an AUHAL input unit delivering recorded frames through
// AudioUnitRender(bus 1). Samples are only counted, never stored or printed.
#include <AudioToolbox/AudioUnit.h>
#include <CoreAudio/AudioHardware.h>
#include <objc/message.h>
#include <objc/runtime.h>
#include <dispatch/dispatch.h>
#include <atomic>
#include <cstdio>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
extern "C" void* const AVMediaTypeAudio;
extern "C" void* const AVMediaTypeVideo;
static AudioUnit unit;
static std::atomic<unsigned> callbacks{0}, renderErrors{0}, nonIncreasing{0}, zeroFrames{0}, badBuffers{0};
static std::atomic<unsigned long long> totalFrames{0};
static double lastSample=-1;
static bool planar;
static unsigned interleavedChannels=1;
static float samples[2][2*4096];
static OSStatus input(void*,AudioUnitRenderActionFlags* flags,const AudioTimeStamp* time,UInt32 bus,UInt32 frames,AudioBufferList*) {
++callbacks;
if(!frames || frames>4096) {++zeroFrames;return noErr;}
if(!(time->mFlags&kAudioTimeStampSampleTimeValid) || time->mSampleTime<=lastSample) ++nonIncreasing;
lastSample=time->mSampleTime;
unsigned char storage[sizeof(AudioBufferList)+sizeof(AudioBuffer)];
auto* list=(AudioBufferList*)storage;
list->mNumberBuffers=planar?2:1;
for(unsigned b=0;b<list->mNumberBuffers;++b) {
list->mBuffers[b].mNumberChannels=planar?1:interleavedChannels;
list->mBuffers[b].mDataByteSize=frames*sizeof(float)*list->mBuffers[b].mNumberChannels;
list->mBuffers[b].mData=samples[b];
}
AudioUnitRenderActionFlags renderFlags=0;
OSStatus status=AudioUnitRender(unit,&renderFlags,time,bus,frames,list);
if(status) ++renderErrors;
else for(unsigned b=0;b<list->mNumberBuffers;++b)
if(list->mBuffers[b].mDataByteSize!=frames*sizeof(float)*list->mBuffers[b].mNumberChannels) ++badBuffers;
totalFrames+=frames;
return noErr;
}
static int authorization() {
Class device=objc_getClass("AVCaptureDevice");
if(!device) {printf("AUDIO_INPUT_AUTH class=0\n");return 1;}
SEL status=sel_registerName("authorizationStatusForMediaType:");
auto query=(long(*)(id,SEL,void*))objc_msgSend;
long audio=query((id)device,status,AVMediaTypeAudio),video=query((id)device,status,AVMediaTypeVideo);
// macOS calls the handler on another queue, never on the requesting thread inside the call.
// Shared with the handler's thread, so atomic; read only after the handler signals.
static std::atomic<int> granted{-1};
static std::atomic<bool> synchronous{false};
const pthread_t caller=pthread_self();
dispatch_semaphore_t done=dispatch_semaphore_create(0);
auto request=(void(*)(id,SEL,void*,void(^)(BOOL)))objc_msgSend;
request((id)device,sel_registerName("requestAccessForMediaType:completionHandler:"),AVMediaTypeAudio,^(BOOL value) {
synchronous=pthread_equal(pthread_self(),caller)!=0;granted=value;dispatch_semaphore_signal(done);
});
long waited=dispatch_semaphore_wait(done,dispatch_time(DISPATCH_TIME_NOW,2*NSEC_PER_SEC));
printf("AUDIO_INPUT_AUTH audio=%ld video=%ld request_granted=%d async=%d\n",audio,video,(int)granted,!waited && !synchronous);
fflush(stdout);
return audio!=3 || video!=2 || waited || granted!=1 || synchronous;
}
static int device(AudioDeviceID* out) {
AudioObjectPropertyAddress p={kAudioHardwarePropertyDefaultInputDevice,kAudioObjectPropertyScopeGlobal,kAudioObjectPropertyElementMaster};
AudioDeviceID id=kAudioObjectUnknown;UInt32 size=sizeof(id);
OSStatus status=AudioObjectGetPropertyData(kAudioObjectSystemObject,&p,0,nullptr,&size,&id);
if(status || id==kAudioObjectUnknown) {printf("AUDIO_INPUT_DEVICE status=%d\n",status);return 1;}
unsigned channels[2]={0,0};
for(unsigned i=0;i<2;++i) {
AudioObjectPropertyAddress c={kAudioDevicePropertyStreamConfiguration,
i?kAudioObjectPropertyScopeOutput:kAudioObjectPropertyScopeInput,kAudioObjectPropertyElementMaster};
unsigned char storage[256]={};size=sizeof(storage);
if(AudioObjectGetPropertyData(id,&c,0,nullptr,&size,storage)) return 2;
auto* list=(AudioBufferList*)storage;
for(unsigned b=0;b<list->mNumberBuffers;++b) channels[i]+=list->mBuffers[b].mNumberChannels;
}
AudioObjectPropertyAddress s={kAudioDevicePropertyStreams,kAudioObjectPropertyScopeInput,kAudioObjectPropertyElementMaster};
UInt32 streams=0;
status=AudioObjectGetPropertyDataSize(id,&s,0,nullptr,&streams);
AudioObjectPropertyAddress d={kAudioHardwarePropertyDevices,kAudioObjectPropertyScopeGlobal,kAudioObjectPropertyElementMaster};
AudioDeviceID devices[16];size=sizeof(devices);
bool listed=false;
if(!AudioObjectGetPropertyData(kAudioObjectSystemObject,&d,0,nullptr,&size,devices))
for(unsigned i=0;i<size/sizeof(AudioDeviceID);++i) listed|=devices[i]==id;
printf("AUDIO_INPUT_DEVICE input_channels=%u output_channels=%u input_streams=%u listed=%d\n",
channels[0],channels[1],(unsigned)(streams/sizeof(AudioStreamID)),listed);
fflush(stdout);
*out=id;
return channels[0]!=2 || channels[1]!=0 || status || !streams || !listed;
}
static int record(AudioDeviceID id,bool nonInterleaved,unsigned channels,unsigned milliseconds) {
AudioComponentDescription description={kAudioUnitType_Output,kAudioUnitSubType_HALOutput,kAudioUnitManufacturer_Apple,0,0};
AudioComponent component=AudioComponentFindNext(nullptr,&description);
if(!component || AudioComponentInstanceNew(component,&unit) || !unit) return 1;
planar=nonInterleaved;interleavedChannels=channels;callbacks=0;renderErrors=0;nonIncreasing=0;zeroFrames=0;badBuffers=0;totalFrames=0;lastSample=-1;
UInt32 on=1,off=0;
if(AudioUnitSetProperty(unit,kAudioOutputUnitProperty_EnableIO,kAudioUnitScope_Input,1,&on,sizeof(on))) return 2;
if(AudioUnitSetProperty(unit,kAudioOutputUnitProperty_EnableIO,kAudioUnitScope_Output,0,&off,sizeof(off))) return 3;
// Vivox (the interleaved stereo run) sets the capture device on element 1.
const AudioUnitElement deviceElement=channels==2?1:0;
if(AudioUnitSetProperty(unit,kAudioOutputUnitProperty_CurrentDevice,kAudioUnitScope_Global,deviceElement,&id,sizeof(id))) return 4;
AudioDeviceID current=0;UInt32 size=sizeof(current);
if(AudioUnitGetProperty(unit,kAudioOutputUnitProperty_CurrentDevice,kAudioUnitScope_Global,deviceElement,&current,&size) || current!=id) return 5;
AudioStreamBasicDescription format=nonInterleaved
? AudioStreamBasicDescription{44100,kAudioFormatLinearPCM,kAudioFormatFlagIsFloat|kAudioFormatFlagIsPacked|kAudioFormatFlagIsNonInterleaved,4,1,4,2,32,0}
: channels==2
// Vivox's recording format: interleaved Float32 stereo at the device rate.
? AudioStreamBasicDescription{44100,kAudioFormatLinearPCM,kAudioFormatFlagIsFloat|kAudioFormatFlagIsPacked,8,1,8,2,32,0}
: AudioStreamBasicDescription{48000,kAudioFormatLinearPCM,kAudioFormatFlagIsFloat|kAudioFormatFlagIsPacked,4,1,4,1,32,0};
OSStatus status=AudioUnitSetProperty(unit,kAudioUnitProperty_StreamFormat,kAudioUnitScope_Output,1,&format,sizeof(format));
if(status) {printf("AUDIO_INPUT_FORMAT status=%d\n",status);return 6;}
UInt32 frames=480;
if(AudioUnitSetProperty(unit,kAudioDevicePropertyBufferFrameSize,kAudioUnitScope_Global,0,&frames,sizeof(frames))) return 7;
AURenderCallbackStruct callback={input,nullptr};
if(AudioUnitSetProperty(unit,kAudioOutputUnitProperty_SetInputCallback,kAudioUnitScope_Global,0,&callback,sizeof(callback))) return 8;
if(AudioUnitInitialize(unit)) return 9;
status=AudioOutputUnitStart(unit);
if(status) {printf("AUDIO_INPUT_START status=%d\n",status);return 10;}
usleep(milliseconds*1000);
OSStatus stopped=AudioOutputUnitStop(unit);
unsigned count=callbacks;
usleep(100000);
bool quiet=callbacks==count;
AudioUnitUninitialize(unit);
AudioComponentInstanceDispose(unit);
double seconds=double(totalFrames)/format.mSampleRate;
printf("AUDIO_INPUT_RECORD planar=%d channels=%u rate=%.0f callbacks=%u frames=%llu seconds=%.3f render_errors=%u bad_buffers=%u "
"nonincreasing=%u zero_frames=%u stop_status=%d quiet_after_stop=%d\n",nonInterleaved,(unsigned)format.mChannelsPerFrame,format.mSampleRate,count,
(unsigned long long)totalFrames,seconds,(unsigned)renderErrors,(unsigned)badBuffers,(unsigned)nonIncreasing,
(unsigned)zeroFrames,stopped,quiet);
fflush(stdout);
// Startup latency aside, most of the recording time must have been delivered.
return !count || renderErrors || badBuffers || nonIncreasing || zeroFrames || stopped || !quiet
|| seconds<milliseconds/2000.0;
}
// Vivox's playback unit sets interleaved Float32 stereo at 32 kHz on input scope element 0;
// only the format is checked, nothing is played.
static int playbackFormat() {
AudioComponentDescription description={kAudioUnitType_Output,kAudioUnitSubType_HALOutput,kAudioUnitManufacturer_Apple,0,0};
AudioComponent component=AudioComponentFindNext(nullptr,&description);
AudioUnit playback=nullptr;
if(!component || AudioComponentInstanceNew(component,&playback) || !playback) return 1;
AudioStreamBasicDescription format={32000,kAudioFormatLinearPCM,kAudioFormatFlagIsFloat|kAudioFormatFlagIsPacked,8,1,8,2,32,0};
OSStatus status=AudioUnitSetProperty(playback,kAudioUnitProperty_StreamFormat,kAudioUnitScope_Input,0,&format,sizeof(format));
AudioStreamBasicDescription unsignedFormat={44100,kAudioFormatLinearPCM,kAudioFormatFlagIsPacked,4,1,4,2,16,0};
OSStatus rejected=AudioUnitSetProperty(playback,kAudioUnitProperty_StreamFormat,kAudioUnitScope_Input,0,&unsignedFormat,sizeof(unsignedFormat));
AudioComponentInstanceDispose(playback);
printf("AUDIO_PLAYBACK_FORMAT interleaved_status=%d unsigned_status=%d\n",(int)status,(int)rejected);
fflush(stdout);
return status!=noErr || rejected==noErr;
}
int main() {
int failures=0;
failures+=authorization()!=0;
AudioDeviceID id=kAudioObjectUnknown;
if(device(&id)) return 100+failures;
int result=record(id,false,1,1000);
if(result) printf("AUDIO_INPUT_RECORD failed step=%d\n",result);
failures+=result!=0;
result=record(id,true,2,400);
if(result) printf("AUDIO_INPUT_RECORD failed step=%d planar=1\n",result);
failures+=result!=0;
result=record(id,false,2,400);
if(result) printf("AUDIO_INPUT_RECORD failed step=%d interleaved_stereo=1\n",result);
failures+=result!=0;
failures+=playbackFormat()!=0;
printf("AUDIO_INPUT_CHECK failures=%d\n",failures);
return failures;
}