다음을 통해 공유


ACX 다중 스택 교차 드라이버 통신

이 항목에서는 ACX(Audio Class eXtensions) 다중 스택 크로스 드라이버 통신에 대한 요약을 제공합니다.

ACX에 대한 일반적인 내용은 ACX 오디오 클래스 확장 개요 ACX 개체 요약을 참조하세요.

ACX 대상에 대한 기본 정보는 ACX 대상 및 드라이버 동기화ACX IO 요청 패킷 IRP를 참조하세요.

단일 스택 오디오 드라이버

레거시 PortCls 및 KS 오디오 클래스 드라이버는 "단일 스택" 오디오 드라이버만 지원합니다. 레거시 오디오 프레임워크는 하나의 미니포트 드라이버와만 통신하고 인터페이스합니다. 필요한 경우 다른 드라이버 스택과의 통신 및 동기화를 관리하는 것은 미니포트 드라이버의 맡입니다.

ACX는 단일 스택 오디오 드라이버를 완벽하게 지원합니다. 오디오 개발자는 현재 Portcls 및 KS 미니포트 드라이버를 ACX 기반 드라이버로 바꾸면서 다른 스택과 관련하여 동일한 동작을 유지할 수 있습니다. 오디오 하위 시스템이 다중 오디오 스택을 사용하는 경우 이 항목의 다음 섹션에 설명된 대로 ACX에서 다중 스택 지원을 사용하고 ACX가 이러한 모든 스택을 동기화하도록 하는 것이 더 좋습니다.

다중 스택 오디오 드라이버 - 구성 요소화

오디오 경로는 다양한 드라이버 스택에서 처리하는 여러 하드웨어 구성 요소를 통과하여 완전한 오디오 환경을 만드는 것이 매우 일반적입니다. 시스템에서는 다음 다이어그램과 같이 다양한 오디오 기술 공급업체에서 DSP, CODEC 및 AMP 기능을 구현하는 것이 일반적입니다.

다중 스택 오디오 시스템의 DSP, CODEC 및 AMP 간의 관계를 보여 주는 다이어그램

잘 정의된 표준이 없는 다중 스택 아키텍처에서 각 공급업체는 고유한 전용 인터페이스 및 통신 프로토콜을 정의해야 합니다. 이러한 스택 간의 동기화 소유권을 가져와서 드라이버가 서로 통신하는 간단한 재사용 가능한 패턴을 제공하여 다중 스택 오디오 드라이버의 개발을 용이하게 하는 ACX의 목표입니다.

ACX를 사용하는 예제 시스템 DSP, CODEC 및 AMP 하드웨어 디자인은 다음 소프트웨어 아키텍처에서 지원될 수 있습니다.

DSP, CODEC 및 AMP 구성 요소에 대한 별도의 드라이버 및 ACX 스택이 있는 ACX 아키텍처를 보여 주는 다이어그램

ACX는 특정 구성 요소 형식 또는 구성 요소의 특정 정렬에 의존하지 않으므로 표시된 DSP, CODEC 및 AMP 대신 구성 요소 형식의 모든 형식을 사용할 수 있습니다.

타사 드라이버는 잘 정의된 프로토콜을 사용하여 ACX를 통해 서로 통신합니다. 이 방법의 한 가지 장점은 인접한 소프트웨어 스택을 변경하지 않고도 단일 스택을 다른 공급업체의 다른 스택으로 바꿀 수 있다는 것입니다. ACX(오디오 클래스 확장) 프레임워크의 주요 목표 중 하나는 여러 공급업체의 구성 요소에서 어셈블된 다중 스택 오디오 드라이버를 개발하는 데 필요한 노력을 간소화하는 것입니다.

ACX 대상 통신 예제 - 회로

이 예제 코드는 AcxTargetCircuit 및 AcxTargetCircuitGetWdfIoTarget 을 사용하여 다른 스택에 의해 노출되는 원격 회로와 통신하는 방법을 보여 있습니다. ACX 회로에 대한 자세한 내용은 acxcircuit.h를 참조 하세요.

이 매우 복잡한 집계기는 회로를 찾은 다음 AcxTargetCircuitGetWdfIoTarget을 사용하여 ioTarget을 만듭니다. 그런 다음 사용자 지정 WDF 보내기 옵션을 설정하고 비동기적으로 요청을 보냅니다. 마지막으로 보내기의 상태 검사 요청이 전송되었는지 확인합니다.

