Skip to content

AudYoFlo: Classes: HjvxMicroConnection

jvxgit edited this page Feb 9, 2023 · 2 revisions

In this section the use of the micro connection is described. Micro connections are used to create local chains of signal processing flows in signal processing components. They may be used to extend parts of an existing chain or they might be used to start and stop specific functionalities at runtime.

A micro connection defined in the corresponding library which is located here. In the following two use-cases shall be described:

  1. The simple use: This use-case is rather simple as it operates independently from the "main" chain process which feeds a processing component. In this case, the chain can be allocated and deallocated during procesing without any need to start the main processing chain.
  2. The integrative approach. This use case allows to fill a gap in the processing chain by means of a linked list of components specified as a micro connection.

The Simple Use-case

The simple use-case is shown in the following Figure:

grafik

Note the two options in the figure to realize either option a or option b.

In the simple setup data flows from the device to a node and from there to the device again. The processing happens in the Node which - with option a - copies the input samples to the output position.

In the next step, we realize option b in which another node is involved do the actual signal processing. Since the data flows independently of the selection option a or b we can activate / deactivate option b while data flows on the fly!

Setup the processing (engaged) node

First we need to engage the processing node. We do so as follows in the node which is responsible for the data flow either option a or b:

// Declare a container to get pointer references
refComp<IjvxNode> retRef;

// Request an instance of the processing node from the host. The reqInstTool function
// also performs a typecheck.
retRef = reqInstTool<IjvxNode>(_common_set.theToolsHost, JVX_COMPONENT_AUDIO_NODE, 
                               JVX_SIZE_UNSELECTED, "jvxAuNBinauralRender");

// If the request was successful we steer the new node into the active state
if (retRef.cpPtr)
{
        // From initial state to active. 
	resL = retRef.cpPtr->initialize(_common_set.theHostRef);
	resL = retRef.cpPtr->select(static_cast<IjvxObject*>(this));
	resL = retRef.cpPtr->activate();
}

Allocation of the Micro Connection

In the next step we need to allocate the micro connection:

std::string name = "Micro Connection Channel";
// Allocate micro connection
JVX_DSP_SAFE_ALLOCATE_OBJECT(
	microConn, HjvxMicroConnection(
		name.c_str(),
		false,
		name.c_str(),
		0, ("local-temp-lane #" + numName).c_str(),
		JVX_COMPONENT_ACCESS_SUB_COMPONENT, JVX_COMPONENT_AUDIO_DEVICE, "", NULL));

microConn->set_location_info(jvxComponentIdentification(JVX_SIZE_SLOT_OFF_SYSTEM, 
			JVX_SIZE_SLOT_OFF_SYSTEM, JVX_SIZE_UNSELECTED));

The micro connection is a component object but it is located "off system" to prevent it from interfering with the host at the "on system" slot/subslot positions. As well it is characterized as a sub-component of type audio device.

In the next step, the micro connection has to be activated as follows:

microConn->activate_connection(
        _common_set.theHostRef,
	retRef.cpPtr,
	"default", "default", "default", "default",
	("mConn_" + numName).c_str(), false, 
        static_cast<IjvxObject*>(this),
	connId);

The arguments in this call are as follows:

  • Argument #0 is the host reference to have access to the host when building up the chain.
  • Argument #1 is a pointer to type IjvxHiddenInterface. This reference will be requested to find the input and the output connector of the involved node.
  • Arguments #2-#3 are the names of the input and the output connectors of the involved node.
  • Arguments #4 is the name of the master component. The micro connection itself will be the master of the chain.
  • Argument #5 is the name of the output and the input connector of the master.
  • Argument #6 is the object reference that will be the owner of the micro connection. It is useful to make the micro connection a dependence of the object to, e..g., prevent any entries in the config file.
  • Argument #7 is a connection id which represents the connection from the device to the node and back. The dependence will isolate the process of the local chain from modifications within the system (e.g. the sequencer) as only the owner may address the chain.

Connecting the involved connections

The micro connection will then be connected. This means that the chain is constructed as shown in the following figure:

grafik

