diff --git a/src/CoreAudio/AudioToolbox/AUComponent.cpp b/src/CoreAudio/AudioToolbox/AUComponent.cpp
index d48eec1ef..1deb304e9 100644
--- a/src/CoreAudio/AudioToolbox/AUComponent.cpp
+++ b/src/CoreAudio/AudioToolbox/AUComponent.cpp
@@ -22,13 +22,16 @@ along with Darling. If not, see .
#include
#include
#include "ComponentDispatch.h"
+#include "../AudioTrace.h"
const CFStringRef kAudioComponentRegistrationsChangedNotification = CFSTR("AudioComponentRegistrationsChangedNotification");
const CFStringRef kAudioComponentInstanceInvalidationNotification = CFSTR("AudioComponentInstanceInvalidationNotification");
OSStatus AudioOutputUnitStart(AudioUnit ci)
{
- return dispatchCall(ci, kAudioOutputUnitStartSelect);
+ OSStatus status = dispatchCall(ci, kAudioOutputUnitStartSelect);
+ rclolAudioTrace("UNIT_START unit=%p status=%d\n", (void*) ci, (int) status);
+ return status;
}
OSStatus AudioOutputUnitStop(AudioUnit ci)
@@ -38,7 +41,9 @@ OSStatus AudioOutputUnitStop(AudioUnit ci)
OSStatus AudioUnitInitialize(AudioUnit inUnit)
{
- return dispatchCall(inUnit, kAudioUnitInitializeSelect);
+ OSStatus status = dispatchCall(inUnit, kAudioUnitInitializeSelect);
+ rclolAudioTrace("UNIT_INITIALIZE unit=%p status=%d\n", (void*) inUnit, (int) status);
+ return status;
}
OSStatus AudioUnitUninitialize(AudioUnit inUnit)
@@ -54,7 +59,9 @@ AudioUnitGetPropertyInfo(AudioUnit inUnit,
UInt32 * _Nullable outDataSize,
Boolean * _Nullable outWritable)
{
- return dispatchCall(inUnit, kAudioUnitGetPropertyInfoSelect, inID, inScope, inElement, outDataSize, outWritable);
+ OSStatus status = dispatchCall(inUnit, kAudioUnitGetPropertyInfoSelect, inID, inScope, inElement, outDataSize, outWritable);
+ rclolAudioTrace("UNIT_PROPINFO unit=%p prop=%u scope=%u elem=%u status=%d\n", (void*) inUnit, (unsigned) inID, (unsigned) inScope, (unsigned) inElement, (int) status);
+ return status;
}
OSStatus
@@ -65,7 +72,16 @@ AudioUnitGetProperty(AudioUnit inUnit,
void * outData,
UInt32 * ioDataSize)
{
- return dispatchCall(inUnit, kAudioUnitGetPropertySelect, inID, inScope, inElement, outData, ioDataSize);
+ OSStatus status = dispatchCall(inUnit, kAudioUnitGetPropertySelect, inID, inScope, inElement, outData, ioDataSize);
+ if (inID == 8 && status == noErr && outData && ioDataSize && *ioDataSize >= sizeof(AudioStreamBasicDescription))
+ {
+ const AudioStreamBasicDescription* d = static_cast(outData);
+ rclolAudioTrace("UNIT_GOT_FORMAT unit=%p scope=%u elem=%u rate=%.0f id=%08x flags=%08x bpf=%u ch=%u bits=%u\n",
+ (void*) inUnit, (unsigned) inScope, (unsigned) inElement, d->mSampleRate, (unsigned) d->mFormatID, (unsigned) d->mFormatFlags,
+ (unsigned) d->mBytesPerFrame, (unsigned) d->mChannelsPerFrame, (unsigned) d->mBitsPerChannel);
+ }
+ rclolAudioTrace("UNIT_GET unit=%p prop=%u scope=%u elem=%u status=%d\n", (void*) inUnit, (unsigned) inID, (unsigned) inScope, (unsigned) inElement, (int) status);
+ return status;
}
OSStatus
@@ -76,7 +92,16 @@ AudioUnitSetProperty(AudioUnit inUnit,
const void * _Nullable inData,
UInt32 inDataSize)
{
- return dispatchCall(inUnit, kAudioUnitSetPropertySelect, inID, inScope, inElement, inData, inDataSize);
+ if (inID == 8 /* kAudioUnitProperty_StreamFormat */ && inData && inDataSize >= sizeof(AudioStreamBasicDescription))
+ {
+ const AudioStreamBasicDescription* d = static_cast(inData);
+ rclolAudioTrace("UNIT_SET_FORMAT unit=%p scope=%u elem=%u rate=%.0f id=%08x flags=%08x bpp=%u fpp=%u bpf=%u ch=%u bits=%u\n",
+ (void*) inUnit, (unsigned) inScope, (unsigned) inElement, d->mSampleRate, (unsigned) d->mFormatID, (unsigned) d->mFormatFlags,
+ (unsigned) d->mBytesPerPacket, (unsigned) d->mFramesPerPacket, (unsigned) d->mBytesPerFrame, (unsigned) d->mChannelsPerFrame, (unsigned) d->mBitsPerChannel);
+ }
+ OSStatus status = dispatchCall(inUnit, kAudioUnitSetPropertySelect, inID, inScope, inElement, inData, inDataSize);
+ rclolAudioTrace("UNIT_SET unit=%p prop=%u scope=%u elem=%u size=%u status=%d\n", (void*) inUnit, (unsigned) inID, (unsigned) inScope, (unsigned) inElement, (unsigned) inDataSize, (int) status);
+ return status;
}
OSStatus
@@ -103,7 +128,9 @@ AudioUnitAddRenderNotify(AudioUnit inUnit,
AURenderCallback inProc,
void * _Nullable inProcUserData)
{
- return dispatchCall(inUnit, kAudioUnitAddRenderNotifySelect, inProc, inProcUserData);
+ OSStatus status = dispatchCall(inUnit, kAudioUnitAddRenderNotifySelect, inProc, inProcUserData);
+ rclolAudioTrace("UNIT_ADD_RENDER_NOTIFY unit=%p status=%d\n", (void*) inUnit, (int) status);
+ return status;
}
OSStatus
@@ -121,7 +148,14 @@ AudioUnitGetParameter(AudioUnit inUnit,
AudioUnitElement inElement,
AudioUnitParameterValue * outValue)
{
- return dispatchCall(inUnit, kAudioUnitGetParameterSelect, inID, inScope, inElement, outValue);
+ OSStatus status = dispatchCall(inUnit, kAudioUnitGetParameterSelect, inID, inScope, inElement, outValue);
+ {
+ static unsigned count = 0;
+ if (rclolAudioTraceEnabled() && (count++ < 200 || count % 500 == 0))
+ rclolAudioTrace("UNIT_GET_PARAM unit=%p id=%u scope=%u elem=%u value=%g status=%d\n", (void*) inUnit,
+ (unsigned) inID, (unsigned) inScope, (unsigned) inElement, (outValue && !status) ? (double) *outValue : 0.0, (int) status);
+ }
+ return status;
}
OSStatus
@@ -132,7 +166,14 @@ AudioUnitSetParameter(AudioUnit inUnit,
AudioUnitParameterValue inValue,
UInt32 inBufferOffsetInFrames)
{
- return dispatchCall(inUnit, kAudioUnitSetParameterSelect, inID, inScope, inElement, inValue, inBufferOffsetInFrames);
+ OSStatus status = dispatchCall(inUnit, kAudioUnitSetParameterSelect, inID, inScope, inElement, inValue, inBufferOffsetInFrames);
+ {
+ static unsigned count = 0;
+ if (rclolAudioTraceEnabled() && (count++ < 200 || count % 500 == 0))
+ rclolAudioTrace("UNIT_SET_PARAM unit=%p id=%u scope=%u elem=%u value=%g offset=%u status=%d\n", (void*) inUnit,
+ (unsigned) inID, (unsigned) inScope, (unsigned) inElement, (double) inValue, (unsigned) inBufferOffsetInFrames, (int) status);
+ }
+ return status;
}
OSStatus
diff --git a/src/CoreAudio/AudioToolbox/AUGraph.cpp b/src/CoreAudio/AudioToolbox/AUGraph.cpp
index ca20b90df..e90b37163 100644
--- a/src/CoreAudio/AudioToolbox/AUGraph.cpp
+++ b/src/CoreAudio/AudioToolbox/AUGraph.cpp
@@ -1 +1,548 @@
+#include "../AudioTrace.h"
+/*
+This file is part of Darling.
+
+Darling is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Darling is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Darling. If not, see .
+*/
#include "AUGraph.h"
+#include
+#include
+#include
+#include
+#include
+
+// An AUGraph is a set of nodes (audio units created from component descriptions), connections
+// between them and render callbacks feeding their inputs. Connections and callbacks are recorded
+// while the graph is edited and applied to the audio units when the graph is initialized.
+
+namespace
+{
+struct Node
+{
+ AUNode id;
+ AudioComponentDescription description;
+ AudioUnit unit = nullptr;
+};
+}
+
+struct AUGraphData
+{
+ std::recursive_mutex mutex;
+ std::vector nodes;
+ std::vector connections;
+ std::vector callbacks;
+ AUNode nextNode = 1;
+ bool open = false;
+ bool initialized = false;
+ bool running = false;
+};
+
+namespace
+{
+Node* findNode(AUGraphData* graph, AUNode id)
+{
+ for (Node& node : graph->nodes)
+ {
+ if (node.id == id)
+ return &node;
+ }
+ return nullptr;
+}
+
+bool isOutputNode(const Node& node)
+{
+ return node.description.componentType == kAudioUnitType_Output;
+}
+
+// Undoes whatever initialization has done to the units, keeping the nodes
+void uninitializeUnits(AUGraphData* graph)
+{
+ for (Node& node : graph->nodes)
+ {
+ if (node.unit)
+ AudioUnitUninitialize(node.unit);
+ }
+ graph->initialized = false;
+}
+}
+
+static OSStatus AUGraphOpenImpl(AUGraph inGraph);
+static OSStatus AUGraphConnectNodeInputImpl(AUGraph inGraph, AUNode inSourceNode, UInt32 inSourceOutputNumber, AUNode inDestNode, UInt32 inDestInputNumber);
+static OSStatus AUGraphSetNodeInputCallbackImpl(AUGraph inGraph, AUNode inDestNode, UInt32 inDestInputNumber, const AURenderCallbackStruct* inInputCallback);
+static OSStatus AUGraphInitializeImpl(AUGraph inGraph);
+static OSStatus AUGraphStartImpl(AUGraph inGraph);
+OSStatus NewAUGraph(AUGraph* outGraph)
+{
+ rclolAudioTrace("NEW_GRAPH\n");
+ if (!outGraph)
+ return kAudio_ParamError;
+
+ *outGraph = new AUGraphData;
+ return noErr;
+}
+
+OSStatus DisposeAUGraph(AUGraph inGraph)
+{
+ if (!inGraph)
+ return kAudio_ParamError;
+
+ AUGraphStop(inGraph);
+ AUGraphUninitialize(inGraph);
+ AUGraphClose(inGraph);
+ delete inGraph;
+ return noErr;
+}
+
+OSStatus AUGraphAddNode(AUGraph inGraph, const AudioComponentDescription* inDescription, AUNode* outNode)
+{
+ if(inDescription) rclolAudioTrace("ADD_NODE type=%08x subtype=%08x\n",inDescription->componentType,inDescription->componentSubType);
+ if (!inGraph || !inDescription || !outNode)
+ return kAudio_ParamError;
+
+ std::lock_guard lock(inGraph->mutex);
+
+ Node node;
+ node.id = inGraph->nextNode;
+ node.description = *inDescription;
+
+ if (inGraph->open)
+ {
+ // A node added to an open graph gets its unit right away
+ AudioComponent component = AudioComponentFindNext(nullptr, inDescription);
+ OSStatus status = component ? AudioComponentInstanceNew(component, &node.unit)
+ : kAudioUnitErr_InvalidProperty;
+ rclolAudioTrace("GRAPH_NODE_UNIT subtype=%08x unit=%p status=%d\n", (unsigned) node.description.componentSubType, (void*) node.unit, (int) status);
+ if (status != noErr)
+ return status;
+ }
+
+ inGraph->nextNode++;
+ inGraph->nodes.push_back(node);
+ *outNode = node.id;
+ return noErr;
+}
+
+OSStatus AUGraphRemoveNode(AUGraph inGraph, AUNode inNode)
+{
+ if (!inGraph)
+ return kAudio_ParamError;
+
+ std::lock_guard lock(inGraph->mutex);
+
+ auto node = std::find_if(inGraph->nodes.begin(), inGraph->nodes.end(), [&](const Node& n) { return n.id == inNode; });
+ if (node == inGraph->nodes.end())
+ return kAudioUnitErr_InvalidProperty;
+
+ if (node->unit)
+ {
+ AudioUnitUninitialize(node->unit);
+ AudioComponentInstanceDispose(node->unit);
+ }
+ inGraph->nodes.erase(node);
+
+ inGraph->connections.erase(std::remove_if(inGraph->connections.begin(), inGraph->connections.end(),
+ [&](const AUNodeConnection& c) { return c.sourceNode == inNode || c.destNode == inNode; }), inGraph->connections.end());
+ inGraph->callbacks.erase(std::remove_if(inGraph->callbacks.begin(), inGraph->callbacks.end(),
+ [&](const AUNodeRenderCallback& c) { return c.destNode == inNode; }), inGraph->callbacks.end());
+ return noErr;
+}
+
+OSStatus AUGraphGetNodeCount(AUGraph inGraph, UInt32* outNumberOfNodes)
+{
+ if (!inGraph || !outNumberOfNodes)
+ return kAudio_ParamError;
+
+ std::lock_guard lock(inGraph->mutex);
+ *outNumberOfNodes = (UInt32) inGraph->nodes.size();
+ return noErr;
+}
+
+OSStatus AUGraphGetIndNode(AUGraph inGraph, UInt32 inIndex, AUNode* outNode)
+{
+ if (!inGraph || !outNode)
+ return kAudio_ParamError;
+
+ std::lock_guard lock(inGraph->mutex);
+ if (inIndex >= inGraph->nodes.size())
+ return kAudio_ParamError;
+
+ *outNode = inGraph->nodes[inIndex].id;
+ return noErr;
+}
+
+OSStatus AUGraphNodeInfo(AUGraph inGraph, AUNode inNode, AudioComponentDescription* outDescription, AudioUnit* outAudioUnit)
+{
+ if (!inGraph)
+ return kAudio_ParamError;
+
+ std::lock_guard lock(inGraph->mutex);
+
+ Node* node = findNode(inGraph, inNode);
+ if (!node)
+ return kAudioUnitErr_InvalidProperty;
+
+ if (outDescription)
+ *outDescription = node->description;
+ if (outAudioUnit)
+ {
+ // The unit only exists while the graph is open
+ if (!node->unit)
+ return kAudioUnitErr_Uninitialized;
+ *outAudioUnit = node->unit;
+ }
+ return noErr;
+}
+
+static OSStatus AUGraphOpenImpl(AUGraph inGraph)
+{
+ if (!inGraph)
+ return kAudio_ParamError;
+
+ std::lock_guard lock(inGraph->mutex);
+ if (inGraph->open)
+ return noErr;
+
+ for (Node& node : inGraph->nodes)
+ {
+ AudioComponent component = AudioComponentFindNext(nullptr, &node.description);
+ OSStatus status = component ? AudioComponentInstanceNew(component, &node.unit)
+ : kAudioUnitErr_InvalidProperty;
+ rclolAudioTrace("GRAPH_NODE_UNIT subtype=%08x unit=%p status=%d\n", (unsigned) node.description.componentSubType, (void*) node.unit, (int) status);
+ if (status != noErr)
+ {
+ for (Node& created : inGraph->nodes)
+ {
+ if (created.unit)
+ {
+ AudioComponentInstanceDispose(created.unit);
+ created.unit = nullptr;
+ }
+ }
+ return status;
+ }
+ }
+
+ inGraph->open = true;
+ return noErr;
+}
+
+OSStatus AUGraphClose(AUGraph inGraph)
+{
+ if (!inGraph)
+ return kAudio_ParamError;
+
+ std::lock_guard lock(inGraph->mutex);
+ if (inGraph->running)
+ AUGraphStop(inGraph);
+ if (inGraph->initialized)
+ uninitializeUnits(inGraph);
+
+ for (Node& node : inGraph->nodes)
+ {
+ if (node.unit)
+ {
+ AudioComponentInstanceDispose(node.unit);
+ node.unit = nullptr;
+ }
+ }
+ inGraph->open = false;
+ return noErr;
+}
+
+static OSStatus AUGraphConnectNodeInputImpl(AUGraph inGraph, AUNode inSourceNode, UInt32 inSourceOutputNumber, AUNode inDestNode, UInt32 inDestInputNumber)
+{
+ if (!inGraph)
+ return kAudio_ParamError;
+
+ std::lock_guard lock(inGraph->mutex);
+ if (!findNode(inGraph, inSourceNode) || !findNode(inGraph, inDestNode))
+ return kAudioUnitErr_InvalidProperty;
+
+ // An input is fed by one source: a new connection or callback replaces the previous one
+ AUGraphDisconnectNodeInput(inGraph, inDestNode, inDestInputNumber);
+
+ AUNodeConnection connection;
+ connection.sourceNode = inSourceNode;
+ connection.sourceOutputNumber = inSourceOutputNumber;
+ connection.destNode = inDestNode;
+ connection.destInputNumber = inDestInputNumber;
+ inGraph->connections.push_back(connection);
+ return noErr;
+}
+
+OSStatus AUGraphDisconnectNodeInput(AUGraph inGraph, AUNode inDestNode, UInt32 inDestInputNumber)
+{
+ if (!inGraph)
+ return kAudio_ParamError;
+
+ std::lock_guard lock(inGraph->mutex);
+ inGraph->connections.erase(std::remove_if(inGraph->connections.begin(), inGraph->connections.end(),
+ [&](const AUNodeConnection& c) { return c.destNode == inDestNode && c.destInputNumber == inDestInputNumber; }), inGraph->connections.end());
+ inGraph->callbacks.erase(std::remove_if(inGraph->callbacks.begin(), inGraph->callbacks.end(),
+ [&](const AUNodeRenderCallback& c) { return c.destNode == inDestNode && c.destInputNumber == inDestInputNumber; }), inGraph->callbacks.end());
+ return noErr;
+}
+
+OSStatus AUGraphClearConnections(AUGraph inGraph)
+{
+ if (!inGraph)
+ return kAudio_ParamError;
+
+ std::lock_guard lock(inGraph->mutex);
+ inGraph->connections.clear();
+ inGraph->callbacks.clear();
+ return noErr;
+}
+
+static OSStatus AUGraphSetNodeInputCallbackImpl(AUGraph inGraph, AUNode inDestNode, UInt32 inDestInputNumber, const AURenderCallbackStruct* inInputCallback)
+{
+ if (!inGraph || !inInputCallback)
+ return kAudio_ParamError;
+
+ std::lock_guard lock(inGraph->mutex);
+ if (!findNode(inGraph, inDestNode))
+ return kAudioUnitErr_InvalidProperty;
+
+ AUGraphDisconnectNodeInput(inGraph, inDestNode, inDestInputNumber);
+
+ AUNodeRenderCallback callback;
+ callback.destNode = inDestNode;
+ callback.destInputNumber = inDestInputNumber;
+ callback.cback = *inInputCallback;
+ inGraph->callbacks.push_back(callback);
+ return noErr;
+}
+
+OSStatus AUGraphGetNumberOfInteractions(AUGraph inGraph, UInt32* outNumInteractions)
+{
+ if (!inGraph || !outNumInteractions)
+ return kAudio_ParamError;
+
+ std::lock_guard lock(inGraph->mutex);
+ *outNumInteractions = (UInt32) (inGraph->connections.size() + inGraph->callbacks.size());
+ return noErr;
+}
+
+OSStatus AUGraphGetInteractionInfo(AUGraph inGraph, UInt32 inInteractionIndex, AUNodeInteraction* outInteraction)
+{
+ if (!inGraph || !outInteraction)
+ return kAudio_ParamError;
+
+ std::lock_guard lock(inGraph->mutex);
+
+ if (inInteractionIndex < inGraph->connections.size())
+ {
+ outInteraction->nodeInteractionType = 1; // kAUNodeInteraction_Connection
+ outInteraction->nodeInteraction.connection = inGraph->connections[inInteractionIndex];
+ return noErr;
+ }
+
+ inInteractionIndex -= (UInt32) inGraph->connections.size();
+ if (inInteractionIndex < inGraph->callbacks.size())
+ {
+ outInteraction->nodeInteractionType = 2; // kAUNodeInteraction_InputCallback
+ outInteraction->nodeInteraction.inputCallback = inGraph->callbacks[inInteractionIndex];
+ return noErr;
+ }
+ return kAudio_ParamError;
+}
+
+static OSStatus AUGraphInitializeImpl(AUGraph inGraph)
+{
+ if (!inGraph)
+ return kAudio_ParamError;
+
+ std::lock_guard lock(inGraph->mutex);
+ if (!inGraph->open)
+ return kAudioUnitErr_Uninitialized;
+ if (inGraph->initialized)
+ return noErr;
+
+ for (const AUNodeConnection& connection : inGraph->connections)
+ {
+ Node* source = findNode(inGraph, connection.sourceNode);
+ Node* dest = findNode(inGraph, connection.destNode);
+ if (!source || !dest)
+ return kAudioUnitErr_InvalidProperty;
+
+ AudioUnitConnection unitConnection;
+ unitConnection.sourceAudioUnit = source->unit;
+ unitConnection.sourceOutputNumber = connection.sourceOutputNumber;
+ unitConnection.destInputNumber = connection.destInputNumber;
+
+ OSStatus status = AudioUnitSetProperty(dest->unit, kAudioUnitProperty_MakeConnection, kAudioUnitScope_Input,
+ connection.destInputNumber, &unitConnection, sizeof(unitConnection));
+ if (status != noErr)
+ return status;
+ }
+
+ for (const AUNodeRenderCallback& callback : inGraph->callbacks)
+ {
+ Node* dest = findNode(inGraph, callback.destNode);
+ if (!dest)
+ return kAudioUnitErr_InvalidProperty;
+
+ OSStatus status = AudioUnitSetProperty(dest->unit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input,
+ callback.destInputNumber, &callback.cback, sizeof(callback.cback));
+ if (status != noErr)
+ return status;
+ }
+
+ for (Node& node : inGraph->nodes)
+ {
+ OSStatus status = AudioUnitInitialize(node.unit);
+ if (status != noErr)
+ {
+ uninitializeUnits(inGraph);
+ return status;
+ }
+ }
+
+ inGraph->initialized = true;
+ return noErr;
+}
+
+OSStatus AUGraphUninitialize(AUGraph inGraph)
+{
+ if (!inGraph)
+ return kAudio_ParamError;
+
+ std::lock_guard lock(inGraph->mutex);
+ if (inGraph->running)
+ AUGraphStop(inGraph);
+ if (inGraph->initialized)
+ uninitializeUnits(inGraph);
+ return noErr;
+}
+
+static OSStatus AUGraphStartImpl(AUGraph inGraph)
+{
+ if (!inGraph)
+ return kAudio_ParamError;
+
+ std::lock_guard lock(inGraph->mutex);
+ if (!inGraph->initialized)
+ return kAudioUnitErr_Uninitialized;
+ if (inGraph->running)
+ return noErr;
+
+ // Pulling starts at the output units
+ for (Node& node : inGraph->nodes)
+ {
+ if (isOutputNode(node))
+ {
+ OSStatus status = AudioOutputUnitStart(node.unit);
+ if (status != noErr)
+ return status;
+ }
+ }
+
+ inGraph->running = true;
+ return noErr;
+}
+
+OSStatus AUGraphStop(AUGraph inGraph)
+{
+ if (!inGraph)
+ return kAudio_ParamError;
+
+ std::lock_guard lock(inGraph->mutex);
+ if (!inGraph->running)
+ return noErr;
+
+ for (Node& node : inGraph->nodes)
+ {
+ if (node.unit && isOutputNode(node))
+ AudioOutputUnitStop(node.unit);
+ }
+
+ inGraph->running = false;
+ return noErr;
+}
+
+OSStatus AUGraphIsOpen(AUGraph inGraph, Boolean* outIsOpen)
+{
+ if (!inGraph || !outIsOpen)
+ return kAudio_ParamError;
+
+ std::lock_guard lock(inGraph->mutex);
+ *outIsOpen = inGraph->open;
+ return noErr;
+}
+
+OSStatus AUGraphIsInitialized(AUGraph inGraph, Boolean* outIsInitialized)
+{
+ if (!inGraph || !outIsInitialized)
+ return kAudio_ParamError;
+
+ std::lock_guard lock(inGraph->mutex);
+ *outIsInitialized = inGraph->initialized;
+ return noErr;
+}
+
+OSStatus AUGraphIsRunning(AUGraph inGraph, Boolean* outIsRunning)
+{
+ if (!inGraph || !outIsRunning)
+ return kAudio_ParamError;
+
+ std::lock_guard lock(inGraph->mutex);
+ *outIsRunning = inGraph->running;
+ return noErr;
+}
+
+OSStatus AUGraphUpdate(AUGraph inGraph, Boolean* outIsUpdated)
+{
+ if (!inGraph)
+ return kAudio_ParamError;
+
+ // Edits are applied when the graph is initialized, so there is never a pending change to apply
+ if (outIsUpdated)
+ *outIsUpdated = true;
+ return noErr;
+}
+
+OSStatus AUGraphOpen(AUGraph inGraph)
+{
+ OSStatus status = AUGraphOpenImpl(inGraph);
+ rclolAudioTrace("GRAPH_OPEN status=%d\n", (int) status);
+ return status;
+}
+
+OSStatus AUGraphConnectNodeInput(AUGraph inGraph, AUNode inSourceNode, UInt32 inSourceOutputNumber, AUNode inDestNode, UInt32 inDestInputNumber)
+{
+ OSStatus status = AUGraphConnectNodeInputImpl(inGraph, inSourceNode, inSourceOutputNumber, inDestNode, inDestInputNumber);
+ rclolAudioTrace("GRAPH_CONNECTNODEINPUT status=%d\n", (int) status);
+ return status;
+}
+
+OSStatus AUGraphSetNodeInputCallback(AUGraph inGraph, AUNode inDestNode, UInt32 inDestInputNumber, const AURenderCallbackStruct* inInputCallback)
+{
+ OSStatus status = AUGraphSetNodeInputCallbackImpl(inGraph, inDestNode, inDestInputNumber, inInputCallback);
+ rclolAudioTrace("GRAPH_SETNODEINPUTCALLBACK status=%d\n", (int) status);
+ return status;
+}
+
+OSStatus AUGraphInitialize(AUGraph inGraph)
+{
+ OSStatus status = AUGraphInitializeImpl(inGraph);
+ rclolAudioTrace("GRAPH_INITIALIZE status=%d\n", (int) status);
+ return status;
+}
+
+OSStatus AUGraphStart(AUGraph inGraph)
+{
+ OSStatus status = AUGraphStartImpl(inGraph);
+ rclolAudioTrace("GRAPH_START status=%d\n", (int) status);
+ return status;
+}
diff --git a/src/CoreAudio/AudioToolbox/AudioComponentManager.mm b/src/CoreAudio/AudioToolbox/AudioComponentManager.mm
index 5fa33817d..8588e69d8 100644
--- a/src/CoreAudio/AudioToolbox/AudioComponentManager.mm
+++ b/src/CoreAudio/AudioToolbox/AudioComponentManager.mm
@@ -1,3 +1,4 @@
+#include "../AudioTrace.h"
/*
This file is part of Darling.
@@ -18,6 +19,8 @@ along with Darling. If not, see .
*/
#include "AudioComponentManager.h"
#include
+#include
+#include
#include
#include
@@ -210,6 +213,11 @@ std::vector AudioComponentManager::findMatching(const AudioCompo
}
}
+ // Prefer a newer user component over an older system implementation of
+ // the same audio unit. Retain the existing order for equal versions.
+ std::stable_sort(rv.begin(),rv.end(),[this](AudioComponent a,AudioComponent b) {
+ return m_components.at(a).version>m_components.at(b).version;
+ });
return rv;
}
@@ -274,6 +282,11 @@ OSStatus AudioComponentManager::instantiate(AudioComponent c, AudioComponentInst
return invalidComponentID;
RegisteredComponent& regd = it->second;
+ static unsigned traceCount=0;
+ if(rclolAudioTraceEnabled() && traceCount++<16)
+ rclolAudioTrace("AUDIO_COMPONENT version=%u subtype=%08x user_override=%d\n",
+ regd.version,regd.desc.componentSubType,
+ regd.bundlePath.find("DarlingRiotAudio.component")!=std::string::npos);
if (!regd.factory)
{
// Load the bundle
diff --git a/src/CoreAudio/AudioToolbox/AudioConverter.cpp b/src/CoreAudio/AudioToolbox/AudioConverter.cpp
index 202121cd9..5672fcba1 100644
--- a/src/CoreAudio/AudioToolbox/AudioConverter.cpp
+++ b/src/CoreAudio/AudioToolbox/AudioConverter.cpp
@@ -19,16 +19,39 @@ along with Darling. If not, see .
#include "AudioConverter.h"
#include "AudioConverterImpl.h"
+#include "../AudioTrace.h"
#include
#include
#include
+#include
+#include
+#include
+#include
+
+// Validate opaque-handle ownership before dereferencing a disposal argument.
+static std::mutex converterMutex;
+static std::unordered_set liveConverters;
+
+static bool audioTrace() {
+ static const bool enabled = rclolAudioTraceEnabled();
+ return enabled;
+}
+
OSStatus AudioConverterNew(const AudioStreamBasicDescription* inSourceFormat, const AudioStreamBasicDescription* inDestinationFormat, AudioConverterRef* outAudioConverter)
{
- return AudioConverter::create(inSourceFormat, inDestinationFormat, (AudioConverter**)outAudioConverter);
+ OSStatus status = AudioConverter::create(inSourceFormat, inDestinationFormat, (AudioConverter**)outAudioConverter);
+ if (status == noErr && outAudioConverter && *outAudioConverter) {
+ std::lock_guard lock(converterMutex);
+ liveConverters.insert(*outAudioConverter);
+ }
+ if (audioTrace())
+ std::fprintf(stderr, "RIOT_AUDIO new status=%d converter=%p\n", status,
+ outAudioConverter ? *outAudioConverter : nullptr);
+ return status;
}
-OSStatus AudioConverterNewSpecific(const AudioStreamBasicDescription* inSourceFormat, const AudioStreamBasicDescription* inDestinationFormat, UInt32 inNumberClassDescriptions, AudioClassDescription* nClassDescriptions, AudioConverterRef* outAudioConverter)
+OSStatus AudioConverterNewSpecific(const AudioStreamBasicDescription* inSourceFormat, const AudioStreamBasicDescription* inDestinationFormat, UInt32 inNumberClassDescriptions, const AudioClassDescription* nClassDescriptions, AudioConverterRef* outAudioConverter)
{
// FIXME
return AudioConverterNew(inSourceFormat, inDestinationFormat, outAudioConverter);
@@ -47,6 +70,15 @@ OSStatus AudioConverterReset(AudioConverterRef inAudioConverter)
OSStatus AudioConverterDispose(AudioConverterRef inAudioConverter)
{
+ if (audioTrace())
+ std::fprintf(stderr, "RIOT_AUDIO dispose converter=%p\n", inAudioConverter);
+ if (!inAudioConverter)
+ return paramErr;
+ {
+ std::lock_guard lock(converterMutex);
+ if (!liveConverters.erase(inAudioConverter))
+ return paramErr;
+ }
delete (AudioConverter*)inAudioConverter;
return noErr;
}
diff --git a/src/CoreAudio/AudioToolbox/AudioConverterImpl.cpp b/src/CoreAudio/AudioToolbox/AudioConverterImpl.cpp
index dca819b06..fc4016e60 100644
--- a/src/CoreAudio/AudioToolbox/AudioConverterImpl.cpp
+++ b/src/CoreAudio/AudioToolbox/AudioConverterImpl.cpp
@@ -67,6 +67,11 @@ void AudioConverter::flush()
OSStatus AudioConverter::create(const AudioStreamBasicDescription* inSourceFormat, const AudioStreamBasicDescription* inDestinationFormat, AudioConverter** out)
{
TRACE2(inSourceFormat, inDestinationFormat);
+ if (!out)
+ return paramErr;
+ *out = nullptr;
+ if (!inSourceFormat || !inDestinationFormat)
+ return paramErr;
// TODO: non-interleaved audio
@@ -118,6 +123,7 @@ OSStatus AudioConverter::create(const AudioStreamBasicDescription* inSourceForma
if (avcodec_open2((*out)->m_decoder, codecIn, nullptr) < 0)
{
delete *out;
+ *out = nullptr;
std::cerr << "AudioConverter::create(): avcodec_open() failed, format in = " << std::hex << inSourceFormat->mFormatID << ", out = " << inDestinationFormat->mFormatID << std::dec << std::endl;
return paramErr;
diff --git a/src/CoreAudio/AudioToolbox/ComponentDispatch.h b/src/CoreAudio/AudioToolbox/ComponentDispatch.h
index ee6a9b171..8505c3553 100644
--- a/src/CoreAudio/AudioToolbox/ComponentDispatch.h
+++ b/src/CoreAudio/AudioToolbox/ComponentDispatch.h
@@ -43,7 +43,12 @@ OSStatus dispatchCall(AudioComponentInstance inUnit, SInt16 sel, Args... args)
AudioComponentPlugInInterface* iface = AudioComponentManager::instance()->instanceInterface(inUnit);
AudioComponentMethod method = iface->Lookup(sel);
if (method != nullptr)
- return method(iface, args...);
+ {
+ // The lookup type is variadic. Calling it directly promotes Float32
+ // parameters to double, while audio-unit entry points expect float.
+ using Method = OSStatus (*)(void*, Args...);
+ return reinterpret_cast(method)(iface, args...);
+ }
else
return badComponentSelector;
}
diff --git a/src/CoreAudio/CoreAudio/AudioConverterShim.c b/src/CoreAudio/CoreAudio/AudioConverterShim.c
new file mode 100644
index 000000000..6e85e8230
--- /dev/null
+++ b/src/CoreAudio/CoreAudio/AudioConverterShim.c
@@ -0,0 +1,90 @@
+/* Minimal AudioConverter for Darling's CoreAudio framework.
+ *
+ * Apple's CoreAudio re-exports these AudioToolbox entry points, and
+ * third-party code (e.g. Discord voice) binds them against CoreAudio.
+ * AudioToolbox cannot be re-exported here (it depends on CoreAudio),
+ * so this TU provides small self-contained implementations:
+ * - format-identical conversion works (direct frame copy);
+ * - anything else returns kAudioConverterErr_InvalidInputSize/
+ * codec-unavailable rather than crashing the caller at bind time.
+ */
+#include
+#include
+#include
+#include
+#include
+
+struct DarlingAudioConverter {
+ AudioStreamBasicDescription src;
+ AudioStreamBasicDescription dst;
+};
+
+OSStatus AudioConverterNew(const AudioStreamBasicDescription* inSourceFormat,
+ const AudioStreamBasicDescription* inDestinationFormat,
+ AudioConverterRef* outAudioConverter) {
+ struct DarlingAudioConverter* c;
+ if (!inSourceFormat || !inDestinationFormat || !outAudioConverter)
+ return paramErr;
+ c = (struct DarlingAudioConverter*)malloc(sizeof(*c));
+ if (!c)
+ return memFullErr;
+ c->src = *inSourceFormat;
+ c->dst = *inDestinationFormat;
+ *outAudioConverter = (AudioConverterRef)c;
+ return noErr;
+}
+
+OSStatus AudioConverterDispose(AudioConverterRef inAudioConverter) {
+ if (!inAudioConverter)
+ return paramErr;
+ free(inAudioConverter);
+ return noErr;
+}
+
+static int asbd_equal(const AudioStreamBasicDescription* a,
+ const AudioStreamBasicDescription* b) {
+ return a->mSampleRate == b->mSampleRate &&
+ a->mFormatID == b->mFormatID &&
+ a->mFormatFlags == b->mFormatFlags &&
+ a->mBytesPerPacket == b->mBytesPerPacket &&
+ a->mFramesPerPacket == b->mFramesPerPacket &&
+ a->mBytesPerFrame == b->mBytesPerFrame &&
+ a->mChannelsPerFrame == b->mChannelsPerFrame &&
+ a->mBitsPerChannel == b->mBitsPerChannel;
+}
+
+OSStatus AudioConverterFillComplexBuffer(AudioConverterRef inAudioConverter,
+ AudioConverterComplexInputDataProc inInputDataProc,
+ void* inInputDataProcUserData,
+ UInt32* ioOutputDataPacketSize,
+ AudioBufferList* outOutputData,
+ AudioStreamPacketDescription* outPacketDescription) {
+ struct DarlingAudioConverter* c = (struct DarlingAudioConverter*)inAudioConverter;
+ AudioBufferList inBuf;
+ UInt32 packets, bytes;
+ OSStatus st;
+
+ (void)outPacketDescription;
+ if (!c || !inInputDataProc || !ioOutputDataPacketSize || !outOutputData)
+ return paramErr;
+ if (!asbd_equal(&c->src, &c->dst))
+ return kAudioConverterErr_UnspecifiedError;
+ if (c->src.mBytesPerPacket == 0 || outOutputData->mNumberBuffers < 1)
+ return paramErr;
+
+ packets = *ioOutputDataPacketSize;
+ bytes = packets * c->src.mBytesPerPacket;
+ if (bytes > outOutputData->mBuffers[0].mDataByteSize)
+ bytes = outOutputData->mBuffers[0].mDataByteSize;
+
+ inBuf.mNumberBuffers = 1;
+ inBuf.mBuffers[0].mNumberChannels = c->src.mChannelsPerFrame;
+ inBuf.mBuffers[0].mDataByteSize = bytes;
+ inBuf.mBuffers[0].mData = outOutputData->mBuffers[0].mData;
+ st = inInputDataProc((AudioConverterRef)c, &packets, &inBuf, NULL, inInputDataProcUserData);
+ if (st != noErr)
+ return st;
+ *ioOutputDataPacketSize = packets;
+ outOutputData->mBuffers[0].mDataByteSize = packets * c->dst.mBytesPerPacket;
+ return noErr;
+}
diff --git a/src/CoreAudio/CoreAudio/AudioHardware.cpp b/src/CoreAudio/CoreAudio/AudioHardware.cpp
index 7243e6a62..647c8aff7 100644
--- a/src/CoreAudio/CoreAudio/AudioHardware.cpp
+++ b/src/CoreAudio/CoreAudio/AudioHardware.cpp
@@ -1,3 +1,4 @@
+#include "../AudioTrace.h"
/*
This file is part of Darling.
@@ -25,6 +26,9 @@ along with Darling. If not, see .
#include
#include
#include
+#include
+#include
+#include
#include
#include "stub.h"
@@ -38,7 +42,7 @@ static void initObjects()
g_objects.insert(std::make_pair(kAudioObjectSystemObject, std::make_unique(kAudioObjectSystemObject)));
g_objects.insert(std::make_pair(kAudioObjectSystemObject + 1, std::make_unique(kAudioObjectSystemObject + 1)));
g_objects.insert(std::make_pair(kAudioObjectSystemObject + 2, std::make_unique(kAudioObjectSystemObject + 2)));
- g_objects.insert(std::make_pair(kAudioObjectSystemObject + 1, std::make_unique(kAudioObjectSystemObject + 3, "event")));
+ g_objects.insert(std::make_pair(kAudioObjectSystemObject + 3, std::make_unique(kAudioObjectSystemObject + 3, "event")));
});
}
@@ -58,6 +62,35 @@ static AudioHardwareImpl* GetSystemObject()
return GetObject(kAudioObjectSystemObject);
}
+// Stream IDs are distinct from their owning device IDs. Chromium enumerates
+// these objects to determine input/output direction before opening audio.
+static OSStatus GetStreamPropertyData(AudioObjectID id,
+ const AudioObjectPropertyAddress* property, UInt32* size, void* data)
+{
+ if (!property || !size) return paramErr;
+ AudioHardwareImpl* device=GetObject(id&0xffffu);
+ if (!device) return kAudioHardwareBadObjectError;
+ UInt32 value;
+ const void* source=&value;
+ UInt32 required=sizeof(value);
+ switch(property->mSelector) {
+ case kAudioStreamPropertyDirection:
+ value=device->id()==kAudioObjectSystemObject+2?1:0;break;
+ case kAudioStreamPropertyTerminalType:
+ value=0;break;
+ case kAudioStreamPropertyVirtualFormat:
+ case kAudioStreamPropertyPhysicalFormat:
+ source=&device->asbd();required=sizeof(AudioStreamBasicDescription);break;
+ default:return kAudioHardwareUnknownPropertyError;
+ }
+ if(data) {
+ if(*sizemSelector,inAddress->mScope,has);
+ return has;
+ }
AudioHardwareImpl* obj = GetObject(inObjectID);
- if (!obj)
+ if (!obj) {
+ rclolAudioTrace("HAS object=%u selector=%08x BADOBJECT\n",inObjectID,inAddress->mSelector);
return 0;
+ }
- return obj->hasProperty(inAddress);
+ bool has=obj->hasProperty(inAddress);
+ rclolAudioTrace("HAS object=%u selector=%08x scope=%08x elem=%u has=%d\n",inObjectID,inAddress->mSelector,inAddress->mScope,inAddress->mElement,has);
+ return has;
}
OSStatus AudioObjectIsPropertySettable(AudioObjectID inObjectID,
const AudioObjectPropertyAddress* inAddress, Boolean* outIsSettable)
{
AudioHardwareImpl* obj = GetObject(inObjectID);
- if (!obj)
+ if (!obj) {
+ rclolAudioTrace("SETTABLE object=%08x selector=%08x BADOBJECT\n",inObjectID,inAddress->mSelector);
return kAudioHardwareBadObjectError;
+ }
- return obj->isPropertySettable(inAddress, outIsSettable);
+ OSStatus status=obj->isPropertySettable(inAddress, outIsSettable);
+ rclolAudioTrace("SETTABLE object=%u selector=%08x scope=%08x elem=%u status=%d settable=%d\n",inObjectID,inAddress->mSelector,inAddress->mScope,inAddress->mElement,status,status?0:*outIsSettable);
+ return status;
}
OSStatus AudioObjectGetPropertyDataSize(AudioObjectID inObjectID,
const AudioObjectPropertyAddress* inAddress, UInt32 inQualifierDataSize,
const void* inQualifierData, UInt32* outDataSize)
{
+ if((inObjectID&0xffff0000u)==0x44410000u) {
+ OSStatus status=GetStreamPropertyData(inObjectID,inAddress,outDataSize,nullptr);
+ rclolAudioTrace("SIZE stream=%08x selector=%08x scope=%08x status=%d\n",inObjectID,inAddress->mSelector,inAddress->mScope,status);
+ return status;
+ }
AudioHardwareImpl* obj = GetObject(inObjectID);
- if (!obj)
+ if (!obj) {
+ rclolAudioTrace("SIZE object=%08x selector=%08x BADOBJECT\n",inObjectID,inAddress->mSelector);
return kAudioHardwareBadObjectError;
+ }
- return obj->getPropertyDataSize(inAddress, inQualifierDataSize,
- inQualifierData, outDataSize);
+ OSStatus status=obj->getPropertyDataSize(inAddress,inQualifierDataSize,inQualifierData,outDataSize);
+ rclolAudioTrace("SIZE object=%u selector=%08x scope=%08x status=%d size=%u\n",inObjectID,inAddress->mSelector,inAddress->mScope,status,status?0:*outDataSize);
+ return status;
}
OSStatus AudioObjectGetPropertyData(AudioObjectID inObjectID,
const AudioObjectPropertyAddress* inAddress, UInt32 inQualifierDataSize,
const void* inQualifierData, UInt32* ioDataSize, void* outData)
{
+ if(inObjectID==kAudioObjectSystemObject && inAddress->mSelector==kAudioHardwarePropertyDeviceForUID) {
+ if(!outData || !ioDataSize || *ioDataSize(outData);
+ if(!value->mInputData || value->mInputDataSize!=sizeof(CFStringRef) || !value->mOutputData || value->mOutputDataSize(value->mInputData);
+ AudioDeviceID found=kAudioObjectUnknown;
+ AudioObjectPropertyAddress property={kAudioDevicePropertyDeviceUID,kAudioObjectPropertyScopeGlobal,kAudioObjectPropertyElementMaster};
+ for(AudioDeviceID id=kAudioObjectSystemObject+1;id<=kAudioObjectSystemObject+3;++id) {
+ CFStringRef uid=nullptr;UInt32 size=sizeof(uid);
+ if(GetObject(id)->getPropertyData(&property,0,nullptr,&size,&uid)==noErr && uid) {
+ bool match=CFEqual(uid,wanted);CFRelease(uid);
+ if(match) { found=id;break; }
+ }
+ }
+ *static_cast(value->mOutputData)=found;
+ value->mOutputDataSize=sizeof(found);*ioDataSize=sizeof(*value);return noErr;
+ }
+ if((inObjectID&0xffff0000u)==0x44410000u) {
+ OSStatus status=GetStreamPropertyData(inObjectID,inAddress,ioDataSize,outData);
+ rclolAudioTrace("GET stream=%08x selector=%08x scope=%08x status=%d\n",inObjectID,inAddress->mSelector,inAddress->mScope,status);
+ return status;
+ }
AudioHardwareImpl* obj = GetObject(inObjectID);
- if (!obj)
+ if (!obj) {
+ rclolAudioTrace("GET object=%08x selector=%08x BADOBJECT\n",inObjectID,inAddress->mSelector);
return kAudioHardwareBadObjectError;
+ }
- return obj->getPropertyData(inAddress, inQualifierDataSize, inQualifierData,
- ioDataSize, outData);
+ static unsigned traceCount=0;
+ if(rclolAudioTraceEnabled() && traceCount++<100)
+ fprintf(stderr,"AUDIO_HARDWARE_GET object=%u property=%08x scope=%08x\n",inObjectID,inAddress->mSelector,inAddress->mScope);
+ OSStatus status=obj->getPropertyData(inAddress,inQualifierDataSize,inQualifierData,ioDataSize,outData);
+ rclolAudioTrace("GET object=%u selector=%08x scope=%08x elem=%u status=%d size=%u\n",inObjectID,inAddress->mSelector,inAddress->mScope,inAddress->mElement,status,status?0:*ioDataSize);
+ return status;
}
OSStatus AudioObjectSetPropertyData(AudioObjectID inObjectID,
@@ -114,11 +196,15 @@ OSStatus AudioObjectSetPropertyData(AudioObjectID inObjectID,
const void* inQualifierData, UInt32 inDataSize, const void* inData)
{
AudioHardwareImpl* obj = GetObject(inObjectID);
- if (!obj)
+ if (!obj) {
+ rclolAudioTrace("SET object=%08x selector=%08x scope=%08x elem=%u BADOBJECT\n",inObjectID,inAddress->mSelector,inAddress->mScope,inAddress->mElement);
return kAudioHardwareBadObjectError;
+ }
- return obj->setPropertyData(inAddress, inQualifierDataSize, inQualifierData,
- inDataSize, inData);
+ OSStatus status=obj->setPropertyData(inAddress,inQualifierDataSize,inQualifierData,inDataSize,inData);
+ rclolAudioTrace("SET object=%u selector=%08x scope=%08x elem=%u size=%u value=%g status=%d\n",inObjectID,inAddress->mSelector,inAddress->mScope,inAddress->mElement,inDataSize,
+ (inData && inDataSize==sizeof(Float32))?(double)*(const Float32*)inData:0.0,status);
+ return status;
}
OSStatus AudioObjectAddPropertyListener(AudioObjectID inObjectID,
@@ -129,7 +215,9 @@ OSStatus AudioObjectAddPropertyListener(AudioObjectID inObjectID,
if (!obj)
return kAudioHardwareBadObjectError;
- return obj->addPropertyListener(inAddress, inListener, inClientData);
+ OSStatus status=obj->addPropertyListener(inAddress,inListener,inClientData);
+ rclolAudioTrace("ADD_LISTENER object=%u selector=%08x status=%d\n",inObjectID,inAddress->mSelector,status);
+ return status;
}
OSStatus AudioObjectRemovePropertyListener(AudioObjectID inObjectID,
diff --git a/src/CoreAudio/CoreAudio/AudioHardwareImpl.cpp b/src/CoreAudio/CoreAudio/AudioHardwareImpl.cpp
index f006ba51e..e0e4b4984 100644
--- a/src/CoreAudio/CoreAudio/AudioHardwareImpl.cpp
+++ b/src/CoreAudio/CoreAudio/AudioHardwareImpl.cpp
@@ -21,6 +21,8 @@ along with Darling. If not, see .
#include
#include
#include
+#include
+#include
#include
AudioHardwareImpl::AudioHardwareImpl(AudioObjectID myId)
@@ -33,7 +35,7 @@ AudioHardwareImpl::AudioHardwareImpl(AudioObjectID myId)
m_asbd.mFormatID = kAudioFormatLinearPCM;
m_asbd.mFormatFlags = kLinearPCMFormatFlagIsFloat | kAudioFormatFlagsNativeEndian;
m_asbd.mFramesPerPacket = 1;
- m_asbd.mBytesPerFrame = UInt32(m_asbd.mBytesPerPacket = m_asbd.mChannelsPerFrame * m_asbd.mSampleRate * m_asbd.mBitsPerChannel / 8);
+ m_asbd.mBytesPerFrame = UInt32(m_asbd.mBytesPerPacket = m_asbd.mChannelsPerFrame * m_asbd.mBitsPerChannel / 8);
}
@@ -132,6 +134,9 @@ OSStatus AudioHardwareImpl::isPropertySettable(const AudioObjectPropertyAddress*
{
switch (inAddress->mSelector)
{
+ case kAudioHardwarePropertyRunLoop:
+ if(m_myId!=kAudioObjectSystemObject) break;
+ *outIsSettable=true;return noErr;
case kAudioDevicePropertyVolumeScalar:
case kAudioDevicePropertyVolumeDecibels:
*outIsSettable = true;
@@ -151,6 +156,12 @@ OSStatus AudioHardwareImpl::getPropertyData(const AudioObjectPropertyAddress* in
{
switch (inAddress->mSelector)
{
+ case kAudioHardwarePropertyRunLoop: {
+ if(m_myId!=kAudioObjectSystemObject) return kAudioHardwareUnknownPropertyError;
+ if(outData && *ioDataSize(outData); asbd && *ioDataSize >= sizeof(AudioStreamBasicDescription))
@@ -160,6 +171,23 @@ OSStatus AudioHardwareImpl::getPropertyData(const AudioObjectPropertyAddress* in
*ioDataSize = sizeof(AudioStreamBasicDescription);
return kAudioHardwareNoError;
}
+ case kAudioDevicePropertyBufferFrameSize: {
+ if(!m_asbd.mBytesPerFrame) return kAudioHardwareBadStreamError;
+ if(outData && *ioDataSizemMinimum=128;range->mMaximum=2048; }
+ *ioDataSize=sizeof(AudioValueRange);return noErr;
+ }
+ case kAudioDevicePropertyLatency:
+ case kAudioDevicePropertySafetyOffset: {
+ if(outData && *ioDataSize(outData);
+ layout->mChannelLayoutTag=m_asbd.mChannelsPerFrame==2
+ ? kAudioChannelLayoutTag_Stereo : kAudioChannelLayoutTag_Mono;
+ }
+ *ioDataSize=size;return noErr;
+ }
case kAudioDevicePropertyPreferredChannelsForStereo:
{
if (UInt32* b = static_cast(outData); outData && *ioDataSize >= sizeof(UInt32)*2)
{
- b[0] = 0;
- b[1] = 1;
+ b[0] = 1;
+ b[1] = 2;
}
*ioDataSize = sizeof(UInt32)*2;
return kAudioHardwareNoError;
@@ -253,12 +295,26 @@ OSStatus AudioHardwareImpl::getPropertyString(CFStringRef str, UInt32* ioDataSiz
OSStatus AudioHardwareImpl::setPropertyData(const AudioObjectPropertyAddress* inAddress, UInt32 inQualifierDataSize,
const void* inQualifierData, UInt32 inDataSize, const void* inData)
{
+ if(inAddress->mSelector==kAudioHardwarePropertyRunLoop && m_myId==kAudioObjectSystemObject) {
+ if(!inData || inDataSize!=sizeof(CFRunLoopRef)) return kAudioHardwareBadPropertySizeError;
+ // NULL selects HAL-owned threads, which is the PulseAudio backend's
+ // existing mode. A caller-supplied run loop is not supported yet.
+ return *(const CFRunLoopRef*)inData ? kAudioHardwareUnsupportedOperationError : noErr;
+ }
+
switch (inAddress->mSelector)
{
case kAudioDevicePropertyBufferSize:
{
return setBufferSize(*static_cast(inData));
}
+ case kAudioDevicePropertyBufferFrameSize: {
+ if(!inData || inDataSize!=sizeof(UInt32)) return kAudioHardwareBadPropertySizeError;
+ UInt32 frames=*(const UInt32*)inData;
+ if(frames<128 || frames>2048 || !m_asbd.mBytesPerFrame ||
+ frames>UINT32_MAX/m_asbd.mBytesPerFrame) return paramErr;
+ return setBufferSize(frames*m_asbd.mBytesPerFrame);
+ }
case kAudioDevicePropertyVolumeScalar:
{
// TODO: Implement setting the volume
@@ -283,7 +339,9 @@ OSStatus AudioHardwareImpl::setPropertyData(const AudioObjectPropertyAddress* in
memcpy(&m_asbd, setting, sizeof(m_asbd));
m_asbd.mFramesPerPacket = 1;
- m_asbd.mBytesPerFrame = UInt32(m_asbd.mBytesPerPacket = m_asbd.mChannelsPerFrame * m_asbd.mSampleRate * m_asbd.mBitsPerChannel / 8);
+ m_asbd.mBytesPerFrame = UInt32(m_asbd.mBytesPerPacket =
+ ((m_asbd.mFormatFlags & kAudioFormatFlagIsNonInterleaved) ? 1 : m_asbd.mChannelsPerFrame)
+ * m_asbd.mBitsPerChannel / 8);
return kAudioHardwareNoError;
}
diff --git a/src/CoreAudio/CoreAudio/AudioHardwareImpl.h b/src/CoreAudio/CoreAudio/AudioHardwareImpl.h
index f74d9b0c3..655e90d51 100644
--- a/src/CoreAudio/CoreAudio/AudioHardwareImpl.h
+++ b/src/CoreAudio/CoreAudio/AudioHardwareImpl.h
@@ -86,8 +86,7 @@ protected:
uint32_t m_bufferSize = 8192;
CFStringRef m_name = CFSTR("unknown"), m_uid = CFSTR("unknown"), m_manufacturer = CFSTR("unknown"), m_modelUid = CFSTR("unknown");
- AudioStreamBasicDescription m_asbd;
+ AudioStreamBasicDescription m_asbd = {};
};
#endif /* AUDIOHARDWAREIMPL_H */
-
diff --git a/src/CoreAudio/CoreAudio/CMakeLists.txt b/src/CoreAudio/CoreAudio/CMakeLists.txt
index 5f10c2f92..089c94127 100644
--- a/src/CoreAudio/CoreAudio/CMakeLists.txt
+++ b/src/CoreAudio/CoreAudio/CMakeLists.txt
@@ -8,6 +8,7 @@ set(coreaudio_sources
AudioHardware.cpp
AudioHardwareImpl.cpp
HostTime.mm
+ AudioConverterShim.c
pulse/AudioHardwareImplPA.cpp
pulse/AudioHardwareImplPAInput.cpp
pulse/AudioHardwareImplPAOutput.cpp
diff --git a/src/CoreAudio/CoreAudio/HostTime.mm b/src/CoreAudio/CoreAudio/HostTime.mm
index 879fb48a1..fb99e99d1 100644
--- a/src/CoreAudio/CoreAudio/HostTime.mm
+++ b/src/CoreAudio/CoreAudio/HostTime.mm
@@ -1,8 +1,32 @@
#include "CoreAudio/HostTime.h"
#include "stub.h"
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
UInt64 AudioGetCurrentHostTime(void)
{
- STUB();
- return 0;
+ return mach_absolute_time();
+}
+
+UInt64 AudioConvertHostTimeToNanos(UInt64 inHostTime)
+{
+ static mach_timebase_info_data_t sTimebase = { 0, 0 };
+ if (sTimebase.denom == 0)
+ (void)mach_timebase_info(&sTimebase);
+ return inHostTime * sTimebase.numer / sTimebase.denom;
+}
+
+UInt64 AudioConvertNanosToHostTime(UInt64 inNanos)
+{
+ static mach_timebase_info_data_t sTimebase = { 0, 0 };
+ if (sTimebase.denom == 0)
+ (void)mach_timebase_info(&sTimebase);
+ return inNanos * sTimebase.denom / sTimebase.numer;
+}
+
+#ifdef __cplusplus
}
+#endif
diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.cpp b/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.cpp
index 85315086f..c43382686 100644
--- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.cpp
+++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.cpp
@@ -26,6 +26,8 @@ along with Darling. If not, see .
#include
#include
#include
+#include
+#include
AudioHardwareImplPA::AudioHardwareImplPA(AudioObjectID myId, const char* paRole)
: AudioHardwareImpl(myId), m_paRole(paRole)
@@ -37,8 +39,34 @@ AudioHardwareImplPA::AudioHardwareImplPA(AudioObjectID myId, const char* paRole)
AudioHardwareImplPA::~AudioHardwareImplPA()
{
- if (m_context)
- pa_context_unref(m_context);
+ for(auto& stream:m_streams) stream.second->stop();
+ m_streams.clear();
+ if(m_context && m_loop) {
+ m_loop->sync(^{ pa_context_disconnect(m_context);pa_context_unref(m_context); });
+ m_context=nullptr;
+ }
+}
+
+void AudioHardwareImplPA::syncPA(void (^block)(void)) {
+ if(m_loop) m_loop->sync(block);else block();
+}
+
+OSStatus AudioHardwareImplPA::stop(AudioDeviceIOProcID inProcID) {
+ __block OSStatus result = noErr;
+ syncPA(^{
+ auto it = m_streams.find(inProcID);
+ if (it == m_streams.end()) {
+ result = kAudioHardwareNotRunningError;
+ return;
+ }
+ auto* stream = it->second.release();
+ m_streams.erase(it);
+ stream->stop();
+ // An IOProc may stop itself. Keep its C++ stream alive until the
+ // current PulseAudio callback has unwound on this serial queue.
+ m_loop->async(^{ delete stream; });
+ });
+ return result;
}
OSStatus AudioHardwareImplPA::getPropertyData(const AudioObjectPropertyAddress* inAddress, UInt32 inQualifierDataSize,
@@ -46,6 +74,18 @@ OSStatus AudioHardwareImplPA::getPropertyData(const AudioObjectPropertyAddress*
{
switch (inAddress->mSelector)
{
+ case kAudioDevicePropertyStreams:
+ {
+ if (m_myId == kAudioObjectSystemObject) return kAudioHardwareUnknownPropertyError;
+ bool input=m_myId==kAudioObjectSystemObject+2;
+ bool matches=inAddress->mScope==kAudioObjectPropertyScopeGlobal ||
+ inAddress->mScope==(input?kAudioObjectPropertyScopeInput:kAudioObjectPropertyScopeOutput);
+ UInt32 size=matches?sizeof(AudioStreamID):0;
+ if(outData && *ioDataSize(outData); devId && *ioDataSize >= sizeof(AudioDeviceID))
@@ -115,112 +155,47 @@ AudioHardwareStream* AudioHardwareImplPA::createStream(AudioDeviceIOProc callbac
extern "C" char ***_NSGetArgv(void);
-static void paContextStateCB(pa_context* c, void* priv)
-{
- void (^cb)(pa_context*) = (void (^)(pa_context*)) priv;
-
- pa_context_state_t state = pa_context_get_state(c);
- if (state == PA_CONTEXT_READY)
- {
- // std::cout << "PA_CONTEXT_READY\n";
-
- cb(c);
- Block_release(cb);
-
- // FIXME: We won't be notified about later disconnects...
- pa_context_set_state_callback(c, nullptr, nullptr);
- }
- else if (state == PA_CONTEXT_FAILED)
- {
- std::cerr << "PulseAudio error: PA_CONTEXT_FAILED\n";
-
- cb(nullptr);
- Block_release(cb);
- pa_context_set_state_callback(c, nullptr, nullptr);
- }
-}
-
-static const char* appNameFromExecutable()
-{
- const char* name = (*_NSGetArgv())[0];
- const char* p = std::strrchr(name, '/');
-
- if (p && *(p+1))
- return p+1;
- return name;
+struct PAContextReady {
+ std::mutex mutex;
+ std::condition_variable condition;
+ bool finished=false;
+};
+
+static void paContextStateCB(pa_context* context,void* userdata) {
+ auto state=pa_context_get_state(context);
+ if(state!=PA_CONTEXT_READY && state!=PA_CONTEXT_FAILED && state!=PA_CONTEXT_TERMINATED) return;
+ auto* ready=static_cast(userdata);
+ std::lock_guard lock(ready->mutex);
+ ready->finished=true;
+ ready->condition.notify_one();
}
-void AudioHardwareImplPA::getPAContext(void (^cb)(pa_context*))
-{
- // TODO: destruction
- static std::mutex mutex;
-
- if (m_context == nullptr)
- {
- std::unique_lock l(mutex);
-
- if (m_context == nullptr)
- {
- const char* appname = appNameFromExecutable();
- const char* appid = "org.darlinghq.some-app";
-
- pa_proplist* proplist = pa_proplist_new();
-
- // Try to find a better application name & id from the bundle
- CFBundleRef mainBundle = CFBundleGetMainBundle();
- if (mainBundle)
- {
- CFStringRef ident = CFBundleGetIdentifier(mainBundle);
- if (ident)
- appid = CFStringGetCStringPtr(ident, kCFStringEncodingUTF8);
-
- CFDictionaryRef infoDict = CFBundleGetLocalInfoDictionary(mainBundle);
- if (!infoDict)
- infoDict = CFBundleGetInfoDictionary(mainBundle);
-
- if (infoDict)
- {
- CFStringRef name = (CFStringRef) CFDictionaryGetValue(infoDict, CFSTR("CFBundleDisplayName"));
- if (!name)
- name = (CFStringRef) CFDictionaryGetValue(infoDict, CFSTR("CFBundleName"));
- if (name)
- appname = CFStringGetCStringPtr(name, kCFStringEncodingUTF8);
- }
- }
-
- pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, appname);
- pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, appid);
- if (m_paRole)
- pa_proplist_sets(proplist, PA_PROP_MEDIA_ROLE, m_paRole);
- // pa_proplist_sets(proplist, PA_PROP_APPLICATION_ICON_NAME, "icon-name");
- // pa_proplist_sets(proplist, PA_PROP_MEDIA_ROLE, "game");
-
- m_loop.reset(new PADispatchMainLoop);
-
- m_context = pa_context_new_with_proplist(m_loop->getAPI(), appname, proplist);
- pa_proplist_free(proplist);
-
- if (!m_context)
- {
- cb(nullptr);
- return;
- }
-
- pa_context_set_state_callback(m_context, paContextStateCB, Block_copy(cb));
-
- if (pa_context_connect(m_context, nullptr, PA_CONTEXT_NOFLAGS, nullptr) < 0)
- {
- std::cerr << "pa_context_connect() returned an error\n";
- pa_context_set_state_callback(m_context, nullptr, nullptr);
- cb(nullptr);
- return;
- }
-
- m_loop->resume();
- }
- }
- else
- cb(m_context);
+void AudioHardwareImplPA::getPAContext(void (^cb)(pa_context*)) {
+ // Serialize initial connection and reuse. The callback always runs on the
+ // PulseAudio queue after readiness, with the stream fully constructed.
+ static std::mutex creationMutex;
+ std::unique_lock creationLock(creationMutex);
+ if(!m_context) {
+ pa_proplist* properties=pa_proplist_new();
+ pa_proplist_sets(properties,PA_PROP_APPLICATION_NAME,"Darling CoreAudio");
+ pa_proplist_sets(properties,PA_PROP_APPLICATION_ID,"org.darlinghq.coreaudio");
+ if(m_paRole) pa_proplist_sets(properties,PA_PROP_MEDIA_ROLE,m_paRole);
+ m_loop.reset(new PADispatchMainLoop);
+ m_context=pa_context_new_with_proplist(m_loop->getAPI(),"Darling CoreAudio",properties);
+ pa_proplist_free(properties);
+ if(!m_context) { cb(nullptr);return; }
+ PAContextReady ready;
+ pa_context_set_state_callback(m_context,paContextStateCB,&ready);
+ int connected=pa_context_connect(m_context,nullptr,PA_CONTEXT_NOFLAGS,nullptr);
+ m_loop->resume();
+ if(connected>=0) {
+ std::unique_lock lock(ready.mutex);
+ ready.condition.wait_for(lock,std::chrono::seconds(10),[&]{return ready.finished;});
+ }
+ // Remove the stack-backed readiness callback before leaving its scope.
+ m_loop->sync(^{pa_context_set_state_callback(m_context,nullptr,nullptr);});
+ }
+ m_loop->sync(^{cb(pa_context_get_state(m_context)==PA_CONTEXT_READY?m_context:nullptr);});
}
pa_sample_spec AudioHardwareImplPA::paSampleSpecForASBD(const AudioStreamBasicDescription& asbd, bool* convertSignedUnsigned)
diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.h b/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.h
index 8421491cd..03a530bfd 100644
--- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.h
+++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareImplPA.h
@@ -38,6 +38,9 @@ public:
const void* inQualifierData, UInt32 inDataSize, const void* inData) override;
void getPAContext(void (^cb)(pa_context*));
+ void syncPA(void (^block)(void));
+ dispatch_queue_t paQueue() const { return m_loop ? m_loop->queue() : nullptr; }
+ OSStatus stop(AudioDeviceIOProcID inProcID) override;
static pa_sample_spec paSampleSpecForASBD(const AudioStreamBasicDescription& asbd, bool* convertSignedUnsigned = nullptr);
protected:
AudioHardwareStream* createStream(AudioDeviceIOProc callback, void* clientData) override;
@@ -49,4 +52,3 @@ private:
};
#endif
-
diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.cpp b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.cpp
index 0db6ebda7..3f3bd6542 100644
--- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.cpp
+++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.cpp
@@ -26,6 +26,11 @@ along with Darling. If not, see .
AudioHardwareStreamPA::AudioHardwareStreamPA(AudioHardwareImplPA* hw, AudioDeviceIOProc callback, void* clientData)
: AudioHardwareStream(hw, false), m_callback(callback), m_clientData(clientData)
{
+}
+
+void AudioHardwareStreamPA::initialize()
+{
+ auto* hw=static_cast(m_hw);
hw->getPAContext(^(pa_context* context) {
if (!context)
{
@@ -42,6 +47,7 @@ AudioHardwareStreamPA::AudioHardwareStreamPA(AudioHardwareImplPA* hw, AudioDevic
}
m_stream = pa_stream_new(context, "CoreAudio", &spec, nullptr);
+ if(!m_stream) return;
//pa_stream_set_state_callback(m_stream, [](pa_stream *s, void *userdata) {
//std::cout << "Stream state: " << pa_stream_get_state(s) << std::endl;
@@ -59,8 +65,8 @@ AudioHardwareStreamPA::AudioHardwareStreamPA(AudioHardwareImplPA* hw, AudioDevic
AudioHardwareStreamPA::~AudioHardwareStreamPA()
{
- if (m_stream)
- pa_stream_unref(m_stream);
+ auto* hw=static_cast(m_hw);
+ hw->syncPA(^{if(m_stream) {pa_stream_unref(m_stream);m_stream=nullptr;}});
}
void AudioHardwareStreamPA::start()
@@ -70,9 +76,20 @@ void AudioHardwareStreamPA::start()
void AudioHardwareStreamPA::stop()
{
- std::unique_lock l(m_stopMutex);
- pa_stream_disconnect(m_stream);
- m_running = false;
+ auto* hw=static_cast(m_hw);
+ hw->syncPA(^{
+ // All stream operations and callbacks are serialized by syncPA.
+ // A callback may call stop() itself; taking its mutex here deadlocks.
+ m_running=false;
+ if(m_stream) {
+ // Disconnect may leave queued protocol work holding a stream ref.
+ // Detach client callbacks before the C++ owner can be destroyed.
+ pa_stream_set_write_callback(m_stream,nullptr,nullptr);
+ pa_stream_set_read_callback(m_stream,nullptr,nullptr);
+ pa_stream_set_state_callback(m_stream,nullptr,nullptr);
+ pa_stream_disconnect(m_stream);
+ }
+ });
}
// This function seems to only convert unsigned to signed, but it works both ways in practice
diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.h b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.h
index 0425ae97f..fdf08d2dc 100644
--- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.h
+++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPA.h
@@ -34,18 +34,17 @@ public:
void stop(/*void(^cbDone)()*/) override;
protected:
+ void initialize();
virtual void start();
void transformSignedUnsigned(AudioBufferList* abl) const;
protected:
AudioDeviceIOProc m_callback;
void* m_clientData;
- pa_stream* m_stream;
- void(^m_cbDone)();
+ pa_stream* m_stream = nullptr;
+ void(^m_cbDone)() = nullptr;
bool m_convertSignedUnsigned = false;
bool m_running = false;
- std::mutex m_stopMutex;
};
#endif /* AUDIOHARDWARESTREAMPA_H */
-
diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.cpp b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.cpp
index d6825f1d5..fd9efc988 100644
--- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.cpp
+++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAInput.cpp
@@ -21,10 +21,37 @@ along with Darling. If not, see .
#include "AudioHardwareImplPA.h"
#include
#include
+#include
+#include
+#include
+#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)
{
+ initialize();
// m_dump.open("/tmp/dump.raw", std::ios_base::out | std::ios_base::binary);
}
@@ -32,25 +59,26 @@ void AudioHardwareStreamPAInput::paStreamReadCB(pa_stream* s, size_t length, voi
{
AudioHardwareStreamPAInput* This = static_cast(self);
- std::unique_lock l(This->m_stopMutex);
if (!This->m_running)
return;
- 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(alloca(sizeof(AudioBufferList) + sizeof(AudioBuffer)));
- std::vector hole;
// 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(static_cast(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
+++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.cpp
@@ -21,71 +21,318 @@ along with Darling. If not, see .
#include "AudioHardwareImplPA.h"
#include
#include
+#include
+#include "../../AudioTrace.h"
+
+// Real CoreAudio calls an output IOProc once per IO cycle: evenly spaced at the buffer
+// period, with inOutputTime.mHostTime advancing by one buffer duration per call. Clients
+// such as Chromium's AUHALStream rely on that (they read the previous renderer buffer and
+// request the next one on every call), so PulseAudio's bursty write requests must not
+// drive the IOProc directly. Instead a dispatch timer on the PA queue ticks at the block
+// period and renders at most one block per tick (plus bounded catch-up after a late wakeup).
+
+static const unsigned kPrefillBlocks = 8; // == tlength
+static const unsigned kMaxCatchUpBlocks = 2; // extra blocks after a late wakeup
+static const unsigned kServoWindowTicks = 32;
+
+static bool rateLimited(unsigned& counter, unsigned head, unsigned every) {
+ if (!rclolAudioTraceEnabled()) return false;
+ unsigned n = counter++;
+ return n < head || n % every == 0;
+}
+static unsigned g_wcbEvents = 0, g_iopEvents = 0, g_tickEvents = 0, g_paceEvents = 0;
+static bool wcbShouldTrace() { return rateLimited(g_wcbEvents, 50, 1000); }
+static bool iopShouldTrace() { return rateLimited(g_iopEvents, 200, 500); }
+static bool tickShouldTrace() { return rateLimited(g_tickEvents, 200, 500); }
+static bool paceShouldTrace() { return rateLimited(g_paceEvents, 100, 100); }
+
+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 uint64_t nowNs() { return hostToNs(mach_absolute_time()); }
AudioHardwareStreamPAOutput::AudioHardwareStreamPAOutput(AudioHardwareImplPA* hw, AudioDeviceIOProc callback, void* clientData)
: AudioHardwareStreamPA(hw, callback, clientData)
{
+ initialize();
}
-void AudioHardwareStreamPAOutput::paStreamWriteCB(pa_stream* s, size_t length, void* self)
+AudioHardwareStreamPAOutput::~AudioHardwareStreamPAOutput()
{
- AudioHardwareStreamPAOutput* This = static_cast(self);
- std::unique_lock l(This->m_stopMutex);
+ // Normally already done by stop(); the timer must never outlive this object.
+ static_cast(m_hw)->syncPA(^{ m_running = false; cancelTimer(); });
+}
- if (!This->m_running)
+void AudioHardwareStreamPAOutput::stop()
+{
+ // Runs on (or synchronously with) the PA queue, so no tick is in flight unless stop()
+ // was called from inside the IOProc, which renderBlock() handles via m_running.
+ // Cancellation guarantees the timer handler is never invoked again.
+ static_cast(m_hw)->syncPA(^{ m_running = false; cancelTimer(); });
+ AudioHardwareStreamPA::stop();
+}
+
+void AudioHardwareStreamPAOutput::cancelTimer()
+{
+ if (m_timer)
{
- pa_stream_cork(This->m_stream, true, [](pa_stream*, int, void*) {}, nullptr);
- return;
+ dispatch_source_cancel(m_timer);
+ dispatch_release(m_timer);
+ m_timer = nullptr;
+ }
+}
+
+uint64_t AudioHardwareStreamPAOutput::outputLatencyNs(size_t writable, bool* fromPA)
+{
+ pa_usec_t usec = 0;
+ int negative = 0;
+ if (pa_stream_get_latency(m_stream, &usec, &negative) == 0)
+ {
+ *fromPA = true;
+ return negative ? 0 : uint64_t(usec) * 1000;
}
+ // No timing info yet: at least account for what is queued in the PA buffer.
+ *fromPA = false;
+ const pa_buffer_attr* attr = pa_stream_get_buffer_attr(m_stream);
+ const pa_sample_spec* spec = pa_stream_get_sample_spec(m_stream);
+ if (!attr || !spec || writable == size_t(-1) || attr->tlength <= writable)
+ return 0;
+ return uint64_t(pa_bytes_to_usec(attr->tlength - writable, spec)) * 1000;
+}
+
+AudioHardwareStreamPAOutput::RenderResult AudioHardwareStreamPAOutput::renderBlock(bool trace)
+{
+ if (!m_running || m_corked || !m_stream || !m_blockBytes)
+ return RenderResult::Stopped;
- // std::cout << "AudioHardwareStreamPAOutput::paStreamWriteCB()\n";
+ const size_t writable = pa_stream_writable_size(m_stream);
+ if (writable == size_t(-1) || writable < m_blockBytes)
+ return RenderResult::NoSpace;
- AudioTimeStamp fake = {0};
AudioBufferList* abl = static_cast(alloca(sizeof(AudioBufferList) + sizeof(AudioBuffer)));
+ // Non-interleaved (planar) audio would have multiple buffers, but PA doesn't even support that AFAIK
+ abl->mNumberBuffers = 1;
+ abl->mBuffers[0].mNumberChannels = m_hw->asbd().mChannelsPerFrame;
+
+ size_t rqsize = m_blockBytes;
+ if (pa_stream_begin_write(m_stream, (void**)&abl->mBuffers[0].mData, &rqsize) < 0 || !rqsize)
+ {
+ if (paceShouldTrace()) rclolAudioTrace("PA_OUT begin_write failed rqsize=%zu\n", rqsize);
+ return RenderResult::NoSpace;
+ }
+ if (rqsize < m_blockBytes)
+ {
+ // Never hand the client a partial block (real HALs use a fixed frame count).
+ if (paceShouldTrace()) rclolAudioTrace("PA_OUT granted rqsize=%zu < block=%zu: cancel\n", rqsize, m_blockBytes);
+ pa_stream_cancel_write(m_stream);
+ return RenderResult::NoSpace;
+ }
+ abl->mBuffers[0].mDataByteSize = m_blockBytes;
+
+ bool latencyFromPA = false;
+ const uint64_t latencyNs = outputLatencyNs(writable, &latencyFromPA);
+ const double rate = m_hw->asbd().mSampleRate;
+ const double latencyFrames = rate > 0 ? double(latencyNs) * rate / 1e9 : 0;
+ const uint64_t nowHost = mach_absolute_time();
- size_t done = 0;
+ AudioTimeStamp now = {0};
+ now.mHostTime = nowHost;
+ // Device position currently being played: the block about to be rendered starts
+ // latencyFrames after it, matching the host-time difference below.
+ now.mSampleTime = std::max(0.0, double(m_frames) - latencyFrames);
+ now.mRateScalar = 1.0;
+ now.mFlags = kAudioTimeStampHostTimeValid | kAudioTimeStampSampleTimeValid | kAudioTimeStampRateScalarValid;
- while (done < length && This->m_running)
+ AudioTimeStamp output = now;
+ output.mHostTime = nowHost + nsToHost(latencyNs);
+ output.mSampleTime = double(m_frames);
+
+ const double dHostMs = m_lastIOProcHost ? double(hostToNs(nowHost - m_lastIOProcHost)) / 1e6 : 0;
+ m_lastIOProcHost = nowHost;
+
+ OSStatus status = m_callback(m_hw->id(), &now, nullptr, nullptr, abl, &output, m_clientData);
+ if (trace)
+ rclolAudioTrace("PA_OUT iop dHostMs=%.3f writable=%zu latencyMs=%.2f(%s) frames=%llu status=%d size=%u\n",
+ dHostMs, writable, double(latencyNs) / 1e6, latencyFromPA ? "pa" : "est",
+ (unsigned long long) m_frames, (int) status, (unsigned) abl->mBuffers[0].mDataByteSize);
+
+ if (!m_running)
{
- // Non-interleaved (planar) audio would have multiple buffers, but PA doesn't even support that AFAIK
- abl->mNumberBuffers = 1;
+ // The client stopped/disconnected the stream during its IOProc.
+ pa_stream_cancel_write(m_stream);
+ return RenderResult::Stopped;
+ }
+ if (status != noErr || !abl->mBuffers[0].mDataByteSize)
+ {
+ if (paceShouldTrace()) rclolAudioTrace("PA_OUT IOProc failed/empty status=%d size=%u: CORK\n", (int) status,
+ (unsigned) abl->mBuffers[0].mDataByteSize);
+ pa_stream_cancel_write(m_stream);
+ pa_stream_cork(m_stream, true, [](pa_stream*, int, void*) {}, nullptr);
+ m_corked = true;
+ return RenderResult::Stopped;
+ }
- abl->mBuffers[0].mNumberChannels = 2;
- // abl->mBuffers[0].mData = This->m_buffer;
+ if (m_convertSignedUnsigned)
+ transformSignedUnsigned(abl);
- // std::cout << "AudioHardwareStreamPAOutput() length req=" << length << ", done=" << done << std::endl;
- size_t rqsize = std::min(This->m_bufferSize, length-done);
- pa_stream_begin_write(This->m_stream, (void**) &abl->mBuffers[0].mData, &rqsize);
+ // Always submit exactly one block so the sample clock stays block-aligned.
+ int rv = pa_stream_write(m_stream, abl->mBuffers[0].mData, m_blockBytes, nullptr, 0, PA_SEEK_RELATIVE);
+ if (rv != 0)
+ {
+ if (paceShouldTrace()) rclolAudioTrace("PA_OUT pa_stream_write rv=%d\n", rv);
+ return RenderResult::NoSpace;
+ }
+ m_frames += m_blockFrames;
+ return RenderResult::Written;
+}
- abl->mBuffers[0].mDataByteSize = rqsize;
+uint64_t AudioHardwareStreamPAOutput::deadlineNs() const
+{
+ return m_anchorNs + uint64_t(std::max(0.0, double(m_tickIndex) * m_periodNs - m_advanceNs));
+}
- // Call the client for more data
- OSStatus status = This->m_callback(This->m_hw->id(), &fake, nullptr, nullptr, abl, &fake, This->m_clientData);
- if (status != noErr || !abl->mBuffers[0].mDataByteSize)
- {
- // std::cerr << "AudioDeviceIOProc returned " << status << ", corking...\n";
+void AudioHardwareStreamPAOutput::armTimer(uint64_t now)
+{
+ if (!m_timer)
+ return;
+ const uint64_t deadline = deadlineNs();
+ const int64_t delta = deadline > now ? int64_t(deadline - now) : 0;
+ dispatch_source_set_timer(m_timer, dispatch_time(DISPATCH_TIME_NOW, delta), DISPATCH_TIME_FOREVER, 100 * NSEC_PER_USEC);
+}
- pa_stream_cork(This->m_stream, true, [](pa_stream*, int, void*) {}, nullptr);
- break;
- }
- else
- {
- if (This->m_convertSignedUnsigned)
- This->transformSignedUnsigned(abl);
-
- // std::cout << "AudioHardwareStreamPAOutput::paStreamWriteCB(): got " << abl->mBuffers[0].mDataByteSize << " bytes\n";
- int rv = pa_stream_write(This->m_stream, abl->mBuffers[0].mData, abl->mBuffers[0].mDataByteSize, nullptr, 0, PA_SEEK_RELATIVE);
- if (rv != 0)
- {
- // std::cerr << "pa_stream_write() failed\n";
- break;
- }
- //else
- // std::cout << "pa_stream_write() OK\n";
- }
+void AudioHardwareStreamPAOutput::prefill()
+{
+ if (!m_running || m_corked || m_primed || !m_stream || pa_stream_get_state(m_stream) != PA_STREAM_READY)
+ return;
+
+ // The only burst: fill the target buffer once at start.
+ unsigned blocks = 0;
+ RenderResult r = RenderResult::Written;
+ while (blocks < kPrefillBlocks && (r = renderBlock(iopShouldTrace())) == RenderResult::Written)
+ ++blocks;
+ if (rclolAudioTraceEnabled())
+ rclolAudioTrace("PA_OUT prefill blocks=%u result=%d periodMs=%.3f blockFrames=%u\n", blocks, (int) r,
+ m_periodNs / 1e6, m_blockFrames);
+ if (r == RenderResult::Stopped || !blocks)
+ return;
+
+ m_primed = true;
+ m_anchorNs = nowNs();
+ m_tickIndex = 1;
+ m_advanceNs = 0;
+ armTimer(m_anchorNs);
+}
+
+void AudioHardwareStreamPAOutput::tick()
+{
+ if (!m_running || m_corked || !m_timer)
+ return;
+ if (!m_primed)
+ {
+ prefill();
+ return;
+ }
+
+ uint64_t now = nowNs();
+ uint64_t deadline = deadlineNs();
+ if (now < deadline)
+ {
+ // Early wakeup (leeway); wait for the real deadline.
+ armTimer(now);
+ return;
+ }
+
+ const bool trace = tickShouldTrace();
+ const size_t writable = pa_stream_writable_size(m_stream);
+ if (writable != size_t(-1))
+ m_windowMinWritable = std::min(m_windowMinWritable, writable);
+
+ const uint64_t behind = uint64_t(double(now - deadline) / m_periodNs);
+ unsigned toRender = 1;
+ if (behind > kMaxCatchUpBlocks)
+ {
+ // Far behind (stalled queue, suspended process): don't burst, restart the clock.
+ if (paceShouldTrace())
+ rclolAudioTrace("PA_OUT reanchor lateMs=%.3f behindBlocks=%llu\n", double(now - deadline) / 1e6,
+ (unsigned long long) behind);
+ m_anchorNs = now;
+ m_tickIndex = 0;
+ m_advanceNs = 0;
+ }
+ else if (behind)
+ {
+ toRender += unsigned(behind);
+ if (paceShouldTrace())
+ rclolAudioTrace("PA_OUT catchup lateMs=%.3f blocks=%u\n", double(now - deadline) / 1e6, toRender);
+ }
+
+ if (trace)
+ rclolAudioTrace("PA_OUT tick lateMs=%.3f writable=%zu render=%u\n", double(now - deadline) / 1e6, writable, toRender);
+
+ for (unsigned i = 0; i < toRender; i++)
+ {
+ RenderResult r = renderBlock(iopShouldTrace());
+ if (r == RenderResult::Stopped)
+ return; // also covers stop() from inside the IOProc (timer already cancelled)
+ if (r == RenderResult::NoSpace && paceShouldTrace())
+ rclolAudioTrace("PA_OUT skip: writable=%zu < block=%zu\n", pa_stream_writable_size(m_stream), m_blockBytes);
+ // The IO cycle elapses either way; a skipped cycle means PA is consuming slower than us.
+ ++m_tickIndex;
+ }
- done += abl->mBuffers[0].mDataByteSize;
+ // Drift servo: the PA sink clock isn't our clock. If the buffer stayed under-filled for a
+ // whole window (min writable >= 2 blocks), run ~6% faster for the next window. Each tick
+ // stays a single, evenly spaced IOProc call.
+ if (++m_windowTicks >= kServoWindowTicks)
+ {
+ const bool low = m_windowMinWritable != SIZE_MAX && m_windowMinWritable >= 2 * m_blockBytes;
+ const double step = low ? m_periodNs / 16 : 0;
+ if (step != m_servoStepNs && paceShouldTrace())
+ rclolAudioTrace("PA_OUT servo minWritable=%zu step=%s\n", m_windowMinWritable, low ? "fast" : "off");
+ m_servoStepNs = step;
+ m_windowMinWritable = SIZE_MAX;
+ m_windowTicks = 0;
}
+ m_advanceNs += m_servoStepNs;
+
+ armTimer(nowNs());
+}
+
+void AudioHardwareStreamPAOutput::paStreamWriteCB(pa_stream* s, size_t length, void* self)
+{
+ AudioHardwareStreamPAOutput* This = static_cast(self);
+
+ if (!This->m_running)
+ {
+ if (wcbShouldTrace()) rclolAudioTrace("PA_WRITE_CB length=%zu not-running: corking\n", length);
+ pa_stream_cork(This->m_stream, true, [](pa_stream*, int, void*) {}, nullptr);
+ return;
+ }
+
+ // Rendering is paced by the IO timer; the write request only triggers the initial prefill.
+ if (wcbShouldTrace())
+ rclolAudioTrace("PA_WRITE_CB length=%zu writable=%zu primed=%d corked=%d\n", length,
+ pa_stream_writable_size(s), This->m_primed, This->m_corked);
+ if (!This->m_timer)
+ {
+ // No pacing timer could be created: fall back to rendering whole blocks on demand.
+ while (This->renderBlock(iopShouldTrace()) == RenderResult::Written) {}
+ return;
+ }
+ if (!This->m_primed)
+ This->prefill();
}
void AudioHardwareStreamPAOutput::start()
@@ -93,10 +340,44 @@ void AudioHardwareStreamPAOutput::start()
struct pa_buffer_attr battr;
battr.fragsize = uint32_t(-1);
- battr.maxlength = uint32_t(m_bufferSize);
- battr.minreq = uint32_t(-1);
- battr.prebuf = battr.maxlength;
- battr.tlength = uint32_t(-1);
+ // Size the server buffer in whole IOProc blocks so every IO cycle can supply one.
+ uint32_t bpf = m_hw->asbd().mBytesPerFrame;
+ uint32_t block = bpf ? (m_hw->bufferSize() / bpf) * bpf : m_bufferSize;
+ if (!block)
+ block = m_bufferSize;
+ // ~46 ms target at 256 frames/44.1 kHz: a 2-block target underran, since each
+ // block waits on a cross-process round trip to the client's renderer.
+ battr.maxlength = block * 16;
+ battr.minreq = block;
+ battr.prebuf = block * 2;
+ battr.tlength = block * kPrefillBlocks;
+
+ // Real HALs always call the IOProc with the same frame count (kAudioDevicePropertyBufferFrameSize).
+ m_blockFrames = bpf ? m_hw->bufferSize() / bpf : 0;
+ m_blockBytes = size_t(m_blockFrames) * bpf;
+ const double rate = m_hw->asbd().mSampleRate;
+ m_periodNs = rate > 0 ? double(m_blockFrames) * 1e9 / rate : 0;
+
+ // start() runs on the PA queue, so the timer shares its serialization with all pa_* calls.
+ // Disabled: Darling's dispatch timers fire 3-35 ms late (one stalled 30 s), which starved
+ // PA into silence. Render on PA's write requests instead; the output timestamps still
+ // come from PA's latency, so blocks within one request get correctly spaced play times.
+ static const bool kUsePacingTimer = false;
+ dispatch_queue_t queue = static_cast(m_hw)->paQueue();
+ if (kUsePacingTimer && queue && m_blockBytes && m_periodNs > 0)
+ {
+ m_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
+ if (m_timer)
+ {
+ dispatch_source_set_event_handler(m_timer, ^{ tick(); });
+ // Resumed immediately (a suspended source can't be released); armed after prefill.
+ dispatch_source_set_timer(m_timer, DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 0);
+ dispatch_resume(m_timer);
+ }
+ }
+ if (rclolAudioTraceEnabled())
+ rclolAudioTrace("PA_OUT start block=%zu frames=%u periodMs=%.3f timer=%d\n", m_blockBytes, m_blockFrames,
+ m_periodNs / 1e6, m_timer != nullptr);
pa_stream_set_write_callback(m_stream, paStreamWriteCB, this);
@@ -105,7 +386,5 @@ void AudioHardwareStreamPAOutput::start()
|PA_STREAM_AUTO_TIMING_UPDATE) /* PA_STREAM_START_CORKED */,
nullptr, nullptr);
- // pa_stream_cork(m_stream, false, [](pa_stream*, int, void*) {}, nullptr);
AudioHardwareStreamPA::start();
}
-
diff --git a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.h b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.h
index 735474128..8d73f9902 100644
--- a/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.h
+++ b/src/CoreAudio/CoreAudio/pulse/AudioHardwareStreamPAOutput.h
@@ -20,15 +20,46 @@ along with Darling. If not, see .
#ifndef AUDIOHARDWARESTREAMPAOUTPUT_H
#define AUDIOHARDWARESTREAMPAOUTPUT_H
#include "AudioHardwareStreamPA.h"
+#include
+#include
class AudioHardwareStreamPAOutput : public AudioHardwareStreamPA
{
public:
AudioHardwareStreamPAOutput(AudioHardwareImplPA* hw, AudioDeviceIOProc callback, void* clientData);
+ ~AudioHardwareStreamPAOutput();
+ void stop() override;
private:
+ enum class RenderResult { Written, NoSpace, Stopped };
+
+ uint64_t m_frames = 0;
void start() override;
static void paStreamWriteCB(pa_stream* s, size_t length, void* self);
+
+ // IO cycle pacing; everything below is only touched on the PA queue.
+ void cancelTimer();
+ void prefill();
+ void tick();
+ void armTimer(uint64_t nowNs);
+ uint64_t deadlineNs() const;
+ RenderResult renderBlock(bool trace);
+ uint64_t outputLatencyNs(size_t writable, bool* fromPA);
+
+ dispatch_source_t m_timer = nullptr;
+ size_t m_blockBytes = 0;
+ uint32_t m_blockFrames = 0;
+ double m_periodNs = 0;
+ bool m_primed = false;
+ bool m_corked = false;
+ // Deadline k is m_anchorNs + k * m_periodNs - m_advanceNs.
+ uint64_t m_anchorNs = 0;
+ uint64_t m_tickIndex = 0;
+ double m_advanceNs = 0;
+ // Drift servo: smallest writable size seen over the current window of ticks.
+ size_t m_windowMinWritable = SIZE_MAX;
+ unsigned m_windowTicks = 0;
+ double m_servoStepNs = 0;
+ uint64_t m_lastIOProcHost = 0;
};
#endif /* AUDIOHARDWARESTREAMPA_H */
-
diff --git a/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.cpp b/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.cpp
index 63f3545e2..4a2d8ed62 100644
--- a/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.cpp
+++ b/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.cpp
@@ -23,6 +23,8 @@ PADispatchMainLoop::PADispatchMainLoop()
{
// Create GCD queue
m_queue = dispatch_queue_create("org.darlinghq.coreaudio.pulseaudio", nullptr);
+ m_sources=dispatch_group_create();
+ dispatch_queue_set_specific(m_queue,this,this,nullptr);
// PulseAudio expects the event loop to be suspended while we're creating the context.
// I couldn't find any mention of this in the documentation, but figured it out the hard way.
@@ -32,9 +34,22 @@ PADispatchMainLoop::PADispatchMainLoop()
PADispatchMainLoop::~PADispatchMainLoop()
{
resume();
+ dispatch_group_wait(m_sources,DISPATCH_TIME_FOREVER);
+ dispatch_sync(m_queue, ^{});
+ dispatch_release(m_sources);
dispatch_release(m_queue);
}
+void PADispatchMainLoop::sync(void (^block)(void)) {
+ if(dispatch_get_specific(this)==this) block();
+ else {resume();dispatch_sync(m_queue,block);}
+}
+
+void PADispatchMainLoop::async(void (^block)(void)) {
+ resume();
+ dispatch_async(m_queue, block);
+}
+
void PADispatchMainLoop::suspend()
{
if (!m_suspended)
@@ -87,6 +102,8 @@ struct dual_source
pa_io_event_flags_t events;
void* userdata;
int fd;
+ bool freed=false;
+ unsigned cancellations=2;
};
pa_io_event* PADispatchMainLoop::io_new(pa_mainloop_api *a, int fd, pa_io_event_flags_t events, pa_io_event_cb_t cb, void *userdata)
@@ -108,22 +125,25 @@ pa_io_event* PADispatchMainLoop::io_new(pa_mainloop_api *a, int fd, pa_io_event_
dual->sourceWrite = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, fd, 0, q);
dispatch_source_set_event_handler(dual->sourceRead, ^{
- dual->callback(This->getAPI(), reinterpret_cast(dual), fd, PA_IO_EVENT_INPUT, dual->userdata);
+ if(!dual->freed && dual->readResumed) dual->callback(This->getAPI(), reinterpret_cast(dual), fd, PA_IO_EVENT_INPUT, dual->userdata);
});
dispatch_source_set_event_handler(dual->sourceWrite, ^{
// std::cout << "PADispatchMainLoop::io_new(): write event on fd " << fd << std::endl;
- dual->callback(This->getAPI(), reinterpret_cast(dual), fd, PA_IO_EVENT_OUTPUT, dual->userdata);
+ if(!dual->freed && dual->writeResumed) dual->callback(This->getAPI(), reinterpret_cast(dual), fd, PA_IO_EVENT_OUTPUT, dual->userdata);
});
- dispatch_source_set_cancel_handler(dual->sourceWrite, ^{
- if (dual->destroy)
- {
- dual->destroy(This->getAPI(), reinterpret_cast(dual), dual->userdata);
- dual->destroy = nullptr;
- }
- delete dual;
- });
+ dispatch_group_enter(This->m_sources);
+ dispatch_group_enter(This->m_sources);
+ void (^cancel)(void)=^{
+ if(--dual->cancellations==0) {
+ if(dual->destroy) dual->destroy(This->getAPI(),reinterpret_cast(dual),dual->userdata);
+ delete dual;
+ }
+ dispatch_group_leave(This->m_sources);
+ };
+ dispatch_source_set_cancel_handler(dual->sourceRead,cancel);
+ dispatch_source_set_cancel_handler(dual->sourceWrite,cancel);
if (events & (PA_IO_EVENT_INPUT | PA_IO_EVENT_ERROR | PA_IO_EVENT_HANGUP))
{
@@ -185,6 +205,7 @@ void PADispatchMainLoop::io_free(pa_io_event *e)
{
dual_source* dual = reinterpret_cast(e);
// std::cout << "PADispatchMainLoop::io_free(): fd=" << dual->fd << std::endl;
+ dual->freed=true;
if (!dual->readResumed)
dispatch_resume(dual->sourceRead);
@@ -218,6 +239,7 @@ struct pa_timer
struct timespec when;
struct linux_timeval tv;
pa_time_event_destroy_cb_t destroy = nullptr;
+ bool freed=false;
};
pa_time_event *PADispatchMainLoop::time_new(pa_mainloop_api *a, const struct timeval *tv, pa_time_event_cb_t cb, void *userdata)
@@ -232,22 +254,24 @@ pa_time_event *PADispatchMainLoop::time_new(pa_mainloop_api *a, const struct tim
timer->source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q);
- timer->when.tv_sec = real_tv->tv_sec;
- timer->when.tv_nsec = real_tv->tv_usec * 1000; // convert to ns
- timer->tv = *real_tv;
+ timer->when.tv_sec = real_tv ? real_tv->tv_sec : 0;
+ timer->when.tv_nsec = real_tv ? real_tv->tv_usec * 1000 : 0;
+ timer->tv = real_tv ? *real_tv : linux_timeval{};
// std::cout << "PADispatchMainLoop::time_new(), sec=" << real_tv->tv_sec << ", usec=" << real_tv->tv_usec << std::endl;
- dispatch_source_set_timer(timer->source, dispatch_walltime(&timer->when, 0), 0, 0);
+ dispatch_source_set_timer(timer->source, real_tv ? dispatch_walltime(&timer->when, 0) : DISPATCH_TIME_FOREVER, DISPATCH_TIME_FOREVER, 0);
dispatch_source_set_event_handler(timer->source, ^{
// std::cout << "PADispatchMainLoop::time_new(): fired\n";
- cb(This->getAPI(), reinterpret_cast(timer), reinterpret_cast(&timer->tv), timer->userdata);
+ if(!timer->freed) cb(This->getAPI(), reinterpret_cast(timer), reinterpret_cast(&timer->tv), timer->userdata);
});
+ dispatch_group_enter(This->m_sources);
dispatch_source_set_cancel_handler(timer->source, ^{
if (timer->destroy)
timer->destroy(This->getAPI(), reinterpret_cast(timer), timer->userdata);
delete timer;
+ dispatch_group_leave(This->m_sources);
});
dispatch_resume(timer->source);
@@ -259,6 +283,7 @@ void PADispatchMainLoop::time_restart(pa_time_event *e, const struct timeval *tv
{
const linux_timeval* real_tv = reinterpret_cast(tv);
pa_timer* timer = reinterpret_cast(e);
+ if(!tv) { dispatch_source_set_timer(timer->source,DISPATCH_TIME_FOREVER,DISPATCH_TIME_FOREVER,0);return; }
timer->when.tv_sec = real_tv->tv_sec;
timer->when.tv_nsec = real_tv->tv_usec * 1000; // convert to ns
@@ -266,7 +291,7 @@ void PADispatchMainLoop::time_restart(pa_time_event *e, const struct timeval *tv
// std::cout << "PADispatchMainLoop::time_restart(), sec=" << real_tv->tv_sec << ", usec=" << real_tv->tv_usec << std::endl;
- dispatch_source_set_timer(timer->source, dispatch_walltime(&timer->when, 0), 0, 0);
+ dispatch_source_set_timer(timer->source, dispatch_walltime(&timer->when, 0), DISPATCH_TIME_FOREVER, 0);
}
void PADispatchMainLoop::time_set_destroy(pa_time_event *e, pa_time_event_destroy_cb_t cb)
@@ -281,6 +306,7 @@ void PADispatchMainLoop::time_free(pa_time_event *e)
pa_timer* timer = reinterpret_cast(e);
// std::cout << "PADispatchMainLoop::time_free()\n";
+ timer->freed=true;
dispatch_source_cancel(timer->source);
dispatch_release(timer->source);
@@ -289,6 +315,8 @@ void PADispatchMainLoop::time_free(pa_time_event *e)
struct defer_event
{
bool active = true;
+ bool scheduled = true;
+ bool freed = false;
void* userdata;
pa_defer_event_cb_t callback;
pa_defer_event_destroy_cb_t destroy = nullptr;
@@ -298,7 +326,8 @@ struct defer_event
void PADispatchMainLoop::defer_event_fire(void* context)
{
defer_event* ev = static_cast(context);
- if (ev->active)
+ ev->scheduled=false;
+ if (ev->active && !ev->freed)
{
pa_mainloop_api* a = ev->loop->getAPI();
dispatch_queue_t q = static_cast(a->userdata)->m_queue;
@@ -307,8 +336,10 @@ void PADispatchMainLoop::defer_event_fire(void* context)
ev->callback(a, static_cast(context), ev->userdata);
- if (ev->active)
- dispatch_async_f(q, context, defer_event_fire);
+ if(ev->active && !ev->freed && !ev->scheduled) {
+ ev->scheduled=true;
+ dispatch_async_f(q,context,defer_event_fire);
+ }
}
}
@@ -322,7 +353,7 @@ pa_defer_event *PADispatchMainLoop::defer_new(pa_mainloop_api *a, pa_defer_event
e->loop = This;
e->userdata = userdata;
e->callback = cb;
-
+ dispatch_group_enter(This->m_sources);
dispatch_async_f(q, e, defer_event_fire);
return reinterpret_cast(e);
@@ -342,9 +373,11 @@ void PADispatchMainLoop::defer_enable(pa_defer_event *e, int b)
// std::cout << "PADispatchMainLoop::defer_enable(): with userdata=" << ev->userdata << ", enable=" << b << std::endl;
- if (!ev->active && b)
- dispatch_async_f(q, e, defer_event_fire);
- ev->active = !!b;
+ ev->active=!!b;
+ if(ev->active && !ev->freed && !ev->scheduled) {
+ ev->scheduled=true;
+ dispatch_async_f(q,e,defer_event_fire);
+ }
}
void PADispatchMainLoop::defer_free(pa_defer_event *e)
@@ -355,10 +388,11 @@ void PADispatchMainLoop::defer_free(pa_defer_event *e)
// std::cout << "PADispatchMainLoop::defer_free(): with userdata=" << ev->userdata << std::endl;
ev->active = false;
-
+ ev->freed=true;
dispatch_async(q, ^{
if (ev->destroy)
ev->destroy(ev->loop->getAPI(), e, ev->userdata);
+ dispatch_group_leave(ev->loop->m_sources);
delete ev;
});
}
diff --git a/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.h b/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.h
index ff83d1e15..1e86f9e99 100644
--- a/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.h
+++ b/src/CoreAudio/CoreAudio/pulse/PADispatchMainLoop.h
@@ -30,8 +30,12 @@ public:
void suspend();
void resume();
+ void sync(void (^block)(void));
+ void async(void (^block)(void));
pa_mainloop_api* getAPI();
+ // The serial queue all pa_* calls and callbacks run on.
+ dispatch_queue_t queue() const { return m_queue; }
private:
static pa_io_event* io_new(pa_mainloop_api *a, int fd, pa_io_event_flags_t events, pa_io_event_cb_t cb, void *userdata);
static void io_enable(pa_io_event *e, pa_io_event_flags_t events);
@@ -50,6 +54,7 @@ private:
static void defer_event_fire(void* context);
private:
dispatch_queue_t m_queue;
+ dispatch_group_t m_sources;
pa_mainloop_api m_api = {0};
bool m_suspended = true;
};
diff --git a/src/CoreAudio/CoreAudioComponent/AUHAL.cpp b/src/CoreAudio/CoreAudioComponent/AUHAL.cpp
index fe4d1eaa1..769b06586 100644
--- a/src/CoreAudio/CoreAudioComponent/AUHAL.cpp
+++ b/src/CoreAudio/CoreAudioComponent/AUHAL.cpp
@@ -1,3 +1,4 @@
+#include "../AudioTrace.h"
/*
This file is part of Darling.
@@ -19,6 +20,15 @@ along with Darling. If not, see .
#include "AUHAL.h"
#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
#pragma GCC visibility push(default)
AUDIOCOMPONENT_ENTRY(AUOutputBaseFactory, 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();
}
+UInt32 AUHAL::GetChannelLayoutTags(AudioUnitScope scope,AudioUnitElement element,AudioChannelLayoutTag* tags) {
+ if(element!=kOutputBus || (scope!=kAudioUnitScope_Input && scope!=kAudioUnitScope_Output)) return 0;
+ UInt32 channels=GetStreamFormat(scope,element).mChannelsPerFrame;
+ if(tags) *tags=channels==1?kAudioChannelLayoutTag_Mono:channels==2?kAudioChannelLayoutTag_Stereo:(kAudioChannelLayoutTag_DiscreteInOrder|channels);
+ return 1;
+}
+
+UInt32 AUHAL::GetAudioChannelLayout(AudioUnitScope scope,AudioUnitElement element,AudioChannelLayout* layout,Boolean& writable) {
+ AudioChannelLayoutTag tag;
+ if(!GetChannelLayoutTags(scope,element,&tag)) return 0;
+ writable=!m_running;
+ if(layout) { layout->mChannelLayoutTag=tag;layout->mChannelBitmap=0;layout->mNumberChannelDescriptions=0; }
+ return offsetof(AudioChannelLayout,mChannelDescriptions);
+}
+
+OSStatus AUHAL::SetAudioChannelLayout(AudioUnitScope scope,AudioUnitElement element,const AudioChannelLayout* layout) {
+ AudioChannelLayoutTag tag;
+ if(!layout || !GetChannelLayoutTags(scope,element,&tag)) return kAudioUnitErr_InvalidElement;
+ if(layout->mChannelLayoutTag==tag) return noErr;
+ /* darling-lol: a layout is acceptable when it describes the stream's number of channels, whichever way it is written:
+ * explicit channel descriptions (League sends two: left, right), a channel bitmap, or another tag with that count.
+ * Only the exact tag was accepted, so the game's stereo description list was refused (-10851) and it rebuilt its
+ * audio graph in a loop. */
+ const UInt32 wanted=GetStreamFormat(scope,element).mChannelsPerFrame;
+ UInt32 channels;
+ if(layout->mChannelLayoutTag==kAudioChannelLayoutTag_UseChannelDescriptions) channels=layout->mNumberChannelDescriptions;
+ else if(layout->mChannelLayoutTag==kAudioChannelLayoutTag_UseChannelBitmap) channels=__builtin_popcount(layout->mChannelBitmap);
+ else channels=layout->mChannelLayoutTag&0xffff;
+ return channels==wanted?noErr:kAudioUnitErr_InvalidPropertyValue;
+}
+
bool AUHAL::CanScheduleParameters() const
{
return true;
@@ -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()
@@ -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 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 lk(m_dataAvailableMutex);
+ const UInt32 frames = std::min(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(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(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(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 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(source)[i]));
+ else if (sampleBytes == 2)
+ for (UInt32 i = 0; i < count; i++)
+ peak = std::max(peak, std::abs(reinterpret_cast(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()
{
+ if(rclolAudioTraceEnabled()) rclolAudioTrace("AUDIO_UNIT_NATIVE_START output=%d input=%d\n",m_enableOutput,m_enableInput);
if (m_running)
return noErr;
if (m_enableOutput)
{
// std::cout << "Output is enabled, starting playback\n";
- AudioDeviceCreateIOProcID(m_outputDevice, playbackCallback, this, &m_outputProcID);
+ OSStatus status=AudioDeviceCreateIOProcID(m_outputDevice,playbackCallback,this,&m_outputProcID);
+ if(status) return status;
// m_auhalData.open("/tmp/auhal.raw", std::ios_base::binary | std::ios_base::out);
const CAStreamBasicDescription& desc = GetStreamFormat(kAudioUnitScope_Input, kOutputBus);
- AudioDeviceSetProperty(m_outputDevice, nullptr, 0, false, kAudioDevicePropertyStreamFormat, sizeof(AudioStreamBasicDescription), &desc);
- AudioDeviceStart(m_outputDevice, m_outputProcID);
+ AudioStreamBasicDescription hardware=desc;
+ hardware.mFormatFlags&=~kAudioFormatFlagIsNonInterleaved;
+ hardware.mBytesPerFrame=hardware.mChannelsPerFrame*hardware.mBitsPerChannel/8;
+ hardware.mBytesPerPacket=hardware.mBytesPerFrame;
+ status=AudioDeviceSetProperty(m_outputDevice,nullptr,0,false,kAudioDevicePropertyStreamFormat,sizeof(hardware),&hardware);
+ if(!status) status=AudioDeviceStart(m_outputDevice,m_outputProcID);
+ if(status) {AudioDeviceDestroyIOProcID(m_outputDevice,m_outputProcID);m_outputProcID=0;return status;}
}
if (m_enableInput)
{
- 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 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);
+ m_outputProcID=0;
}
if (m_inputProcID)
{
AudioDeviceStop(m_inputDevice, m_inputProcID);
AudioDeviceDestroyIOProcID(m_inputDevice, m_inputProcID);
+ m_inputProcID=0;
}
-
+ m_running=false;
return noErr;
}
+// Volume/mute/gain related property selectors (HAL device properties and AUHAL parameters).
+static bool rclolVolumeRelated(UInt32 id)
+{
+ switch (id)
+ {
+ case 'volm': case 'vold': case 'vdb#': case 'mute': case 'thru':
+ case 'pvol': case 'pvod': case 'mvol': case 'mbal': case 'span':
+ case 'svlm': case 'smut': case 'v2db': case 'db2v':
+ return true;
+ }
+ return false;
+}
+
OSStatus AUHAL::SetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement, const void* inData, UInt32 inDataSize)
{
+ if (rclolAudioTraceEnabled() && rclolVolumeRelated(inID))
+ rclolAudioTrace("AUHAL_SET_VOLPROP property=%08x scope=%u element=%u size=%u value=%g\n",inID,inScope,inElement,inDataSize,
+ (inData && inDataSize==sizeof(Float32))?(double)*(const Float32*)inData:0.0);
switch (inID)
{
+ case kAudioDevicePropertyBufferFrameSize: {
+ // An input-only unit (EnableIO output=0) sizes its input device's IO cycle.
+ AudioObjectPropertyAddress p={inID,kAudioObjectPropertyScopeOutput,kAudioObjectPropertyElementMaster};
+ 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);
@@ -161,7 +347,14 @@ OSStatus AUHAL::SetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, Au
ca_require(inDataSize == sizeof(AudioDeviceID), InvalidPropertyValue);
const AudioDeviceID* dev = static_cast(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;
}
}
- return AUBase::SetProperty(inID, inScope, inElement, inData, inDataSize);
+ {
+ OSStatus status=AUBase::SetProperty(inID,inScope,inElement,inData,inDataSize);
+ if(status && rclolAudioTraceEnabled()) rclolAudioTrace("AUDIO_UNIT_SET property=%u scope=%u element=%u status=%d\n",inID,inScope,inElement,status);
+ return status;
+ }
InvalidPropertyValue:
return kAudioUnitErr_InvalidPropertyValue;
}
OSStatus AUHAL::GetPropertyInfo(AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement, UInt32& outDataSize, Boolean& outWritable)
{
+ if (rclolAudioTraceEnabled() && rclolVolumeRelated(inID))
+ rclolAudioTrace("AUHAL_INFO_VOLPROP property=%08x scope=%u element=%u\n",inID,inScope,inElement);
switch (inID)
{
+ case kAudioDevicePropertyBufferFrameSize:
+ outDataSize=sizeof(UInt32);outWritable=true;return noErr;
case kAudioOutputUnitProperty_SetInputCallback:
{
outDataSize = sizeof(AURenderCallbackStruct);
outWritable = true;
- break;
+ return noErr;
}
case kAudioOutputUnitProperty_EnableIO:
{
@@ -210,13 +411,23 @@ OSStatus AUHAL::GetPropertyInfo(AudioUnitPropertyID inID, AudioUnitScope inScope
return noErr;
}
}
- return AUBase::GetPropertyInfo(inID, inScope, inElement, outDataSize, outWritable);
+ OSStatus status=AUBase::GetPropertyInfo(inID,inScope,inElement,outDataSize,outWritable);
+ if(status && rclolAudioTraceEnabled()) rclolAudioTrace("AUDIO_UNIT_INFO property=%u scope=%u element=%u status=%d\n",inID,inScope,inElement,status);
+ return status;
}
OSStatus AUHAL::GetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement, void* outData)
{
+ if (rclolAudioTraceEnabled() && rclolVolumeRelated(inID))
+ rclolAudioTrace("AUHAL_GET_VOLPROP property=%08x scope=%u element=%u\n",inID,inScope,inElement);
switch (inID)
{
+ case kAudioDevicePropertyBufferFrameSize: {
+ const bool input=m_enableInput && !m_enableOutput;
+ AudioObjectPropertyAddress p={inID,input?kAudioObjectPropertyScopeInput:kAudioObjectPropertyScopeOutput,kAudioObjectPropertyElementMaster};
+ UInt32 size=sizeof(UInt32);
+ return AudioObjectGetPropertyData(input?m_inputDevice:m_outputDevice,&p,0,nullptr,&size,outData);
+ }
case kAudioOutputUnitProperty_SetInputCallback:
{
memcpy(outData, &m_outputAvailableCb, sizeof(m_outputAvailableCb));
@@ -228,12 +439,12 @@ OSStatus AUHAL::GetProperty(AudioUnitPropertyID inID, AudioUnitScope inScope, Au
if (inElement == kOutputBus)
{
UInt32 value = m_enableOutput;
- memcpy(&outData, &value, sizeof(value));
+ memcpy(outData, &value, sizeof(value));
}
else if (inElement == kInputBus)
{
UInt32 value = m_enableInput;
- memcpy(&outData, &value, sizeof(value));
+ memcpy(outData, &value, sizeof(value));
}
else
return kAudioUnitErr_InvalidElement;
@@ -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);
}
+// Diagnostic only: classify each rendered block (see AUDIO_BLOCK_STATE).
+void AUHAL::traceBlock(const AudioTimeStamp* inNow, const AudioTimeStamp* inOutputTime, OSStatus result,
+ const CAStreamBasicDescription& desc, UInt32 nFrames, AudioBufferList* out)
+{
+ static std::atomic stateLines(0);
+ static mach_timebase_info_data_t tb={0,0};
+ if(!tb.denom) mach_timebase_info(&tb);
+ BlockTrace& t=m_blockTrace;
+ const unsigned long long cb=++t.callbacks;
+ if(result) {
+ if(++t.pullFailures<=50 || t.pullFailures%200==0)
+ rclolAudioTrace("AUDIO_PULL_FAIL unit=%p cb=%llu status=%d frames=%u count=%llu\n",
+ (void*)this,cb,(int)result,nFrames,t.pullFailures);
+ return;
+ }
+ if(!desc.IsFloat() || desc.mBitsPerChannel!=32 || !nFrames || !desc.mChannelsPerFrame) return;
+ const bool planar=desc.mFormatFlags&kAudioFormatFlagIsNonInterleaved;
+ const UInt32 ch=desc.mChannelsPerFrame;
+ const float* base=planar?(const float*)m_playbackSamples.data():(const float*)out->mBuffers[0].mData;
+ if(!base) return;
+ auto at=[&](UInt32 c,UInt32 f)->float { return planar?base[(size_t)c*nFrames+f]:base[(size_t)f*ch+c]; };
+ float peak[2]={0,0}; double sq[2]={0,0}; bool halves[2]={true,true}; bool zero=true;
+ const UInt32 half=nFrames/2;
+ for(UInt32 c=0;c1?sqrt(sq[1]/nFrames):0;
+ bool same=false;
+ const size_t total=(size_t)nFrames*ch;
+ if(t.havePrev && t.prev.size()==total) {
+ same=true;
+ for(UInt32 c=0;cmSampleTime;
+ if(t.haveTime) {
+ dSample=sample-t.lastSample;
+ dHostMs=(double)(inOutputTime->mHostTime-t.lastHost)*tb.numer/tb.denom/1e6;
+ if(dHostMs>t.maxGapMs) t.maxGapMs=dHostMs;
+ }
+ t.haveTime=true; t.lastSample=sample; t.lastHost=inOutputTime->mHostTime;
+ const bool transition=state!=t.state;
+ if(transition && t.state>=0 && stateLines.fetch_add(1)<2000)
+ rclolAudioTrace("AUDIO_BLOCK_STATE unit=%p from=%s to=%s cb=%llu frames=%u peakL=%g peakR=%g rmsL=%g rmsR=%g\n",
+ (void*)this,names[t.state],names[state],cb,nFrames,peak[0],peak[1],rms0,rms1);
+ if(cb<=40 || cb%200==0 || transition)
+ rclolAudioTrace("AUDIO_BLOCK unit=%p cb=%llu state=%s frames=%u peakL=%g peakR=%g rmsL=%g rmsR=%g "
+ "halvesL=%d halvesR=%d same=%d zero=%d outSample=%.0f nowSample=%.0f dSample=%.0f outHost=%llu nowHost=%llu "
+ "dHostMs=%.3f maxGapMs=%.3f\n",
+ (void*)this,cb,names[state],nFrames,peak[0],peak[1],rms0,rms1,(int)halves[0],(int)halves[1],(int)same,(int)zero,
+ sample,inNow->mSampleTime,dSample,(unsigned long long)inOutputTime->mHostTime,
+ (unsigned long long)inNow->mHostTime,dHostMs,t.maxGapMs);
+ if(cb%2000==0)
+ rclolAudioTrace("AUDIO_BLOCK_SUMMARY unit=%p cb=%llu silent=%llu live=%llu repeat=%llu halves=%llu pullFail=%llu maxGapMs=%.3f\n",
+ (void*)this,cb,t.stateCounts[0],t.stateCounts[1],t.stateCounts[2],t.stateCounts[3],t.pullFailures,t.maxGapMs);
+ t.state=state;
+}
+
OSStatus AUHAL::doPlayback(const AudioTimeStamp* inNow, AudioBufferList* outOutputData, const AudioTimeStamp* inOutputTime)
{
// std::cout << "AUHAL::DoPlayback()\n";
+ static unsigned stackTraces=0;
+ if(rclolAudioTraceEnabled() && stackTraces++<2)
+ rclolAudioTrace("AUDIO_CALLBACK_STACK bytes=%zu\n",pthread_get_stacksize_np(pthread_self()));
if (!HasInput(0))
{
// std::cerr << "No connection\n";
@@ -290,9 +585,75 @@ OSStatus AUHAL::doPlayback(const AudioTimeStamp* inNow, AudioBufferList* outOutp
AudioUnitRenderActionFlags flags = kAudioUnitRenderAction_PreRender;
const CAStreamBasicDescription& desc = GetStreamFormat(kAudioUnitScope_Input, kOutputBus);
- UInt32 nFrames = outOutputData->mBuffers[0].mDataByteSize / (desc.mBytesPerFrame / outOutputData->mBuffers[0].mNumberChannels);
- result = GetInput(kOutputBus)->PullInputWithBufferList(flags, *inNow, kOutputBus, nFrames, outOutputData);
-
+ if(outOutputData->mNumberBuffers!=1 || !desc.mChannelsPerFrame ||
+ !desc.mBitsPerChannel || desc.mBitsPerChannel%8) return kAudioUnitErr_FormatNotSupported;
+ UInt32 sampleBytes=desc.mBitsPerChannel/8;
+ UInt32 frameBytes=desc.mChannelsPerFrame*sampleBytes;
+ UInt32 nFrames=outOutputData->mBuffers[0].mDataByteSize/frameBytes;
+ if(desc.mFormatFlags&kAudioFormatFlagIsNonInterleaved) {
+ m_playbackSamples.resize(nFrames*frameBytes);
+ // Preserve fresh-buffer silence for callbacks that leave samples unwritten.
+ std::fill(m_playbackSamples.begin(),m_playbackSamples.end(),0);
+ m_playbackBufferList.resize(offsetof(AudioBufferList,mBuffers)+desc.mChannelsPerFrame*sizeof(AudioBuffer));
+ AudioBufferList* planar=(AudioBufferList*)m_playbackBufferList.data();
+ planar->mNumberBuffers=desc.mChannelsPerFrame;
+ for(UInt32 c=0;cmBuffers[c].mNumberChannels=1;
+ planar->mBuffers[c].mDataByteSize=nFrames*sampleBytes;
+ planar->mBuffers[c].mData=m_playbackSamples.data()+c*nFrames*sampleBytes;
+ }
+ result=GetInput(kOutputBus)->PullInputWithBufferList(flags,*inNow,kOutputBus,nFrames,planar);
+ if(!result) {
+ auto* destination=(unsigned char*)outOutputData->mBuffers[0].mData;
+ for(UInt32 f=0;fmBuffers[c].mData+f*sampleBytes,sampleBytes);
+ }
+ } else {
+ result=GetInput(kOutputBus)->PullInputWithBufferList(flags,*inNow,kOutputBus,nFrames,outOutputData);
+ }
+
+ if(rclolAudioTraceEnabled()) traceBlock(inNow,inOutputTime,result,desc,nFrames,outOutputData);
+
+ // Diagnostic: Chromium's audio source can start handing out one identical
+ // block forever (heard as static). Log the timestamp Darling supplied at the
+ // moment that begins, so we can tell whether our clock inputs look sane.
+ {
+ const unsigned char* produced;
+ size_t producedBytes;
+ if(desc.mFormatFlags&kAudioFormatFlagIsNonInterleaved) {
+ produced=m_playbackSamples.data();
+ producedBytes=(size_t)nFrames*frameBytes;
+ } else {
+ produced=(const unsigned char*)outOutputData->mBuffers[0].mData;
+ producedBytes=outOutputData->mBuffers[0].mDataByteSize;
+ }
+ uint64_t sum=1469598103934665603ull;
+ for(size_t i=0;imHostTime,inOutputTime->mSampleTime,
+ (unsigned)inOutputTime->mFlags);
+ } else {
+ repeatRun=0;
+ }
+ lastSum=sum;
+ }
+
+ static unsigned traceRenders=0;
+ if (rclolAudioTraceEnabled() && traceRenders++ < 16) {
+ float peak=0;
+ if (!result && desc.IsFloat() && sampleBytes==4) {
+ const float* samples=static_cast(outOutputData->mBuffers[0].mData);
+ for(UInt32 i=0;imBuffers[0].mDataByteSize/4;++i)
+ peak=std::max(peak,std::abs(samples[i]));
+ }
+ rclolAudioTrace("AUDIO_RENDER status=%d flags=%u frames=%u peak=%g\n",result,flags,nFrames,peak);
+ }
// 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 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(inInputData->mBuffers[0].mData);
+ const UInt32 totalFrames = inInputData->mBuffers[0].mDataByteSize / frameBytes;
+ // AudioUnitRender refuses more than kAudioUnitProperty_MaximumFramesPerSlice frames.
+ const UInt32 maxFrames = std::max(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 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..fad727962 100644
--- a/src/CoreAudio/CoreAudioComponent/AUHAL.h
+++ b/src/CoreAudio/CoreAudioComponent/AUHAL.h
@@ -25,6 +25,7 @@ along with Darling. If not, see .
#include
#include
#include
+#include
class AUHAL : public AUBase
{
@@ -32,8 +33,12 @@ public:
AUHAL(AudioComponentInstance inInstance, bool supportRecording = true);
~AUHAL();
+ UInt32 GetChannelLayoutTags(AudioUnitScope scope,AudioUnitElement element,AudioChannelLayoutTag* tags) override;
+ UInt32 GetAudioChannelLayout(AudioUnitScope scope,AudioUnitElement element,AudioChannelLayout* layout,Boolean& writable) override;
+ 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;
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);
+ void traceBlock(const AudioTimeStamp* inNow, const AudioTimeStamp* inOutputTime, OSStatus result, const CAStreamBasicDescription& desc, UInt32 nFrames, AudioBufferList* out);
OSStatus doRecord(const AudioTimeStamp* inNow, const AudioBufferList* inInputData, const AudioTimeStamp* inInputTime);
protected:
@@ -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 m_buffer;
size_t m_bufferSize = 0, m_bufferUsed = 0;
+
+ // Playback callbacks run serially on the device's PulseAudio queue.
+ // Keep planar conversion storage across callbacks and start/stop cycles.
+ std::vector m_playbackSamples;
+ std::vector m_playbackBufferList;
+
+ // Diagnostic-only block statistics (touched only when audio tracing is on).
+ struct BlockTrace {
+ unsigned long long callbacks = 0, pullFailures = 0;
+ unsigned long long stateCounts[4] = {0,0,0,0};
+ int state = -1;
+ std::vector prev;
+ bool havePrev = false;
+ bool haveTime = false;
+ double lastSample = 0;
+ unsigned long long lastHost = 0;
+ double maxGapMs = 0;
+ } m_blockTrace;
};
#endif
diff --git a/src/CoreAudio/CoreAudioComponent/CMakeLists.txt b/src/CoreAudio/CoreAudioComponent/CMakeLists.txt
index 38584e263..a4cf87529 100644
--- a/src/CoreAudio/CoreAudioComponent/CMakeLists.txt
+++ b/src/CoreAudio/CoreAudioComponent/CMakeLists.txt
@@ -29,6 +29,7 @@ set(component_sources
PublicUtility/CAVectorUnit.cpp
AUHAL.cpp
SystemOutputAU.cpp
+ MatrixMixerAU.cpp
DefaultOutputAU.cpp
)
diff --git a/src/CoreAudio/CoreAudioComponent/DefaultOutputAU.cpp b/src/CoreAudio/CoreAudioComponent/DefaultOutputAU.cpp
index 8b92b6c50..18a57e359 100644
--- a/src/CoreAudio/CoreAudioComponent/DefaultOutputAU.cpp
+++ b/src/CoreAudio/CoreAudioComponent/DefaultOutputAU.cpp
@@ -18,6 +18,8 @@ along with Darling. If not, see .
*/
#include "DefaultOutputAU.h"
+#include
+#include
#pragma GCC visibility push(default)
AUDIOCOMPONENT_ENTRY(AUOutputBaseFactory, DefaultOutputAU);
#pragma GCC visibility pop
@@ -32,11 +34,27 @@ DefaultOutputAU::DefaultOutputAU(AudioComponentInstance inInstance)
{
}
+/* darling-lol: League's game process sets the default device as the current device of its DefaultOutput unit. Refusing that
+ * (kAudioUnitErr_InvalidProperty) made it throw its audio graph away and build a new one in a loop, so it never played
+ * anything. Accepting it harmless: the unit plays on the default device anyway.
+ *
+ * Only the game process gets this. With the property accepted everywhere, the League client's integrity module went to its
+ * error state (state 5, status -101) on every launch (2 of 2, 2026-10-11) and the client could not start a match;
+ * with the old behaviour it connected again (state 0). */
+static bool callerIsLeagueGame()
+{
+ static const bool game = []{
+ const char* name = getprogname();
+ return name && strcmp(name, "LeagueofLegends") == 0;
+ }();
+ return game;
+}
+
OSStatus DefaultOutputAU::GetPropertyInfo(AudioUnitPropertyID inID, AudioUnitScope inScope, AudioUnitElement inElement, UInt32& outDataSize, Boolean& outWritable)
{
OSStatus status = AUHAL::GetPropertyInfo(inID, inScope, inElement, outDataSize, outWritable);
- if (inID == kAudioOutputUnitProperty_CurrentDevice)
+ if (inID == kAudioOutputUnitProperty_CurrentDevice && !callerIsLeagueGame())
outWritable = false;
return status;
@@ -48,7 +66,9 @@ OSStatus DefaultOutputAU::SetProperty(AudioUnitPropertyID inID, AudioUnitScope i
switch (inID)
{
case kAudioOutputUnitProperty_CurrentDevice:
- return kAudioUnitErr_InvalidProperty;
+ if (!callerIsLeagueGame())
+ return kAudioUnitErr_InvalidProperty;
+ break;
}
return AUHAL::SetProperty(inID, inScope, inElement, inData, inDataSize);
}
diff --git a/src/CoreAudio/CoreAudioComponent/Info.plist b/src/CoreAudio/CoreAudioComponent/Info.plist
index da44a9f18..f658118d8 100644
--- a/src/CoreAudio/CoreAudioComponent/Info.plist
+++ b/src/CoreAudio/CoreAudioComponent/Info.plist
@@ -70,6 +70,24 @@
version
1
+
+ description
+ Float PCM matrix mixer
+ factoryFunction
+ MatrixMixerAUFactory
+ manufacturer
+ appl
+ name
+ Darling: MatrixMixer
+ sandboxSafe
+
+ subtype
+ mxmx
+ type
+ aumx
+ version
+ 1
+
CFBundleExecutable
CoreAudio
diff --git a/src/CoreAudio/include/CoreAudio/AudioHardwareBase.h b/src/CoreAudio/include/CoreAudio/AudioHardwareBase.h
index 102614203..9b80671a6 100644
--- a/src/CoreAudio/include/CoreAudio/AudioHardwareBase.h
+++ b/src/CoreAudio/include/CoreAudio/AudioHardwareBase.h
@@ -13,12 +13,12 @@ typedef UInt32 AudioObjectPropertySelector;
typedef UInt32 AudioObjectPropertyScope;
typedef UInt32 AudioObjectPropertyElement;
-struct AudioObjectPropertyAddress
+typedef struct AudioObjectPropertyAddress
{
AudioObjectPropertySelector mSelector;
AudioObjectPropertyScope mScope;
AudioObjectPropertyElement mElement;
-};
+} AudioObjectPropertyAddress;
enum
{
@@ -87,6 +87,14 @@ enum
};
+typedef AudioObjectID AudioStreamID;
+enum {
+ kAudioStreamPropertyDirection = 'sdir',
+ kAudioStreamPropertyTerminalType = 'term',
+ kAudioStreamPropertyVirtualFormat = 'sfmt',
+ kAudioStreamPropertyPhysicalFormat = 'pft ',
+};
+
// TODO: A lot more is missing
#ifdef __cplusplus
@@ -94,4 +102,3 @@ enum
#endif
#endif /* AUDIOHARDWAREBASE_H */
-
diff --git a/src/frameworks/AVFoundation/include/AVFoundation/AVCaptureDevice.h b/src/frameworks/AVFoundation/include/AVFoundation/AVCaptureDevice.h
index cb2896957..b2c25fbd4 100644
--- a/src/frameworks/AVFoundation/include/AVFoundation/AVCaptureDevice.h
+++ b/src/frameworks/AVFoundation/include/AVFoundation/AVCaptureDevice.h
@@ -18,7 +18,22 @@
*/
#include
+#include
+
+typedef NS_ENUM(NSInteger, AVAuthorizationStatus) {
+ AVAuthorizationStatusNotDetermined = 0,
+ AVAuthorizationStatusRestricted = 1,
+ AVAuthorizationStatusDenied = 2,
+ AVAuthorizationStatusAuthorized = 3,
+};
@interface AVCaptureDevice : NSObject
++ (NSArray *)devices;
++ (NSArray *)devicesWithMediaType:(AVMediaType)mediaType;
++ (AVCaptureDevice *)defaultDeviceWithMediaType:(AVMediaType)mediaType;
+
++ (AVAuthorizationStatus)authorizationStatusForMediaType:(AVMediaType)mediaType;
++ (void)requestAccessForMediaType:(AVMediaType)mediaType completionHandler:(void (^)(BOOL granted))handler;
+
@end
diff --git a/src/frameworks/AVFoundation/src/AVCaptureDevice.m b/src/frameworks/AVFoundation/src/AVCaptureDevice.m
index cab525b91..4b3dd6541 100644
--- a/src/frameworks/AVFoundation/src/AVCaptureDevice.m
+++ b/src/frameworks/AVFoundation/src/AVCaptureDevice.m
@@ -18,9 +18,75 @@
*/
#import
+#include
+#include
+#include
@implementation AVCaptureDevice
++ (NSArray *)devices {
+ return [NSArray array];
+}
+
++ (NSArray *)devicesWithMediaType:(AVMediaType)mediaType {
+ (void)mediaType;
+ return [self devices];
+}
+
++ (AVCaptureDevice *)defaultDeviceWithMediaType:(AVMediaType)mediaType {
+ (void)mediaType;
+ return nil;
+}
+
+
+// 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
+{
+ // 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
+{
+ 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 {
return [NSMethodSignature signatureWithObjCTypes: "v@:"];
}
diff --git a/src/frameworks/Carbon/HIToolbox/CMakeLists.txt b/src/frameworks/Carbon/HIToolbox/CMakeLists.txt
index 3f0820484..7d310f61c 100644
--- a/src/frameworks/Carbon/HIToolbox/CMakeLists.txt
+++ b/src/frameworks/Carbon/HIToolbox/CMakeLists.txt
@@ -19,6 +19,7 @@ add_framework(HIToolbox
SOURCES
src/Keyboards.cpp
+ src/SecureInput.c
src/Events.cpp
src/CarbonEventsCore.c
src/CarbonEvents.c
diff --git a/src/frameworks/Carbon/HIToolbox/include/HIToolbox/Events.h b/src/frameworks/Carbon/HIToolbox/include/HIToolbox/Events.h
index 8c37ce2a6..71ed125a5 100644
--- a/src/frameworks/Carbon/HIToolbox/include/HIToolbox/Events.h
+++ b/src/frameworks/Carbon/HIToolbox/include/HIToolbox/Events.h
@@ -19,6 +19,7 @@ OSStatus SetEventParameter(EventRef a, EventParamName b, EventParamType c, UInt3
extern UInt8 LMGetKbdType(void);
+extern UInt8 LMGetKbdLast(void);
enum {
kVK_ANSI_A = 0x00,
diff --git a/src/frameworks/Carbon/HIToolbox/include/HIToolbox/TextInputSources.h b/src/frameworks/Carbon/HIToolbox/include/HIToolbox/TextInputSources.h
index 8d457c216..4b42c9af8 100644
--- a/src/frameworks/Carbon/HIToolbox/include/HIToolbox/TextInputSources.h
+++ b/src/frameworks/Carbon/HIToolbox/include/HIToolbox/TextInputSources.h
@@ -1,6 +1,7 @@
#ifndef _Carbon_TextInputSources_H_
#define _Carbon_TextInputSources_H_
#include
+#include
#ifdef __cplusplus
extern "C" {
@@ -25,6 +26,7 @@ extern TISInputSourceRef TISCopyCurrentKeyboardInputSource(void);
extern void* TISGetInputSourceProperty(TISInputSourceRef inputSourceRef, CFStringRef key);
extern TISInputSourceRef TISCopyCurrentASCIICapableKeyboardLayoutInputSource(void);
+extern CFArrayRef TISCreateASCIICapableInputSourceList(void);
#ifdef __cplusplus
}
diff --git a/src/frameworks/Carbon/HIToolbox/src/Events.cpp b/src/frameworks/Carbon/HIToolbox/src/Events.cpp
index a37fc641f..98e29c97a 100644
--- a/src/frameworks/Carbon/HIToolbox/src/Events.cpp
+++ b/src/frameworks/Carbon/HIToolbox/src/Events.cpp
@@ -4,3 +4,10 @@ UInt8 LMGetKbdType(void)
{
return 0;
}
+
+UInt8 LMGetKbdLast(void)
+{
+ // Darling exposes one logical keyboard through its current XKB layout.
+ // Its last-used keyboard type is therefore the current keyboard type.
+ return LMGetKbdType();
+}
diff --git a/src/frameworks/Carbon/HIToolbox/src/TextInputSources.mm b/src/frameworks/Carbon/HIToolbox/src/TextInputSources.mm
index 347cb530c..f73278a35 100644
--- a/src/frameworks/Carbon/HIToolbox/src/TextInputSources.mm
+++ b/src/frameworks/Carbon/HIToolbox/src/TextInputSources.mm
@@ -85,3 +85,10 @@ TISInputSourceRef TISCopyCurrentASCIICapableKeyboardLayoutInputSource(void)
return NULL;
}
+
+CFArrayRef TISCreateASCIICapableInputSourceList(void)
+{
+ /* Until ASCII-capable layouts can be enumerated, return an owned empty
+ * list. A NULL list would crash callers that unconditionally CFRelease. */
+ return CFArrayCreate(NULL, NULL, 0, &kCFTypeArrayCallBacks);
+}
diff --git a/src/frameworks/CoreLocation/include/CoreLocation/CLLocation.h b/src/frameworks/CoreLocation/include/CoreLocation/CLLocation.h
index 090de1df2..fe1f95f3b 100644
--- a/src/frameworks/CoreLocation/include/CoreLocation/CLLocation.h
+++ b/src/frameworks/CoreLocation/include/CoreLocation/CLLocation.h
@@ -19,6 +19,15 @@
#include
+typedef double CLLocationAccuracy;
+
+extern const CLLocationAccuracy kCLLocationAccuracyBestForNavigation;
+extern const CLLocationAccuracy kCLLocationAccuracyBest;
+extern const CLLocationAccuracy kCLLocationAccuracyNearestTenMeters;
+extern const CLLocationAccuracy kCLLocationAccuracyHundredMeters;
+extern const CLLocationAccuracy kCLLocationAccuracyKilometer;
+extern const CLLocationAccuracy kCLLocationAccuracyThreeKilometers;
+
@interface CLLocation : NSObject
@end
diff --git a/src/frameworks/CoreLocation/include/CoreLocation/CLLocationManager.h b/src/frameworks/CoreLocation/include/CoreLocation/CLLocationManager.h
index 9e0fa55c4..993124e76 100644
--- a/src/frameworks/CoreLocation/include/CoreLocation/CLLocationManager.h
+++ b/src/frameworks/CoreLocation/include/CoreLocation/CLLocationManager.h
@@ -20,5 +20,14 @@
#include
@interface CLLocationManager : NSObject
+{
+ id _delegate;
+}
+
+/* darling-lol: real property so callers (Electron's device-permission
+ * path) get a proper accessor instead of falling through to the stub
+ * forwardInvocation, whose 0-arg signature raised
+ * `NSForwardSignatureError: invoked with 3 args, but 2 expected`. */
+@property (nonatomic, assign) id delegate;
@end
diff --git a/src/frameworks/CoreLocation/src/CLLocation.m b/src/frameworks/CoreLocation/src/CLLocation.m
index f396e00e4..5f3965705 100644
--- a/src/frameworks/CoreLocation/src/CLLocation.m
+++ b/src/frameworks/CoreLocation/src/CLLocation.m
@@ -32,3 +32,10 @@
}
@end
+
+const CLLocationAccuracy kCLLocationAccuracyBestForNavigation = -2.0;
+const CLLocationAccuracy kCLLocationAccuracyBest = -1.0;
+const CLLocationAccuracy kCLLocationAccuracyNearestTenMeters = 10.0;
+const CLLocationAccuracy kCLLocationAccuracyHundredMeters = 100.0;
+const CLLocationAccuracy kCLLocationAccuracyKilometer = 1000.0;
+const CLLocationAccuracy kCLLocationAccuracyThreeKilometers = 3000.0;
diff --git a/src/frameworks/CoreLocation/src/CLLocationManager.m b/src/frameworks/CoreLocation/src/CLLocationManager.m
index 0b927c40e..d186494e7 100644
--- a/src/frameworks/CoreLocation/src/CLLocationManager.m
+++ b/src/frameworks/CoreLocation/src/CLLocationManager.m
@@ -21,6 +21,8 @@
@implementation CLLocationManager
+@synthesize delegate = _delegate;
+
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
return [NSMethodSignature signatureWithObjCTypes: "v@:"];
diff --git a/src/frameworks/CoreMedia/src/CoreMedia.c b/src/frameworks/CoreMedia/src/CoreMedia.c
index 3d76e8103..4a975959c 100644
--- a/src/frameworks/CoreMedia/src/CoreMedia.c
+++ b/src/frameworks/CoreMedia/src/CoreMedia.c
@@ -26,6 +26,32 @@
const CFStringRef kCMFormatDescriptionExtension_SampleDescriptionExtensionAtoms = CFSTR("SampleDescriptionExtensionAtoms");
const CFStringRef kCMSampleAttachmentKey_NotSync = CFSTR("NotSync");
+const CFStringRef kCMFormatDescriptionTransferFunction_ITU_R_709_2 = CFSTR("ITU_R_709_2");
+const CFStringRef kCMFormatDescriptionTransferFunction_SMPTE_240M_1995 = CFSTR("SMPTE_240M_1995");
+const CFStringRef kCMFormatDescriptionTransferFunction_UseGamma = CFSTR("UseGamma");
+const CFStringRef kCMFormatDescriptionTransferFunction_ITU_R_2020 = CFSTR("ITU_R_2020");
+const CFStringRef kCMFormatDescriptionTransferFunction_ITU_R_2100_HLG = CFSTR("ITU_R_2100_HLG");
+const CFStringRef kCMFormatDescriptionTransferFunction_SMPTE_ST_2084_PQ = CFSTR("SMPTE_ST_2084_PQ");
+const CFStringRef kCMFormatDescriptionTransferFunction_SMPTE_ST_428_1 = CFSTR("SMPTE_ST_428_1");
+const CFStringRef kCMFormatDescriptionTransferFunction_Linear = CFSTR("Linear");
+const CFStringRef kCMFormatDescriptionTransferFunction_sRGB = CFSTR("sRGB");
+const CFStringRef kCMFormatDescriptionColorPrimaries_ITU_R_709_2 = CFSTR("ITU_R_709_2");
+const CFStringRef kCMFormatDescriptionColorPrimaries_EBU_3213 = CFSTR("EBU_3213");
+const CFStringRef kCMFormatDescriptionColorPrimaries_SMPTE_C = CFSTR("SMPTE_C");
+const CFStringRef kCMFormatDescriptionColorPrimaries_ITU_R_2020 = CFSTR("ITU_R_2020");
+const CFStringRef kCMFormatDescriptionColorPrimaries_P3_D65 = CFSTR("P3_D65");
+const CFStringRef kCMFormatDescriptionColorPrimaries_DCI_P3 = CFSTR("DCI_P3");
+const CFStringRef kCMFormatDescriptionExtension_ContentLightLevelInfo = CFSTR("ContentLightLevelInfo");
+const CFStringRef kCMFormatDescriptionExtension_Depth = CFSTR("Depth");
+const CFStringRef kCMFormatDescriptionExtension_FormatName = CFSTR("FormatName");
+const CFStringRef kCMFormatDescriptionExtension_FullRangeVideo = CFSTR("FullRangeVideo");
+const CFStringRef kCMFormatDescriptionExtension_MasteringDisplayColorVolume = CFSTR("MasteringDisplayColorVolume");
+const CFStringRef kCMSampleAttachmentKey_IsDependedOnByOthers = CFSTR("IsDependedOnByOthers");
+const CFStringRef kCMFormatDescriptionYCbCrMatrix_ITU_R_709_2 = CFSTR("ITU_R_709_2");
+const CFStringRef kCMFormatDescriptionYCbCrMatrix_ITU_R_601_4 = CFSTR("ITU_R_601_4");
+const CFStringRef kCMFormatDescriptionYCbCrMatrix_SMPTE_240M_1995 = CFSTR("SMPTE_240M_1995");
+const CFStringRef kCMFormatDescriptionYCbCrMatrix_ITU_R_2020 = CFSTR("ITU_R_2020");
+
static int verbose = 0;
__attribute__((constructor))
diff --git a/src/frameworks/CoreServices/CMakeLists.txt b/src/frameworks/CoreServices/CMakeLists.txt
index 3fac3be55..aae2c7435 100644
--- a/src/frameworks/CoreServices/CMakeLists.txt
+++ b/src/frameworks/CoreServices/CMakeLists.txt
@@ -82,6 +82,7 @@ reexport(CoreServices Metadata ${Metadata_BUILD})
reexport(CoreServices SearchKit ${SearchKit_BUILD})
reexport(CoreServices SharedFileList ${SharedFileList_BUILD})
reexport(CoreServices OSServices ${OSServices_BUILD})
+reexport(CoreServices Foundation ${CMAKE_BINARY_DIR}/src/external/foundation/Foundation)
set_property(TARGET CoreServices APPEND_STRING PROPERTY
LINK_FLAGS " -Wl,-reexported_symbols_list,${CMAKE_CURRENT_SOURCE_DIR}/reexport.exp")
diff --git a/src/frameworks/CoreServices/src/CarbonCore/UnicodeUtilities.cpp b/src/frameworks/CoreServices/src/CarbonCore/UnicodeUtilities.cpp
index 094a9483d..84810f6ab 100644
--- a/src/frameworks/CoreServices/src/CarbonCore/UnicodeUtilities.cpp
+++ b/src/frameworks/CoreServices/src/CarbonCore/UnicodeUtilities.cpp
@@ -212,8 +212,10 @@ OSStatus UCKeyTranslate(const UCKeyboardLayout *keyLayoutPtr, UInt16 virtualKeyC
const UCKeyToCharTableIndex* tableIndex = (UCKeyToCharTableIndex*) (((char*) keyLayoutPtr) + keyLayoutPtr->keyboardTypeList[0].keyToCharTableIndexOffset);
uint8_t tableNumber = modMaps->defaultTableNum;
- if ((modifierKeyState >> 8) < modMaps->modifiersCount)
- tableNumber = modMaps->tableNum[(modifierKeyState >> 8)];
+ // UCKeyTranslate receives the modifier byte after the caller shifts the
+ // Carbon EventRecord flags. Shifting again loses Shift and Option state.
+ if (modifierKeyState < modMaps->modifiersCount)
+ tableNumber = modMaps->tableNum[modifierKeyState];
UniChar c;
if (virtualKeyCode >= tableIndex->keyToCharTableSize || tableNumber >= tableIndex->keyToCharTableCount)
diff --git a/src/frameworks/CoreServices/src/FSEvents/FSEventsImpl.m b/src/frameworks/CoreServices/src/FSEvents/FSEventsImpl.m
index cf9933a84..6651d50dc 100644
--- a/src/frameworks/CoreServices/src/FSEvents/FSEventsImpl.m
+++ b/src/frameworks/CoreServices/src/FSEvents/FSEventsImpl.m
@@ -23,6 +23,7 @@
#include
#include
#include
+#include
#include
static dispatch_queue_t g_fsEventsQueue = NULL;
@@ -68,6 +69,24 @@ static void rlPerform(void* info)
for (NSString* path in _pathsToWatch)
{
int wd = inotify_add_watch(_fd, [path UTF8String], IN_ATTRIB | IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MODIFY | IN_MOVE);
+ /* darling-lol: a path that doesn't exist yet (e.g. /etc/hosts or
+ * /etc/resolv.conf missing in the prefix) makes inotify_add_watch
+ * return -1. Apple's FSEvents tolerates unwatchable paths and the
+ * stream still starts; Chromium (dns_config_service_posix) treats
+ * a failed start as fatal for its DNS watcher. Skip failures
+ * instead of inserting a bogus -1 wd, and remember the path so a
+ * later event on the directory can still be reported. */
+ if (wd < 0)
+ {
+ if (getenv("DARLING_LOL_FSEVLOG") != NULL)
+ {
+ char b[512];
+ int n = snprintf(b, sizeof(b), "FSEV add_watch failed: %s (errno=%d)\n",
+ [path UTF8String], errno);
+ if (n > 0) write(2, b, n);
+ }
+ continue;
+ }
[wdmap setObject: path
forKey: [NSNumber numberWithInt: wd]];
}
diff --git a/src/frameworks/CoreServices/src/LaunchServices/LSOpen.m b/src/frameworks/CoreServices/src/LaunchServices/LSOpen.m
index dd78f6df4..8fa95a174 100644
--- a/src/frameworks/CoreServices/src/LaunchServices/LSOpen.m
+++ b/src/frameworks/CoreServices/src/LaunchServices/LSOpen.m
@@ -19,8 +19,104 @@ along with Darling. If not, see .
#include
+#include
#include
+#include "LSHostOpen.h"
#include
+#include
+#include
+#include
+
+// darling-lol M2a: minimal bundle launching.
+// Supports: file URLs (or bare POSIX paths) pointing at .app bundles,
+// optionally with an explicit target app via inAppParams->application.
+// The bundle's executable is spawned directly with the emulated posix_spawn
+// (no launchd job registration yet — see _LSLaunchApplication).
+// NOT yet supported (documented deviations):
+// - AppleEvent delivery (oapp/odoc): the app is spawned bare, as if launched
+// by hand. Apps waiting for their initial event will park. Needs #425/#937.
+// - inAppParams->argv / ->environment: not propagated (_LSLaunchApplication
+// takes no argv yet).
+// - inRole / launch flags (kLSLaunchNewInstance etc.): every launch spawns a
+// new instance; no running-app registry, so outPSNs is left untouched.
+// - non-bundle documents without an explicit application: there is no default
+// handler database yet (needs UTI/LaunchServices DB).
+static OSStatus
+LSOpenOneURL(CFURLRef url, const LSApplicationParameters *params)
+{
+ CFStringRef pathStr;
+ char path[4096];
+ struct stat st;
+ CFURLRef appURL = NULL;
+ OSStatus status;
+
+ if (url == NULL)
+ return paramErr;
+
+ if ((params == NULL || params->application == NULL) && _LSIsHostOpenURL(url))
+ return _LSOpenURLOnHost(url);
+
+ // open(1) builds URLs via +URLWithString from a raw path, which may yield
+ // a scheme-less URL object. CFURLCopyFileSystemPath works either way.
+ pathStr = CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle);
+ if (pathStr == NULL)
+ return paramErr;
+
+ if (!CFStringGetCString(pathStr, path, sizeof(path), kCFStringEncodingUTF8))
+ {
+ CFRelease(pathStr);
+ return paramErr;
+ }
+ CFRelease(pathStr);
+
+ if (params != NULL && params->application != NULL)
+ {
+ // Explicit target application wins; the URLs become its documents
+ // (delivered as argv once M2c lands; for now they are dropped).
+ char appPath[4096];
+ CFStringRef appStr;
+ status = FSRefMakePath(params->application, (UInt8*) appPath, sizeof(appPath) - 1);
+ if (status != noErr)
+ return status;
+ appStr = CFStringCreateWithCString(NULL, appPath, kCFStringEncodingUTF8);
+ appURL = CFURLCreateWithFileSystemPath(NULL, appStr, kCFURLPOSIXPathStyle, TRUE);
+ CFRelease(appStr);
+ }
+ else
+ {
+ size_t len = strlen(path);
+ Boolean looksLikeBundle = FALSE;
+
+ if (len > 4 && strcasecmp(path + len - 4, ".app") == 0)
+ looksLikeBundle = TRUE;
+ else if (stat(path, &st) == 0 && S_ISDIR(st.st_mode))
+ {
+ // Bundle without .app suffix (e.g. helper apps, frameworks-as-apps).
+ char plist[4096 + 32];
+ snprintf(plist, sizeof(plist), "%s/Contents/Info.plist", path);
+ if (stat(plist, &st) == 0)
+ looksLikeBundle = TRUE;
+ }
+
+ if (!looksLikeBundle)
+ return kLSApplicationNotFoundErr; // no default-handler DB yet
+
+ CFStringRef tmp = CFStringCreateWithCString(NULL, path, kCFStringEncodingUTF8);
+ appURL = CFURLCreateWithFileSystemPath(NULL, tmp, kCFURLPOSIXPathStyle, TRUE);
+ CFRelease(tmp);
+ }
+
+ if (appURL == NULL)
+ return paramErr;
+
+ status = _LSLaunchApplication(appURL);
+ CFRelease(appURL);
+
+ // _LSLaunchApplication returns 1 on failure instead of a MacErrors code.
+ if (status != noErr)
+ status = fnfErr;
+ return status;
+}
OSStatus
LSOpenURLsWithRole(
@@ -31,8 +127,27 @@ LSOpenURLsWithRole(
ProcessSerialNumber * outPSNs,
CFIndex inMaxPSNCount)
{
- puts("LSOpenURLsWithRole STUB");
- return unimpErr;
+ CFIndex count, i, launched = 0;
+ OSStatus status, firstErr = noErr;
+
+ (void) inRole;
+ (void) inAEParam;
+ (void) outPSNs;
+ (void) inMaxPSNCount;
+
+ if (inURLs == NULL || (count = CFArrayGetCount(inURLs)) <= 0)
+ return paramErr;
+
+ for (i = 0; i < count; i++)
+ {
+ status = LSOpenOneURL(CFArrayGetValueAtIndex(inURLs, i), inAppParams);
+ if (status == noErr)
+ launched++;
+ else if (launched == 0 && firstErr == noErr)
+ firstErr = status;
+ }
+
+ return launched > 0 ? noErr : firstErr;
}
OSStatus
diff --git a/src/frameworks/CoreServices/src/LaunchServices/LaunchServices.c b/src/frameworks/CoreServices/src/LaunchServices/LaunchServices.c
index 854fa1e7f..8a76923c4 100644
--- a/src/frameworks/CoreServices/src/LaunchServices/LaunchServices.c
+++ b/src/frameworks/CoreServices/src/LaunchServices/LaunchServices.c
@@ -19,9 +19,14 @@
#include
#include
+#include "LSHostOpen.h"
#include
#include
-#include
+#include
+#include
+#include
+
+extern char** environ;
static void get_main_executable_info(CFURLRef appURL, CFStringRef *exec, CFStringRef *bundleName);
@@ -77,6 +82,12 @@ OSStatus LSOpenFromURLSpec(const LSLaunchURLSpec *inLaunchSpec, CFURLRef *outLau
if (extension != NULL)
CFRelease(extension);
}
+ else if (_LSIsHostOpenURL(url))
+ {
+ temp = _LSOpenURLOnHost(url);
+ if (temp)
+ ret = temp;
+ }
else
{
printf("We can't handle this scheme yet\n");
@@ -108,8 +119,14 @@ OSStatus _LSLaunchApplication(CFURLRef appPath)
CFRelease(url_plus_exec);
url_plus_exec = CFURLCreateCopyAppendingPathComponent(kCFAllocatorDefault, pre, executable, FALSE);
CFRelease(pre);
- /* Launch the executable */
- executable = CFURLGetString(url_plus_exec);
+ /* Launch the executable. NB: CFURLGetString would give back a
+ * percent-encoded file:// URL, which is not spawnable; extract the
+ * POSIX path instead. */
+ CFRelease(executable);
+ executable = CFURLCopyFileSystemPath(url_plus_exec, kCFURLPOSIXPathStyle);
+ CFRelease(url_plus_exec);
+ if (executable == NULL)
+ return 1;
/* Try to use CFBundleName first */
if (bundleName == NULL)
@@ -122,15 +139,19 @@ OSStatus _LSLaunchApplication(CFURLRef appPath)
CFStringGetFileSystemRepresentation(executable, exec, execSize);
argv[0] = exec;
argv[1] = NULL;
- pid = spawn_via_launchd(name, (const char *const *)argv, NULL);
- if (pid < 0)
+ /* Launch the executable directly. The macOS-correct route is
+ * spawn_via_launchd (launchd job registration, -W waiting, PSN
+ * tracking), but Darling's vproc_mig_spawn2 currently fails the
+ * request (EIO) and nothing in-tree exercises it, so spawn the
+ * child ourselves via the emulated posix_spawn. */
+ int err = posix_spawn(&pid, exec, NULL, NULL, argv, environ);
+ if (err != 0)
{
- printf("%s: Failed to spawn via launchd\n", exec);
+ printf("%s: Failed to posix_spawn: %s\n", exec, strerror(err));
return 1;
}
free(name);
free(exec);
- CFRelease(url_plus_exec);
CFRelease(executable);
}
@@ -184,3 +205,25 @@ err_pre_info:
CFRelease(rs);
CFRelease(info_plist_url);
}
+
+OSStatus LSSetDefaultHandlerForURLScheme(CFStringRef inURLScheme, CFStringRef inHandlerBundleID)
+{
+ /* Darling has no shared LaunchServices database yet; accept and
+ * ignore (callers such as Discord's URL-scheme registration only
+ * need the call to succeed). */
+ (void)inURLScheme;
+ (void)inHandlerBundleID;
+ if (verbose)
+ printf("STUB: LSSetDefaultHandlerForURLScheme\n");
+ return noErr;
+}
+
+OSStatus LSSetDefaultRoleHandlerForContentType(CFStringRef inContentType, LSRolesMask inRole, CFStringRef inHandlerBundleID)
+{
+ (void)inContentType;
+ (void)inRole;
+ (void)inHandlerBundleID;
+ if (verbose)
+ printf("STUB: LSSetDefaultRoleHandlerForContentType\n");
+ return noErr;
+}
diff --git a/src/frameworks/CoreVideo/include/CoreVideo/CVBuffer.h b/src/frameworks/CoreVideo/include/CoreVideo/CVBuffer.h
index 09b70e4cb..2f7f5b89a 100644
--- a/src/frameworks/CoreVideo/include/CoreVideo/CVBuffer.h
+++ b/src/frameworks/CoreVideo/include/CoreVideo/CVBuffer.h
@@ -8,6 +8,8 @@ __BEGIN_DECLS
typedef struct CV_BRIDGED_TYPE(id) __CVBuffer* CVBufferRef;
+extern const CFStringRef kCVBufferPropagatedAttachmentsKey;
+
__END_DECLS
#endif // _COREVIDEO_CVBUFFER_H_
diff --git a/src/frameworks/CoreVideo/include/CoreVideo/CVImageBuffer.h b/src/frameworks/CoreVideo/include/CoreVideo/CVImageBuffer.h
index 937ba8312..45ab80e75 100644
--- a/src/frameworks/CoreVideo/include/CoreVideo/CVImageBuffer.h
+++ b/src/frameworks/CoreVideo/include/CoreVideo/CVImageBuffer.h
@@ -30,6 +30,7 @@ extern const CFStringRef kCVImageBufferTransferFunction_Linear;
extern const CFStringRef kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ;
extern const CFStringRef kCVImageBufferTransferFunction_SMPTE_ST_428_1;
extern const CFStringRef kCVImageBufferTransferFunction_SMPTE_C;
+extern const CFStringRef kCVImageBufferTransferFunction_sRGB;
extern CFStringRef const kCVImageBufferPixelAspectRatioHorizontalSpacingKey;
extern CFStringRef const kCVImageBufferPixelAspectRatioVerticalSpacingKey;
@@ -43,5 +44,7 @@ extern CFStringRef const kCVImageBufferColorPrimaries_SMPTE_C;
extern CFStringRef const kCVImageBufferTransferFunction_ITU_R_709_2;
extern CFStringRef const kCVImageBufferTransferFunction_SMPTE_240M_1995;
extern CFStringRef const kCVImageBufferTransferFunction_UseGamma;
+extern const CFStringRef kCVImageBufferContentLightLevelInfoKey;
+extern const CFStringRef kCVImageBufferMasteringDisplayColorVolumeKey;
#endif // _COREVIDEO_CVIMAGEBUFFER_H_
diff --git a/src/frameworks/CoreVideo/include/CoreVideo/CVPixelBufferPool.h b/src/frameworks/CoreVideo/include/CoreVideo/CVPixelBufferPool.h
index 299caa5b1..bdff6028c 100644
--- a/src/frameworks/CoreVideo/include/CoreVideo/CVPixelBufferPool.h
+++ b/src/frameworks/CoreVideo/include/CoreVideo/CVPixelBufferPool.h
@@ -26,6 +26,7 @@
__BEGIN_DECLS
extern const CFStringRef kCVPixelBufferPoolMaximumBufferAgeKey;
+extern const CFStringRef kCVPixelBufferPoolAllocationThresholdKey;
extern const CFStringRef kCVPixelBufferPoolMinimumBufferCountKey;
__END_DECLS
diff --git a/src/frameworks/CoreVideo/src/CVDisplayLink.mm b/src/frameworks/CoreVideo/src/CVDisplayLink.mm
index 3e9cba226..99e0a5494 100644
--- a/src/frameworks/CoreVideo/src/CVDisplayLink.mm
+++ b/src/frameworks/CoreVideo/src/CVDisplayLink.mm
@@ -1,38 +1,887 @@
#include
+#include
+#include
#include
+#include