NTSTATUS
Aggregator_SendModuleCommand(
    _In_ PAGGREGATOR_RENDER_CIRCUIT_CONTEXT CircuitCtx,
    _In_ ACX_REQUEST_PARAMETERS             Params,
    _Out_ ULONG_PTR *                       OutSize
    )
{
    NTSTATUS                    status = STATUS_NOT_SUPPORTED;
    PKSAUDIOMODULE_PROPERTY     moduleProperty = nullptr;
    ULONG                       aggregationDeviceIndex = 0;
    PLIST_ENTRY                 ple;

    *OutSize = 0;

    moduleProperty = CONTAINING_RECORD(Params.Parameters.Property.Control, KSAUDIOMODULE_PROPERTY, ClassId);;
    aggregationDeviceIndex = AUDIOMODULE_GET_AGGDEVICEID(moduleProperty->InstanceId);

    ple = CircuitCtx->AggregatorCircuit->AggregatorEndpoint->AudioPaths[aggregationDeviceIndex]->TargetCircuitList.Flink;
    while (ple != &CircuitCtx->AggregatorCircuit->AggregatorEndpoint->AudioPaths[aggregationDeviceIndex]->TargetCircuitList)
    {
        PAUDIO_CIRCUIT circuit = (PAUDIO_CIRCUIT)CONTAINING_RECORD(ple, AUDIO_CIRCUIT, ListEntry);

        if (circuit->Modules)
        {
            for(ULONG i = 0; i < circuit->Modules->Count; i++)
            {
                PACX_AUDIOMODULE_DESCRIPTOR descriptor = ((PACX_AUDIOMODULE_DESCRIPTOR)(circuit->Modules + 1) + i);

                // we've identified which aggregation device this call is targeting, 
                // now locate which circuit implements this module. Within an aggregated device,
                // the module class id + instance id must uniquely identify a module. There should
                // never be duplicates.
                if (IsEqualGUIDAligned(descriptor->ClassId, moduleProperty->ClassId) &&
                    descriptor->InstanceId == moduleProperty->InstanceId)
                {
                    WDFREQUEST                  request = NULL;
                    WDF_REQUEST_SEND_OPTIONS    sendOptions;
                    WDF_OBJECT_ATTRIBUTES       attributes;
                    WDFIOTARGET                 ioTarget;

                    // We've now identified which aggregated device this call is targeting.
                    // The cached module information contains the ID adjusted with the aggregation device
                    // index. remove the aggregation device index before forwarding the call to the aggregated circuit.
                    moduleProperty->InstanceId = AUDIOMODULE_GET_INSTANCEID(moduleProperty->InstanceId);

                    ioTarget = AcxTargetCircuitGetWdfIoTarget(circuit->AcxTargetCircuit);

                    WDF_OBJECT_ATTRIBUTES_INIT(&attributes);
                    attributes.ParentObject = CircuitCtx->AggregatorCircuit->Circuit;
                    status = WdfRequestCreate(&attributes, ioTarget, &request);    
                    if (!NT_SUCCESS(status)) 
                    {
                        goto exit;
                    }

                    status = AcxTargetCircuitFormatRequestForProperty(circuit->AcxTargetCircuit, request, &Params);
                    if (!NT_SUCCESS(status))
                    {
                        goto exit;
                    }

                    WDF_REQUEST_SEND_OPTIONS_INIT(&sendOptions, WDF_REQUEST_SEND_OPTION_SYNCHRONOUS);
                    WDF_REQUEST_SEND_OPTIONS_SET_TIMEOUT(&sendOptions, WDF_REL_TIMEOUT_IN_SEC(REQUEST_TIMEOUT_SECONDS));

                    // Whether WdfRequestSend succeeds or fails, we return the status & information, so
                    // there's no need to inspect the result.
                    WdfRequestSend(request, ioTarget, &sendOptions);
                    status = WdfRequestGetStatus(request);
                    *OutSize = WdfRequestGetInformation(request);

                    WdfObjectDelete(request);
                    goto exit;
                }
            }
        }

        ple = ple->Flink;
    }

    status = STATUS_SUCCESS;

exit:
    return status;
}

ACX 대상 통신 예제 - 고정

이 예제 코드에서는 AcxTargetPin을 사용하여 다른 스택에 의해 노출되는 원격 회로의 핀과 통신하는 방법을 보여 있습니다. ACX 핀에 대한 자세한 내용은 acxpin.h를 참조 하세요.

엔드포인트 경로의 동일한 회로에 있는 마지막 볼륨 및 음소거 요소를 선택합니다.