The operation is done as follows:

// Allocate an empty link data descriptor for the input
JVX_DSP_SAFE_ALLOCATE_OBJECT(descr_in, jvxLinkDataDescriptor);
jvx_initDataLinkDescriptor(descr_in);

// Allocate an empty link data descriptor for the output
JVX_DSP_SAFE_ALLOCATE_OBJECT(descr_out, jvxLinkDataDescriptor);
jvx_initDataLinkDescriptor(descr_out);		

// Actual call to build up the chain
resL = microConn->connect_connection(
             descr_in, descr_out,
             nullptr, nullptr, false, false);

The arguments in the call are as follows:

  • Argument #1 is the link-data-descriptor on the input side,
  • Argument #2 is the link-data-descriptor on the output side,
  • Arguments #3-#4 are optional references for callbacks which are not used in this simple case.
  • Argument #5 is a bool to indicate whether to run a test call immediately on connect. This makes indeed not sense since no parameters are set at this point.
  • Argument #6 is a bool to copy back the connection parameters in case the test has been run. This is not needed here since test is not run.

The two link-data-descriptors are of major importance: they are used to specify the processing parameters on the input and the output side. Typically they will be taken from the parameter values as used in the main chain as:

descr_in->con_params = dat.con_params;
descr_in->con_params.number_channels = 1;
newElement.descr_out->con_params = dat.con_params;
newElement.descr_out->con_params.number_channels = 2;

The variable dat in this case contains the parameters of the main chain. However, a deviating constraint is only specified for the number of input and output channels.

In the next step the chain is tested:

JVX_CONNECTION_FEEDBACK_TYPE_DEFINE_CLASS(fdbLoc);
JVX_CONNECTION_FEEDBACK_TYPE_DEFINE_CLASS_INIT(fdbLoc);
resL = microConn->test_connection(JVX_CONNECTION_FEEDBACK_CALL(fdbLoc));

If this test fails, a logic needs to be installed to rearrange the parameters.However if used in very well controlled environments this call should not fail.

Then the chain is prepared:

descr_in->con_data.number_buffers = 1;
resL = microConn->prepare_connection(false, false);

The chain is setup to use 1 buffer (no pipelining). Once this call is complete the buffers involved in processing will have been allocated and stored in link-data-descriptors that are linked to the variables descr_in and descr_out:

grafik

The setup is completed by yielding the start operation:

resL = microConn->start_connection();

Involving the chain for processing

The chain is controlled for processing within the main processing function. Assuming that the main processing function is called data arrived from the device. At (1), this data must be copied into the data buffers within the involved node. Then at (2), the processing must be executed to yield the output data in the buffer in the local sink. This data then must at (3) be copied forward towards the involved device:

grafik

The operation is in code realized as follows:

// Run the renderer
jvxLinkDataDescriptor* ptr = nullptr;

// Run the chain to prepare all buffers
microConn->prepare_process_connection(&ptr);

// Extract buffer pointer to fill in data to the chain
jvxData** bufsToProcess = jvx_process_icon_extract_input_buffers<jvxData>(ptr,     
             *ptr->con_pipeline.idx_stage_ptr);
jvxSize szBytes = jvxDataFormat_getsize(ptr->con_params.format) * ptr->con_params.buffersize;

// Fill the buffer, here it is only 1 buffer
memcpy(bufsToProcess [0], bufsIn[0], szBytes);
			
// Actually process the data into the output buffers. Output buffers available on return
elmS.microConn->process_connection(&ptr);

// Extract raw pointer
jvxData** bufsProcessed = jvx_process_icon_extract_input_buffers<jvxData>(ptr,  
                *ptr->con_pipeline.idx_stage_ptr);			
			
// Complete processing
elmS.microConn->postprocess_process_connection();

memcpy(bufsOut [0], bufsProcessed[0], szBytes);
memcpy(bufsOut [1], bufsProcessed[1], szBytes);

In that code extract, the buffer coming from the device in the outer chain are passed as buffers ptrIn, and the output data is forwarded via the ptrOut buffers.

Deallocating the chain

The chain is deallocated by calling the opposite functions from the allocation in reverse order,

jvxErrorType resL = JVX_NO_ERROR;
resL = microConn->stop_connection();
assert(resL == JVX_NO_ERROR);

resL = microConn->postprocess_connection();
assert(resL == JVX_NO_ERROR);

resL = microConn->disconnect_connection();
assert(resL == JVX_NO_ERROR);

JVX_DSP_SAFE_DELETE_OBJECT(descr_in);
JVX_DSP_SAFE_DELETE_OBJECT(descr_out);

resL = microConn->deactivate_connection();
assert(resL == JVX_NO_ERROR);

JVX_SAFE_DELETE_OBJECT(microConn);

assert(retRef.cpPtr);
resL = retRef.cpPtr->deactivate();
assert(resL == JVX_NO_ERROR);

resL = retRef.cpPtr->unselect();
assert(resL == JVX_NO_ERROR);

resL = retRef.cpPtr->terminate();
assert(resL == JVX_NO_ERROR);

resL = retInstTool<IjvxNode>(_common_set.theToolsHost, JVX_COMPONENT_AUDIO_NODE, retRef, 
              JVX_SIZE_UNSELECTED, "jvxAuNBinauralRender");
assert(resL == JVX_NO_ERROR);

and implements the micro connection operations as well as the micro connection removal and the return of the involved inner node.

Final hint

Note that the execution of the creation and deallocation of the chain typically happens in the main thread whereas the actual usage occurs in the processing thread if the chain is involved on-the-fly. A good approach is to establish the chain at first and then attach a hook within the processing function before or after data processing.

The integrative approach

The integrative approach is not as flexible as the simple approach but it is suited to direct audio data to compontents in a really efficient way. The reduced flexibility is obvious by the constraint that the involved component can not be changed while processing is active - the connection is more integral.

Allocation of micro connection

The allocation of the micro connection happens early in live-time of the using component, e.g., within the activate function.

theMicroConnection = new HjvxMicroConnection(
			"Micro Connection Decoder",
			false,
			"Micro Connection Decoder",
			0, "local-temp-lane",
			JVX_COMPONENT_ACCESS_SUB_COMPONENT,
			JVX_COMPONENT_CUSTOM_DEVICE, "", NULL);
theMicroConnection->set_location_info(jvxComponentIdentification(JVX_SIZE_SLOT_OFF_SYSTEM, 
                        JVX_SIZE_SLOT_OFF_SYSTEM, JVX_SIZE_UNSELECTED));

The micro connection remains in "inactive" state even if the component is connected. Then, all connections are handled within the test function later. If the connection has been done, the chain includes the component with the micro connection as usual:

grafik

On the very first call to the test-function, we now need to allocated the involved component. Before doing so we need to obtain the uId (idProc) of the current process (chain) which our component is part of:

IjvxDataConnectionProcess* theProc = nullptr;
jvxSize idProc= JVX_SIZE_UNSELECTED;
if (_common_set_ldslave.theData_in->con_link.master)
{
    _common_set_ldslave.theData_in->con_link.master->associated_process(&theProc);
    if (theProc)
    {
        theProc->unique_id_connections(&idProc);
    }
}

Then, we allocate and activate the involved component:

refComp<IjvxNode> ptrNode;
ptrNode = reqInstTool<IjvxNode>(_common_set.theToolsHost, JVX_COMPONENT_AUDIO_NODE, JVX_SIZE_UNSELECTED, "myNewNode");

	res = ptrNode.cpPtr->initialize(_common_set.theHostRef);
	res = ptrNode.cpPtr->select(nullptr);
	res = ptrNode.cpPtr->activate();

grafik

In the figure, the involved component is the inner "Node".

Now, we create a new micro connection to link our involved component:

jvxSize uIdl = JVX_SIZE_UNSELECTED;

// Get a unique id first
_common_set.theToolsHost->obtain_unique_id(&uIdl, "My Involved Component ID");