NTSTATUS FindDownstreamVolumeMute(
    _In_    ACXCIRCUIT          Circuit,
    _In_    ACXTARGETCIRCUIT    TargetCircuit
)
{
    NTSTATUS status;
    PDSP_CIRCUIT_CONTEXT circuitCtx;
    ACX_REQUEST_PARAMETERS  params;
    WDF_REQUEST_SEND_OPTIONS sendOptions;
    WDF_OBJECT_ATTRIBUTES attributes;
    WDF_REQUEST_REUSE_PARAMS reuseParams;

    circuitCtx = GetDspCircuitContext(Circuit);

    //
    // Note on behavior: This search algorithm will select the last Volume and Mute elements that are both
    // present in the same circuit in the Endpoint Path.
    // This logic could be updated to select the last Volume and Mute elements, or the first or last
    // Volume or the first or last Mute element.
    //

    //
    // First look through target's pins to determine if there's another circuit downstream.
    // If there is, we'll look at that circuit for volume/mute.
    //
    for (ULONG pinIndex = 0; pinIndex < AcxTargetCircuitGetPinsCount(TargetCircuit); ++pinIndex)
    {
        ACXTARGETPIN targetPin = AcxTargetCircuitGetTargetPin(TargetCircuit, pinIndex);
        ULONG targetPinFlow = 0;
        ACX_REQUEST_PARAMETERS_INIT_PROPERTY(&params,
                                             KSPROPSETID_Pin,
                                             KSPROPERTY_PIN_DATAFLOW,
                                             AcxPropertyVerbGet,
                                             AcxItemTypePin,
                                             AcxTargetPinGetId(targetPin),
                                             nullptr, 0,
                                             &targetPinFlow,
                                             sizeof(targetPinFlow));

        RETURN_NTSTATUS_IF_FAILED(SendProperty(targetPin, &params, nullptr));

        //
        // Searching for the downstream pins. For Render, these are the dataflow out pins
        //
        if (circuitCtx->IsRenderCircuit && targetPinFlow != KSPIN_DATAFLOW_OUT)
        {
            continue;
        }
        else if (!circuitCtx->IsRenderCircuit && targetPinFlow != KSPIN_DATAFLOW_IN)
        {
            continue;
        }

        // Get the target pin's physical connection. We'll do this twice: first to get size and allocate, second to get the connection
        PKSPIN_PHYSICALCONNECTION pinConnection = nullptr;
        auto connection_free = scope_exit([&pinConnection]()
        {
            if (pinConnection)
            {
                ExFreePool(pinConnection);
                pinConnection = nullptr;
            }
        });

        ULONG pinConnectionSize = 0;
        ULONG_PTR info = 0;
        for (ULONG i = 0; i < 2; ++i)
        {
            ACX_REQUEST_PARAMETERS_INIT_PROPERTY(&params,
                                                 KSPROPSETID_Pin,
                                                 KSPROPERTY_PIN_PHYSICALCONNECTION,
                                                 AcxPropertyVerbGet,
                                                 AcxItemTypePin,
                                                 AcxTargetPinGetId(targetPin),
                                                 nullptr, 0,
                                                 pinConnection,
                                                 pinConnectionSize);

            status = SendProperty(targetPin, &params, &info);

            if (status == STATUS_BUFFER_OVERFLOW)
            {
                // Pin connection already allocated, so how did this fail?
                RETURN_NTSTATUS_IF_TRUE(pinConnection != nullptr, status);

                pinConnectionSize = (ULONG)info;
                pinConnection = (PKSPIN_PHYSICALCONNECTION)ExAllocatePool2(POOL_FLAG_NON_PAGED, pinConnectionSize, DRIVER_TAG);
                // RETURN_NTSTATUS_IF_NULL_ALLOC causes compile errors
                RETURN_NTSTATUS_IF_TRUE(pinConnection == nullptr, STATUS_INSUFFICIENT_RESOURCES);
            }
            else if (!NT_SUCCESS(status))
            {
                // There are no more connected circuits. Continue with processing this circuit.
                break;
            }
        }

        if (!NT_SUCCESS(status))
        {
            // There are no more connected circuits. Continue handling this circuit.
            break;
        }

        ACXTARGETCIRCUIT nextTargetCircuit;
        RETURN_NTSTATUS_IF_FAILED(CreateTargetCircuit(Circuit, pinConnection, pinConnectionSize, &nextTargetCircuit));
        auto circuit_free = scope_exit([&nextTargetCircuit]()
        {
            if (nextTargetCircuit)
            {
                WdfObjectDelete(nextTargetCircuit);
                nextTargetCircuit = nullptr;
            }
        });

        RETURN_NTSTATUS_IF_FAILED_UNLESS_ALLOWED(FindDownstreamVolumeMute(Circuit, nextTargetCircuit), STATUS_NOT_FOUND);
        if (circuitCtx->TargetVolumeMuteCircuit == nextTargetCircuit)
        {
            // The nextTargetCircuit is the owner of the volume/mute target elements.
            // We will delete it when the pin is disconnected.
            circuit_free.release();

            // We found volume/mute. Return.
            return STATUS_SUCCESS;
        }

        // There's only one downstream pin on the current targetcircuit, and we just processed it.
        break;
    }

    //
    // Search the target circuit for a volume or mute element.
    // This sample code doesn't support downstream audioengine elements.
    // 
    for (ULONG elementIndex = 0; elementIndex < AcxTargetCircuitGetElementsCount(TargetCircuit); ++elementIndex)
    {
        ACXTARGETELEMENT targetElement = AcxTargetCircuitGetTargetElement(TargetCircuit, elementIndex);
        GUID elementType = AcxTargetElementGetType(targetElement);

        if (IsEqualGUID(elementType, KSNODETYPE_VOLUME) &&
            circuitCtx->TargetVolumeHandler == nullptr)
        {
            // Found Volume
            circuitCtx->TargetVolumeHandler = targetElement;
        }
        if (IsEqualGUID(elementType, KSNODETYPE_MUTE) &&
            circuitCtx->TargetMuteHandler == nullptr)
        {
            // Found Mute
            circuitCtx->TargetMuteHandler = targetElement;
        }
    }

    if (circuitCtx->TargetVolumeHandler && circuitCtx->TargetMuteHandler)
    {
        circuitCtx->TargetVolumeMuteCircuit = TargetCircuit;
        return STATUS_SUCCESS;
    }

    //
    // If we only found one of volume or mute, keep searching for both
    //
    if (circuitCtx->TargetVolumeHandler || circuitCtx->TargetMuteHandler)
    {
        circuitCtx->TargetMuteHandler = circuitCtx->TargetVolumeHandler = nullptr;
    }

    return STATUS_NOT_FOUND;
}