theMicroConnection->activate_connection(
	_common_set.theHostRef,
	ptrNode.cpPtr,
	"default", "default", "default", "default",
	"myNewComponent#" + jvx_size2String(uIdl),
	false, nullptr, idProc);

Note that the id of the process, idProc, comes from the code fragments above. This call transforms the micro connection into a master object and allocates one input and one output connector. Then, the call obtains handles to the input and the output connector of the involved object. In the next step, a new process is created with the name as the 7th argument. The new process is the only part which is globally stored and therefore needs to have a unique name in case we run multiple objects. If the name is not unique the allocation may fail. The micro connection is the master of the process with the connectors connected to input and output of the involved component:

grafik

In the next step the connection will be connected in

res = theMicroConnection->connect_connection(_common_set_ldslave.theData_in,
		&_common_set_ldslave.theData_out,
		static_cast<HjvxMicroConnection_hooks_simple*>(this),
		static_cast<HjvxMicroConnection_hooks_fwd*>(this),
		false);

_common_set_ldslave.theData_in as well as _common_set_ldslave.theData_out are the data link descriptors of the component that uses the involved component. The function runs a local connect between the micro connection master and the connectors of the involved component. In all cases, the micro connection connection does not run a test on connect. Note also that two pointer arguments are passed:

  • HjvxMicroConnection_hooks_simple*
  • HjvxMicroConnection_hooks_fwd*

These two references allow code interaction from outside the micro connection and will be the basis for the smooth integration of the new component.

grafik

Once the actual test call is run, the following steps will be taken:

grafik

We enter the micro connection in the call to test_connection. In this call, the input processing parameters are at first copied to the data link descriptor of the micro connection (_commonset_ldslave.theData_out). Then, the inner chain is tested by calling the test_chain function on the associated process. The test function ends up in the function test_connect_icon of the micro connection. From here, the hook_test_accept call is called. This call updates the data format on the output side of the node (operation 4) and forwards the call to the output connector towards the device/input connector. If the device can not accept the settings, the complaint ends up in the hook_test_negotiate function to perform additional steps.

In the prepare step, the data allocation is run. In the data link descriptor, this comprises the buffers (sub type con_data) and the pipeline structs. In the integration approach, the output is directly forwarded to the subsequent connection in the chain: the prepare call to the next connector is executed in function xyz and returns all processing data. This processing data is then inserted in the input link data descriptor of the micro connection which is the output link data descriptor \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\_commonsetldslave.theData_out of the last node in the concatenation within the micro connection.

grafik

The involved node, hence, will see the next buffers to be those from first element following the local node (1 in the figure). The same applies in the postprocess case: here, the buffer references must be removed. The start and stop calls also can be handled like this.

The functions to be implemented, hook_prepare, hook_start, hook_stop and hook_postprocess, are therefore very simple to only forward the calls:

jvxErrorType
<node>::hook_prepare(JVX_CONNECTION_FEEDBACK_TYPE(fdb))
{
	// Forward the chain	
	return _prepare_connect_icon(true JVX_CONNECTION_FEEDBACK_CALL_A(fdb));
}

jvxErrorType
<node>::hook_postprocess(JVX_CONNECTION_FEEDBACK_TYPE(fdb)) 
{
        // Forward the chain	
	return _postprocess_connect_icon(true JVX_CONNECTION_FEEDBACK_CALL_A(fdb));
}

jvxErrorType
<node>::hook_start(JVX_CONNECTION_FEEDBACK_TYPE(fdb))
{
	// Forward the chain	
	return _start_connect_icon(true, idRuntime JVX_CONNECTION_FEEDBACK_CALL_A(fdb));
}

jvxErrorType
<node>::hook_stop(JVX_CONNECTION_FEEDBACK_TYPE(fdb))
{
	// Forward the chain	
	return _stop_connect_icon(true, nullptr JVX_CONNECTION_FEEDBACK_CALL_A(fdb));
}

Now that the linkage is setup properly, even the processing functions can be implemented in a relatively simple way:

Once the components livetime ends, the micro connection must be removed:

delete theMicroConnection;
theMicroConnection = nullptr;

Micro Connection API listing - Class HjvxMicroConnection