ACX 대상 통신 예제 - Stream

이 예제 코드는 AcxTargetStream을 사용하여 원격 회로의 스트림과 통신하는 방법을 보여줍니다. ACX 스트림 대한 자세한 내용은 acxstreams.h를 참조하세요.


    NTSTATUS                        status;
    PRENDER_DEVICE_CONTEXT          devCtx;
    WDF_OBJECT_ATTRIBUTES           attributes;
    ACXSTREAM                       stream;
    STREAM_CONTEXT *                streamCtx;
    ACXELEMENT                      elements[2] = {0};
    ACX_ELEMENT_CONFIG              elementCfg;
    ELEMENT_CONTEXT *               elementCtx;
    ACX_STREAM_CALLBACKS            streamCallbacks;
    ACX_RT_STREAM_CALLBACKS         rtCallbacks;
    CRenderStreamEngine *           streamEngine = NULL;

    PAGED_CODE();
    UNREFERENCED_PARAMETER(Pin);
    UNREFERENCED_PARAMETER(SignalProcessingMode);
    UNREFERENCED_PARAMETER(VarArguments);

    // This unit-test added support for RAW and DEFAULT.
    ASSERT(IsEqualGUID(*SignalProcessingMode, AUDIO_SIGNALPROCESSINGMODE_RAW) ||
           IsEqualGUID(*SignalProcessingMode, AUDIO_SIGNALPROCESSINGMODE_DEFAULT));
    
    devCtx = GetRenderDeviceContext(Device);
    ASSERT(devCtx != NULL);

    //
    // Init streaming callbacks.
    //
    ACX_STREAM_CALLBACKS_INIT(&streamCallbacks);
    streamCallbacks.EvtAcxStreamPrepareHardware         = EvtStreamPrepareHardware;
    streamCallbacks.EvtAcxStreamReleaseHardware         = EvtStreamReleaseHardware;
    streamCallbacks.EvtAcxStreamRun                     = EvtStreamRun;
    streamCallbacks.EvtAcxStreamPause                   = EvtStreamPause;
    streamCallbacks.EvtAcxStreamAssignDrmContentId      = EvtStreamAssignDrmContentId;

    status = AcxStreamInitAssignAcxStreamCallbacks(StreamInit, &streamCallbacks);
    if (!NT_SUCCESS(status))
    {
        ASSERT(FALSE);
        goto exit;
    }
    
    //
    // Init RT streaming callbacks.
    //
    ACX_RT_STREAM_CALLBACKS_INIT(&rtCallbacks);
    rtCallbacks.EvtAcxStreamGetHwLatency                = EvtStreamGetHwLatency;
    rtCallbacks.EvtAcxStreamAllocateRtPackets           = EvtStreamAllocateRtPackets;
    rtCallbacks.EvtAcxStreamFreeRtPackets               = EvtStreamFreeRtPackets;
    rtCallbacks.EvtAcxStreamSetRenderPacket             = R_EvtStreamSetRenderPacket;
    rtCallbacks.EvtAcxStreamGetCurrentPacket            = EvtStreamGetCurrentPacket;
    rtCallbacks.EvtAcxStreamGetPresentationPosition     = EvtStreamGetPresentationPosition;    
    
    status = AcxStreamInitAssignAcxRtStreamCallbacks(StreamInit, &rtCallbacks);
    if (!NT_SUCCESS(status))
    {
        ASSERT(FALSE);
        goto exit;
    }

    //
    // Create the stream.
    //
    WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&attributes, STREAM_CONTEXT);
    attributes.EvtCleanupCallback = EvtStreamCleanup;
    attributes.EvtDestroyCallback = EvtStreamDestroy;
    status = AcxRtStreamCreate(Device, Circuit, &attributes, &StreamInit, &stream);
    if (!NT_SUCCESS(status)) 
    {
        ASSERT(FALSE);
        goto exit;
    }

    // START-TESTING: inverted create-stream sequence.
    {
        ACXSTREAMBRIDGE             bridge          = NULL;
        ACXPIN                      bridgePin       = NULL;
        ACXTARGETSTREAM             targetStream    = NULL;
        ACX_STREAM_BRIDGE_CONFIG    bridgeCfg;
    
        ACX_STREAM_BRIDGE_CONFIG_INIT(&bridgeCfg);
        bridgeCfg.InModesCount = 0;     // no in-modes. this stream-bridge is manually managed. 
        bridgeCfg.InModes      = NULL; 
        bridgeCfg.OutMode      = NULL;  // no mode, i.e., default (1st) and raw (2nd).
        bridgeCfg.Flags |= AcxStreamBridgeInvertChangeStateSequence;

        WDF_OBJECT_ATTRIBUTES_INIT(&attributes);
        attributes.ParentObject = WdfGetDriver(); // bridge is deleted by driver obj in case of error.

        status = AcxStreamBridgeCreate(Circuit, &attributes, &bridgeCfg, &bridge);
        if (!NT_SUCCESS(status))
        {
            ASSERT(FALSE);
            goto exit;
        }
        
        ...
        
        status = AcxStreamBridgeAddStream(bridge, stream);
        if (!NT_SUCCESS(status))
        {
            ASSERT(FALSE);
            goto exit;
        }

        // Get the Target Stream
        targetStream = AcxStreamBridgeGetTargetStream(bridge, stream);
        if (targetStream == NULL)
        {
            ASSERT(FALSE);
            goto exit;
        }

ACX 대상 통신 예제 - 요소

이 예제 코드는 AcxTargetElement를 사용하여 회로의 요소와 통신하는 방법을 보여줍니다. ACX 대상에 대한 자세한 내용은 acxtargets.h를 참조 하세요.

    _In_    ACXCIRCUIT          Circuit,
    _In_    ACXTARGETCIRCUIT    TargetCircuit

...

    //
    // Search the target circuit for a volume or mute element.
    // This sample code doesn't support downstream audioengine elements.
    // 
    for (ULONG elementIndex = 0; elementIndex < AcxTargetCircuitGetElementsCount(TargetCircuit); ++elementIndex)
    {
        ACXTARGETELEMENT targetElement = AcxTargetCircuitGetTargetElement(TargetCircuit, elementIndex);
        GUID elementType = AcxTargetElementGetType(targetElement);

        if (IsEqualGUID(elementType, KSNODETYPE_VOLUME) &&
            circuitCtx->TargetVolumeHandler == nullptr)
        {
            // Found Volume
            circuitCtx->TargetVolumeHandler = targetElement;
        }
        if (IsEqualGUID(elementType, KSNODETYPE_MUTE) &&
            circuitCtx->TargetMuteHandler == nullptr)
        {
            // Found Mute
            circuitCtx->TargetMuteHandler = targetElement;
        }
    }

참고 항목

ACX 회로

ACX 오디오 클래스 확장 개요

ACX 개체 요약

ACX 참조 설명서

ACX IO 요청 패킷 IRP

ACX 대상 및 드라이버 동기화

ACX 참조 설명서