HjvxMicroConnection::HjvxMicroConnection(
        const char* description, bool multipleObjects, 
	const char* descriptor, jvxBitField featureClass, 
	const char* module_name, jvxComponentAccessType acTp, 
	jvxComponentType tpComp, const char* descrComp, IjvxObject* templ);

Function to activate connection of a single component.

jvxErrorType 
HjvxMicroConnection::activate_connection(

        // Hidden interface of the host
	IjvxHiddenInterface* href,

        // The single involved component
	IjvxHiddenInterface* target,

        // input connector name
	const std::string& iconn,

        // output connector name
	const std::string& oconn,

        // master name for the function of the micro connection
	const std::string& mastern,

        // Name of the connector - input and output - of the micro connection
	const std::string& mioconn,

        // Name of the processor. This name is stored globally and we need a unique name if using multiples
	const std::string& nmprocess,

        // Activate interceptors
	jvxBool interceptors,

        // Allocate the new component as "owned" to prevent any entries in config files
	IjvxObject* theOwner, 

        // ID of the process to depend on
	jvxSize idProcDepends
	)

Function to activate connection multiple components

jvxErrorType
HjvxMicroConnection::activate_connection(

        // Hidden interface of the host
	IjvxHiddenInterface* href,

        // List of involved component, will be concatenated in a straight way
	IjvxHiddenInterface** target,

        // List of input connectors - one connector per component
	const char** iconn,

        // List of input connectors - one connector per component
	const char** oconn,

        // Number of components to be connected
	jvxSize num_objs,

        // master name for the function of the micro connection
	const std::string& mastern,

        // Name of the connector - input and output - of the micro connection
	const std::string& mioconn,

        // Name of the processor. This name is stored globally and we need a unique name if using multiples
	const std::string& nmprocess,

        // Activate interceptors
	jvxBool interceptors,

        // Allocate the new component as "owned" to prevent any entries in config files
	IjvxObject* theOwner,

        // ID of the process to depend on
	jvxSize idProcDepends)

Other involed classes:

Interface HjvxMicroConnection_hooks_simple

jvx_interface HjvxMicroConnection_hooks_simple
{
    virtual jvxErrorType hook_test_negotiate(
        jvxLinkDataDescriptor* proposed, jvxLinkDataDescriptor* reference) = 0;
    virtual jvxErrorType hook_test_accept(jvxLinkDataDescriptor* dataIn  
        JVX_CONNECTION_FEEDBACK_TYPE_A(fdb)) = 0;
    virtual jvxErrorType hook_test_update(jvxLinkDataDescriptor* dataIn  
        JVX_CONNECTION_FEEDBACK_TYPE_A(fdb)) = 0;
    virtual jvxErrorType hook_check_is_ready(jvxBool* is_ready, jvxApiString * astr) = 0;
}
JVX_INTERFACE HjvxMicroConnection_hooks_fwd
{
        virtual jvxErrorType hook_prepare(JVX_CONNECTION_FEEDBACK_TYPE(fdb)) = 0;
	virtual jvxErrorType hook_postprocess(JVX_CONNECTION_FEEDBACK_TYPE(fdb)) = 0;
	virtual jvxErrorType hook_start(JVX_CONNECTION_FEEDBACK_TYPE(fdb)) = 0;
	virtual jvxErrorType hook_stop(JVX_CONNECTION_FEEDBACK_TYPE(fdb)) = 0;
	virtual jvxErrorType hook_process_start(
		jvxSize pipeline_offset,
		jvxSize* idx_stage,
		jvxSize tobeAccessedByStage,
		callback_process_start_in_lock clbk,
		jvxHandle* priv_ptr) = 0;
	virtual jvxErrorType hook_process_stop(
		jvxSize idx_stage,
		jvxBool shift_fwd,
		jvxSize tobeAccessedByStage,
		callback_process_stop_in_lock clbk,
		jvxHandle* priv_ptr) = 0;
	virtual jvxErrorType hook_process(
		jvxSize mt_mask, jvxSize idx_stage) = 0;
}
Clone this wiki locally